1
YUI.add('model-test', function (Y) {
3
var ArrayAssert = Y.ArrayAssert,
5
ObjectAssert = Y.ObjectAssert,
10
// -- Global Suite -------------------------------------------------------------
11
suite = Y.AppTestSuite || (Y.AppTestSuite = new Y.Test.Suite('App Framework'));
13
// -- Model Suite --------------------------------------------------------------
14
modelSuite = new Y.Test.Suite('Model');
16
// -- Model: Lifecycle ---------------------------------------------------------
17
modelSuite.add(new Y.Test.Case({
20
'destroy() should destroy the model instance': function () {
21
var model = new Y.Model();
23
model.sync = function () {
24
Assert.fail('sync should not be called unless the model is being deleted');
27
Assert.isFalse(model.get('destroyed'));
28
Assert.areSame(model, model.destroy(), 'destroy() should be chainable');
29
Assert.isTrue(model.get('destroyed'));
32
'destroy() should call a callback if provided as the only arg': function () {
34
model = new Y.Model();
41
model.destroy(mock.callback);
45
'destroy() should call a callback if provided as the second arg': function () {
47
model = new Y.Model();
54
model.destroy({}, mock.callback);
58
'destroy() should delete the model if the `remove` option is truthy': function () {
61
model = new Y.Model();
68
model.sync = function (action, options, callback) {
71
Assert.areSame('delete', action, 'sync action should be "delete"');
72
Assert.isObject(options, 'options should be an object');
73
Assert.isTrue(options.remove, 'options.delete should be true');
74
Assert.isFunction(callback, 'callback should be a function');
79
model.destroy({remove: true}, mock.callback);
83
'destroy() should remove the model from all lists': function () {
84
var model = new Y.Model(),
85
listOne = new Y.ModelList(),
86
listTwo = new Y.ModelList(),
87
listThree = new Y.ModelList();
93
Assert.areSame(1, listOne.size(), 'model should be added to list one');
94
Assert.areSame(1, listTwo.size(), 'model should be added to list two');
95
Assert.areSame(1, listThree.size(), 'model should be added to list three');
99
Assert.areSame(0, listOne.size(), 'model should be removed from list one');
100
Assert.areSame(0, listTwo.size(), 'model should be removed from list two');
101
Assert.areSame(0, listThree.size(), 'model should be removed from list three');
105
// -- Model: Attributes and Properties -----------------------------------------
106
modelSuite.add(new Y.Test.Case({
107
name: 'Attributes and Properties',
110
this.TestModel = Y.Base.create('testModel', Y.Model, [], {
111
idAttribute: 'customId'
114
customId: {value: ''},
120
tearDown: function () {
121
delete this.TestModel;
124
'Attributes should be settable at instantiation time': function () {
125
var model = new this.TestModel({foo: 'foo'});
126
Assert.areSame('foo', model.get('foo'));
129
'Models should allow ad-hoc attributes': function () {
130
var created = new Date(),
132
model = new Y.Model({
141
Assert.areSame('foo', model.get('foo'), 'ad-hoc foo attribute should be set');
142
Assert.areSame('bar', model.get('bar.a'), 'ad-hoc bar attribute should be set');
143
Assert.areSame('baz', model.get('baz')[0], 'ad-hoc baz attribute should be set');
144
Assert.isNull(model.get('quux'), 'ad-hoc quux attribute should be set');
145
Assert.areSame(0, model.get('zero'), 'ad-hoc zero attribute should be set');
146
Assert.areSame(created, model.get('created'), 'ad-hoc created attribute should be set');
148
ObjectAssert.ownsKeys(['foo', 'bar', 'baz', 'quux', 'zero', 'created'], model.getAttrs(), 'ad-hoc attributes should be returned by getAttrs()');
151
'Custom id attribute should be settable at instantiation time': function () {
154
// We need to set and get the id and customId attributes in various
155
// orders to ensure there are no issues due to the attributes being
158
model = new this.TestModel({customId: 'foo'});
159
Assert.areSame('foo', model.get('customId'));
160
Assert.areSame('foo', model.get('id'));
162
model = new this.TestModel({customId: 'foo'});
163
Assert.areSame('foo', model.get('id'));
164
Assert.areSame('foo', model.get('customId'));
166
model = new this.TestModel({id: 'foo'});
167
Assert.areSame('foo', model.get('customId'));
168
Assert.areSame('foo', model.get('id'));
170
model = new this.TestModel({id: 'foo'});
171
Assert.areSame('foo', model.get('id'));
172
Assert.areSame('foo', model.get('customId'));
175
'`_isYUIModel` property should be true': function () {
176
var model = new this.TestModel();
177
Assert.isTrue(model._isYUIModel);
180
'`id` attribute should be an alias for the custom id attribute': function () {
182
model = new this.TestModel();
184
model.on('change', function (e) {
187
Assert.areSame('foo', e.changed.customId.newVal);
188
Assert.areSame('foo', e.changed.id.newVal);
191
model.set('id', 'foo');
193
Assert.areSame(1, calls);
196
'`changed` property should be a hash of attributes that have changed since last save() or load()': function () {
197
var model = new this.TestModel();
199
Assert.isObject(model.changed);
200
ObjectAssert.ownsNoKeys(model.changed);
202
model.set('foo', 'foo');
203
Assert.areSame('foo', model.changed.foo);
205
model.setAttrs({foo: 'bar', bar: 'baz'});
206
ObjectAssert.areEqual({foo: 'bar', bar: 'baz'}, model.changed);
209
ObjectAssert.ownsNoKeys(model.changed);
211
model.set('foo', 'foo');
213
ObjectAssert.ownsNoKeys(model.changed);
216
'clientId attribute should be automatically generated': function () {
217
var model = new Y.Model();
219
Assert.isString(model.get('clientId'));
220
Assert.isTrue(!!model.get('clientId'));
223
'`lastChange` property should contain attributes that changed in the last `change` event': function () {
224
var model = new this.TestModel();
226
Assert.isObject(model.lastChange);
227
ObjectAssert.ownsNoKeys(model.lastChange);
229
model.set('foo', 'foo');
230
Assert.areSame(1, Y.Object.size(model.lastChange));
231
ObjectAssert.ownsKeys(['newVal', 'prevVal', 'src'], model.lastChange.foo);
232
Assert.areSame('', model.lastChange.foo.prevVal);
233
Assert.areSame('foo', model.lastChange.foo.newVal);
234
Assert.isNull(model.lastChange.foo.src);
236
model.set('bar', 'bar', {src: 'test'});
237
Assert.areSame(1, Y.Object.size(model.lastChange));
238
Assert.areSame('test', model.lastChange.bar.src);
240
model.set('foo', 'bar', {silent: true});
241
Assert.areSame(1, Y.Object.size(model.lastChange));
242
Assert.areSame('bar', model.lastChange.foo.newVal);
245
'`lists` property should be an array of ModelList instances that contain this model': function () {
247
model = new this.TestModel(),
250
new Y.ModelList({model: this.TestModel}),
251
new Y.ModelList({model: this.TestModel})
254
Assert.isArray(model.lists);
256
function onChange() {
260
lists[0].on('*:change', onChange);
261
lists[1].on('*:change', onChange);
266
ArrayAssert.itemsAreSame(lists, model.lists);
268
model.set('foo', 'foo');
270
Assert.areSame(2, calls);
274
// -- Model: Methods -----------------------------------------------------------
275
modelSuite.add(new Y.Test.Case({
279
this.TestModel = Y.Base.create('testModel', Y.Model, [], {}, {
287
tearDown: function () {
288
delete this.TestModel;
291
'generateClientId() should generate a unique client id': function () {
292
var model = new this.TestModel(),
293
firstId = model.generateClientId(),
294
secondId = model.generateClientId();
296
Assert.isString(firstId);
297
Assert.areNotSame(firstId, secondId);
298
Assert.isTrue(firstId.indexOf(this.TestModel.NAME) === 0);
301
'getAsHTML() should return an HTML-escaped attribute value': function () {
302
var value = '<div id="foo">hello!</div>',
303
model = new this.TestModel({foo: value});
305
Assert.areSame(Y.Escape.html(value), model.getAsHTML('foo'));
308
'getAsURL() should return a URL-encoded attribute value': function () {
309
var value = 'foo & bar = baz',
310
model = new this.TestModel({foo: value});
312
Assert.areSame(encodeURIComponent(value), model.getAsURL('foo'));
315
'isModified() should return true if the model is new': function () {
316
var model = new this.TestModel();
317
Assert.isTrue(model.isModified());
319
model = new this.TestModel({id: 'foo'});
320
Assert.isFalse(model.isModified());
323
'isModified() should return true if the model has changed since it was last saved': function () {
324
var model = new this.TestModel({id: 'foo'});
325
Assert.isFalse(model.isModified());
327
model.set('foo', 'bar');
328
Assert.isTrue(model.isModified());
331
Assert.isFalse(model.isModified());
334
'isNew() should return true if the model is new': function () {
335
var model = new this.TestModel();
336
Assert.isTrue(model.isNew());
338
model = new this.TestModel({id: 'foo'});
339
Assert.isFalse(model.isNew());
341
model = new this.TestModel({id: 0});
342
Assert.isFalse(model.isNew());
345
'load() should delegate to sync()': function () {
347
model = new this.TestModel(),
350
model.sync = function (action, options, callback) {
353
Assert.areSame('read', action);
354
Assert.areSame(opts, options);
355
Assert.isFunction(callback);
361
Assert.areSame(1, calls);
364
'load() should reset this.changed when loading succeeds': function () {
365
var model = new this.TestModel();
367
model.set('foo', 'bar');
368
Assert.areSame(1, Y.Object.size(model.changed));
371
Assert.areSame(0, Y.Object.size(model.changed));
374
'load() should be chainable and should call the callback if one was provided': function () {
376
model = new this.TestModel();
378
Assert.areSame(model, model.load());
379
Assert.areSame(model, model.load({}));
381
Assert.areSame(model, model.load(function (err) {
383
Assert.isUndefined(err);
386
Assert.areSame(model, model.load({}, function () {
390
Assert.areSame(2, calls);
393
'parse() should parse a JSON string and return an object': function () {
394
var model = new this.TestModel(),
395
response = model.parse('{"foo": "bar"}');
397
Assert.isObject(response);
398
Assert.areSame('bar', response.foo);
401
'parse() should not try to parse non-strings': function () {
402
var model = new this.TestModel(),
403
array = ['foo', 'bar'],
404
object = {foo: 'bar'};
406
Assert.areSame(array, model.parse(array));
407
Assert.areSame(object, model.parse(object));
410
'save() should delegate to sync()': function () {
412
model = new this.TestModel(),
415
model.sync = function (action, options, callback) {
418
Assert.areSame('create', action);
419
Assert.areSame(opts, options);
420
Assert.isFunction(callback);
422
// Give the model an id so it will no longer be new.
423
callback(null, {id: 'foo'});
428
Assert.areSame('foo', model.get('id'), "model id should be updated after save");
430
model.sync = function (action) {
432
Assert.areSame('update', action);
437
Assert.areSame(2, calls);
440
'save() should reset this.changed when saving succeeds': function () {
441
var model = new this.TestModel();
443
model.set('foo', 'bar');
444
Assert.areSame(1, Y.Object.size(model.changed));
447
Assert.areSame(0, Y.Object.size(model.changed));
450
'save() should be chainable and should call the callback if one was provided': function () {
452
model = new this.TestModel();
454
Assert.areSame(model, model.save());
455
Assert.areSame(model, model.save({}));
457
Assert.areSame(model, model.save(function (err) {
459
Assert.isUndefined(err);
462
Assert.areSame(model, model.save({}, function () {
466
Assert.areSame(2, calls);
469
'set() should set the value of a single attribute': function () {
470
var model = new this.TestModel();
472
Assert.areSame('', model.get('foo'));
473
Assert.areSame(model, model.set('foo', 'bar'), 'set() should be chainable');
474
Assert.areSame('bar', model.get('foo'));
477
'setAttrs() should set the values of multiple attributes': function () {
478
var model = new this.TestModel();
480
Assert.areSame('', model.get('foo'));
481
Assert.areSame('', model.get('bar'));
482
Assert.areSame(model, model.setAttrs({foo: 'foo', bar: 'bar'}), 'setAttrs() should be chainable');
483
Assert.areSame('foo', model.get('foo'));
484
Assert.areSame('bar', model.get('bar'));
487
'sync() should just call the supplied callback by default': function () {
489
model = new this.TestModel();
491
model.sync(function (err) {
493
Assert.isUndefined(err);
496
Assert.areSame(1, calls);
499
"toJSON() should return a copy of the model's attributes, minus excluded ones": function () {
500
var attrs = {id: 'id', foo: 'foo', bar: 'bar'},
501
model = new this.TestModel(attrs),
502
CustomTestModel, json;
504
json = model.toJSON();
505
Assert.areSame(3, Y.Object.size(json));
506
ObjectAssert.ownsKeys(['id', 'foo', 'bar'], json);
507
ObjectAssert.areEqual(attrs, json);
509
// When there's a custom id attribute, the 'id' attribute should be
511
CustomTestModel = Y.Base.create('customTestModel', Y.Model, [], {
512
idAttribute: 'customId'
515
customId: {value: ''},
521
attrs = {customId: 'id', foo: 'foo', bar: 'bar'};
522
model = new CustomTestModel(attrs);
523
json = model.toJSON();
525
Assert.areSame(3, Y.Object.size(json));
526
ObjectAssert.ownsKeys(['customId', 'foo', 'bar'], json);
527
ObjectAssert.areEqual(attrs, json);
530
'undo() should revert the previous change to the model': function () {
531
var attrs = {id: 'id', foo: 'foo', bar: 'bar'},
532
model = new this.TestModel(attrs);
534
ObjectAssert.areEqual(attrs, model.toJSON());
536
model.setAttrs({foo: 'moo', bar: 'quux'});
537
ObjectAssert.areEqual({id: 'id', foo: 'moo', bar: 'quux'}, model.toJSON());
539
Assert.areSame(model, model.undo(), 'undo() should be chainable');
540
ObjectAssert.areEqual(attrs, model.toJSON());
543
'undo() should revert only the specified attributes when attributes are specified': function () {
544
var model = new this.TestModel({id: 'id', foo: 'foo', bar: 'bar'});
546
model.setAttrs({foo: 'moo', bar: 'quux'});
549
ObjectAssert.areEqual({id: 'id', foo: 'foo', bar: 'quux'}, model.toJSON());
552
'undo() should pass options to setAttrs()': function () {
554
model = new this.TestModel({id: 'id', foo: 'foo', bar: 'bar'});
556
model.setAttrs({foo: 'moo', bar: 'quux'});
558
model.on('change', function (e) {
560
Assert.areSame('test', e.changed.foo.src);
563
model.undo(null, {src: 'test'});
564
Assert.areSame(1, calls);
567
'undo() should do nothing when there is no previous change to revert': function () {
568
var model = new this.TestModel();
570
model.on('change', function () {
571
Assert.fail('`change` should not be called');
577
'validate() should only be called on save()': function () {
579
model = new this.TestModel();
581
model.validate = function (attrs, callback) {
583
Y.ObjectAssert.areEqual(model.toJSON(), attrs);
587
model.set('foo', 'bar');
588
model.set('foo', 'baz');
591
Assert.areSame(1, calls);
594
'a validation failure should abort a save() call': function () {
597
model = new this.TestModel(),
600
model.validate = function (attrs, callback) {
602
callback('OMG invalid!');
605
model.sync = function () {
606
Assert.fail('sync() should not be called on validation failure');
609
model.on('error', function (e) {
611
Assert.areSame('OMG invalid!', e.error);
612
Assert.areSame('validate', e.src);
615
model.save(function (err, res) {
617
Assert.areSame('OMG invalid!', err);
618
Assert.isUndefined(res);
621
Assert.areSame(1, calls);
622
Assert.areSame(1, saveCallbacks);
623
Assert.areSame(1, errors);
626
'validate() should be backwards compatible with the 3.4.x synchronous style': function () {
629
model = new this.TestModel();
631
model.on('error', function (e) {
636
model.on('save', function (e) {
640
model.validate = function (attrs) {
641
if (attrs.foo !== 'bar') {
646
model.set('foo', 'bar');
648
Assert.areSame(0, errors);
649
Assert.areSame(1, saves);
651
model.set('foo', 'baz');
653
Assert.areSame(1, errors);
654
Assert.areSame(1, saves);
658
// -- Model: Events ------------------------------------------------------------
659
modelSuite.add(new Y.Test.Case({
663
this.TestModel = Y.Base.create('testModel', Y.Model, [], {}, {
672
tearDown: function () {
673
delete this.TestModel;
676
'`change` event should contain coalesced attribute changes': function () {
678
model = new this.TestModel();
680
model.on('change', function (e) {
683
ObjectAssert.ownsKeys(['foo', 'bar'], e.changed);
684
Assert.areSame(2, Y.Object.size(e.changed));
685
ObjectAssert.ownsKeys(['newVal', 'prevVal', 'src'], e.changed.foo);
686
ObjectAssert.ownsKeys(['newVal', 'prevVal', 'src'], e.changed.bar);
687
Assert.areSame('foo', e.changed.foo.newVal);
688
Assert.areSame('', e.changed.foo.prevVal);
689
Assert.areSame('bar', e.changed.bar.newVal);
690
Assert.areSame('', e.changed.bar.prevVal);
691
Assert.areSame('test', e.changed.foo.src);
692
Assert.areSame('test', e.changed.bar.src);
700
Assert.areSame(1, calls);
703
'`change` event should not fire when the _silent_ option is truthy': function () {
704
var model = new this.TestModel();
706
model.on('change', function (e) {
707
Assert.fail('`change` should not fire');
710
model.set('foo', 'bar', {silent: true});
711
model.setAttrs({bar: 'baz'}, {silent: true});
714
'`change` event facade should contain options passed to set()/setAttrs()': function () {
716
model = new this.TestModel();
718
model.on('change', function (e) {
721
Assert.areSame(e.src, 'test');
722
Assert.areSame(e.foo, 'bar');
728
}, {src: 'test', foo: 'bar'});
730
model.set('foo', 'bar', {
735
Assert.areSame(2, calls);
738
'`error` event should fire when validation fails': function () {
740
model = new this.TestModel();
742
model.validate = function (hash, callback) {
743
callback('ERROR. ERROR. DOES NOT COMPUTE.');
746
model.on('error', function (e) {
749
Assert.areSame('validate', e.src);
750
ObjectAssert.ownsKey('foo', e.attributes);
751
Assert.areSame('bar', e.attributes.foo);
752
Assert.areSame('ERROR. ERROR. DOES NOT COMPUTE.', e.error);
755
model.set('foo', 'bar');
758
Assert.areSame(1, calls);
761
'`error` event should fire when parsing fails': function () {
763
model = new this.TestModel();
765
model.on('error', function (e) {
768
Assert.areSame('parse', e.src);
769
Y.assert(e.error instanceof Error);
770
Assert.areSame('moo', e.response);
775
Assert.areSame(1, calls);
778
'`error` event should fire when a load operation fails': function () {
780
model = new this.TestModel();
782
model.on('error', function (e) {
785
Assert.areSame('load', e.src);
786
Assert.areSame('foo', e.error);
787
Assert.areSame('{"error": true}', e.response);
788
Assert.isObject(e.options);
791
model.sync = function (action, options, callback) {
792
callback('foo', '{"error": true}');
797
Assert.areSame(1, calls);
800
'`error` event should fire when a save operation fails': function () {
802
model = new this.TestModel();
804
model.on('error', function (e) {
807
Assert.areSame('save', e.src);
808
Assert.areSame('foo', e.error);
809
Assert.areSame('{"error": true}', e.response);
810
Assert.isObject(e.options);
813
model.sync = function (action, options, callback) {
814
callback('foo', '{"error": true}');
819
Assert.areSame(1, calls);
822
'`load` event should fire after a successful load operation': function () {
824
model = new this.TestModel();
826
model.on('load', function (e) {
829
Assert.areSame('{"foo": "bar"}', e.response);
830
Assert.isObject(e.options);
831
Assert.isObject(e.parsed);
832
Assert.areSame('bar', e.parsed.foo);
833
Assert.areSame('bar', model.get('foo'), 'load event should fire after attribute changes are applied');
836
model.sync = function (action, options, callback) {
837
callback(null, '{"foo": "bar"}');
840
model.load(function () {
841
Assert.areSame(1, calls, 'load event should fire before the callback runs');
844
Assert.areSame(1, calls, 'load event never fired');
847
'`save` event should fire after a successful save operation': function () {
849
model = new this.TestModel();
851
model.on('save', function (e) {
854
Assert.areSame('{"foo": "bar"}', e.response);
855
Assert.isObject(e.options);
856
Assert.isObject(e.parsed);
857
Assert.areSame('bar', e.parsed.foo);
858
Assert.areSame('bar', model.get('foo'), 'save event should fire after attribute changes are applied');
861
model.sync = function (action, options, callback) {
862
callback(null, '{"foo": "bar"}');
865
model.save(function () {
866
Assert.areSame(1, calls, 'save event should fire before the callback runs');
869
Assert.areSame(1, calls, 'save event never fired');
873
suite.add(modelSuite);
876
requires: ['model', 'model-list', 'test']