PageRenderTime 8ms CodeModel.GetById 3ms app.highlight 95ms RepoModel.GetById 2ms app.codeStats 0ms

/components/backbone/test/model.js

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