PageRenderTime 74ms CodeModel.GetById 2ms app.highlight 63ms RepoModel.GetById 1ms app.codeStats 0ms

/test/backbone/model.js

https://github.com/pomidorus/backbone-associations
JavaScript | 1106 lines | 987 code | 116 blank | 3 comment | 15 complexity | b83c25eead1abc310fc85cdf112d5ec9 MD5 | raw file
   1//adopted from Backbone 1.0.0 test suite
   2$(document).ready(function() {
   3    // test Backbone.Associated instead of Backbone.Model - reset at end of function
   4    var OriginalModel = Backbone.Model;
   5    Backbone.Model = Backbone.AssociatedModel;
   6
   7    var proxy = Backbone.Model.extend();
   8    var klass = Backbone.Collection.extend({
   9        url : function() { return '/collection'; }
  10    });
  11    var doc, collection;
  12
  13    module("Backbone.Model", _.extend(new Environment, {
  14
  15        setup: function() {
  16            Environment.prototype.setup.apply(this, arguments);
  17            doc = new proxy({
  18                id     : '1-the-tempest',
  19                title  : "The Tempest",
  20                author : "Bill Shakespeare",
  21                length : 123
  22            });
  23            collection = new klass();
  24            collection.add(doc);
  25        }
  26
  27    }));
  28
  29    test("initialize", 3, function() {
  30        var Model = Backbone.Model.extend({
  31            initialize: function() {
  32                this.one = 1;
  33                equal(this.collection, collection);
  34            }
  35        });
  36        var model = new Model({}, {collection: collection});
  37        equal(model.one, 1);
  38        equal(model.collection, collection);
  39    });
  40
  41    test("initialize with attributes and options", 1, function() {
  42        var Model = Backbone.Model.extend({
  43            initialize: function(attributes, options) {
  44                this.one = options.one;
  45            }
  46        });
  47        var model = new Model({}, {one: 1});
  48        equal(model.one, 1);
  49    });
  50
  51    test("initialize with parsed attributes", 1, function() {
  52        var Model = Backbone.Model.extend({
  53            parse: function(attrs) {
  54                attrs.value += 1;
  55                return attrs;
  56            }
  57        });
  58        var model = new Model({value: 1}, {parse: true});
  59        equal(model.get('value'), 2);
  60    });
  61
  62    test("initialize with defaults", 2, function() {
  63        var Model = Backbone.Model.extend({
  64            defaults: {
  65                first_name: 'Unknown',
  66                last_name: 'Unknown'
  67            }
  68        });
  69        var model = new Model({'first_name': 'John'});
  70        equal(model.get('first_name'), 'John');
  71        equal(model.get('last_name'), 'Unknown');
  72    });
  73
  74    test("parse can return null", 1, function() {
  75        var Model = Backbone.Model.extend({
  76            parse: function(attrs) {
  77                attrs.value += 1;
  78                return null;
  79            }
  80        });
  81        var model = new Model({value: 1}, {parse: true});
  82        equal(JSON.stringify(model.toJSON()), "{}");
  83    });
  84
  85    test("url", 3, function() {
  86        doc.urlRoot = null;
  87        equal(doc.url(), '/collection/1-the-tempest');
  88        doc.collection.url = '/collection/';
  89        equal(doc.url(), '/collection/1-the-tempest');
  90        doc.collection = null;
  91        raises(function() { doc.url(); });
  92        doc.collection = collection;
  93    });
  94
  95    test("url when using urlRoot, and uri encoding", 2, function() {
  96        var Model = Backbone.Model.extend({
  97            urlRoot: '/collection'
  98        });
  99        var model = new Model();
 100        equal(model.url(), '/collection');
 101        model.set({id: '+1+'});
 102        equal(model.url(), '/collection/%2B1%2B');
 103    });
 104
 105    test("url when using urlRoot as a function to determine urlRoot at runtime", 2, function() {
 106        var Model = Backbone.Model.extend({
 107            urlRoot: function() {
 108                return '/nested/' + this.get('parent_id') + '/collection';
 109            }
 110        });
 111
 112        var model = new Model({parent_id: 1});
 113        equal(model.url(), '/nested/1/collection');
 114        model.set({id: 2});
 115        equal(model.url(), '/nested/1/collection/2');
 116    });
 117
 118    test('url and urlRoot are directly attached if passed in the options', 2, function () {
 119        var model = new Backbone.Model({a: 1}, {url: '/test'});
 120        var model2 = new Backbone.Model({a: 2}, {urlRoot: '/test2'});
 121        equal(model.url, '/test');
 122        equal(model2.urlRoot, '/test2');
 123    });
 124
 125    test("underscore methods", 5, function() {
 126        var model = new Backbone.Model({ 'foo': 'a', 'bar': 'b', 'baz': 'c' });
 127        var model2 = model.clone();
 128        deepEqual(model.keys(), ['foo', 'bar', 'baz']);
 129        deepEqual(model.values(), ['a', 'b', 'c']);
 130        deepEqual(model.invert(), { 'a': 'foo', 'b': 'bar', 'c': 'baz' });
 131        deepEqual(model.pick('foo', 'baz'), {'foo': 'a', 'baz': 'c'});
 132        deepEqual(model.omit('foo', 'bar'), {'baz': 'c'});
 133    });
 134
 135    test("clone", 10, function() {
 136        var a = new Backbone.Model({ 'foo': 1, 'bar': 2, 'baz': 3});
 137        var b = a.clone();
 138        equal(a.get('foo'), 1);
 139        equal(a.get('bar'), 2);
 140        equal(a.get('baz'), 3);
 141        equal(b.get('foo'), a.get('foo'), "Foo should be the same on the clone.");
 142        equal(b.get('bar'), a.get('bar'), "Bar should be the same on the clone.");
 143        equal(b.get('baz'), a.get('baz'), "Baz should be the same on the clone.");
 144        a.set({foo : 100});
 145        equal(a.get('foo'), 100);
 146        equal(b.get('foo'), 1, "Changing a parent attribute does not change the clone.");
 147
 148        var foo = new Backbone.Model({p: 1});
 149        var bar = new Backbone.Model({p: 2});
 150        bar.set(foo.clone().attributes, {unset: true});
 151        equal(foo.get('p'), 1);
 152        equal(bar.get('p'), undefined);
 153    });
 154
 155    test("isNew", 6, function() {
 156        var a = new Backbone.Model({ 'foo': 1, 'bar': 2, 'baz': 3});
 157        ok(a.isNew(), "it should be new");
 158        a = new Backbone.Model({ 'foo': 1, 'bar': 2, 'baz': 3, 'id': -5 });
 159        ok(!a.isNew(), "any defined ID is legal, negative or positive");
 160        a = new Backbone.Model({ 'foo': 1, 'bar': 2, 'baz': 3, 'id': 0 });
 161        ok(!a.isNew(), "any defined ID is legal, including zero");
 162        ok( new Backbone.Model({          }).isNew(), "is true when there is no id");
 163        ok(!new Backbone.Model({ 'id': 2  }).isNew(), "is false for a positive integer");
 164        ok(!new Backbone.Model({ 'id': -5 }).isNew(), "is false for a negative integer");
 165    });
 166
 167    test("get", 2, function() {
 168        equal(doc.get('title'), 'The Tempest');
 169        equal(doc.get('author'), 'Bill Shakespeare');
 170    });
 171
 172    test("escape", 5, function() {
 173        equal(doc.escape('title'), 'The Tempest');
 174        doc.set({audience: 'Bill & Bob'});
 175        equal(doc.escape('audience'), 'Bill & Bob');
 176        doc.set({audience: 'Tim > Joan'});
 177        equal(doc.escape('audience'), 'Tim > Joan');
 178        doc.set({audience: 10101});
 179        equal(doc.escape('audience'), '10101');
 180        doc.unset('audience');
 181        equal(doc.escape('audience'), '');
 182    });
 183
 184    test("has", 10, function() {
 185        var model = new Backbone.Model();
 186
 187        strictEqual(model.has('name'), false);
 188
 189        model.set({
 190            '0': 0,
 191            '1': 1,
 192            'true': true,
 193            'false': false,
 194            'empty': '',
 195            'name': 'name',
 196            'null': null,
 197            'undefined': undefined
 198        });
 199
 200        strictEqual(model.has('0'), true);
 201        strictEqual(model.has('1'), true);
 202        strictEqual(model.has('true'), true);
 203        strictEqual(model.has('false'), true);
 204        strictEqual(model.has('empty'), true);
 205        strictEqual(model.has('name'), true);
 206
 207        model.unset('name');
 208
 209        strictEqual(model.has('name'), false);
 210        strictEqual(model.has('null'), false);
 211        strictEqual(model.has('undefined'), false);
 212    });
 213
 214    test("set and unset", 8, function() {
 215        var a = new Backbone.Model({id: 'id', foo: 1, bar: 2, baz: 3});
 216        var changeCount = 0;
 217        a.on("change:foo", function() { changeCount += 1; });
 218        a.set({'foo': 2});
 219        ok(a.get('foo') == 2, "Foo should have changed.");
 220        ok(changeCount == 1, "Change count should have incremented.");
 221        a.set({'foo': 2}); // set with value that is not new shouldn't fire change event
 222        ok(a.get('foo') == 2, "Foo should NOT have changed, still 2");
 223        ok(changeCount == 1, "Change count should NOT have incremented.");
 224
 225        a.validate = function(attrs) {
 226            equal(attrs.foo, void 0, "validate:true passed while unsetting");
 227        };
 228        a.unset('foo', {validate: true});
 229        equal(a.get('foo'), void 0, "Foo should have changed");
 230        delete a.validate;
 231        ok(changeCount == 2, "Change count should have incremented for unset.");
 232
 233        a.unset('id');
 234        equal(a.id, undefined, "Unsetting the id should remove the id property.");
 235    });
 236
 237    test("#2030 - set with failed validate, followed by another set triggers change", function () {
 238        var attr = 0, main = 0, error = 0;
 239        var Model = Backbone.Model.extend({
 240            validate: function (attr) {
 241                if (attr.x > 1) {
 242                    error++;
 243                    return "this is an error";
 244                }
 245            }
 246        });
 247        var model = new Model({x:0});
 248        model.on('change:x', function () { attr++; });
 249        model.on('change', function () { main++; });
 250        model.set({x:2}, {validate:true});
 251        model.set({x:1}, {validate:true});
 252        deepEqual([attr, main, error], [1, 1, 1]);
 253    });
 254
 255    test("set triggers changes in the correct order", function() {
 256        var value = null;
 257        var model = new Backbone.Model;
 258        model.on('last', function(){ value = 'last'; });
 259        model.on('first', function(){ value = 'first'; });
 260        model.trigger('first');
 261        model.trigger('last');
 262        equal(value, 'last');
 263    });
 264
 265    test("set falsy values in the correct order", 2, function() {
 266        var model = new Backbone.Model({result: 'result'});
 267        model.on('change', function() {
 268            equal(model.changed.result, void 0);
 269            equal(model.previous('result'), false);
 270        });
 271        model.set({result: void 0}, {silent: true});
 272        model.set({result: null}, {silent: true});
 273        model.set({result: false}, {silent: true});
 274        model.set({result: void 0});
 275    });
 276
 277    test("multiple unsets", 1, function() {
 278        var i = 0;
 279        var counter = function(){ i++; };
 280        var model = new Backbone.Model({a: 1});
 281        model.on("change:a", counter);
 282        model.set({a: 2});
 283        model.unset('a');
 284        model.unset('a');
 285        equal(i, 2, 'Unset does not fire an event for missing attributes.');
 286    });
 287
 288    test("unset and changedAttributes", 1, function() {
 289        var model = new Backbone.Model({a: 1});
 290        model.on('change', function() {
 291            ok('a' in model.changedAttributes(), 'changedAttributes should contain unset properties');
 292        });
 293        model.unset('a');
 294    });
 295
 296    test("using a non-default id attribute.", 5, function() {
 297        var MongoModel = Backbone.Model.extend({idAttribute : '_id'});
 298        var model = new MongoModel({id: 'eye-dee', _id: 25, title: 'Model'});
 299        equal(model.get('id'), 'eye-dee');
 300        equal(model.id, 25);
 301        equal(model.isNew(), false);
 302        model.unset('_id');
 303        equal(model.id, undefined);
 304        equal(model.isNew(), true);
 305    });
 306
 307    test("set an empty string", 1, function() {
 308        var model = new Backbone.Model({name : "Model"});
 309        model.set({name : ''});
 310        equal(model.get('name'), '');
 311    });
 312
 313    test("setting an object", 1, function() {
 314        var model = new Backbone.Model({
 315            custom: { foo: 1 }
 316        });
 317        model.on('change', function() {
 318            ok(1);
 319        });
 320        model.set({
 321            custom: { foo: 1 } // no change should be fired
 322        });
 323        model.set({
 324            custom: { foo: 2 } // change event should be fired
 325        });
 326    });
 327
 328    test("clear", 3, function() {
 329        var changed;
 330        var model = new Backbone.Model({id: 1, name : "Model"});
 331        model.on("change:name", function(){ changed = true; });
 332        model.on("change", function() {
 333            var changedAttrs = model.changedAttributes();
 334            ok('name' in changedAttrs);
 335        });
 336        model.clear();
 337        equal(changed, true);
 338        equal(model.get('name'), undefined);
 339    });
 340
 341    test("defaults", 4, function() {
 342        var Defaulted = Backbone.Model.extend({
 343            defaults: {
 344                "one": 1,
 345                "two": 2
 346            }
 347        });
 348        var model = new Defaulted({two: undefined});
 349        equal(model.get('one'), 1);
 350        equal(model.get('two'), 2);
 351        Defaulted = Backbone.Model.extend({
 352            defaults: function() {
 353                return {
 354                    "one": 3,
 355                    "two": 4
 356                };
 357            }
 358        });
 359        model = new Defaulted({two: undefined});
 360        equal(model.get('one'), 3);
 361        equal(model.get('two'), 4);
 362    });
 363
 364    test("change, hasChanged, changedAttributes, previous, previousAttributes", 9, function() {
 365        var model = new Backbone.Model({name: "Tim", age: 10});
 366        deepEqual(model.changedAttributes(), false);
 367        model.on('change', function() {
 368            ok(model.hasChanged('name'), 'name changed');
 369            ok(!model.hasChanged('age'), 'age did not');
 370            ok(_.isEqual(model.changedAttributes(), {name : 'Rob'}), 'changedAttributes returns the changed attrs');
 371            equal(model.previous('name'), 'Tim');
 372            ok(_.isEqual(model.previousAttributes(), {name : "Tim", age : 10}), 'previousAttributes is correct');
 373        });
 374        equal(model.hasChanged(), false);
 375        equal(model.hasChanged(undefined), false);
 376        model.set({name : 'Rob'});
 377        equal(model.get('name'), 'Rob');
 378    });
 379
 380    test("changedAttributes", 3, function() {
 381        var model = new Backbone.Model({a: 'a', b: 'b'});
 382        deepEqual(model.changedAttributes(), false);
 383        equal(model.changedAttributes({a: 'a'}), false);
 384        equal(model.changedAttributes({a: 'b'}).a, 'b');
 385    });
 386
 387    test("change with options", 2, function() {
 388        var value;
 389        var model = new Backbone.Model({name: 'Rob'});
 390        model.on('change', function(model, options) {
 391            value = options.prefix + model.get('name');
 392        });
 393        model.set({name: 'Bob'}, {prefix: 'Mr. '});
 394        equal(value, 'Mr. Bob');
 395        model.set({name: 'Sue'}, {prefix: 'Ms. '});
 396        equal(value, 'Ms. Sue');
 397    });
 398
 399    test("change after initialize", 1, function () {
 400        var changed = 0;
 401        var attrs = {id: 1, label: 'c'};
 402        var obj = new Backbone.Model(attrs);
 403        obj.on('change', function() { changed += 1; });
 404        obj.set(attrs);
 405        equal(changed, 0);
 406    });
 407
 408    test("save within change event", 1, function () {
 409        var env = this;
 410        var model = new Backbone.Model({firstName : "Taylor", lastName: "Swift"});
 411        model.url = '/test';
 412        model.on('change', function () {
 413            model.save();
 414            ok(_.isEqual(env.syncArgs.model, model));
 415        });
 416        model.set({lastName: 'Hicks'});
 417    });
 418
 419    test("validate after save", 2, function() {
 420        var lastError, model = new Backbone.Model();
 421        model.validate = function(attrs) {
 422            if (attrs.admin) return "Can't change admin status.";
 423        };
 424        model.sync = function(method, model, options) {
 425            options.success.call(this, {admin: true});
 426        };
 427        model.on('invalid', function(model, error) {
 428            lastError = error;
 429        });
 430        model.save(null);
 431
 432        equal(lastError, "Can't change admin status.");
 433        equal(model.validationError, "Can't change admin status.");
 434    });
 435
 436    test("save", 2, function() {
 437        doc.save({title : "Henry V"});
 438        equal(this.syncArgs.method, 'update');
 439        ok(_.isEqual(this.syncArgs.model, doc));
 440    });
 441
 442    test("save, fetch, destroy triggers error event when an error occurs", 3, function () {
 443        var model = new Backbone.Model();
 444        model.on('error', function () {
 445            ok(true);
 446        });
 447        model.sync = function (method, model, options) {
 448            options.error();
 449        };
 450        model.save({data: 2, id: 1});
 451        model.fetch();
 452        model.destroy();
 453    });
 454
 455    test("save with PATCH", function() {
 456        doc.clear().set({id: 1, a: 1, b: 2, c: 3, d: 4});
 457        doc.save();
 458        equal(this.syncArgs.method, 'update');
 459        equal(this.syncArgs.options.attrs, undefined);
 460
 461        doc.save({b: 2, d: 4}, {patch: true});
 462        equal(this.syncArgs.method, 'patch');
 463        equal(_.size(this.syncArgs.options.attrs), 2);
 464        equal(this.syncArgs.options.attrs.d, 4);
 465        equal(this.syncArgs.options.attrs.a, undefined);
 466        equal(this.ajaxSettings.data, "{\"b\":2,\"d\":4}");
 467    });
 468
 469    test("save in positional style", 1, function() {
 470        var model = new Backbone.Model();
 471        model.sync = function(method, model, options) {
 472            options.success();
 473        };
 474        model.save('title', 'Twelfth Night');
 475        equal(model.get('title'), 'Twelfth Night');
 476    });
 477
 478    test("save with non-object success response", 2, function () {
 479        var model = new Backbone.Model();
 480        model.sync = function(method, model, options) {
 481            options.success('', options);
 482            options.success(null, options);
 483        };
 484        model.save({testing:'empty'}, {
 485            success: function (model) {
 486                deepEqual(model.attributes, {testing:'empty'});
 487            }
 488        });
 489    });
 490
 491    test("fetch", 2, function() {
 492        doc.fetch();
 493        equal(this.syncArgs.method, 'read');
 494        ok(_.isEqual(this.syncArgs.model, doc));
 495    });
 496
 497    test("destroy", 3, function() {
 498        doc.destroy();
 499        equal(this.syncArgs.method, 'delete');
 500        ok(_.isEqual(this.syncArgs.model, doc));
 501
 502        var newModel = new Backbone.Model;
 503        equal(newModel.destroy(), false);
 504    });
 505
 506    test("non-persisted destroy", 1, function() {
 507        var a = new Backbone.Model({ 'foo': 1, 'bar': 2, 'baz': 3});
 508        a.sync = function() { throw "should not be called"; };
 509        a.destroy();
 510        ok(true, "non-persisted model should not call sync");
 511    });
 512
 513    test("validate", function() {
 514        var lastError;
 515        var model = new Backbone.Model();
 516        model.validate = function(attrs) {
 517            if (attrs.admin != this.get('admin')) return "Can't change admin status.";
 518        };
 519        model.on('invalid', function(model, error) {
 520            lastError = error;
 521        });
 522        var result = model.set({a: 100});
 523        equal(result, model);
 524        equal(model.get('a'), 100);
 525        equal(lastError, undefined);
 526        result = model.set({admin: true});
 527        equal(model.get('admin'), true);
 528        result = model.set({a: 200, admin: false}, {validate:true});
 529        equal(lastError, "Can't change admin status.");
 530        equal(result, false);
 531        equal(model.get('a'), 100);
 532    });
 533
 534    test("validate on unset and clear", 6, function() {
 535        var error;
 536        var model = new Backbone.Model({name: "One"});
 537        model.validate = function(attrs) {
 538            if (!attrs.name) {
 539                error = true;
 540                return "No thanks.";
 541            }
 542        };
 543        model.set({name: "Two"});
 544        equal(model.get('name'), 'Two');
 545        equal(error, undefined);
 546        model.unset('name', {validate: true});
 547        equal(error, true);
 548        equal(model.get('name'), 'Two');
 549        model.clear({validate:true});
 550        equal(model.get('name'), 'Two');
 551        delete model.validate;
 552        model.clear();
 553        equal(model.get('name'), undefined);
 554    });
 555
 556    test("validate with error callback", 8, function() {
 557        var lastError, boundError;
 558        var model = new Backbone.Model();
 559        model.validate = function(attrs) {
 560            if (attrs.admin) return "Can't change admin status.";
 561        };
 562        model.on('invalid', function(model, error) {
 563            boundError = true;
 564        });
 565        var result = model.set({a: 100}, {validate:true});
 566        equal(result, model);
 567        equal(model.get('a'), 100);
 568        equal(model.validationError, null);
 569        equal(boundError, undefined);
 570        result = model.set({a: 200, admin: true}, {validate:true});
 571        equal(result, false);
 572        equal(model.get('a'), 100);
 573        equal(model.validationError, "Can't change admin status.");
 574        equal(boundError, true);
 575    });
 576
 577    test("defaults always extend attrs (#459)", 2, function() {
 578        var Defaulted = Backbone.Model.extend({
 579            defaults: {one: 1},
 580            initialize : function(attrs, opts) {
 581                equal(this.attributes.one, 1);
 582            }
 583        });
 584        var providedattrs = new Defaulted({});
 585        var emptyattrs = new Defaulted();
 586    });
 587
 588    test("Inherit class properties", 6, function() {
 589        var Parent = Backbone.Model.extend({
 590            instancePropSame: function() {},
 591            instancePropDiff: function() {}
 592        }, {
 593            classProp: function() {}
 594        });
 595        var Child = Parent.extend({
 596            instancePropDiff: function() {}
 597        });
 598
 599        var adult = new Parent;
 600        var kid   = new Child;
 601
 602        equal(Child.classProp, Parent.classProp);
 603        notEqual(Child.classProp, undefined);
 604
 605        equal(kid.instancePropSame, adult.instancePropSame);
 606        notEqual(kid.instancePropSame, undefined);
 607
 608        notEqual(Child.prototype.instancePropDiff, Parent.prototype.instancePropDiff);
 609        notEqual(Child.prototype.instancePropDiff, undefined);
 610    });
 611
 612    test("Nested change events don't clobber previous attributes", 4, function() {
 613        new Backbone.Model()
 614            .on('change:state', function(model, newState) {
 615                equal(model.previous('state'), undefined);
 616                equal(newState, 'hello');
 617                // Fire a nested change event.
 618                model.set({other: 'whatever'});
 619            })
 620            .on('change:state', function(model, newState) {
 621                equal(model.previous('state'), undefined);
 622                equal(newState, 'hello');
 623            })
 624            .set({state: 'hello'});
 625    });
 626
 627    test("hasChanged/set should use same comparison", 2, function() {
 628        var changed = 0, model = new Backbone.Model({a: null});
 629        model.on('change', function() {
 630            ok(this.hasChanged('a'));
 631        })
 632            .on('change:a', function() {
 633                changed++;
 634            })
 635            .set({a: undefined});
 636        equal(changed, 1);
 637    });
 638
 639    test("#582, #425, change:attribute callbacks should fire after all changes have occurred", 9, function() {
 640        var model = new Backbone.Model;
 641
 642        var assertion = function() {
 643            equal(model.get('a'), 'a');
 644            equal(model.get('b'), 'b');
 645            equal(model.get('c'), 'c');
 646        };
 647
 648        model.on('change:a', assertion);
 649        model.on('change:b', assertion);
 650        model.on('change:c', assertion);
 651
 652        model.set({a: 'a', b: 'b', c: 'c'});
 653    });
 654
 655    test("#871, set with attributes property", 1, function() {
 656        var model = new Backbone.Model();
 657        model.set({attributes: true});
 658        ok(model.has('attributes'));
 659    });
 660
 661    test("set value regardless of equality/change", 1, function() {
 662        var model = new Backbone.Model({x: []});
 663        var a = [];
 664        model.set({x: a});
 665        ok(model.get('x') === a);
 666    });
 667
 668    test("set same value does not trigger change", 0, function() {
 669        var model = new Backbone.Model({x: 1});
 670        model.on('change change:x', function() { ok(false); });
 671        model.set({x: 1});
 672        model.set({x: 1});
 673    });
 674
 675    test("unset does not fire a change for undefined attributes", 0, function() {
 676        var model = new Backbone.Model({x: undefined});
 677        model.on('change:x', function(){ ok(false); });
 678        model.unset('x');
 679    });
 680
 681    test("set: undefined values", 1, function() {
 682        var model = new Backbone.Model({x: undefined});
 683        ok('x' in model.attributes);
 684    });
 685
 686    test("hasChanged works outside of change events, and true within", 6, function() {
 687        var model = new Backbone.Model({x: 1});
 688        model.on('change:x', function() {
 689            ok(model.hasChanged('x'));
 690            equal(model.get('x'), 1);
 691        });
 692        model.set({x: 2}, {silent: true});
 693        ok(model.hasChanged());
 694        equal(model.hasChanged('x'), true);
 695        model.set({x: 1});
 696        ok(model.hasChanged());
 697        equal(model.hasChanged('x'), true);
 698    });
 699
 700    test("hasChanged gets cleared on the following set", 4, function() {
 701        var model = new Backbone.Model;
 702        model.set({x: 1});
 703        ok(model.hasChanged());
 704        model.set({x: 1});
 705        ok(!model.hasChanged());
 706        model.set({x: 2});
 707        ok(model.hasChanged());
 708        model.set({});
 709        ok(!model.hasChanged());
 710    });
 711
 712    test("save with `wait` succeeds without `validate`", 1, function() {
 713        var model = new Backbone.Model();
 714        model.url = '/test';
 715        model.save({x: 1}, {wait: true});
 716        ok(this.syncArgs.model === model);
 717    });
 718
 719    test("`hasChanged` for falsey keys", 2, function() {
 720        var model = new Backbone.Model();
 721        model.set({x: true}, {silent: true});
 722        ok(!model.hasChanged(0));
 723        ok(!model.hasChanged(''));
 724    });
 725
 726    test("`previous` for falsey keys", 2, function() {
 727        var model = new Backbone.Model({0: true, '': true});
 728        model.set({0: false, '': false}, {silent: true});
 729        equal(model.previous(0), true);
 730        equal(model.previous(''), true);
 731    });
 732
 733    test("`save` with `wait` sends correct attributes", 5, function() {
 734        var changed = 0;
 735        var model = new Backbone.Model({x: 1, y: 2});
 736        model.url = '/test';
 737        model.on('change:x', function() { changed++; });
 738        model.save({x: 3}, {wait: true});
 739        deepEqual(JSON.parse(this.ajaxSettings.data), {x: 3, y: 2});
 740        equal(model.get('x'), 1);
 741        equal(changed, 0);
 742        this.syncArgs.options.success({});
 743        equal(model.get('x'), 3);
 744        equal(changed, 1);
 745    });
 746
 747    test("a failed `save` with `wait` doesn't leave attributes behind", 1, function() {
 748        var model = new Backbone.Model;
 749        model.url = '/test';
 750        model.save({x: 1}, {wait: true});
 751        equal(model.get('x'), void 0);
 752    });
 753
 754    test("#1030 - `save` with `wait` results in correct attributes if success is called during sync", 2, function() {
 755        var model = new Backbone.Model({x: 1, y: 2});
 756        model.sync = function(method, model, options) {
 757            options.success();
 758        };
 759        model.on("change:x", function() { ok(true); });
 760        model.save({x: 3}, {wait: true});
 761        equal(model.get('x'), 3);
 762    });
 763
 764    test("save with wait validates attributes", function() {
 765        var model = new Backbone.Model();
 766        model.url = '/test';
 767        model.validate = function() { ok(true); };
 768        model.save({x: 1}, {wait: true});
 769    });
 770
 771    test("save turns on parse flag", function () {
 772        var Model = Backbone.Model.extend({
 773            sync: function(method, model, options) { ok(options.parse); }
 774        });
 775        new Model().save();
 776    });
 777
 778    test("nested `set` during `'change:attr'`", 2, function() {
 779        var events = [];
 780        var model = new Backbone.Model();
 781        model.on('all', function(event) { events.push(event); });
 782        model.on('change', function() {
 783            model.set({z: true}, {silent:true});
 784        });
 785        model.on('change:x', function() {
 786            model.set({y: true});
 787        });
 788        model.set({x: true});
 789        deepEqual(events, ['change:y', 'change:x', 'change']);
 790        events = [];
 791        model.set({z: true});
 792        deepEqual(events, []);
 793    });
 794
 795    test("nested `change` only fires once", 1, function() {
 796        var model = new Backbone.Model();
 797        model.on('change', function() {
 798            ok(true);
 799            model.set({x: true});
 800        });
 801        model.set({x: true});
 802    });
 803
 804    test("nested `set` during `'change'`", 6, function() {
 805        var count = 0;
 806        var model = new Backbone.Model();
 807        model.on('change', function() {
 808            switch(count++) {
 809                case 0:
 810                    deepEqual(this.changedAttributes(), {x: true});
 811                    equal(model.previous('x'), undefined);
 812                    model.set({y: true});
 813                    break;
 814                case 1:
 815                    deepEqual(this.changedAttributes(), {x: true, y: true});
 816                    equal(model.previous('x'), undefined);
 817                    model.set({z: true});
 818                    break;
 819                case 2:
 820                    deepEqual(this.changedAttributes(), {x: true, y: true, z: true});
 821                    equal(model.previous('y'), undefined);
 822                    break;
 823                default:
 824                    ok(false);
 825            }
 826        });
 827        model.set({x: true});
 828    });
 829
 830    test("nested `change` with silent", 3, function() {
 831        var count = 0;
 832        var model = new Backbone.Model();
 833        model.on('change:y', function() { ok(false); });
 834        model.on('change', function() {
 835            switch(count++) {
 836                case 0:
 837                    deepEqual(this.changedAttributes(), {x: true});
 838                    model.set({y: true}, {silent: true});
 839                    model.set({z: true});
 840                    break;
 841                case 1:
 842                    deepEqual(this.changedAttributes(), {x: true, y: true, z: true});
 843                    break;
 844                case 2:
 845                    deepEqual(this.changedAttributes(), {z: false});
 846                    break;
 847                default:
 848                    ok(false);
 849            }
 850        });
 851        model.set({x: true});
 852        model.set({z: false});
 853    });
 854
 855    test("nested `change:attr` with silent", 0, function() {
 856        var model = new Backbone.Model();
 857        model.on('change:y', function(){ ok(false); });
 858        model.on('change', function() {
 859            model.set({y: true}, {silent: true});
 860            model.set({z: true});
 861        });
 862        model.set({x: true});
 863    });
 864
 865    test("multiple nested changes with silent", 1, function() {
 866        var model = new Backbone.Model();
 867        model.on('change:x', function() {
 868            model.set({y: 1}, {silent: true});
 869            model.set({y: 2});
 870        });
 871        model.on('change:y', function(model, val) {
 872            equal(val, 2);
 873        });
 874        model.set({x: true});
 875    });
 876
 877    test("multiple nested changes with silent", 1, function() {
 878        var changes = [];
 879        var model = new Backbone.Model();
 880        model.on('change:b', function(model, val) { changes.push(val); });
 881        model.on('change', function() {
 882            model.set({b: 1});
 883        });
 884        model.set({b: 0});
 885        deepEqual(changes, [0, 1]);
 886    });
 887
 888    test("basic silent change semantics", 1, function() {
 889        var model = new Backbone.Model;
 890        model.set({x: 1});
 891        model.on('change', function(){ ok(true); });
 892        model.set({x: 2}, {silent: true});
 893        model.set({x: 1});
 894    });
 895
 896    test("nested set multiple times", 1, function() {
 897        var model = new Backbone.Model();
 898        model.on('change:b', function() {
 899            ok(true);
 900        });
 901        model.on('change:a', function() {
 902            model.set({b: true});
 903            model.set({b: true});
 904        });
 905        model.set({a: true});
 906    });
 907
 908    test("#1122 - clear does not alter options.", 1, function() {
 909        var model = new Backbone.Model();
 910        var options = {};
 911        model.clear(options);
 912        ok(!options.unset);
 913    });
 914
 915    test("#1122 - unset does not alter options.", 1, function() {
 916        var model = new Backbone.Model();
 917        var options = {};
 918        model.unset('x', options);
 919        ok(!options.unset);
 920    });
 921
 922    test("#1355 - `options` is passed to success callbacks", 3, function() {
 923        var model = new Backbone.Model();
 924        var opts = {
 925            success: function( model, resp, options ) {
 926                ok(options);
 927            }
 928        };
 929        model.sync = function(method, model, options) {
 930            options.success();
 931        };
 932        model.save({id: 1}, opts);
 933        model.fetch(opts);
 934        model.destroy(opts);
 935    });
 936
 937    test("#1412 - Trigger 'sync' event.", 3, function() {
 938        var model = new Backbone.Model({id: 1});
 939        model.sync = function (method, model, options) { options.success(); };
 940        model.on('sync', function(){ ok(true); });
 941        model.fetch();
 942        model.save();
 943        model.destroy();
 944    });
 945
 946    test("#1365 - Destroy: New models execute success callback.", 2, function() {
 947        new Backbone.Model()
 948            .on('sync', function() { ok(false); })
 949            .on('destroy', function(){ ok(true); })
 950            .destroy({ success: function(){ ok(true); }});
 951    });
 952
 953    test("#1433 - Save: An invalid model cannot be persisted.", 1, function() {
 954        var model = new Backbone.Model;
 955        model.validate = function(){ return 'invalid'; };
 956        model.sync = function(){ ok(false); };
 957        strictEqual(model.save(), false);
 958    });
 959
 960    test("#1377 - Save without attrs triggers 'error'.", 1, function() {
 961        var Model = Backbone.Model.extend({
 962            url: '/test/',
 963            sync: function(method, model, options){ options.success(); },
 964            validate: function(){ return 'invalid'; }
 965        });
 966        var model = new Model({id: 1});
 967        model.on('invalid', function(){ ok(true); });
 968        model.save();
 969    });
 970
 971    test("#1545 - `undefined` can be passed to a model constructor without coersion", function() {
 972        var Model = Backbone.Model.extend({
 973            defaults: { one: 1 },
 974            initialize : function(attrs, opts) {
 975                equal(attrs, undefined);
 976            }
 977        });
 978        var emptyattrs = new Model();
 979        var undefinedattrs = new Model(undefined);
 980    });
 981
 982    asyncTest("#1478 - Model `save` does not trigger change on unchanged attributes", 0, function() {
 983        var Model = Backbone.Model.extend({
 984            sync: function(method, model, options) {
 985                setTimeout(function(){
 986                    options.success();
 987                    start();
 988                }, 0);
 989            }
 990        });
 991        new Model({x: true})
 992            .on('change:x', function(){ ok(false); })
 993            .save(null, {wait: true});
 994    });
 995
 996    test("#1664 - Changing from one value, silently to another, back to original triggers a change.", 1, function() {
 997        var model = new Backbone.Model({x:1});
 998        model.on('change:x', function() { ok(true); });
 999        model.set({x:2},{silent:true});
1000        model.set({x:3},{silent:true});
1001        model.set({x:1});
1002    });
1003
1004    test("#1664 - multiple silent changes nested inside a change event", 2, function() {
1005        var changes = [];
1006        var model = new Backbone.Model();
1007        model.on('change', function() {
1008            model.set({a:'c'}, {silent:true});
1009            model.set({b:2}, {silent:true});
1010            model.unset('c', {silent:true});
1011        });
1012        model.on('change:a change:b change:c', function(model, val) { changes.push(val); });
1013        model.set({a:'a', b:1, c:'item'});
1014        deepEqual(changes, ['a',1,'item']);
1015        deepEqual(model.attributes, {a: 'c', b: 2});
1016    });
1017
1018    test("#1791 - `attributes` is available for `parse`", function() {
1019        var Model = Backbone.Model.extend({
1020            parse: function() { this.has('a'); } // shouldn't throw an error
1021        });
1022        var model = new Model(null, {parse: true});
1023        expect(0);
1024    });
1025
1026    test("silent changes in last `change` event back to original triggers change", 2, function() {
1027        var changes = [];
1028        var model = new Backbone.Model();
1029        model.on('change:a change:b change:c', function(model, val) { changes.push(val); });
1030        model.on('change', function() {
1031            model.set({a:'c'}, {silent:true});
1032        });
1033        model.set({a:'a'});
1034        deepEqual(changes, ['a']);
1035        model.set({a:'a'});
1036        deepEqual(changes, ['a', 'a']);
1037    });
1038
1039    test("#1943 change calculations should use _.isEqual", function() {
1040        var model = new Backbone.Model({a: {key: 'value'}});
1041        model.set('a', {key:'value'}, {silent:true});
1042        equal(model.changedAttributes(), false);
1043    });
1044
1045    test("#1964 - final `change` event is always fired, regardless of interim changes", 1, function () {
1046        var model = new Backbone.Model();
1047        model.on('change:property', function() {
1048            model.set('property', 'bar');
1049        });
1050        model.on('change', function() {
1051            ok(true);
1052        });
1053        model.set('property', 'foo');
1054    });
1055
1056    test("isValid", function() {
1057        var model = new Backbone.Model({valid: true});
1058        model.validate = function(attrs) {
1059            if (!attrs.valid) return "invalid";
1060        };
1061        equal(model.isValid(), true);
1062        equal(model.set({valid: false}, {validate:true}), false);
1063        equal(model.isValid(), true);
1064        model.set({valid:false});
1065        equal(model.isValid(), false);
1066        ok(!model.set('valid', false, {validate: true}));
1067    });
1068
1069    test("#1179 - isValid returns true in the absence of validate.", 1, function() {
1070        var model = new Backbone.Model();
1071        model.validate = null;
1072        ok(model.isValid());
1073    });
1074
1075    test("#1961 - Creating a model with {validate:true} will call validate and use the error callback", function () {
1076        var Model = Backbone.Model.extend({
1077            validate: function (attrs) {
1078                if (attrs.id === 1) return "This shouldn't happen";
1079            }
1080        });
1081        var model = new Model({id: 1}, {validate: true});
1082        equal(model.validationError, "This shouldn't happen");
1083    });
1084
1085    test("toJSON receives attrs during save(..., {wait: true})", 1, function() {
1086        var Model = Backbone.Model.extend({
1087            url: '/test',
1088            toJSON: function() {
1089                strictEqual(this.attributes.x, 1);
1090                return _.clone(this.attributes);
1091            }
1092        });
1093        var model = new Model;
1094        model.save({x: 1}, {wait: true});
1095    });
1096
1097    test("#2034 - nested set with silent only triggers one change", 1, function() {
1098        var model = new Backbone.Model();
1099        model.on('change', function() {
1100            model.set({b: true}, {silent: true});
1101            ok(true);
1102        });
1103        model.set({a: true});
1104    });
1105
1106});