can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
1,744 lines (1,631 loc) • 40.6 kB
JavaScript
/* global Person: true */
/* global CustomId: true */
/* global Test: true */
/* global ObjectDef: true */
/* global Abortion: true */
/* global Storage: true */
/* global Base: true */
/* global Product: true */
/* global Organisation: true */
/* global My: true */
steal("can/model", "can/test", "can/util/fixture", "steal-qunit", function () {
QUnit.module('can/model', {
setup: function () {}
});
var isDojo = typeof dojo !== 'undefined';
test('shadowed id', function () {
var MyModel = can.Model.extend({
id: 'foo'
}, {
foo: function () {
return this.attr('foo');
}
});
var newModel = new MyModel({});
ok(newModel.isNew(), 'new model is isNew');
var oldModel = new MyModel({
foo: 'bar'
});
ok(!oldModel.isNew(), 'old model is not new');
equal(oldModel.foo(), 'bar', 'method can coexist with attribute');
});
test('findAll deferred', function () {
can.Model('Person', {
findAll: function (params, success, error) {
var self = this;
return can.ajax({
url: '/people',
data: params,
fixture: can.test.fixture('model/test/people.json'),
dataType: 'json'
})
.pipe(function (data) {
return self.models(data);
});
}
}, {});
stop();
var people = Person.findAll({});
people.then(function (people) {
equal(people.length, 1, 'we got a person back');
equal(people[0].name, 'Justin', 'Got a name back');
equal(people[0].constructor.shortName, 'Person', 'got a class back');
start();
});
});
test('findAll rejects non-array (#384)', function () {
var Person = can.Model.extend({
findAll: function (params, success, error) {
var dfd = can.Deferred();
setTimeout(function () {
dfd.resolve({
stuff: {}
});
}, 100);
return dfd;
}
}, {});
stop();
Person.findAll({})
.then(function () {
ok(false, 'This should not succeed');
}, function (err) {
ok(err instanceof Error, 'Got an error');
equal(err.message, 'Could not get any raw data while converting using .models');
start();
});
});
asyncTest('findAll deferred reject', function () {
// This test is automatically paused
function rejectDeferred(df) {
setTimeout(function () {
df.reject();
}, 100);
}
function resolveDeferred(df) {
setTimeout(function () {
df.resolve();
}, 100);
}
can.Model('Person', {
findAll: function (params, success, error) {
var df = can.Deferred();
if (params.resolve) {
resolveDeferred(df);
} else {
rejectDeferred(df);
}
return df;
}
}, {});
var people_reject = Person.findAll({
resolve: false
});
var people_resolve = Person.findAll({
resolve: true
});
setTimeout(function () {
people_reject.done(function () {
ok(false, 'This deferred should be rejected');
});
people_reject.fail(function () {
ok(true, 'The deferred is rejected');
});
people_resolve.done(function () {
ok(true, 'This deferred is resolved');
});
people_resolve.fail(function () {
ok(false, 'The deferred should be resolved');
});
// continue the test
start();
}, 200);
});
if (window.jQuery) {
asyncTest('findAll abort', function () {
expect(4);
var df;
can.Model('Person', {
findAll: function (params, success, error) {
df = can.Deferred();
df.then(function () {
ok(!params.abort, 'not aborted');
}, function () {
ok(params.abort, 'aborted');
});
return df.promise({
abort: function () {
df.reject();
}
});
}
}, {});
Person.findAll({
abort: false
})
.done(function () {
ok(true, 'resolved');
});
var resolveDf = df;
var abortPromise = Person.findAll({
abort: true
})
.fail(function () {
ok(true, 'failed');
});
setTimeout(function () {
resolveDf.resolve();
abortPromise.abort();
// continue the test
start();
}, 200);
});
}
test('findOne deferred', function () {
if (window.jQuery) {
can.Model('Person', {
findOne: function (params, success, error) {
var self = this;
return can.ajax({
url: '/people/5',
data: params,
fixture: can.test.fixture('model/test/person.json'),
dataType: 'json'
})
.pipe(function (data) {
return self.model(data);
});
}
}, {});
} else {
can.Model('Person', {
findOne: can.test.fixture('model/test/person.json')
}, {});
}
stop();
var person = Person.findOne({});
person.then(function (person) {
equal(person.name, 'Justin', 'Got a name back');
equal(person.constructor.shortName, 'Person', 'got a class back');
start();
});
});
test('save deferred', function () {
can.Model('Person', {
create: function (attrs, success, error) {
return can.ajax({
url: '/people',
data: attrs,
type: 'post',
dataType: 'json',
fixture: function () {
return {
id: 5
};
},
success: success
});
}
}, {});
var person = new Person({
name: 'Justin'
}),
personD = person.save();
stop();
personD.then(function (person) {
start();
equal(person.id, 5, 'we got an id');
});
});
test('update deferred', function () {
can.Model('Person', {
update: function (id, attrs, success, error) {
return can.ajax({
url: '/people/' + id,
data: attrs,
type: 'post',
dataType: 'json',
fixture: function () {
return {
thing: 'er'
};
},
success: success
});
}
}, {});
var person = new Person({
name: 'Justin',
id: 5
}),
personD = person.save();
stop();
personD.then(function (person) {
start();
equal(person.thing, 'er', 'we got updated');
});
});
test('destroy deferred', function () {
can.Model('Person', {
destroy: function (id, success, error) {
return can.ajax({
url: '/people/' + id,
type: 'post',
dataType: 'json',
fixture: function () {
return {
thing: 'er'
};
},
success: success
});
}
}, {});
var person = new Person({
name: 'Justin',
id: 5
}),
personD = person.destroy();
stop();
personD.then(function (person) {
start();
equal(person.thing, 'er', 'we got destroyed');
});
});
test('models', function () {
can.Model('Person', {
prettyName: function () {
return 'Mr. ' + this.name;
}
});
var people = Person.models([{
id: 1,
name: 'Justin'
}]);
equal(people[0].prettyName(), 'Mr. Justin', 'wraps wrapping works');
});
test('.models with custom id', function () {
can.Model('CustomId', {
findAll: can.test.path('model/test/customids.json'),
id: '_id'
}, {
getName: function () {
return this.name;
}
});
var results = CustomId.models([{
'_id': 1,
'name': 'Justin'
}, {
'_id': 2,
'name': 'Brian'
}]);
equal(results.length, 2, 'Got two items back');
equal(results[0].name, 'Justin', 'First name right');
equal(results[1].name, 'Brian', 'Second name right');
});
/*
test("async setters", function(){
can.Model("Test.AsyncModel",{
setName : function(newVal, success, error){
setTimeout(function(){
success(newVal)
}, 100)
}
});
var model = new Test.AsyncModel({
name : "justin"
});
equal(model.name, "justin","property set right away")
//makes model think it is no longer new
model.id = 1;
var count = 0;
model.bind('name', function(ev, newName){
equal(newName, "Brian",'new name');
equal(++count, 1, "called once");
ok(new Date() - now > 0, "time passed")
start();
})
var now = new Date();
model.attr('name',"Brian");
stop();
})*/
test('binding', 2, function () {
can.Model('Person');
var inst = new Person({
foo: 'bar'
});
inst.bind('foo', function (ev, val) {
ok(true, 'updated');
equal(val, 'baz', 'values match');
});
inst.attr('foo', 'baz');
});
test('auto methods', function () {
//turn off fixtures
can.fixture.on = false;
var School = can.Model.extend('Jquery.Model.Models.School', {
findAll: can.test.path('model/test/{type}.json'),
findOne: can.test.path('model/test/{id}.json'),
create: 'GET ' + can.test.path('model/test/create.json'),
update: 'GET ' + can.test.path('model/test/update{id}.json')
}, {});
stop();
School.findAll({
type: 'schools'
}, function (schools) {
ok(schools, 'findAll Got some data back');
equal(schools[0].constructor.shortName, 'School', 'there are schools');
School.findOne({
id: '4'
}, function (school) {
ok(school, 'findOne Got some data back');
equal(school.constructor.shortName, 'School', 'a single school');
new School({
name: 'Highland'
})
.save(function (school) {
equal(school.name, 'Highland', 'create gets the right name');
school.attr({
name: 'LHS'
})
.save(function () {
start();
equal(school.name, 'LHS', 'create gets the right name');
can.fixture.on = true;
});
});
});
});
});
test('isNew', function () {
var p = new Person();
ok(p.isNew(), 'nothing provided is new');
var p2 = new Person({
id: null
});
ok(p2.isNew(), 'null id is new');
var p3 = new Person({
id: 0
});
ok(!p3.isNew(), '0 is not new');
});
test('findAll string', function () {
can.fixture.on = false;
can.Model('Test.Thing', {
findAll: can.test.path('model/test/findAll.json') + ''
}, {});
stop();
Test.Thing.findAll({}, function (things) {
equal(things.length, 1, 'got an array');
equal(things[0].id, 1, 'an array of things');
start();
can.fixture.on = true;
});
});
test('Model events', function () {
expect(12);
var order = 0;
can.Model('Test.Event', {
create: function (attrs) {
var def = isDojo ? new dojo.Deferred() : new can.Deferred();
def.resolve({
id: 1
});
return def;
},
update: function (id, attrs, success) {
var def = isDojo ? new dojo.Deferred() : new can.Deferred();
def.resolve(attrs);
return def;
},
destroy: function (id, success) {
var def = isDojo ? new dojo.Deferred() : new can.Deferred();
def.resolve({});
return def;
}
}, {});
stop();
Test.Event.bind('created', function (ev, passedItem) {
ok(this === Test.Event, 'got model');
ok(passedItem === item, 'got instance');
equal(++order, 1, 'order');
passedItem.save();
})
.bind('updated', function (ev, passedItem) {
equal(++order, 2, 'order');
ok(this === Test.Event, 'got model');
ok(passedItem === item, 'got instance');
passedItem.destroy();
})
.bind('destroyed', function (ev, passedItem) {
equal(++order, 3, 'order');
ok(this === Test.Event, 'got model');
ok(passedItem === item, 'got instance');
start();
});
var item = new Test.Event();
item.bind('created', function () {
ok(true, 'created');
})
.bind('updated', function () {
ok(true, 'updated');
})
.bind('destroyed', function () {
ok(true, 'destroyed');
});
item.save();
});
test('removeAttr test', function () {
can.Model('Person');
var person = new Person({
foo: 'bar'
});
equal(person.foo, 'bar', 'property set');
person.removeAttr('foo');
equal(person.foo, undefined, 'property removed');
var attrs = person.attr();
equal(attrs.foo, undefined, 'attrs removed');
});
test('save error args', function () {
var Foo = can.Model.extend('Testin.Models.Foo', {
create: '/testinmodelsfoos.json'
}, {});
var st = '{type: "unauthorized"}';
can.fixture('/testinmodelsfoos.json', function (request, response) {
response(401, st);
});
stop();
new Foo({})
.save(function () {
ok(false, 'success should not be called');
start();
}, function (jQXHR) {
ok(true, 'error called');
ok(jQXHR.getResponseHeader, 'jQXHR object');
start();
});
});
test('object definitions', function () {
can.Model('ObjectDef', {
findAll: {
url: '/test/place',
dataType: 'json'
},
findOne: {
url: '/objectdef/{id}',
timeout: 1000
},
create: {},
update: {},
destroy: {}
}, {});
can.fixture('GET /objectdef/{id}', function (original) {
equal(original.timeout, 1000, 'timeout set');
return {
yes: true
};
});
can.fixture('GET /test/place', function (original) {
return [original.data];
});
stop();
ObjectDef.findOne({
id: 5
}, function () {
start();
});
stop();
// Do find all, pass some attrs
ObjectDef.findAll({
start: 0,
count: 10,
myflag: 1
}, function (data) {
start();
equal(data[0].myflag, 1, 'my flag set');
});
stop();
// Do find all with slightly different attrs than before,
// and notice when leaving one out the other is still there
ObjectDef.findAll({
start: 0,
count: 10
}, function (data) {
start();
equal(data[0].myflag, undefined, 'my flag is undefined');
});
});
test('aborting create update and destroy', function () {
stop();
var delay = can.fixture.delay;
can.fixture.delay = 1000;
can.fixture('POST /abort', function () {
ok(false, 'we should not be calling the fixture');
return {};
});
can.Model('Abortion', {
create: 'POST /abort',
update: 'POST /abort',
destroy: 'POST /abort'
}, {});
var deferred = new Abortion({
name: 'foo'
})
.save(function () {
ok(false, 'success create');
start();
}, function () {
ok(true, 'create error called');
deferred = new Abortion({
name: 'foo',
id: 5
})
.save(function () {
ok(false, 'save called');
start();
}, function () {
ok(true, 'error called in update');
deferred = new Abortion({
name: 'foo',
id: 5
})
.destroy(function () {}, function () {
ok(true, 'destroy error called');
can.fixture.delay = delay;
start();
});
setTimeout(function () {
deferred.abort();
}, 10);
});
setTimeout(function () {
deferred.abort();
}, 10);
});
setTimeout(function () {
deferred.abort();
}, 10);
});
test('store binding', function () {
can.Model('Storage');
var s = new Storage({
id: 1,
thing: {
foo: 'bar'
}
});
ok(!Storage.store[1], 'not stored');
var func = function () {};
s.bind('foo', func);
ok(Storage.store[1], 'stored');
s.unbind('foo', func);
ok(!Storage.store[1], 'not stored');
var s2 = new Storage({});
s2.bind('foo', func);
s2.attr('id', 5);
ok(Storage.store[5], 'stored');
s2.unbind('foo', func);
ok(!Storage.store[5], 'not stored');
});
test('store ajax binding', function () {
var Guy = can.Model.extend({
findAll: '/guys',
findOne: '/guy/{id}'
}, {});
can.fixture('GET /guys', function () {
return [{
id: 1
}];
});
can.fixture('GET /guy/{id}', function () {
return {
id: 1
};
});
stop();
can.when(Guy.findOne({
id: 1
}), Guy.findAll())
.then(function (guyRes, guysRes2) {
equal(guyRes.id, 1, 'got a guy id 1 back');
equal(guysRes2[0].id, 1, 'got guys w/ id 1 back');
ok(guyRes === guysRes2[0], 'guys are the same');
// check the store is empty
setTimeout(function () {
var id;
start();
for (id in Guy.store) {
ok(false, 'there should be nothing in the store');
}
}, 1);
});
});
test('store instance updates', function () {
var Guy, updateCount;
Guy = can.Model.extend({
findAll: 'GET /guys'
}, {});
updateCount = 0;
can.fixture('GET /guys', function () {
var guys = [{
id: 1,
updateCount: updateCount,
nested: {
count: updateCount
}
}];
updateCount++;
return guys;
});
stop();
Guy.findAll({}, function (guys) {
start();
guys[0].bind('updated', function () {});
ok(Guy.store[1], 'instance stored');
equal(Guy.store[1].updateCount, 0, 'updateCount is 0');
equal(Guy.store[1].nested.count, 0, 'nested.count is 0');
});
Guy.findAll({}, function (guys) {
equal(Guy.store[1].updateCount, 1, 'updateCount is 1');
equal(Guy.store[1].nested.count, 1, 'nested.count is 1');
});
});
/*
test("store instance update removed fields", function(){
var Guy, updateCount, remove;
Guy = can.Model.extend({
findAll : 'GET /guys'
},{});
remove = false;
can.fixture("GET /guys", function(){
var guys = [{id: 1, name: 'mikey', age: 35, likes: ['soccer', 'fantasy baseball', 'js', 'zelda'], dislikes: ['backbone', 'errors']}];
if(remove) {
delete guys[0].name;
guys[0].likes = [];
delete guys[0].dislikes;
}
remove = true;
return guys;
});
stop();
Guy.findAll({}, function(guys){
start();
guys[0].bind('updated', function(){});
ok(Guy.store[1], 'instance stored');
equal(Guy.store[1].name, 'mikey', 'name is mikey')
equal(Guy.store[1].likes.length, 4, 'mikey has 4 likes')
equal(Guy.store[1].dislikes.length, 2, 'mikey has 2 dislikes')
})
Guy.findAll({}, function(guys){
equal(Guy.store[1].name, undefined, 'name is undefined')
equal(Guy.store[1].likes.length, 0, 'no likes')
equal(Guy.store[1].dislikes, undefined, 'dislikes removed')
})
})
*/
test('templated destroy', function () {
var MyModel = can.Model.extend({
destroy: '/destroyplace/{id}'
}, {});
can.fixture('/destroyplace/{id}', function (original) {
ok(true, 'fixture called');
equal(original.url, '/destroyplace/5', 'urls match');
return {};
});
stop();
new MyModel({
id: 5
})
.destroy(function () {
start();
});
can.fixture('/product/{id}', function (original) {
equal(original.data.id, 9001, 'Changed ID is correctly set.');
start();
return {};
});
Base = can.Model.extend({
id: '_id'
}, {});
Product = Base({
destroy: 'DELETE /product/{id}'
}, {});
new Product({
_id: 9001
})
.destroy();
stop();
});
test('extended templated destroy', function () {
var MyModel = can.Model({
destroy: '/destroyplace/{attr1}/{attr2}/{id}'
}, {});
can.fixture('/destroyplace/{attr1}/{attr2}/{id}', function (original) {
ok(true, 'fixture called');
equal(original.url, '/destroyplace/foo/bar/5', 'urls match');
return {};
});
stop();
new MyModel({
id: 5,
attr1: 'foo',
attr2: 'bar'
})
.destroy(function () {
start();
});
can.fixture('/product/{attr3}/{id}', function (original) {
equal(original.data.id, 9001, 'Changed ID is correctly set.');
start();
return {};
});
Base = can.Model({
id: '_id'
}, {});
Product = Base({
destroy: 'DELETE /product/{attr3}/{id}'
}, {});
new Product({
_id: 9001,
attr3: 'great'
})
.destroy();
stop();
});
test('overwrite makeFindAll', function () {
var store = {};
var LocalModel = can.Model.extend({
makeFindOne: function (findOne) {
return function (params, success, error) {
var def = new can.Deferred(),
data = store[params.id];
def.then(success, error);
// make the ajax request right away
var findOneDeferred = findOne(params);
if (data) {
var instance = this.model(data);
findOneDeferred.then(function (data) {
instance.updated(data);
}, function () {
can.trigger(instance, 'error', data);
});
def.resolve(instance);
} else {
findOneDeferred.then(can.proxy(function (data) {
var instance = this.model(data);
store[instance[this.id]] = data;
def.resolve(instance);
}, this), function (data) {
def.reject(data);
});
}
return def;
};
}
}, {
updated: function (attrs) {
can.Model.prototype.updated.apply(this, arguments);
store[this[this.constructor.id]] = this.serialize();
}
});
can.fixture('/food/{id}', function (settings) {
return count === 0 ? {
id: settings.data.id,
name: 'hot dog'
} : {
id: settings.data.id,
name: 'ice water'
};
});
var Food = LocalModel({
findOne: '/food/{id}'
}, {});
stop();
var count = 0;
Food.findOne({
id: 1
}, function (food) {
count = 1;
ok(true, 'empty findOne called back');
food.bind('name', function () {
ok(true, 'name changed');
equal(count, 2, 'after last find one');
equal(this.name, 'ice water');
start();
});
Food.findOne({
id: 1
}, function (food2) {
count = 2;
ok(food2 === food, 'same instances');
equal(food2.name, 'hot dog');
});
});
});
test('inheriting unique model names', function () {
var Foo = can.Model.extend({});
var Bar = can.Model.extend({});
ok(Foo.fullName !== Bar.fullName, 'fullNames not the same');
});
test('model list attr', function () {
can.Model('Person', {}, {});
var list1 = new Person.List(),
list2 = new Person.List([
new Person({
id: 1
}),
new Person({
id: 2
})
]);
equal(list1.length, 0, 'Initial empty list has length of 0');
list1.attr(list2);
equal(list1.length, 2, 'Merging using attr yields length of 2');
});
test('destroying a model impact the right list', function () {
can.Model('Person', {
destroy: function (id, success) {
var def = isDojo ? new dojo.Deferred() : new can.Deferred();
def.resolve({});
return def;
}
}, {});
can.Model('Organisation', {
destroy: function (id, success) {
var def = isDojo ? new dojo.Deferred() : new can.Deferred();
def.resolve({});
return def;
}
}, {});
var people = new Person.List([
new Person({
id: 1
}),
new Person({
id: 2
})
]),
orgs = new Organisation.List([
new Organisation({
id: 1
}),
new Organisation({
id: 2
})
]);
// you must be bound to the list to get this
people.bind('length', function () {});
orgs.bind('length', function () {});
// set each person to have an organization
people[0].attr('organisation', orgs[0]);
people[1].attr('organisation', orgs[1]);
equal(people.length, 2, 'Initial Person.List has length of 2');
equal(orgs.length, 2, 'Initial Organisation.List has length of 2');
orgs[0].destroy();
equal(people.length, 2, 'After destroying orgs[0] Person.List has length of 2');
equal(orgs.length, 1, 'After destroying orgs[0] Organisation.List has length of 1');
});
test('uses attr with isNew', function () {
// TODO this does not seem to be consistent expect(2);
var old = can.__observe;
can.__observe = function (object, attribute) {
if (attribute === 'id') {
ok(true, 'used attr');
}
};
var m = new can.Model({
id: 4
});
m.isNew();
can.__observe = old;
});
test('extends defaults by calling base method', function () {
var M1 = can.Model.extend({
defaults: {
foo: 'bar'
}
}, {});
var M2 = M1({});
equal(M2.defaults.foo, 'bar');
});
test('.models updates existing list if passed', 4, function () {
var Model = can.Model.extend({});
var list = Model.models([{
id: 1,
name: 'first'
}, {
id: 2,
name: 'second'
}]);
list.bind('add', function (ev, newData) {
equal(newData.length, 3, 'Got all new items at once');
});
var newList = Model.models([{
id: 3,
name: 'third'
}, {
id: 4,
name: 'fourth'
}, {
id: 5,
name: 'fifth'
}], list);
equal(list, newList, 'Lists are the same');
equal(newList.attr('length'), 3, 'List has new items');
equal(list[0].name, 'third', 'New item is the first one');
});
test('calling destroy with unsaved model triggers destroyed event (#181)', function () {
var MyModel = can.Model.extend({}, {}),
newModel = new MyModel(),
list = new MyModel.List(),
deferred;
// you must bind to a list for this feature
list.bind('length', function () {});
list.push(newModel);
equal(list.attr('length'), 1, 'List length as expected');
deferred = newModel.destroy();
ok(deferred, '.destroy returned a Deferred');
equal(list.attr('length'), 0, 'Unsaved model removed from list');
deferred.done(function (data) {
ok(data === newModel, 'Resolved with destroyed model as described in docs');
});
});
test('model removeAttr (#245)', function () {
var MyModel = can.Model.extend({}),
model;
can.Model._reqs++;
// pretend it is live bound
model = MyModel.model({
id: 0,
index: 2,
name: 'test'
});
model = MyModel.model({
id: 0,
name: 'text updated'
});
equal(model.attr('name'), 'text updated', 'attribute updated');
equal(model.attr('index'), 2, 'Index attribute still remains');
MyModel = can.Model.extend({
removeAttr: true
}, {});
can.Model._reqs++;
// pretend it is live bound
model = MyModel.model({
id: 0,
index: 2,
name: 'test'
});
model = MyModel.model({
id: 0,
name: 'text updated'
});
equal(model.attr('name'), 'text updated', 'attribute updated');
deepEqual(model.attr(), {
id: 0,
name: 'text updated'
}, 'Index attribute got removed');
});
test('.model on create and update (#301)', function () {
var MyModel = can.Model.extend({
create: 'POST /todo',
update: 'PUT /todo',
model: function (data) {
return can.Model.model.call(this, data.item);
}
}, {}),
id = 0,
updateTime;
can.fixture('POST /todo', function (original, respondWith, settings) {
id++;
return {
item: can.extend(original.data, {
id: id
})
};
});
can.fixture('PUT /todo', function (original, respondWith, settings) {
updateTime = new Date()
.getTime();
return {
item: {
updatedAt: updateTime
}
};
});
stop();
MyModel.bind('created', function (ev, created) {
start();
deepEqual(created.attr(), {
id: 1,
name: 'Dishes'
}, '.model works for create');
})
.bind('updated', function (ev, updated) {
start();
deepEqual(updated.attr(), {
id: 1,
name: 'Laundry',
updatedAt: updateTime
}, '.model works for update');
});
var instance = new MyModel({
name: 'Dishes'
}),
saveD = instance.save();
stop();
saveD.then(function () {
instance.attr('name', 'Laundry')
.save();
});
});
test('List params uses findAll', function () {
stop();
can.fixture('/things', function (request) {
equal(request.data.param, 'value', 'params passed');
return [{
id: 1,
name: 'Thing One'
}];
});
var Model = can.Model.extend({
findAll: '/things'
}, {});
var items = new Model.List({
param: 'value'
});
items.bind('add', function (ev, items, index) {
equal(items[0].name, 'Thing One', 'items added');
start();
});
});
test('destroy not calling callback for new instances (#403)', function () {
var Recipe = can.Model.extend({}, {});
expect(1);
stop();
new Recipe({
name: 'mow grass'
})
.destroy(function (recipe) {
ok(true, 'Destroy called');
start();
});
});
test('.model should always serialize Observes (#444)', function () {
var ConceptualDuck = can.Model.extend({
defaults: {
sayeth: 'Abstractly \'quack\''
}
}, {});
var ObserveableDuck = can.Map({}, {});
equal('quack', ConceptualDuck.model(new ObserveableDuck({
sayeth: 'quack'
}))
.sayeth);
});
test('string configurable model and models functions (#128)', function () {
var StrangeProp = can.Model.extend({
model: 'foo',
models: 'bar'
}, {});
var strangers = StrangeProp.models({
bar: [{
foo: {
id: 1,
name: 'one'
}
}, {
foo: {
id: 2,
name: 'two'
}
}]
});
deepEqual(strangers.attr(), [{
id: 1,
name: 'one'
}, {
id: 2,
name: 'two'
}]);
});
test('create deferred does not resolve to the same instance', function () {
var Todo = can.Model.extend({
create: function () {
var def = new can.Deferred();
def.resolve({
id: 5
});
return def;
}
}, {});
var handler = function () {};
var t = new Todo({
name: 'Justin'
});
t.bind('name', handler);
var def = t.save();
stop();
def.then(function (todo) {
ok(todo === t, 'same instance');
start();
ok(Todo.store[5] === t, 'instance put in store');
t.unbind('name', handler);
});
});
test("Model#save should not replace attributes with their default values (#560)", function () {
can.fixture("POST /person.json", function (request, response) {
return {
createdAt: "now"
};
});
var Person = can.Model.extend({
update: 'POST /person.json'
}, {
name: 'Example name'
});
var person = new Person({
id: 5,
name: 'Justin'
}),
personD = person.save();
stop();
personD.then(function (person) {
start();
equal(person.name, "Justin", "Model name attribute value is preserved after save");
});
});
test(".parseModel as function on create and update (#560)", function () {
var MyModel = can.Model.extend({
create: 'POST /todo',
update: 'PUT /todo',
parseModel: function (data) {
return data.item;
}
}, {
aDefault: "foo"
}),
id = 0,
updateTime;
can.fixture('POST /todo', function (original, respondWith, settings) {
id++;
return {
item: can.extend(original.data, {
id: id
})
};
});
can.fixture('PUT /todo', function (original, respondWith, settings) {
updateTime = new Date()
.getTime();
return {
item: {
updatedAt: updateTime
}
};
});
stop();
MyModel.bind('created', function (ev, created) {
start();
deepEqual(created.attr(), {
id: 1,
name: 'Dishes',
aDefault: "bar"
}, '.model works for create');
})
.bind('updated', function (ev, updated) {
start();
deepEqual(updated.attr(), {
id: 1,
name: 'Laundry',
updatedAt: updateTime
}, '.model works for update');
});
var instance = new MyModel({
name: 'Dishes',
aDefault: "bar"
}),
saveD = instance.save();
stop();
saveD.then(function () {
instance.attr('name', 'Laundry');
instance.removeAttr("aDefault");
instance.save();
});
});
test(".parseModel as string on create and update (#560)", function () {
var MyModel = can.Model.extend({
create: 'POST /todo',
update: 'PUT /todo',
parseModel: "item"
}, {
aDefault: "foo"
}),
id = 0,
updateTime;
can.fixture('POST /todo', function (original, respondWith, settings) {
id++;
return {
item: can.extend(original.data, {
id: id
})
};
});
can.fixture('PUT /todo', function (original, respondWith, settings) {
updateTime = new Date()
.getTime();
return {
item: {
updatedAt: updateTime
}
};
});
stop();
MyModel.bind('created', function (ev, created) {
start();
deepEqual(created.attr(), {
id: 1,
name: 'Dishes',
aDefault: "bar"
}, '.model works for create');
})
.bind('updated', function (ev, updated) {
start();
deepEqual(updated.attr(), {
id: 1,
name: 'Laundry',
updatedAt: updateTime
}, '.model works for update');
});
var instance = new MyModel({
name: 'Dishes',
aDefault: "bar"
}),
saveD = instance.save();
stop();
saveD.then(function () {
instance.attr('name', 'Laundry');
instance.removeAttr("aDefault");
instance.save();
});
});
test("parseModels and findAll", function () {
var array = [{
id: 1,
name: "first"
}];
can.fixture("/mymodels", function () {
return array;
});
var MyModel = can.Model.extend({
findAll: "/mymodels",
parseModels: function (raw, xhr) {
// only check this if jQuery because its deferreds can resolve with multiple args
if (window.jQuery) {
ok(xhr, "xhr object provided");
}
equal(array, raw, "got passed raw data");
return {
data: raw,
count: 1000
};
}
}, {});
stop();
MyModel.findAll({}, function (models) {
equal(models.count, 1000);
start();
});
});
test("parseModels and parseModel and findAll", function () {
can.fixture("/mymodels", function () {
return {
myModels: [{
myModel: {
id: 1,
name: "first"
}
}]
};
});
var MyModel = can.Model.extend({
findAll: "/mymodels",
parseModels: "myModels",
parseModel: "myModel"
}, {});
stop();
MyModel.findAll({}, function (models) {
deepEqual(models.attr(), [{
id: 1,
name: "first"
}], "correct models returned");
start();
});
});
test("findAll rejects when parseModels returns non-array data #1662", function(){
can.fixture("/mymodels", function () {
return {
status: 'success',
message: ''
};
});
var MyModel = can.Model.extend({
findAll: "/mymodels",
parseModels: function(raw) {
raw.data = undefined;
return raw;
}
}, {});
stop();
MyModel.findAll({})
.then(function(){
ok(false, 'This should not succeed');
start();
}, function(err){
ok(err instanceof Error, 'Got an error');
equal(err.message, 'Could not get any raw data while converting using .models');
start();
});
});
test("Nested lists", function(){
var Teacher = can.Model.extend({});
var teacher = new Teacher();
teacher.attr("locations", [{id: 1, name: "Chicago"}, {id: 2, name: "LA"}]);
ok(!(teacher.attr('locations') instanceof Teacher.List), 'nested list is not an instance of Teacher.List');
ok(!(teacher.attr('locations')[0] instanceof Teacher), 'nested map is not an instance of Teacher');
});
test("#501 - resource definition - create", function() {
can.fixture("/foods", function() {
return [];
});
var FoodModel = can.Model.extend({
resource: "/foods"
}, {});
stop();
var steak = new FoodModel({name: "steak"});
steak.save(function(food) {
equal(food.name, "steak", "create created the correct model");
start();
});
});
test("#501 - resource definition - findAll", function() {
can.fixture("/drinks", function() {
return [{
id: 1,
name: "coke"
}];
});
var DrinkModel = can.Model.extend({
resource: "/drinks"
}, {});
stop();
DrinkModel.findAll({}, function(drinks) {
deepEqual(drinks.attr(), [{
id: 1,
name: "coke"
}], "findAll returned the correct models");
start();
});
});
test("#501 - resource definition - findOne", function() {
can.fixture("GET /clothes/{id}", function() {
return [{
id: 1,
name: "pants"
}];
});
var ClothingModel = can.Model.extend({
resource: "/clothes"
}, {});
stop();
ClothingModel.findOne({id: 1}, function(item) {
equal(item[0].name, "pants", "findOne returned the correct model");
start();
});
});
test("#501 - resource definition - remove trailing slash(es)", function() {
can.fixture("POST /foods", function() {
return [];
});
var FoodModel = can.Model.extend({
resource: "/foods//////"
}, {});
stop();
var steak = new FoodModel({name: "steak"});
steak.save(function(food) {
equal(food.name, "steak", "removed trailing '/' and created the correct model");
start();
});
});
test("model list destroy after calling replace", function(){
expect(2);
var map = new can.Model({name: "map1"});
var map2 = new can.Model({name: "map2"});
var list = new can.Model.List([map, map2]);
list.bind('destroyed', function(ev){
ok(true, 'trigger destroyed');
});
can.trigger(map, 'destroyed');
list.replace([map2]);
can.trigger(map2, 'destroyed');
});
test("a model defined with a fullName has findAll working (#1034)", function(){
var List = can.List.extend();
can.Model.extend("My.Model",{
List: List
},{});
equal(List.Map, My.Model, "list's Map points to My.Model");
});
test("providing parseModels works", function(){
var MyModel = can.Model.extend({
parseModel: "modelData"
},{});
var data = MyModel.parseModel({modelData: {id: 1}});
equal(data.id,1, "correctly used parseModel");
});
test('#1089 - resource definition - inheritance', function() {
can.fixture('GET /things/{id}', function() {
return { id: 0, name: 'foo' };
});
var Base = can.Model.extend();
var Thing = Base.extend({
resource: '/things'
}, {});
stop();
Thing.findOne({ id: 0 }, function(thing) {
equal(thing.name, 'foo', 'found model in inherited model');
start();
}, function(e, msg) {
ok(false, msg);
start();
});
});
test('#1089 - resource definition - CRUD overrides', function() {
can.fixture('GET /foos/{id}', function() {
return { id: 0, name: 'foo' };
});
can.fixture('POST /foos', function() {
return { id: 1 };
});
can.fixture('PUT /foos/{id}', function() {
return { id: 1, updated: true };
});
can.fixture('GET /bars', function() {
return [{}];
});
var Thing = can.Model.extend({
resource: '/foos',
findAll: 'GET /bars',
update: {
url: '/foos/{id}',
type: 'PUT'
},
create: function() {
return can.ajax({
url: '/foos',
type: 'POST'
});
}
}, {});
var alldfd = Thing.findAll(),
onedfd = Thing.findOne({ id: 0 }),
postdfd = new Thing().save();
stop();
can.when(alldfd, onedfd, postdfd)
.then(function(things, thing, newthing) {
equal(things.length, 1, 'findAll override called');
equal(thing.name, 'foo', 'resource findOne called');
equal(newthing.id, 1, 'post override called with function');
newthing.save(function(res) {
ok(res.updated, 'put override called with object');
start();
});
})
.fail(function() {
ok(false, 'override request failed');
start();
});
});
test("findAll not called if List constructor argument is deferred (#1074)", function() {
var count = 0;
var Foo = can.Model.extend({
findAll: function() {
count++;
return can.Deferred();
}
}, {});
new Foo.List(Foo.findAll());
equal(count, 1, "findAll called only once.");
});
test("static methods do not get overwritten with resource property set (#1309)", function() {
var Base = can.Model.extend({
resource: '/path',
findOne: function() {
var dfd = can.Deferred();
dfd.resolve({
text: 'Base findAll'
});
return dfd;
}
}, {});
stop();
Base.findOne({}).then(function(model) {
ok(model instanceof Base);
deepEqual(model.attr(), {
text: 'Base findAll'
});
start();
}, function() {
ok(false, 'Failed handler should not be called.');
});
});
test("parseModels does not get overwritten if already implemented in base class (#1246, #1272)", 5, function() {
var Base = can.Model.extend({
findOne: function() {
var dfd = can.Deferred();
dfd.resolve({
text: 'Base findOne'
});
return dfd;
},
parseModel: function(attributes) {
deepEqual(attributes, {
text: 'Base findOne'
}, 'parseModel called');
attributes.parsed = true;
return attributes;
}
}, {});
var Extended = Base.extend({}, {});
stop();
Extended.findOne({}).then(function(model) {
ok(model instanceof Base);
ok(model instanceof Extended);
deepEqual(model.attr(), {
text: 'Base findOne',
parsed: true
});
start();
}, function() {
ok(false, 'Failed handler should not be called.');
});
var Third = Extended.extend({
findOne: function() {
var dfd = can.Deferred();
dfd.resolve({
nested: {
text: 'Third findOne'
}
});
return dfd;
},
parseModel: 'nested'
}, {});
Third.findOne({}).then(function(model) {
equal(model.attr('text'), 'Third findOne', 'correct findOne used');
});
});
test("Models with no id (undefined or null) are not placed in store (#1358)", function(){
var MyStandardModel = can.Model.extend({});
var MyCustomModel = can.Model.extend({id:"ID"}, {});
var myID = null;
var instanceNull = new MyStandardModel ({id:myID});
var instanceUndefined = new MyStandardModel ({});
var instanceCustom = new MyCustomModel({ID:myID});
instanceNull.bind('change', function(){});
instanceUndefined.bind('change', function(){});
instanceCustom.bind('change', function(){});
ok(typeof MyStandardModel.store[instanceNull.id] === "undefined", "Model should not be added to store when id is null");
ok(typeof MyStandardModel.store[instanceUndefined.id] === "undefined", "Model should not be added to store when id is undefined");
ok(typeof MyCustomModel.store[instanceCustom[instanceCustom.constructor.id]] === "undefined", "Model should not be added to store when id is null");
});
test("Models should be removed from store when instance.removeAttr('id') is called", function(){
var Task = can.Model.extend({},{});
var t1 = new Task({id: 1, name: "MyTask"});
t1.bind('change', function(){});
ok(Task.store[t1.id].name === "MyTask", "Model should be in store");
t1.removeAttr("id");
ok(typeof Task.store[t1.id] === "undefined", "Model should be removed from store when `id` is removed");
});
});