can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
692 lines (688 loc) • 16.2 kB
JavaScript
/*jshint undef:false,unused:false*/
steal("can/map/attributes", "can/model", "can/util/fixture", "can/test", "steal-qunit", function () {
QUnit.module('can/map/attributes');
test('literal converters and serializes', function () {
can.Map('Task1', {
attributes: {
createdAt: 'date'
},
convert: {
date: function (d) {
var months = [
'jan',
'feb',
'mar'
];
return months[d.getMonth()];
}
},
serialize: {
date: function (d) {
var months = {
'jan': 0,
'feb': 1,
'mar': 2
};
return months[d];
}
}
}, {});
can.Map('Task2', {
attributes: {
createdAt: 'date'
},
convert: {
date: function (d) {
var months = [
'apr',
'may',
'jun'
];
return months[d.getMonth()];
}
},
serialize: {
date: function (d) {
var months = {
'apr': 0,
'may': 1,
'jun': 2
};
return months[d];
}
}
}, {});
var d = new Date();
d.setMonth(1, 1);
var task1 = new Task1({
createdAt: d,
name: 'Task1'
});
d.setMonth(2, 1);
var task2 = new Task2({
createdAt: d,
name: 'Task2'
});
equal(task1.createdAt, 'feb', 'Task1 model convert');
equal(task2.createdAt, 'jun', 'Task2 model convert');
equal(task1.serialize()
.createdAt, 1, 'Task1 model serialize');
equal(task2.serialize()
.createdAt, 2, 'Task2 model serialize');
equal(task1.serialize()
.name, 'Task1', 'Task1 model default serialized');
equal(task2.serialize()
.name, 'Task2', 'Task2 model default serialized');
});
var makeClasses = function () {
can.Map('AttrTest.Person', {
serialize: function () {
return 'My name is ' + this.name;
}
});
can.Map('AttrTest.Loan');
can.Map('AttrTest.Issue');
AttrTest.Person.model = function (data) {
return new this(data);
};
AttrTest.Loan.models = function (data) {
return can.map(data, function (l) {
return new AttrTest.Loan(l);
});
};
AttrTest.Issue.models = function (data) {
return can.map(data, function (l) {
return new AttrTest.Issue(l);
});
};
can.Map('AttrTest.Customer', {
attributes: {
person: 'AttrTest.Person.model',
loans: 'AttrTest.Loan.models',
issues: 'AttrTest.Issue.models'
}
}, {});
};
test('default converters', function () {
var num = 1318541064012;
equal(can.Map.convert.date(num)
.getTime(), num, 'converted to a date with a number');
var str = 'Dec 25, 1995';
ok(can.Map.convert.date(str) instanceof Date, 'converted to a date with a string');
});
test('basic observe associations', function () {
makeClasses();
var c = new AttrTest.Customer({
person: {
id: 1,
name: 'Justin'
},
issues: [],
loans: [{
amount: 1000,
id: 2
}, {
amount: 19999,
id: 3
}]
});
equal(c.person.name, 'Justin', 'association present');
equal(c.person.constructor, AttrTest.Person, 'belongs to association typed');
equal(c.issues.length, 0);
equal(c.loans.length, 2);
equal(c.loans[0].constructor, AttrTest.Loan);
});
test('single seralize w/ attr name', function () {
var Breakfast = can.Map({
attributes: {
time: 'date',
name: 'default'
},
serialize: {
date: function (d) {
return d.getTime();
}
}
}, {});
var time = new Date();
var b = new Breakfast({
time: time,
name: 'eggs'
});
equal(b.serialize('time'), time.getTime());
ok(b.serialize());
});
test('defaults', function () {
var Zelda = can.Map({
defaults: {
sword: 'Wooden Sword',
shield: false,
hearts: 3,
rupees: 0
}
}, {});
var link = new Zelda({
rupees: 255
});
equal(link.attr('sword'), 'Wooden Sword');
equal(link.attr('rupees'), 255);
});
test('nested model attr', function () {
can.Model('NestedAttrTest.User', {}, {});
can.Model('NestedAttrTest.Task', {
attributes: {
owner: 'NestedAttrTest.User.model'
}
}, {});
can.Model('NestedAttrTest.Project', {
attributes: {
creator: 'NestedAttrTest.User.model'
}
}, {});
var michael = NestedAttrTest.User.model({
id: 17,
name: 'Michael'
});
var amy = NestedAttrTest.User.model({
id: 29,
name: 'Amy'
});
michael.bind('foo', function () {});
amy.bind('foo', function () {});
var task = NestedAttrTest.Task.model({
id: 1,
name: 'Do it!',
owner: {
id: 17
}
});
var project = NestedAttrTest.Project.model({
id: 1,
name: 'Get Things Done',
creator: {
id: 17
}
});
task.bind('foo', function () {});
project.bind('foo', function () {});
equal(task.attr('owner.name'), 'Michael', 'owner hash correctly modeled');
equal(project.attr('creator.name'), 'Michael', 'creator hash correctly modeled');
task.attr({
owner: {
id: 29,
name: 'Amy'
}
});
equal(task.attr('owner.name'), 'Amy', 'Task correctly updated to Amy user model');
equal(task.attr('owner.id'), 29, 'Task correctly updated to Amy user model');
equal(project.attr('creator.name'), 'Michael', 'Project creator should still be Michael');
equal(project.attr('creator.id'), 17, 'Project creator should still be Michael');
equal(NestedAttrTest.User.store[17].id, 17, 'The model store should still have Michael associated by his id');
});
test('attr() should respect convert functions for lists when updating', function () {
can.Model('ListTest.User', {}, {});
can.Model.List('ListTest.User.List', {}, {});
can.Model('ListTest.Task', {
attributes: {
project: 'ListTest.Project.model'
}
}, {});
can.Model('ListTest.Project', {
attributes: {
members: 'ListTest.User.models'
}
}, {});
var task = ListTest.Task.model({
id: 1,
name: 'Do it!',
project: {
id: 789,
name: 'Get stuff done',
members: []
}
});
equal(task.project.members instanceof ListTest.User.List, true, 'the list is a User List');
task.attr({
id: 1,
project: {
id: 789,
members: [{
id: 72,
name: 'Andy'
}, {
id: 74,
name: 'Michael'
}]
}
});
equal(task.project.members instanceof ListTest.User.List, true, 'the list is a User List');
equal(task.project.members.length, 2, 'The members were added');
equal(task.project.members[0] instanceof ListTest.User, true, 'The members was converted to a model object');
equal(task.project.members[1] instanceof ListTest.User, true, 'The user was converted to a model object');
});
test('plugin passes old value to converter', 2, function () {
var Ob = can.Map('AttrOldVal', {
oldVal: function (val, oldVal) {
if (val === 'first') {
ok(!oldVal, 'First time does not have an old value');
}
if (val === 'second') {
equal(oldVal, 'first', 'Old value is correct');
}
return val;
},
attributes: {
test: 'AttrOldVal.oldVal'
}
}, {});
var o = new Ob({
test: 'first'
});
o.attr('test', 'second');
});
test('attr does not blow away old observable when going from empty to having items (#160)', function () {
can.Model('EmptyListTest.User', {}, {});
can.Model.List('EmptyListTest.User.List', {}, {});
can.Model('EmptyListTest.Project', {
attributes: {
members: 'EmptyListTest.User.models'
}
}, {});
var project = EmptyListTest.Project.model({
id: 789,
members: []
});
var oldCid = project.attr('members')
._cid;
project.attr({
members: [{
id: 1,
name: 'bob'
}]
});
deepEqual(project.attr('members')
._cid, oldCid, 'should be the same observe, so that views bound to the old one get updates');
equal(project.attr('members')
.length, 1, 'list should have bob in it');
});
test('Default converters and boolean fix (#247)', function () {
var MyObserve = can.Map({
attributes: {
enabled: 'boolean',
time: 'date',
age: 'number'
}
}, {}),
obs = new MyObserve({
enabled: 'false',
time: 1358980553275,
age: '26'
});
deepEqual(obs.attr('enabled'), false, 'Attribute got converted to boolean false');
obs.attr('enabled', 'true');
deepEqual(obs.attr('enabled'), true, 'Attribute got converted to boolean true');
obs.attr('enabled', '0');
deepEqual(obs.attr('enabled'), false, 'Attribute got converted to boolean false');
obs.attr('enabled', '1');
deepEqual(obs.attr('enabled'), true, 'Attribute got converted to boolean true');
deepEqual(obs.attr('age'), 26, 'Age converted from string to number');
ok(obs.attr('time') instanceof Date, 'Attribute is a date');
});
test('Nested converters called twice (#174)', function () {
OtherThing = can.Model({
attributes: {
score: 'capacity'
},
convert: {
capacity: function (val) {
return val * 10;
}
}
}, {});
Thing = can.Model({
attributes: {
otherThing: 'OtherThing.model'
},
findOne: 'GET /things/{id}'
}, {});
var t = new Thing({
'name': 'My Thing',
'otherThing': {
'score': 1
},
'id': 'ALLCACHES'
});
t.attr({
'otherThing': {
'score': 2
},
'id': 'ALLCACHES'
});
equal(t.attr('otherThing.score'), 20, 'converter called correctly');
});
// tests that the workaround listed in #208 works with the correct call signature and data
test('Nested converters called with merged data', function () {
var MyObserve = can.Map({
attributes: {
nested: 'nested'
},
convert: {
nested: function (data, oldVal) {
if (oldVal instanceof MyObserve) {
return oldVal.attr(data);
}
return new MyObserve(data);
}
}
}, {});
var obs = new MyObserve({
nested: {
name: 'foo',
count: 1
}
});
var nested = obs.attr('nested');
obs.attr({
nested: {
count: 2
}
});
ok(nested === obs.attr('nested'), 'same object');
});
test('Recursive attributes', function () {
var Player = can.Model({
attributes: {
team: 'Team.model'
},
findAll: 'GET /players',
destroy: 'DELETE /players/{id}'
}, {});
var Team = can.Model({
attributes: {
players: 'Player.models'
},
findAll: 'GET /teams',
destroy: 'DELETE /teams/{id}'
}, {});
var PLAYERS = [{
'id': 1,
'name': 'Ryan Braun',
'number': 8,
'team': {
'id': 25,
'name': 'Brewers',
'players': [{
'id': 1,
'name': 'Ryan Braun',
'number': 8
}, {
'id': 2,
'name': 'Yovani Gallardo',
'number': 55
}, {
'id': 3,
'name': 'Jean Segura',
'number': 12
}]
}
}, {
'id': 2,
'name': 'Yovani Gallardo',
'number': 55,
'team': {
'id': 25,
'name': 'Brewers',
'players': [{
'id': 1,
'name': 'Ryan Braun',
'number': 8
}, {
'id': 2,
'name': 'Yovani Gallardo',
'number': 55
}, {
'id': 3,
'name': 'Jean Segura',
'number': 12
}]
}
}, {
'id': 3,
'name': 'Jean Segura',
'number': 12,
'team': {
'id': 25,
'name': 'Brewers',
'players': [{
'id': 1,
'name': 'Ryan Braun',
'number': 8
}, {
'id': 2,
'name': 'Yovani Gallardo',
'number': 55
}, {
'id': 3,
'name': 'Jean Segura',
'number': 12
}]
}
}];
can.fixture('GET /players', function () {
return PLAYERS;
});
can.fixture('DELETE /players/{id}', function (req) {
var id = req && req.data && req.data.id;
if (id !== undefined) {
for (var i = 0, player; player = PLAYERS[i]; i++) {
if (player.id + '' === id + '') {
PLAYERS.splice(i, 1);
return id;
}
}
}
return false;
});
stop();
can.when(Player.findAll())
.then(function (players) {
equal(players.length, 3, 'Players loaded');
can.when(players[2].destroy())
.then(function (g) {
can.when(Player.findAll())
.then(function (players) {
equal(players.length, 2, 'One player destroyed');
start();
});
});
});
});
test('store instances (#457)', function () {
var Game = can.Model.extend({
attributes: {
players: 'players'
},
convert: {
players: function (data) {
return Player.models(data);
}
},
findOne: 'GET /games/{id}'
}, {});
var Player = can.Model.extend({
attributes: {
games: 'games'
},
convert: {
'games': function (data) {
return Game.models(data);
}
}
}, {});
can.Model._reqs++;
var game = Game.model({
'id': '1',
'name': 'Fantasy Baseball',
'league': 'League of Kings',
'players': [{
'id': '55',
'name': 'Malamonsters',
'games': [{
'id': '1',
'name': 'Fantasy Baseball',
'league': 'League of Kings'
}]
}]
});
var mismatchFound = false;
game.attr('players')
.each(function (p) {
p.attr('games')
.each(function (g) {
if (game.attr('id') === g.attr('id') && game._cid !== g._cid) {
mismatchFound = true;
}
});
});
equal(mismatchFound, false, 'Model instances match');
can.Model._reqs--;
});
test('Converter functions', function () {
var Value = can.Map.extend({
attributes: {
value: function (orig) {
return orig * 100;
}
}
}, {});
var testValue = new Value({
value: 0.823
});
equal(testValue.attr('value'), 82.3, 'Value got multiplied');
});
test('Convert can.Map constructs passed as attributes (#293)', 4, function () {
var Sword = can.Map.extend({
getPower: function () {
return this.attr('power') * 100;
}
});
var Level = can.Map.extend({
getName: function () {
return 'Level: ' + this.attr('name');
}
});
var Zelda = can.Map.extend({
attributes: {
sword: Sword,
levelsCompleted: Level
}
}, {});
var link = new Zelda({
sword: {
name: 'Wooden Sword',
power: 0.2
},
levelsCompleted: [{
id: 1,
name: 'Aquamentus'
}, {
id: 2,
name: 'Dodongo'
}]
});
ok(link.attr('sword') instanceof Sword, 'Sword got converted');
equal(link.attr('sword')
.getPower(), 20, 'Got sword power!');
ok(link.attr('levelsCompleted') instanceof Level.List, 'Got a level list');
equal(link.attr('levelsCompleted.0')
.getName(), 'Level: Aquamentus', 'Entry got converted as well');
});
test('Convert can.Model using .model and .models (#293)', 5, function () {
var Sword = can.Model.extend({
findAll: 'GET /swords',
model: function (data) {
data.test = 'Used .model';
return new this(data);
}
}, {
getPower: function () {
return this.attr('power') * 100;
}
});
var Level = can.Model.extend({
findAll: 'GET /levels',
models: function (array) {
can.each(array, function (current, index) {
current.index = index;
});
return can.Model.models.call(this, array);
}
}, {});
var Zelda = can.Model.extend({
attributes: {
sword: Sword,
levelsCompleted: Level
}
}, {});
var link = Zelda.model({
sword: {
name: 'Wooden Sword',
power: 0.2
},
levelsCompleted: [{
id: 1,
name: 'Aquamentus'
}, {
id: 2,
name: 'Dodongo'
}]
});
ok(link.attr('sword') instanceof Sword, 'Sword got converted');
equal(link.attr('sword')
.getPower(), 20, 'Got sword power!');
equal(link.attr('sword.test'), 'Used .model', 'Data ran through Sword.model');
ok(link.attr('levelsCompleted') instanceof Level.List, 'Got a level list');
equal(link.attr('levelsCompleted.1.index'), 1, 'Data ran through Level.models');
});
test('Maximum call stack size exceeded with global models (#476)', function () {
stop();
var Character = can.Model.extend({
attributes: {
game: function () {
return Game.model.apply(Game, arguments);
}
}
}, {});
window.Game = can.Model.extend({
attributes: {
characters: function () {
return Character.models.apply(Character, arguments);
}
},
findOne: function () {
var dfd = can.Deferred();
setTimeout(function () {
dfd.resolve({
'id': 'LOZ',
'name': 'The Legend of Zelda',
'characters': [{
'id': '1',
'name': 'Link',
'game': {
'id': 'LOZ'
}
}]
});
}, 100);
return dfd;
}
}, {});
window.Game.findOne({
id: 'LOZ'
})
.then(function (g) {
ok(g.serialize(), 'No error serializing the object');
start();
});
});
});