/test/model.js
JavaScript | 426 lines | 377 code | 46 blank | 3 comment | 8 complexity | d4706e23d73505bd5f2e766074970dcf MD5 | raw file
1$(document).ready(function() {
2
3 module("Backbone.Model");
4
5 // Variable to catch the last request.
6 window.lastRequest = null;
7
8 window.originalSync = Backbone.sync;
9
10 // Stub out Backbone.request...
11 Backbone.sync = function() {
12 lastRequest = _.toArray(arguments);
13 };
14
15 var attrs = {
16 id : '1-the-tempest',
17 title : "The Tempest",
18 author : "Bill Shakespeare",
19 length : 123
20 };
21
22 var proxy = Backbone.Model.extend();
23 var doc = new proxy(attrs);
24
25 var klass = Backbone.Collection.extend({
26 url : function() { return '/collection'; }
27 });
28
29 var collection = new klass();
30 collection.add(doc);
31
32 test("Model: initialize", function() {
33 var Model = Backbone.Model.extend({
34 initialize: function() {
35 this.one = 1;
36 equals(this.collection, collection);
37 }
38 });
39 var model = new Model({}, {collection: collection});
40 equals(model.one, 1);
41 equals(model.collection, collection);
42 });
43
44 test("Model: initialize with attributes and options", function() {
45 var Model = Backbone.Model.extend({
46 initialize: function(attributes, options) {
47 this.one = options.one;
48 }
49 });
50 var model = new Model({}, {one: 1});
51 equals(model.one, 1);
52 });
53
54 test("Model: url", function() {
55 equals(doc.url(), '/collection/1-the-tempest');
56 doc.collection.url = '/collection/';
57 equals(doc.url(), '/collection/1-the-tempest');
58 doc.collection = null;
59 var failed = false;
60 try {
61 doc.url();
62 } catch (e) {
63 failed = true;
64 }
65 equals(failed, true);
66 doc.collection = collection;
67 });
68
69 test("Model: url when using urlRoot, and uri encoding", function() {
70 var Model = Backbone.Model.extend({
71 urlRoot: '/collection'
72 });
73 var model = new Model();
74 equals(model.url(), '/collection');
75 model.set({id: '+1+'});
76 equals(model.url(), '/collection/%2B1%2B');
77 });
78
79 test("Model: clone", function() {
80 attrs = { 'foo': 1, 'bar': 2, 'baz': 3};
81 a = new Backbone.Model(attrs);
82 b = a.clone();
83 equals(a.get('foo'), 1);
84 equals(a.get('bar'), 2);
85 equals(a.get('baz'), 3);
86 equals(b.get('foo'), a.get('foo'), "Foo should be the same on the clone.");
87 equals(b.get('bar'), a.get('bar'), "Bar should be the same on the clone.");
88 equals(b.get('baz'), a.get('baz'), "Baz should be the same on the clone.");
89 a.set({foo : 100});
90 equals(a.get('foo'), 100);
91 equals(b.get('foo'), 1, "Changing a parent attribute does not change the clone.");
92 });
93
94 test("Model: isNew", function() {
95 attrs = { 'foo': 1, 'bar': 2, 'baz': 3};
96 a = new Backbone.Model(attrs);
97 ok(a.isNew(), "it should be new");
98 attrs = { 'foo': 1, 'bar': 2, 'baz': 3, 'id': -5 };
99 a = new Backbone.Model(attrs);
100 ok(!a.isNew(), "any defined ID is legal, negative or positive");
101 attrs = { 'foo': 1, 'bar': 2, 'baz': 3, 'id': 0 };
102 a = new Backbone.Model(attrs);
103 ok(!a.isNew(), "any defined ID is legal, including zero");
104 ok( new Backbone.Model({ }).isNew(), "is true when there is no id");
105 ok(!new Backbone.Model({ 'id': 2 }).isNew(), "is false for a positive integer");
106 ok(!new Backbone.Model({ 'id': -5 }).isNew(), "is false for a negative integer");
107 });
108
109 test("Model: get", function() {
110 equals(doc.get('title'), 'The Tempest');
111 equals(doc.get('author'), 'Bill Shakespeare');
112 });
113
114 test("Model: escape", function() {
115 equals(doc.escape('title'), 'The Tempest');
116 doc.set({audience: 'Bill & Bob'});
117 equals(doc.escape('audience'), 'Bill & Bob');
118 doc.set({audience: 'Tim > Joan'});
119 equals(doc.escape('audience'), 'Tim > Joan');
120 doc.set({audience: 10101});
121 equals(doc.escape('audience'), '10101');
122 doc.unset('audience');
123 equals(doc.escape('audience'), '');
124 });
125
126 test("Model: has", function() {
127 attrs = {};
128 a = new Backbone.Model(attrs);
129 equals(a.has("name"), false);
130 _([true, "Truth!", 1, false, '', 0]).each(function(value) {
131 a.set({'name': value});
132 equals(a.has("name"), true);
133 });
134 a.unset('name');
135 equals(a.has('name'), false);
136 _([null, undefined]).each(function(value) {
137 a.set({'name': value});
138 equals(a.has("name"), false);
139 });
140 });
141
142 test("Model: set and unset", function() {
143 attrs = {id: 'id', foo: 1, bar: 2, baz: 3};
144 a = new Backbone.Model(attrs);
145 var changeCount = 0;
146 a.bind("change:foo", function() { changeCount += 1; });
147 a.set({'foo': 2});
148 ok(a.get('foo')== 2, "Foo should have changed.");
149 ok(changeCount == 1, "Change count should have incremented.");
150 a.set({'foo': 2}); // set with value that is not new shouldn't fire change event
151 ok(a.get('foo')== 2, "Foo should NOT have changed, still 2");
152 ok(changeCount == 1, "Change count should NOT have incremented.");
153
154 a.unset('foo');
155 ok(a.get('foo')== null, "Foo should have changed");
156 ok(changeCount == 2, "Change count should have incremented for unset.");
157
158 a.unset('id');
159 equals(a.id, undefined, "Unsetting the id should remove the id property.");
160 });
161
162 test("Model: multiple unsets", function() {
163 var i = 0;
164 var counter = function(){ i++; };
165 var model = new Backbone.Model({a: 1});
166 model.bind("change:a", counter);
167 model.set({a: 2});
168 model.unset('a');
169 model.unset('a');
170 equals(i, 2, 'Unset does not fire an event for missing attributes.');
171 });
172
173 test("Model: using a non-default id attribute.", function() {
174 var MongoModel = Backbone.Model.extend({idAttribute : '_id'});
175 var model = new MongoModel({id: 'eye-dee', _id: 25, title: 'Model'});
176 equals(model.get('id'), 'eye-dee');
177 equals(model.id, 25);
178 equals(model.isNew(), false);
179 model.unset('_id');
180 equals(model.id, undefined);
181 equals(model.isNew(), true);
182 });
183
184 test("Model: set an empty string", function() {
185 var model = new Backbone.Model({name : "Model"});
186 model.set({name : ''});
187 equals(model.get('name'), '');
188 });
189
190 test("Model: clear", function() {
191 var changed;
192 var model = new Backbone.Model({name : "Model"});
193 model.bind("change:name", function(){ changed = true; });
194 model.clear();
195 equals(changed, true);
196 equals(model.get('name'), undefined);
197 });
198
199 test("Model: defaults", function() {
200 var Defaulted = Backbone.Model.extend({
201 defaults: {
202 "one": 1,
203 "two": 2
204 }
205 });
206 var model = new Defaulted({two: null});
207 equals(model.get('one'), 1);
208 equals(model.get('two'), null);
209 Defaulted = Backbone.Model.extend({
210 defaults: function() {
211 return {
212 "one": 3,
213 "two": 4
214 };
215 }
216 });
217 var model = new Defaulted({two: null});
218 equals(model.get('one'), 3);
219 equals(model.get('two'), null);
220 });
221
222 test("Model: change, hasChanged, changedAttributes, previous, previousAttributes", function() {
223 var model = new Backbone.Model({name : "Tim", age : 10});
224 equals(model.changedAttributes(), false);
225 model.bind('change', function() {
226 ok(model.hasChanged('name'), 'name changed');
227 ok(!model.hasChanged('age'), 'age did not');
228 ok(_.isEqual(model.changedAttributes(), {name : 'Rob'}), 'changedAttributes returns the changed attrs');
229 equals(model.previous('name'), 'Tim');
230 ok(_.isEqual(model.previousAttributes(), {name : "Tim", age : 10}), 'previousAttributes is correct');
231 });
232 model.set({name : 'Rob'}, {silent : true});
233 equals(model.hasChanged(), true);
234 equals(model.hasChanged('name'), true);
235 model.change();
236 equals(model.get('name'), 'Rob');
237 });
238
239 test("Model: change with options", function() {
240 var value;
241 var model = new Backbone.Model({name: 'Rob'});
242 model.bind('change', function(model, options) {
243 value = options.prefix + model.get('name');
244 });
245 model.set({name: 'Bob'}, {silent: true});
246 model.change({prefix: 'Mr. '});
247 equals(value, 'Mr. Bob');
248 model.set({name: 'Sue'}, {prefix: 'Ms. '});
249 equals(value, 'Ms. Sue');
250 });
251
252 test("Model: change after initialize", function () {
253 var changed = 0;
254 var attrs = {id: 1, label: 'c'};
255 var obj = new Backbone.Model(attrs);
256 obj.bind('change', function() { changed += 1; });
257 obj.set(attrs);
258 equals(changed, 0);
259 });
260
261 test("Model: save within change event", function () {
262 var model = new Backbone.Model({firstName : "Taylor", lastName: "Swift"});
263 model.bind('change', function () {
264 model.save();
265 ok(_.isEqual(lastRequest[1], model));
266 });
267 model.set({lastName: 'Hicks'});
268 });
269
270 test("Model: save", function() {
271 doc.save({title : "Henry V"});
272 equals(lastRequest[0], 'update');
273 ok(_.isEqual(lastRequest[1], doc));
274 });
275
276 test("Model: fetch", function() {
277 doc.fetch();
278 ok(lastRequest[0], 'read');
279 ok(_.isEqual(lastRequest[1], doc));
280 });
281
282 test("Model: destroy", function() {
283 doc.destroy();
284 equals(lastRequest[0], 'delete');
285 ok(_.isEqual(lastRequest[1], doc));
286 });
287
288 test("Model: non-persisted destroy", function() {
289 attrs = { 'foo': 1, 'bar': 2, 'baz': 3};
290 a = new Backbone.Model(attrs);
291 a.sync = function() { throw "should not be called"; };
292 ok(a.destroy(), "non-persisted model should not call sync");
293 });
294
295 test("Model: validate", function() {
296 var lastError;
297 var model = new Backbone.Model();
298 model.validate = function(attrs) {
299 if (attrs.admin) return "Can't change admin status.";
300 };
301 model.bind('error', function(model, error) {
302 lastError = error;
303 });
304 var result = model.set({a: 100});
305 equals(result, model);
306 equals(model.get('a'), 100);
307 equals(lastError, undefined);
308 result = model.set({admin: true}, {silent: true});
309 equals(lastError, undefined);
310 equals(model.get('admin'), true);
311 result = model.set({a: 200, admin: true});
312 equals(result, false);
313 equals(model.get('a'), 100);
314 equals(lastError, "Can't change admin status.");
315 });
316
317 test("Model: validate on unset and clear", function() {
318 var error;
319 var model = new Backbone.Model({name: "One"});
320 model.validate = function(attrs) {
321 if ("name" in attrs) {
322 if (!attrs.name) {
323 error = true;
324 return "No thanks.";
325 }
326 }
327 };
328 model.set({name: "Two"});
329 equals(model.get('name'), 'Two');
330 equals(error, undefined);
331 model.unset('name');
332 equals(error, true);
333 equals(model.get('name'), 'Two');
334 model.clear();
335 equals(model.get('name'), 'Two');
336 delete model.validate;
337 model.clear();
338 equals(model.get('name'), undefined);
339 });
340
341 test("Model: validate with error callback", function() {
342 var lastError, boundError;
343 var model = new Backbone.Model();
344 model.validate = function(attrs) {
345 if (attrs.admin) return "Can't change admin status.";
346 };
347 var callback = function(model, error) {
348 lastError = error;
349 };
350 model.bind('error', function(model, error) {
351 boundError = true;
352 });
353 var result = model.set({a: 100}, {error: callback});
354 equals(result, model);
355 equals(model.get('a'), 100);
356 equals(lastError, undefined);
357 equals(boundError, undefined);
358 result = model.set({a: 200, admin: true}, {error: callback});
359 equals(result, false);
360 equals(model.get('a'), 100);
361 equals(lastError, "Can't change admin status.");
362 equals(boundError, undefined);
363 });
364
365 test("Model: defaults always extend attrs (#459)", function() {
366 var Defaulted = Backbone.Model.extend({
367 defaults: {one: 1},
368 initialize : function(attrs, opts) {
369 equals(attrs.one, 1);
370 }
371 });
372 var providedattrs = new Defaulted({});
373 var emptyattrs = new Defaulted();
374 });
375
376 test("Model: Inherit class properties", function() {
377 var Parent = Backbone.Model.extend({
378 instancePropSame: function() {},
379 instancePropDiff: function() {}
380 }, {
381 classProp: function() {}
382 });
383 var Child = Parent.extend({
384 instancePropDiff: function() {}
385 });
386
387 var adult = new Parent;
388 var kid = new Child;
389
390 equals(Child.classProp, Parent.classProp);
391 notEqual(Child.classProp, undefined);
392
393 equals(kid.instancePropSame, adult.instancePropSame);
394 notEqual(kid.instancePropSame, undefined);
395
396 notEqual(Child.prototype.instancePropDiff, Parent.prototype.instancePropDiff);
397 notEqual(Child.prototype.instancePropDiff, undefined);
398 });
399
400 test("Model: Nested change events don't clobber previous attributes", function() {
401 var A = Backbone.Model.extend({
402 initialize: function() {
403 this.bind("change:state", function(a, newState) {
404 equals(a.previous('state'), undefined);
405 equals(newState, 'hello');
406 // Fire a nested change event.
407 this.set({ other: "whatever" });
408 });
409 }
410 });
411
412 var B = Backbone.Model.extend({
413 initialize: function() {
414 this.get("a").bind("change:state", function(a, newState) {
415 equals(a.previous('state'), undefined);
416 equals(newState, 'hello');
417 });
418 }
419 });
420
421 a = new A();
422 b = new B({a: a});
423 a.set({state: 'hello'});
424 });
425
426});