PageRenderTime 101ms CodeModel.GetById 6ms app.highlight 78ms RepoModel.GetById 1ms app.codeStats 1ms

/test/tests.js

https://github.com/sventschui/Backbone-relational
JavaScript | 4888 lines | 3579 code | 1058 blank | 251 comment | 78 complexity | 8f0948fcac86011646b4902c6b28e15f MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1/* vim: set tabstop=4 softtabstop=4 shiftwidth=4 noexpandtab: */
   2// documentation on writing tests here: http://docs.jquery.com/QUnit
   3// example tests: https://github.com/jquery/qunit/blob/master/test/same.js
   4// more examples: https://github.com/jquery/jquery/tree/master/test/unit
   5// jQueryUI examples: https://github.com/jquery/jquery-ui/tree/master/tests/unit
   6
   7//sessionStorage.clear();
   8if ( !window.console ) {
   9	var names = [ 'log', 'debug', 'info', 'warn', 'error', 'assert', 'dir', 'dirxml',
  10	'group', 'groupEnd', 'time', 'timeEnd', 'count', 'trace', 'profile', 'profileEnd' ];
  11	window.console = {};
  12	for ( var i = 0; i < names.length; ++i )
  13		window.console[ names[i] ] = function() {};
  14}
  15
  16$(document).ready(function() {
  17	window.requests = [];
  18
  19	Backbone.ajax = function( settings ) {
  20		var callbackContext = settings.context || this,
  21			dfd = new $.Deferred();
  22
  23		dfd = _.extend( settings, dfd );
  24
  25		dfd.respond = function( status, responseText ) {
  26			/**
  27			 * Trigger success/error with arguments like jQuery would:
  28			 * // Success/Error
  29			 * if ( isSuccess ) {
  30			 *   deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
  31			 * } else {
  32			 *   deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
  33			 * }
  34			 */
  35			if ( status >= 200 && status < 300 || status === 304 ) {
  36				_.isFunction( settings.success ) && settings.success( responseText, 'success', dfd );
  37				dfd.resolveWith( callbackContext, [ responseText, 'success', dfd ] );
  38			}
  39			else {
  40				_.isFunction( settings.error ) && settings.error( responseText, 'error', 'Internal Server Error' );
  41				dfd.rejectWith( callbackContext, [ dfd, 'error', 'Internal Server Error' ] );
  42			}
  43		};
  44
  45		// Add the request before triggering callbacks that may get us in here again
  46		window.requests.push( dfd );
  47
  48		// If a `response` has been defined, execute it.
  49		// If status < 299, trigger 'success'; otherwise, trigger 'error'
  50		if ( settings.response && settings.response.status ) {
  51			dfd.respond( settings.response.status, settings.response.responseText );
  52		}
  53
  54		return dfd;
  55	};
  56	
  57	Backbone.Model.prototype.url = function() {
  58		// Use the 'resource_uri' if possible
  59		var url = this.get( 'resource_uri' );
  60		
  61		// Try to have the collection construct a url
  62		if ( !url && this.collection ) {
  63			url = this.collection.url && _.isFunction( this.collection.url ) ? this.collection.url() : this.collection.url;
  64		}
  65		
  66		// Fallback to 'urlRoot'
  67		if ( !url && this.urlRoot ) {
  68			url = this.urlRoot + this.id;
  69		}
  70		
  71		if ( !url ) {
  72			throw new Error( 'Url could not be determined!' );
  73		}
  74		
  75		return url;
  76	};
  77
  78
  79	/**
  80	 * 'Zoo'
  81	 */
  82
  83	window.Zoo = Backbone.RelationalModel.extend({
  84		urlRoot: '/zoo/',
  85
  86		relations: [
  87			{
  88				type: Backbone.HasMany,
  89				key: 'animals',
  90				relatedModel: 'Animal',
  91				includeInJSON: [ 'id', 'species' ],
  92				collectionType: 'AnimalCollection',
  93				reverseRelation: {
  94					key: 'livesIn',
  95					includeInJSON: [ 'id', 'name' ]
  96				}
  97			},
  98			{ // A simple HasMany without reverse relation
  99				type: Backbone.HasMany,
 100				key: 'visitors',
 101				relatedModel: 'Visitor'
 102			}
 103		],
 104
 105		toString: function() {
 106			return 'Zoo (' + this.id + ')';
 107		}
 108	});
 109
 110	window.Animal = Backbone.RelationalModel.extend({
 111		urlRoot: '/animal/',
 112
 113		relations: [
 114			{ // A simple HasOne without reverse relation
 115				type: Backbone.HasOne,
 116				key: 'favoriteFood',
 117				relatedModel: 'Food'
 118			}
 119		],
 120
 121		// For validation testing. Wikipedia says elephants are reported up to 12.000 kg. Any more, we must've weighted wrong ;).
 122		validate: function( attrs ) {
 123			if ( attrs.species === 'elephant' && attrs.weight && attrs.weight > 12000 ) {
 124				return "Too heavy.";
 125			}
 126		},
 127
 128		toString: function() {
 129			return 'Animal (' + this.id + ')';
 130		}
 131	});
 132
 133	window.AnimalCollection = Backbone.Collection.extend({
 134		model: Animal
 135	});
 136
 137	window.Food = Backbone.RelationalModel.extend({
 138		urlRoot: '/food/'
 139	});
 140
 141	window.Visitor = Backbone.RelationalModel.extend();
 142
 143
 144	/**
 145	 * House/Person/Job/Company
 146	 */
 147
 148	window.House = Backbone.RelationalModel.extend({
 149		relations: [{
 150			type: Backbone.HasMany,
 151			key: 'occupants',
 152			relatedModel: 'Person',
 153			reverseRelation: {
 154				key: 'livesIn',
 155				includeInJSON: false
 156			}
 157		}],
 158
 159		toString: function() {
 160			return 'House (' + this.id + ')';
 161		}
 162	});
 163
 164	window.User = Backbone.RelationalModel.extend({
 165		urlRoot: '/user/',
 166
 167		toString: function() {
 168			return 'User (' + this.id + ')';
 169		}
 170	});
 171
 172	window.Person = Backbone.RelationalModel.extend({
 173		relations: [
 174			{
 175				// Create a cozy, recursive, one-to-one relationship
 176				type: Backbone.HasOne,
 177				key: 'likesALot',
 178				relatedModel: 'Person',
 179				reverseRelation: {
 180					type: Backbone.HasOne,
 181					key: 'likedALotBy'
 182				}
 183			},
 184			{
 185				type: Backbone.HasOne,
 186				key: 'user',
 187				keyDestination: 'user_id',
 188				relatedModel: 'User',
 189				includeInJSON: Backbone.Model.prototype.idAttribute,
 190				reverseRelation: {
 191					type: Backbone.HasOne,
 192					includeInJSON: 'name',
 193					key: 'person'
 194				}
 195			},
 196			{
 197				type: 'HasMany',
 198				key: 'jobs',
 199				relatedModel: 'Job',
 200				reverseRelation: {
 201					key: 'person'
 202				}
 203			}
 204		],
 205
 206		toString: function() {
 207			return 'Person (' + this.id + ')';
 208		}
 209	});
 210
 211	window.PersonCollection = Backbone.Collection.extend({
 212		model: Person
 213	});
 214
 215	window.Password = Backbone.RelationalModel.extend({
 216		relations: [{
 217			type: Backbone.HasOne,
 218			key: 'user',
 219			relatedModel: 'User',
 220			reverseRelation: {
 221				type: Backbone.HasOne,
 222				key: 'password'
 223			}
 224		}],
 225
 226		toString: function() {
 227			return 'Password (' + this.id + ')';
 228		}
 229	});
 230	
 231	// A link table between 'Person' and 'Company', to achieve many-to-many relations
 232	window.Job = Backbone.RelationalModel.extend({
 233		defaults: {
 234			'startDate': null,
 235			'endDate': null
 236		},
 237
 238		toString: function() {
 239			return 'Job (' + this.id + ')';
 240		}
 241	});
 242
 243	window.Company = Backbone.RelationalModel.extend({
 244		relations: [{
 245				type: 'HasMany',
 246				key: 'employees',
 247				relatedModel: 'Job',
 248				reverseRelation: {
 249					key: 'company'
 250				}
 251			},
 252			{
 253				type: 'HasOne',
 254				key: 'ceo',
 255				relatedModel: 'Person',
 256				reverseRelation: {
 257					key: 'runs'
 258				}
 259			}
 260		],
 261
 262		toString: function() {
 263			return 'Company (' + this.id + ')';
 264		}
 265	});
 266
 267
 268	/**
 269	 * Node/NodeList
 270	 */
 271
 272	window.Node = Backbone.RelationalModel.extend({
 273		urlRoot: '/node/',
 274
 275		relations: [{
 276				type: Backbone.HasOne,
 277				key: 'parent',
 278				relatedModel: 'Node',
 279				reverseRelation: {
 280					key: 'children'
 281				}
 282			}
 283		],
 284
 285		toString: function() {
 286			return 'Node (' + this.id + ')';
 287		}
 288	});
 289
 290	window.NodeList = Backbone.Collection.extend({
 291		model: Node
 292	});
 293
 294
 295	/**
 296	 * Customer/Address/Shop/Agent
 297	 */
 298
 299	window.Customer = Backbone.RelationalModel.extend({
 300		urlRoot: '/customer/',
 301
 302		toString: function() {
 303			return 'Customer (' + this.id + ')';
 304		}
 305	});
 306
 307	window.CustomerCollection = Backbone.Collection.extend({
 308		model: Customer,
 309
 310		initialize: function( models, options ) {
 311			options || (options = {});
 312			this.url = options.url;
 313		}
 314	});
 315
 316	window.Address = Backbone.RelationalModel.extend({
 317		urlRoot: '/address/',
 318
 319		toString: function() {
 320			return 'Address (' + this.id + ')';
 321		}
 322	});
 323
 324	window.Shop = Backbone.RelationalModel.extend({
 325		relations: [
 326			{
 327				type: Backbone.HasMany,
 328				key: 'customers',
 329				collectionType: 'CustomerCollection',
 330				collectionOptions: function( instance ) {
 331					return { 'url': 'shop/' + instance.id + '/customers/' };
 332				},
 333				relatedModel: 'Customer',
 334				autoFetch: true
 335			},
 336			{
 337				type: Backbone.HasOne,
 338				key: 'address',
 339				relatedModel: 'Address',
 340				autoFetch: {
 341					success: function( model, response ) {
 342						response.successOK = true;
 343					},
 344					error: function( model, response ) {
 345						response.errorOK = true;
 346					}
 347				}
 348			}
 349		],
 350
 351		toString: function() {
 352			return 'Shop (' + this.id + ')';
 353		}
 354	});
 355
 356	window.Agent = Backbone.RelationalModel.extend({
 357		urlRoot: '/agent/',
 358
 359		relations: [
 360			{
 361				type: Backbone.HasMany,
 362				key: 'customers',
 363				relatedModel: 'Customer',
 364				includeInJSON: Backbone.RelationalModel.prototype.idAttribute
 365			},
 366			{
 367				type: Backbone.HasOne,
 368				key: 'address',
 369				relatedModel: 'Address',
 370				autoFetch: false
 371			}
 372		],
 373
 374		toString: function() {
 375			return 'Agent (' + this.id + ')';
 376		}
 377	});
 378
 379	/**
 380	 * Reset variables that are persistent across tests, specifically `window.requests` and the state of
 381	 * `Backbone.Relational.store`.
 382	 */
 383	function reset() {
 384		// Reset last ajax requests
 385		window.requests = [];
 386
 387		Backbone.Relational.store.reset();
 388		Backbone.Relational.store.addModelScope( window );
 389		Backbone.Relational.eventQueue = new Backbone.BlockingQueue();
 390	}
 391
 392	/**
 393	 * Initialize a few models that are used in a large number of tests
 394	 */
 395	function initObjects() {
 396		reset();
 397
 398		window.person1 = new Person({
 399			id: 'person-1',
 400			name: 'boy',
 401			likesALot: 'person-2',
 402			resource_uri: 'person-1',
 403			user: { id: 'user-1', login: 'dude', email: 'me@gmail.com', resource_uri: 'user-1' }
 404		});
 405
 406		window.person2 = new Person({
 407			id: 'person-2',
 408			name: 'girl',
 409			likesALot: 'person-1',
 410			resource_uri: 'person-2'
 411		});
 412
 413		window.person3 = new Person({
 414			id: 'person-3',
 415			resource_uri: 'person-3'
 416		});
 417
 418		window.oldCompany = new Company({
 419			id: 'company-1',
 420			name: 'Big Corp.',
 421			ceo: {
 422				name: 'Big Boy'
 423			},
 424			employees: [ { person: 'person-3' } ], // uses the 'Job' link table to achieve many-to-many. No 'id' specified!
 425			resource_uri: 'company-1'
 426		});
 427
 428		window.newCompany = new Company({
 429			id: 'company-2',
 430			name: 'New Corp.',
 431			employees: [ { person: 'person-2' } ],
 432			resource_uri: 'company-2'
 433		});
 434
 435		window.ourHouse = new House({
 436			id: 'house-1',
 437			location: 'in the middle of the street',
 438			occupants: ['person-2'],
 439			resource_uri: 'house-1'
 440		});
 441
 442		window.theirHouse = new House({
 443			id: 'house-2',
 444			location: 'outside of town',
 445			occupants: [],
 446			resource_uri: 'house-2'
 447		});
 448	}
 449
 450	module ( "General / Backbone", { setup: reset } );
 451
 452		test( "Prototypes, constructors and inheritance", function() {
 453			// This stuff makes my brain hurt a bit. So, for reference:
 454			var Model = Backbone.Model.extend(),
 455				i = new Backbone.Model(),
 456				iModel = new Model();
 457
 458			var RelModel= Backbone.RelationalModel.extend(),
 459				iRel = new Backbone.RelationalModel(),
 460				iRelModel = new RelModel();
 461
 462			// Both are functions, so their `constructor` is `Function`
 463			ok( Backbone.Model.constructor === Backbone.RelationalModel.constructor );
 464
 465			ok( Backbone.Model !== Backbone.RelationalModel );
 466			ok( Backbone.Model === Backbone.Model.prototype.constructor );
 467			ok( Backbone.RelationalModel === Backbone.RelationalModel.prototype.constructor );
 468			ok( Backbone.Model.prototype.constructor !== Backbone.RelationalModel.prototype.constructor );
 469
 470			ok( Model.prototype instanceof Backbone.Model );
 471			ok( !( Model.prototype instanceof Backbone.RelationalModel ) );
 472			ok( RelModel.prototype instanceof Backbone.Model );
 473			ok( Backbone.RelationalModel.prototype instanceof Backbone.Model );
 474			ok( RelModel.prototype instanceof Backbone.RelationalModel );
 475
 476			ok( i instanceof Backbone.Model );
 477			ok( !( i instanceof Backbone.RelationalModel ) );
 478			ok( iRel instanceof Backbone.Model );
 479			ok( iRel instanceof Backbone.RelationalModel );
 480
 481			ok( iModel instanceof Backbone.Model );
 482			ok( !( iModel instanceof Backbone.RelationalModel ) );
 483			ok( iRelModel instanceof Backbone.Model );
 484			ok( iRelModel instanceof Backbone.RelationalModel );
 485		});
 486
 487		test('Collection#set', 1, function() {
 488			var a = new Backbone.Model({id: 3, label: 'a'} ),
 489				b = new Backbone.Model({id: 2, label: 'b'} ),
 490				col = new Backbone.Collection([a]);
 491
 492			col.set([a,b], {add: true, merge: false, remove: true});
 493			ok( col.length === 2 );
 494		});
 495
 496
 497	module( "Backbone.Semaphore", { setup: reset } );
 498
 499	
 500		test( "Unbounded", 10, function() {
 501			var semaphore = _.extend( {}, Backbone.Semaphore );
 502			ok( !semaphore.isLocked(), 'Semaphore is not locked initially' );
 503			semaphore.acquire();
 504			ok( semaphore.isLocked(), 'Semaphore is locked after acquire' );
 505			semaphore.acquire();
 506			equal( semaphore._permitsUsed, 2 ,'_permitsUsed should be incremented 2 times' );
 507			
 508			semaphore.setAvailablePermits( 4 );
 509			equal( semaphore._permitsAvailable, 4 ,'_permitsAvailable should be 4' );
 510			
 511			semaphore.acquire();
 512			semaphore.acquire();
 513			equal( semaphore._permitsUsed, 4 ,'_permitsUsed should be incremented 4 times' );
 514			
 515			try {
 516				semaphore.acquire();
 517			}
 518			catch( ex ) {
 519				ok( true, 'Error thrown when attempting to acquire too often' );
 520			}
 521			
 522			semaphore.release();
 523			equal( semaphore._permitsUsed, 3 ,'_permitsUsed should be decremented to 3' );
 524			
 525			semaphore.release();
 526			semaphore.release();
 527			semaphore.release();
 528			equal( semaphore._permitsUsed, 0 ,'_permitsUsed should be decremented to 0' );
 529			ok( !semaphore.isLocked(), 'Semaphore is not locked when all permits are released' );
 530			
 531			try {
 532				semaphore.release();
 533			}
 534			catch( ex ) {
 535				ok( true, 'Error thrown when attempting to release too often' );
 536			}
 537		});
 538	
 539	
 540	module( "Backbone.BlockingQueue", { setup: reset } );
 541	
 542	
 543		test( "Block", function() {
 544			var queue = new Backbone.BlockingQueue();
 545			var count = 0;
 546			var increment = function() { count++; };
 547			var decrement = function() { count--; };
 548			
 549			queue.add( increment );
 550			ok( count === 1, 'Increment executed right away' );
 551			
 552			queue.add( decrement );
 553			ok( count === 0, 'Decrement executed right away' );
 554			
 555			queue.block();
 556			queue.add( increment );
 557			
 558			ok( queue.isLocked(), 'Queue is blocked' );
 559			equal( count, 0, 'Increment did not execute right away' );
 560			
 561			queue.block();
 562			queue.block();
 563			
 564			equal( queue._permitsUsed, 3 ,'_permitsUsed should be incremented to 3' );
 565			
 566			queue.unblock();
 567			queue.unblock();
 568			queue.unblock();
 569			
 570			equal( count, 1, 'Increment executed' );
 571		});
 572	
 573	
 574	module( "Backbone.Store", { setup: initObjects } );
 575	
 576	
 577		test( "Initialized", function() {
 578			// `initObjects` instantiates models of the following types: `Person`, `Job`, `Company`, `User`, `House` and `Password`.
 579			equal( Backbone.Relational.store._collections.length, 6, "Store contains 6 collections" );
 580		});
 581		
 582		test( "getObjectByName", function() {
 583			equal( Backbone.Relational.store.getObjectByName( 'Backbone.RelationalModel' ), Backbone.RelationalModel );
 584		});
 585
 586		test( "Add and remove from store", function() {
 587			var coll = Backbone.Relational.store.getCollection( person1 );
 588			var length = coll.length;
 589			
 590			var person = new Person({
 591				id: 'person-10',
 592				name: 'Remi',
 593				resource_uri: 'person-10'
 594			});
 595			
 596			ok( coll.length === length + 1, "Collection size increased by 1" );
 597			
 598			var request = person.destroy();
 599			// Trigger the 'success' callback to fire the 'destroy' event
 600			request.success();
 601			
 602			ok( coll.length === length, "Collection size decreased by 1" );
 603		});
 604
 605		test( "addModelScope", function() {
 606			var models = {};
 607			Backbone.Relational.store.addModelScope( models );
 608
 609			models.Book = Backbone.RelationalModel.extend({
 610				relations: [{
 611					type: Backbone.HasMany,
 612					key: 'pages',
 613					relatedModel: 'Page',
 614					createModels: false,
 615					reverseRelation: {
 616						key: 'book'
 617					}
 618				}]
 619			});
 620			models.Page = Backbone.RelationalModel.extend();
 621
 622			var book = new models.Book();
 623			var page = new models.Page({ book: book });
 624
 625			ok( book.relations.length === 1 );
 626			ok( book.get( 'pages' ).length === 1 );
 627		});
 628
 629		test( "addModelScope with submodels and namespaces", function() {
 630			var ns = {};
 631			ns.People = {};
 632			Backbone.Relational.store.addModelScope( ns );
 633
 634			ns.People.Person = Backbone.RelationalModel.extend({
 635				subModelTypes: {
 636					'Student': 'People.Student'
 637				},
 638				iam: function() { return "I am an abstract person"; }
 639			});
 640
 641			ns.People.Student = ns.People.Person.extend({
 642				iam: function() { return "I am a student"; }
 643			});
 644
 645			ns.People.PersonCollection = Backbone.Collection.extend({
 646				model: ns.People.Person
 647			});
 648
 649			var people = new ns.People.PersonCollection([{name: "Bob", type: "Student"}]);
 650
 651			ok( people.at(0).iam() === "I am a student" );
 652		});
 653
 654		test( "removeModelScope", function() {
 655			var models = {};
 656			Backbone.Relational.store.addModelScope( models );
 657
 658			models.Page = Backbone.RelationalModel.extend();
 659
 660			ok( Backbone.Relational.store.getObjectByName( 'Page' ) === models.Page );
 661			ok( Backbone.Relational.store.getObjectByName( 'Person' ) === window.Person );
 662
 663			Backbone.Relational.store.removeModelScope( models );
 664
 665			ok( !Backbone.Relational.store.getObjectByName( 'Page' ) );
 666			ok( Backbone.Relational.store.getObjectByName( 'Person' ) === window.Person );
 667
 668			Backbone.Relational.store.removeModelScope( window );
 669
 670			ok( !Backbone.Relational.store.getObjectByName( 'Person' ) );
 671		});
 672
 673		test( "unregister", function() {
 674			var animalStoreColl = Backbone.Relational.store.getCollection( Animal ),
 675				animals = null,
 676				animal = null;
 677
 678			// Single model
 679			animal = new Animal( { id: 'a1' } );
 680			ok( Backbone.Relational.store.find( Animal, 'a1' ) === animal );
 681
 682			Backbone.Relational.store.unregister( animal );
 683			ok( Backbone.Relational.store.find( Animal, 'a1' ) === null );
 684
 685			animal = new Animal( { id: 'a2' } );
 686			ok( Backbone.Relational.store.find( Animal, 'a2' ) === animal );
 687
 688			animal.trigger( 'relational:unregister', animal );
 689			ok( Backbone.Relational.store.find( Animal, 'a2' ) === null );
 690
 691			ok( animalStoreColl.size() === 0 );
 692
 693			// Collection
 694			animals = new AnimalCollection( [ { id: 'a3' }, { id: 'a4' } ] );
 695			animal = animals.first();
 696
 697			ok( Backbone.Relational.store.find( Animal, 'a3' ) === animal );
 698			ok( animalStoreColl.size() === 2 );
 699
 700			Backbone.Relational.store.unregister( animals );
 701			ok( Backbone.Relational.store.find( Animal, 'a3' ) === null );
 702
 703			ok( animalStoreColl.size() === 0 );
 704
 705			// Store collection
 706			animals = new AnimalCollection( [ { id: 'a5' }, { id: 'a6' } ] );
 707			ok( animalStoreColl.size() === 2 );
 708
 709			Backbone.Relational.store.unregister( animalStoreColl );
 710			ok( animalStoreColl.size() === 0 );
 711
 712			// Model type
 713			animals = new AnimalCollection( [ { id: 'a7' }, { id: 'a8' } ] );
 714			ok( animalStoreColl.size() === 2 );
 715
 716			Backbone.Relational.store.unregister( Animal );
 717			ok( animalStoreColl.size() === 0 );
 718		});
 719
 720		test( "`eventQueue` is unblocked again after a duplicate id error", 3, function() {
 721			var node = new Node( { id: 1 } );
 722
 723			ok( Backbone.Relational.eventQueue.isBlocked() === false );
 724
 725			try {
 726				duplicateNode = new Node( { id: 1 } );
 727			}
 728			catch( error ) {
 729				ok( true, "Duplicate id error thrown" );
 730			}
 731
 732			ok( Backbone.Relational.eventQueue.isBlocked() === false );
 733		});
 734
 735		test( "Don't allow setting a duplicate `id`", 4, function() {
 736			var a = new Zoo(); // This object starts with no id.
 737			var b = new Zoo( { 'id': 42 } );  // This object starts with an id of 42.
 738
 739			equal( b.id, 42 );
 740
 741			try {
 742				a.set( 'id', 42 );
 743			}
 744			catch( error ) {
 745				ok( true, "Duplicate id error thrown" );
 746			}
 747
 748			ok( !a.id, "a.id=" + a.id );
 749			equal( b.id, 42 );
 750		});
 751		
 752		test( "Models are created from objects, can then be found, destroyed, cannot be found anymore", function() {
 753			var houseId = 'house-10';
 754			var personId = 'person-10';
 755			
 756			var anotherHouse = new House({
 757				id: houseId,
 758				location: 'no country for old men',
 759				resource_uri: houseId,
 760				occupants: [{
 761					id: personId,
 762					name: 'Remi',
 763					resource_uri: personId
 764				}]
 765			});
 766			
 767			ok( anotherHouse.get('occupants') instanceof Backbone.Collection, "Occupants is a Collection" );
 768			ok( anotherHouse.get('occupants').get( personId ) instanceof Person, "Occupants contains the Person with id='" + personId + "'" );
 769			
 770			var person = Backbone.Relational.store.find( Person, personId );
 771			
 772			ok( person, "Person with id=" + personId + " is found in the store" );
 773			
 774			var request = person.destroy();
 775			// Trigger the 'success' callback to fire the 'destroy' event
 776			request.success();
 777			
 778			person = Backbone.Relational.store.find( Person, personId );
 779			
 780			ok( !person, personId + " is not found in the store anymore" );
 781			ok( !anotherHouse.get('occupants').get( personId ), "Occupants no longer contains the Person with id='" + personId + "'" );
 782			
 783			request = anotherHouse.destroy();
 784			// Trigger the 'success' callback to fire the 'destroy' event
 785			request.success();
 786			
 787			var house = Backbone.Relational.store.find( House, houseId );
 788			
 789			ok( !house, houseId + " is not found in the store anymore" );
 790		});
 791		
 792		
 793		test( "Model.collection is the first collection a Model is added to by an end-user (not its Backbone.Store collection!)", function() {
 794			var person = new Person( { id: 5, name: 'New guy' } );
 795			var personColl = new PersonCollection();
 796			personColl.add( person );
 797			ok( person.collection === personColl );
 798		});
 799
 800		test( "Models don't get added to the store until the get an id", function() {
 801			var storeColl = Backbone.Relational.store.getCollection( Node ),
 802				node1 = new Node( { id: 1 } ),
 803				node2 = new Node();
 804
 805			ok( storeColl.contains( node1 ) );
 806			ok( !storeColl.contains( node2 ) );
 807
 808			node2.set( { id: 2 } );
 809
 810			ok( storeColl.contains( node1 ) );
 811		});
 812		
 813		test( "All models can be found after adding them to a Collection via 'Collection.reset'", function() {
 814			var nodes = [
 815				{ id: 1, parent: null },
 816				{ id: 2, parent: 1 },
 817				{ id: 3, parent: 4 },
 818				{ id: 4, parent: 1 }
 819			];
 820			
 821			var nodeList = new NodeList();
 822			nodeList.reset( nodes );
 823			
 824			var storeColl = Backbone.Relational.store.getCollection( Node );
 825			equal( storeColl.length, 4, "Every Node is in Backbone.Relational.store" );
 826			ok( Backbone.Relational.store.find( Node, 1 ) instanceof Node, "Node 1 can be found" );
 827			ok( Backbone.Relational.store.find( Node, 2 ) instanceof Node, "Node 2 can be found" );
 828			ok( Backbone.Relational.store.find( Node, 3 ) instanceof Node, "Node 3 can be found" );
 829			ok( Backbone.Relational.store.find( Node, 4 ) instanceof Node, "Node 4 can be found" );
 830		});
 831		
 832		test( "Inheritance creates and uses a separate collection", function() {
 833			var whale = new Animal( { id: 1, species: 'whale' } );
 834			ok( Backbone.Relational.store.find( Animal, 1 ) === whale );
 835			
 836			var numCollections = Backbone.Relational.store._collections.length;
 837			
 838			var Mammal = Animal.extend({
 839				urlRoot: '/mammal/'
 840			});
 841			
 842			var lion = new Mammal( { id: 1, species: 'lion' } );
 843			var donkey = new Mammal( { id: 2, species: 'donkey' } );
 844			
 845			equal( Backbone.Relational.store._collections.length, numCollections + 1 );
 846			ok( Backbone.Relational.store.find( Animal, 1 ) === whale );
 847			ok( Backbone.Relational.store.find( Mammal, 1 ) === lion );
 848			ok( Backbone.Relational.store.find( Mammal, 2 ) === donkey );
 849			
 850			var Primate = Mammal.extend({
 851				urlRoot: '/primate/'
 852			});
 853			
 854			var gorilla = new Primate( { id: 1, species: 'gorilla' } );
 855			
 856			equal( Backbone.Relational.store._collections.length, numCollections + 2 );
 857			ok( Backbone.Relational.store.find( Primate, 1 ) === gorilla );
 858		});
 859		
 860		test( "Inheritance with `subModelTypes` uses the same collection as the model's super", function() {
 861			var Mammal = Animal.extend({
 862				subModelTypes: {
 863					'primate': 'Primate',
 864					'carnivore': 'Carnivore'
 865				}
 866			});
 867
 868			window.Primate = Mammal.extend();
 869			window.Carnivore = Mammal.extend();
 870
 871			var lion = new Carnivore( { id: 1, species: 'lion' } );
 872			var wolf = new Carnivore( { id: 2, species: 'wolf' } );
 873
 874			var numCollections = Backbone.Relational.store._collections.length;
 875
 876			var whale = new Mammal( { id: 3, species: 'whale' } );
 877
 878			equal( Backbone.Relational.store._collections.length, numCollections, "`_collections` should have remained the same" );
 879
 880			ok( Backbone.Relational.store.find( Mammal, 1 ) === lion );
 881			ok( Backbone.Relational.store.find( Mammal, 2 ) === wolf );
 882			ok( Backbone.Relational.store.find( Mammal, 3 ) === whale );
 883			ok( Backbone.Relational.store.find( Carnivore, 1 ) === lion );
 884			ok( Backbone.Relational.store.find( Carnivore, 2 ) === wolf );
 885			ok( Backbone.Relational.store.find( Carnivore, 3 ) !== whale );
 886
 887			var gorilla = new Primate( { id: 4, species: 'gorilla' } );
 888
 889			equal( Backbone.Relational.store._collections.length, numCollections, "`_collections` should have remained the same" );
 890
 891			ok( Backbone.Relational.store.find( Animal, 4 ) !== gorilla );
 892			ok( Backbone.Relational.store.find( Mammal, 4 ) === gorilla );
 893			ok( Backbone.Relational.store.find( Primate, 4 ) === gorilla );
 894
 895			delete window.Primate;
 896			delete window.Carnivore;
 897		});
 898
 899		test( "findOrCreate does not modify attributes hash if parse is used, prior to creating new model", function () {
 900			var model = Backbone.RelationalModel.extend({
 901				parse: function( response ) {
 902					response.id = response.id + 'something';
 903					return response;
 904				}
 905			});
 906			var attributes = {id: 42, foo: "bar"};
 907			var testAttributes = {id: 42, foo: "bar"};
 908
 909			model.findOrCreate( attributes, { parse: true, merge: false, create: false } );
 910
 911			ok( _.isEqual( attributes, testAttributes ), "attributes hash should not be modified" );
 912		});
 913		
 914
 915	module( "Backbone.RelationalModel", { setup: initObjects } );
 916		
 917		test( "Return values: set returns the Model", function() {
 918			var personId = 'person-10';
 919			var person = new Person({
 920				id: personId,
 921				name: 'Remi',
 922				resource_uri: personId
 923			});
 924			
 925			var result = person.set( { 'name': 'Hector' } );
 926			ok( result === person, "Set returns the model" );
 927		});
 928
 929		test( "`clear`", function() {
 930			var person = new Person( { id: 'person-10' } );
 931
 932			ok( person === Person.findOrCreate( 'person-10' ) );
 933
 934			person.clear();
 935
 936			ok( !person.id );
 937
 938			ok( !Person.findOrCreate( 'person-10' ) );
 939
 940			person.set( { id: 'person-10' } );
 941
 942			ok( person === Person.findOrCreate( 'person-10' ) );
 943		});
 944		
 945		test( "getRelations", function() {
 946			var relations = person1.getRelations();
 947
 948			equal( relations.length, 6 );
 949
 950			ok( _.every( relations, function( rel ) {
 951					return rel instanceof Backbone.Relation;
 952				})
 953			);
 954		});
 955		
 956		test( "getRelation", function() {
 957			var userRel = person1.getRelation( 'user' );
 958
 959			ok( userRel instanceof Backbone.HasOne );
 960			equal( userRel.key, 'user' );
 961
 962			var jobsRel = person1.getRelation( 'jobs' );
 963
 964			ok( jobsRel instanceof Backbone.HasMany );
 965			equal( jobsRel.key, 'jobs' );
 966
 967			ok( person1.getRelation( 'nope' ) == null );
 968		});
 969		
 970		test( "getAsync on a HasOne relation", function() {
 971			var errorCount = 0;
 972			var person = new Person({
 973				id: 'person-10',
 974				resource_uri: 'person-10',
 975				user: 'user-10'
 976			});
 977
 978			var idsToFetch = person.getIdsToFetch( 'user' );
 979			deepEqual( idsToFetch, [ 'user-10' ] );
 980			
 981			var request = person.getAsync( 'user', { error: function() {
 982					errorCount++;
 983				}
 984			});
 985			
 986			ok( _.isObject( request ) && request.always && request.done && request.fail );
 987			equal( window.requests.length, 1, "A single request has been made" );
 988			ok( person.get( 'user' ) instanceof User );
 989			
 990			// Triggering the 'error' callback should destroy the model
 991			window.requests[ 0 ].error();
 992			// Trigger the 'success' callback on the `destroy` call to actually fire the 'destroy' event
 993			_.last( window.requests ).success();
 994
 995			ok( !person.get( 'user' ), "User has been destroyed & removed" );
 996			equal( errorCount, 1, "The error callback executed successfully" );
 997			
 998			var person2 = new Person({
 999				id: 'person-11',
1000				resource_uri: 'person-11'
1001			});
1002			
1003			request = person2.getAsync( 'user' );
1004			equal( window.requests.length, 1, "No request was made" );
1005		});
1006		
1007		test( "getAsync on a HasMany relation", function() {
1008			var errorCount = 0;
1009			var zoo = new Zoo({
1010				animals: [ { id: 'monkey-1' }, 'lion-1', 'zebra-1' ]
1011			});
1012
1013			var idsToFetch = zoo.getIdsToFetch( 'animals' );
1014			deepEqual( idsToFetch, [ 'lion-1', 'zebra-1' ] );
1015			
1016			//
1017			// Case 1: separate requests for each model
1018			//
1019			window.requests = [];
1020			var request = zoo.getAsync( 'animals', { error: function() { errorCount++; } } );
1021
1022			ok( _.isObject( request ) && request.always && request.done && request.fail );
1023			equal( window.requests.length, 2, "Two requests have been made (a separate one for each animal)" );
1024			equal( zoo.get( 'animals' ).length, 3, "Three animals in the zoo" );
1025			
1026			// Triggering the 'error' callback for one request should destroy the model
1027			window.requests[ 0 ].error();
1028			// Trigger the 'success' callback on the `destroy` call to actually fire the 'destroy' event
1029			_.last( window.requests ).success();
1030			
1031			equal( zoo.get( 'animals' ).length, 2, "Two animals left in the zoo" );
1032			equal( errorCount, 1, "The error callback executed successfully" );
1033			
1034			//
1035			// Case 2: one request per fetch (generated by the collection)
1036			//
1037			// Give 'zoo' a custom url function that builds a url to fetch a set of models from their ids
1038			window.requests = [];
1039			errorCount = 0;
1040
1041			zoo.get( 'animals' ).url = function( models ) {
1042				return '/animal/' + ( models ? 'set/' + _.pluck( models, 'id' ).join(';') + '/' : '' );
1043			};
1044			
1045			// Set two new animals to be fetched; both should be fetched in a single request
1046			zoo.set( { animals: [ 'monkey-1', 'lion-2', 'zebra-2' ] } );
1047			
1048			equal( zoo.get( 'animals' ).length, 1 );
1049
1050			// `getAsync` creates two placeholder models for the ids present in the relation.
1051			window.requests = [];
1052			request = zoo.getAsync( 'animals', { error: function() { errorCount++; } } );
1053			
1054			ok( _.isObject( request ) && request.always && request.done && request.fail );
1055			equal( window.requests.length, 1 );
1056			equal( _.last( window.requests ).url, '/animal/set/lion-2;zebra-2/' );
1057			equal( zoo.get('animals').length, 3, "Three animals in the zoo" );
1058			
1059			// Triggering the 'error' callback (some error occured during fetching) should trigger the 'destroy' event
1060			// on both fetched models, but should NOT actually make 'delete' requests to the server!
1061			_.last( window.requests ).error();
1062			equal( window.requests.length, 1, "An error occured when fetching, but no DELETE requests are made to the server while handling local cleanup." );
1063			
1064			equal( zoo.get( 'animals' ).length, 1, "Both animals are destroyed" );
1065			equal( errorCount, 2, "The error callback executed successfully for both models" );
1066			
1067			// Try to re-fetch; nothing left to get though
1068			window.requests = [];
1069			request = zoo.getAsync( 'animals' );
1070			
1071			equal( window.requests.length, 0 );
1072			equal( zoo.get( 'animals' ).length, 1 );
1073
1074			// Re-fetch the existing model
1075			window.requests = [];
1076			request = zoo.getAsync( 'animals', { refresh: true } );
1077
1078			equal( window.requests.length, 1 );
1079			equal( _.last( window.requests ).url, '/animal/set/monkey-1/' );
1080			equal( zoo.get( 'animals' ).length, 1 );
1081
1082			// An error while refreshing an existing model shouldn't affect it
1083			window.requests[ 0 ].error();
1084			equal( zoo.get( 'animals' ).length, 1 );
1085		});
1086
1087		test( "getAsync", 8, function() {
1088			var zoo = Zoo.findOrCreate( { id: 'z-1', animals: [ 'cat-1' ] } );
1089
1090			zoo.on( 'add:animals', function( animal ) {
1091				console.log( 'add:animals=%o', animal );
1092				animal.on( 'change:favoriteFood', function( model, food ) {
1093					console.log( '%s eats %s', animal.get( 'name' ), food.get( 'name' ) );
1094				});
1095			});
1096
1097			zoo.getAsync( 'animals' ).done( function( animals ) {
1098				ok( animals instanceof AnimalCollection );
1099				ok( animals.length === 1 );
1100
1101				var cat = zoo.get( 'animals' ).at( 0 );
1102				equal( cat.get( 'name' ), 'Tiger' );
1103
1104				cat.getAsync( 'favoriteFood' ).done( function( food ) {
1105					equal( food.get( 'name' ), 'Cheese', 'Favorite food is cheese' );
1106				});
1107			});
1108
1109			equal( zoo.get( 'animals' ).length, 1 );
1110			equal( window.requests.length, 1 );
1111			equal( _.last( window.requests ).url, '/animal/cat-1' );
1112
1113			// Declare success
1114			_.last( window.requests ).respond( 200, { id: 'cat-1', name: 'Tiger', favoriteFood: 'f-2' } );
1115			equal( window.requests.length, 2 );
1116
1117			_.last( window.requests ).respond( 200, { id: 'f-2', name: 'Cheese' } );
1118		});
1119
1120		test( "autoFetch a HasMany relation", function() {
1121			var shopOne = new Shop({
1122				id: 'shop-1',
1123				customers: ['customer-1', 'customer-2']
1124			});
1125
1126			equal( requests.length, 2, "Two requests to fetch the users has been made" );
1127			requests.length = 0;
1128
1129			var shopTwo = new Shop({
1130				id: 'shop-2',
1131				customers: ['customer-1', 'customer-3']
1132			});
1133
1134			equal( requests.length, 1, "A request to fetch a user has been made" ); //as customer-1 has already been fetched
1135		});
1136
1137		test( "autoFetch on a HasOne relation (with callbacks)", function() {
1138			var shopThree = new Shop({
1139				id: 'shop-3',
1140				address: 'address-3'
1141			});
1142
1143			equal( requests.length, 1, "A request to fetch the address has been made" );
1144			
1145			var res = { successOK: false, errorOK: false };
1146			
1147			requests[0].success( res );
1148			equal( res.successOK, true, "The success() callback has been called" );
1149			requests.length = 0;
1150
1151			var shopFour = new Shop({
1152				id: 'shop-4',
1153				address: 'address-4'
1154			});
1155
1156			equal( requests.length, 1, "A request to fetch the address has been made" );
1157			requests[0].error( res );
1158			equal( res.errorOK, true, "The error() callback has been called" );
1159		});
1160
1161		test( "autoFetch false by default", function() {
1162			var agentOne = new Agent({
1163				id: 'agent-1',
1164				customers: ['customer-4', 'customer-5']
1165			});
1166
1167			equal( requests.length, 0, "No requests to fetch the customers has been made as autoFetch was not defined" );
1168
1169			agentOne = new Agent({
1170				id: 'agent-2',
1171				address: 'address-5'
1172			});
1173
1174			equal( requests.length, 0, "No requests to fetch the customers has been made as autoFetch was set to false" );
1175		});
1176
1177		test( "`clone`", function() {
1178			var user = person1.get( 'user' );
1179
1180			// HasOne relations should stay with the original model
1181			var newPerson = person1.clone();
1182
1183			ok( newPerson.get( 'user' ) === null );
1184			ok( person1.get( 'user' ) === user );
1185		});
1186
1187		test( "`save` (with `wait`)", function() {
1188			var node1 = new Node({ id: '1', parent: '3', name: 'First node' } ),
1189				node2 = new Node({ id: '2', name: 'Second node' });
1190
1191			// Set node2's parent to node1 in a request with `wait: true`
1192			var request = node2.save( 'parent', node1, { wait: true } ),
1193				json = JSON.parse( request.data );
1194
1195			ok( _.isObject( json.parent ) );
1196			equal( json.parent.id, '1' );
1197			equal( node2.get( 'parent' ), null );
1198
1199			request.success();
1200
1201			equal( node2.get( 'parent' ), node1 );
1202
1203			// Save a new node as node2's parent, only specified as JSON in the call to save
1204			request = node2.save( 'parent', { id: '3', parent: '2', name: 'Third node' }, { wait: true } );
1205			json = JSON.parse( request.data );
1206
1207			ok( _.isObject( json.parent ) );
1208			equal( json.parent.id, '3' );
1209			equal( node2.get( 'parent' ), node1 );
1210
1211			request.success();
1212
1213			var node3 = node2.get( 'parent' );
1214
1215			ok( node3 instanceof Node );
1216			equal( node3.id, '3' );
1217
1218			// Try to reset node2's parent to node1, but fail the request
1219			request = node2.save( 'parent', node1, { wait: true } );
1220			request.error();
1221
1222			equal( node2.get( 'parent' ), node3 );
1223
1224			// See what happens for different values of `includeInJSON`...
1225			// For `Person.user`, just the `idAttribute` should be serialized to the keyDestination `user_id`
1226			var user1 = person1.get( 'user' );
1227			request = person1.save( 'user', null, { wait: true } );
1228			json = JSON.parse( request.data );
1229			console.log( request, json );
1230
1231			equal( person1.get( 'user' ), user1 );
1232
1233			request.success( json );
1234
1235			equal( person1.get( 'user' ), null );
1236
1237			request = person1.save( 'user', user1, { wait: true } );
1238			json = JSON.parse( request.data );
1239
1240			equal( json.user_id, user1.id );
1241			equal( person1.get( 'user' ), null );
1242
1243			request.success( json );
1244
1245			equal( person1.get( 'user' ), user1 );
1246
1247			// Save a collection with `wait: true`
1248			var zoo = new Zoo( { id: 'z1' } ),
1249				animal1 = new Animal( { id: 'a1', species: 'Goat', name: 'G' } ),
1250				coll = new Backbone.Collection( [ { id: 'a2', species: 'Rabbit', name: 'R' }, animal1 ] );
1251
1252			request = zoo.save( 'animals', coll, { wait: true } );
1253			json = JSON.parse( request.data );
1254			console.log( request, json );
1255
1256			ok( zoo.get( 'animals' ).length === 0 );
1257
1258			request.success( json );
1259
1260			ok( zoo.get( 'animals' ).length === 2 );
1261			console.log( animal1 );
1262		});
1263
1264		test( "`Collection.create` (with `wait`)", function() {
1265			var nodeColl = new NodeList(),
1266				nodesAdded = 0;
1267
1268			nodeColl.on( 'add', function( model, collection, options ) {
1269				nodesAdded++;
1270			});
1271
1272			nodeColl.create({ id: '3', parent: '2', name: 'Third node' }, { wait: true });
1273			ok( nodesAdded === 0 );
1274			requests[ requests.length - 1 ].success();
1275			ok( nodesAdded === 1 );
1276
1277			nodeColl.create({ id: '4', name: 'Third node' }, { wait: true });
1278			ok( nodesAdded === 1 );
1279			requests[ requests.length - 1 ].error();
1280			ok( nodesAdded === 1 );
1281		});
1282		
1283		test( "`toJSON`: simple cases", function() {
1284			var node = new Node({ id: '1', parent: '3', name: 'First node' });
1285			new Node({ id: '2', parent: '1', name: 'Second node' });
1286			new Node({ id: '3', parent: '2', name: 'Third node' });
1287			
1288			var json = node.toJSON();
1289
1290			ok( json.children.length === 1 );
1291		});
1292
1293		test("'toJSON' should return null for relations that are set to null, even when model is not fetched", function() {
1294			var person = new Person( { user : 'u1' } );
1295
1296			equal( person.toJSON().user_id, 'u1' );
1297			person.set( 'user', null );
1298			equal( person.toJSON().user_id, null );
1299
1300			person = new Person( { user: new User( { id : 'u2' } ) } );
1301
1302			equal( person.toJSON().user_id, 'u2' );
1303			person.set( { user: 'unfetched_user_id' } );
1304			equal( person.toJSON().user_id, 'unfetched_user_id' );
1305		});
1306
1307		test( "`toJSON` should include ids for 'unknown' or 'missing' models (if `includeInJSON` is `idAttribute`)", function() {
1308			// See GH-191
1309
1310			// `Zoo` shouldn't be affected; `animals.includeInJSON` is not equal to `idAttribute`
1311			var zoo = new Zoo({ id: 'z1', animals: [ 'a1', 'a2' ] }),
1312				zooJSON = zoo.toJSON();
1313
1314			ok( _.isArray( zooJSON.animals ) );
1315			equal( zooJSON.animals.length, 0, "0 animals in zooJSON; it serializes an array of attributes" );
1316
1317			var a1 = new Animal( { id: 'a1' } );
1318			zooJSON = zoo.toJSON();
1319			equal( zooJSON.animals.length, 1, "1 animals in zooJSON; it serializes an array of attributes" );
1320
1321			// Agent -> Customer; `idAttribute` on a HasMany
1322			var agent = new Agent({ id: 'a1', customers: [ 'c1', 'c2' ] } ),
1323				agentJSON = agent.toJSON();
1324
1325			ok( _.isArray( agentJSON.customers ) );
1326			equal( agentJSON.customers.length, 2, "2 customers in agentJSON; it serializes the `idAttribute`" );
1327
1328			var c1 = new Customer( { id: 'c1' } );
1329			equal( agent.get( 'customers' ).length, 1, '1 customer in agent' );
1330
1331			agentJSON = agent.toJSON();
1332			equal( agentJSON.customers.length, 2, "2 customers in agentJSON; `idAttribute` for 1 missing, other existing" );
1333
1334			//c1.destroy();
1335
1336			//agentJSON = agent.toJSON();
1337			//equal( agentJSON.customers.length, 1, "1 customer in agentJSON; `idAttribute` for 1 missing, other destroyed" );
1338
1339			agent.set( 'customers', [ 'c1', 'c3' ] );
1340			var c3 = new Customer( { id: 'c3' } );
1341
1342			agentJSON = agent.toJSON();
1343			equal( agentJSON.customers.length, 2, "2 customers in agentJSON; 'c1' already existed, 'c3' created" );
1344
1345			agent.get( 'customers' ).remove( c1 );
1346
1347			agentJSON = agent.toJSON();
1348			equal( agentJSON.customers.length, 1, "1 customer in agentJSON; 'c1' removed, 'c3' still in there" );
1349
1350			// Person -> User; `idAttribute` on a HasOne
1351			var person = new Person({ id: 'p1', user: 'u1' } ),
1352				personJSON = person.toJSON();
1353
1354			equal( personJSON.user_id, 'u1', "`user_id` gets set in JSON" );
1355
1356			var u1 = new User( { id: 'u1' } );
1357			personJSON = person.toJSON();
1358			ok( u1.get( 'person' ) === person );
1359			equal( personJSON.user_id, 'u1', "`user_id` gets set in JSON" );
1360
1361			person.set( 'user', 'u1' );
1362			personJSON = person.toJSON();
1363			equal( personJSON.user_id, 'u1', "`user_id` gets set in JSON" );
1364
1365			u1.destroy();
1366			personJSON = person.toJSON();
1367			ok( !u1.get( 'person' ) );
1368			equal( personJSON.user_id, 'u1', "`user_id` still gets set in JSON" );
1369		});
1370
1371		test( "`toJSON` should include ids for unregistered models (if `includeInJSON` is `idAttribute`)", function() {
1372	
1373			// Person -> User; `idAttribute` on a HasOne
1374			var person = new Person({ id: 'p1', user: 'u1' } ),
1375				personJSON = person.toJSON();
1376
1377			equal( personJSON.user_id, 'u1', "`user_id` gets set in JSON even though no user obj exists" );
1378
1379			var u1 = new User( { id: 'u1' } );
1380			personJSON = person.toJSON();
1381			ok( u1.get( 'person' ) === person );
1382			equal( personJSON.user_id, 'u1', "`user_id` gets set in JSON after matching user obj is created" );
1383
1384			Backbone.Relational.store.unregister(u1);
1385
1386			personJSON = person.toJSON();
1387			equal( personJSON.user_id, 'u1', "`user_id` gets set in JSON after user was unregistered from store" );
1388		});
1389
1390		test( "`parse` gets called through `findOrCreate`", function() {
1391			var parseCalled = 0;
1392			Zoo.prototype.parse = Animal.prototype.parse = function( resp, options ) {
1393				parseCalled++;
1394				return resp;
1395			};
1396
1397			var zoo = Zoo.findOrCreate({
1398				id: '1',
1399				name: 'San Diego Zoo',
1400				animals: [ { id: 'a' } ]
1401			}, { parse: true } );
1402			var animal = zoo.get( 'animals' ).first();
1403
1404			ok( animal.get( 'livesIn' ) );
1405			ok( animal.get( 'livesIn' ) instanceof Zoo );
1406			ok( animal.get( 'livesIn' ).get( 'animals' ).get( animal ) === animal );
1407
1408			// `parse` gets called by `findOrCreate` directly when trying to lookup `1`,
1409			// and the parsed attributes are passed to `build` (called from `findOrCreate`) with `{ parse: false }`,
1410			// rather than having `parse` called again by the Zoo constructor.
1411			ok( parseCalled === 1, 'parse called 1 time? ' + parseCalled );
1412
1413			parseCalled = 0;
1414
1415			animal = new Animal({ id: 'b' });
1416			animal.set({
1417				id: 'b',
1418				livesIn: {
1419					id: '2',
1420					name: 'San Diego Zoo',
1421					animals: [ 'b' ]
1422				}
1423			}, { parse: true } );
1424
1425			ok( animal.get( 'livesIn' ) );
1426			ok( animal.get( 'livesIn' ) instanceof Zoo );
1427			ok( animal.get( 'livesIn' ).get( 'animals' ).get( animal ) === animal );
1428
1429			ok( parseCalled === 0, 'parse called 0 times? ' + parseCalled );
1430
1431			// Reset `parse` methods
1432			Zoo.prototype.parse = Animal.prototype.parse = Backbone.RelationalModel.prototype.parse;
1433		});
1434
1435		test( "`Collection#parse` with RelationalModel simple case", function() {
1436			var Contact = Backbone.RelationalModel.extend({
1437				parse: function( response ) {
1438					response.bar = response.foo * 2;
1439					return response;
1440				}
1441			});
1442			var Contacts = Backbone.Collection.extend({
1443				model: Contact,
1444				url: '/contacts',
1445				parse: function( response ) {
1446					return response.items;
1447				}
1448			});
1449
1450			var contacts = new Contacts();
1451			contacts.fetch({
1452				// fake response for testing
1453				response: {
1454					status: 200,
1455					responseText: { items: [ { foo: 1 }, { foo: 2 } ] }
1456				}
1457			});
1458
1459			equal( contacts.length, 2, 'Collection response was fetched properly' );
1460			var contact = contacts.first();
1461			ok( contact , 'Collection has a non-null item' );
1462			ok( contact instanceof Contact, '... of the type type' );
1463			equal( contact.get('foo'), 1, '... with correct fetched value' );
1464			equal( contact.get('bar'), 2, '... with correct parsed value' );
1465		});
1466
1467		test( "By default, `parse` should only get called on top-level objects; not for nested models and collections", function() {
1468			var companyData = {
1469				'data': {
1470					'id': 'company-1',
1471					'contacts': [
1472						{
1473							'id': '1'
1474						},
1475						{
1476							'id': '2'
1477						}
1478					]
1479				}
1480			};
1481
1482			var Contact = Backbone.RelationalModel.extend();
1483			var Contacts = Backbone.Collection.extend({
1484				model: Contact
1485			});
1486
1487			var Company = Backbone.RelationalModel.extend({
1488				urlRoot: '/company/',
1489				relations: [{
1490					type: Backbone.HasMany,
1491					key: 'contacts',
1492					relatedModel: Contact,
1493					collectionType: Contacts
1494				}]
1495			});
1496
1497			var parseCalled = 0;
1498			Company.prototype.parse = Contact.prototype.parse = Contacts.prototype.parse = function( resp, options ) {
1499				parseCalled++;
1500				return resp.data || resp;
1501			};
1502
1503			var company = new Company( companyData, { parse: true } ),
1504				contacts = company.get( 'contacts' ),
1505				contact = contacts.first();
1506
1507			ok( company.id === 'company-1' );
1508			ok( contact && contact.id === '1', 'contact exists' );
1509			ok( parseCalled === 1, 'parse called 1 time? ' + parseCalled );
1510
1511			// simulate what would happen if company.fetch() was called.
1512			company.fetch({
1513				parse: true,
1514				response: {
1515					status: 200,
1516					responseText: _.clone( companyData )
1517				}
1518			});
1519
1520			ok( parseCalled === 2, 'parse called 2 times? ' + parseCalled );
1521
1522			ok( contacts === company.get( 'contacts' ), 'contacts collection is same instance after fetch' );
1523			equal( contacts.length, 2, '... with correct length' );
1524			ok( contact && contact.id === '1', 'contact exists' );
1525			ok( contact === contacts.first(), '... and same model instances' );
1526		});
1527
1528		test( "constructor.findOrCreate", function() {
1529			var personColl = Backbone.Relational.store.getCollection( person1 ),
1530				origPersonCollSize = personColl.length;
1531
1532			// Just find an existing model
1533			var person = Person.findOrCreate( person1.id );
1534
1535			ok( person === person1 );
1536			ok( origPersonCollSize === personColl.length, "Existing person was found (none created)" );
1537
1538			// Update an existing model
1539			person = Person.findOrCreate( { id: person1.id, name: 'dude' } );
1540
1541			equal( person.get( 'name' ), 'dude' );
1542			equal( person1.get( 'name' ), 'dude' );
1543
1544			ok( origPersonCollSize === personColl.length, "Existing person was updated (none created)" );
1545
1546			// Look for a non-existent person; 'options.create' is false
1547			person = Person.findOrCreate( { id: 5001 }, { create: false } );
1548
1549			ok( !person );
1550			ok( origPersonCollSize === personColl.length, "No person was found (none created)" );
1551
1552			// Create a new model
1553			person = Person.findOrCreate( { id: 5001 } );
1554
1555			ok( person instanceof Person );
1556			ok( origPersonCollSize + 1 === personColl.length, "No person was found (1 created)" );
1557
1558			// Find when options.merge is false
1559			person = Person.findOrCreate( { id: person1.id, name: 'phil' }, { merge: false } );
1560
1561			equal( person.get( 'name' ), 'dude' );
1562			equal( person1.get( 'name' ), 'dude' );
1563		});
1564
1565		test( "constructor.find", function() {
1566			var personColl = Backbone.Relational.store.getCollection( person1 ),
1567			origPersonCollSize = personColl.length;
1568
1569			// Look for a non-existent person
1570			person = Person.find( { id: 5001 } );
1571			ok( !person );
1572		});
1573
1574		test( "change events in relation can use changedAttributes properly", function() {
1575			var scope = {};
1576			Backbone.Relational.store.addModelScope( scope );
1577
1578			scope.PetAnimal = Backbone.RelationalModel.extend({
1579				subModelTypes: {
1580					'cat': 'Cat',
1581					'dog': 'Dog'
1582				}
1583			});
1584			scope.Dog = scope.PetAnimal.extend();
1585			scope.Cat = scope.PetAnimal.extend();
1586
1587			scope.PetOwner = Backbone.RelationalModel.extend({
1588				relations: [{
1589					type: Backbone.HasMany,
1590					key: 'pets',
1591					relatedModel: scope.PetAnimal,
1592					reverseRelation: {
1593						key: 'owner'
1594					}
1595				}]
1596			});
1597
1598			var owner = new scope.PetOwner( { id: 'owner-2354' } );
1599			var animal = new scope.Dog( { type: 'dog', id: '238902', color: 'blue' } );
1600			equal( animal.get('color'), 'blue', 'animal starts out blue' );
1601
1602			var changes = 0, changedAttrs = null;
1603			animal.on('change', function(model, options) {
1604				changes++;
1605				changedAttrs = model.changedAttributes();
1606			});
1607
1608			animal.set( { color: 'green' } );
1609			equal( changes, 1, 'change event gets called after animal.set' );
1610			equal( changedAttrs.color, 'green', '... with correct properties in "changedAttributes"' );
1611
1612			owner.set(owner.parse({
1613				id: 'owner-2354',
1614				pets: [ { id: '238902', type: 'dog', color: 'red' } ]
1615			}));
1616
1617			equal( animal.get('color'), 'red', 'color gets updated properly' );
1618			equal( changes, 2, 'change event gets called after owner.set' );
1619			equal( changedAttrs.color, 'red', '... with correct properties in "changedAttributes"' );
1620		});
1621
1622		test( 'change events should not fire on new items in Collection#set', function() {
1623			var modelChangeEvents = 0,
1624				collectionChangeEvents = 0;
1625
1626			var Animal2 = Animal.extend({
1627				initialize: function(options) {
1628					this.on( 'all', function( name, event ) {
1629						//console.log( 'Animal2: %o', arguments );
1630						if ( name.indexOf( 'change' ) === 0 ) {
1631							modelChangeEvents++;
1632						}
1633					});
1634				}
1635			});
1636
1637			var AnimalCollection2 = AnimalCollection.extend({
1638				model: Animal2,
1639
1640				initialize: function(options) {
1641					this.on( 'all', function( name, event ) {
1642						//console.log( 'AnimalCollection2: %o', arguments );
1643						if ( name.indexOf('change') === 0 ) {
1644							collectionChangeEvents++;
1645						}
1646					});
1647				}
1648			});
1649
1650			var zoo = new Zoo( { id: 'zoo-1' } );
1651
1652			var coll = new AnimalCollection2();
1653			coll.set( [{
1654				id: 'animal-1',
1655				livesIn: 'zoo-1'
1656			}] );
1657
1658			equal( collectionChangeEvents, 0, 'no change event should be triggered on the collection' );
1659
1660			modelChangeEvents = collectionChangeEvents = 0;
1661
1662			coll.at( 0 ).set( 'name', 'Willie' );
1663
1664			equal( modelChangeEvents, 2, 'change event should be triggered' );
1665		});
1666
1667	
1668	module( "Backbone.RelationalModel inheritance (`subModelTypes`)", { setup: reset } );
1669
1670		test( "Object building based on type, when using explicit collections" , function() {
1671			var scope = {};
1672			Backbone.Relational.store.addModelScope( scope );
1673
1674			scope.Mammal = Animal.extend({
1675				subModelTypes: {
1676					'primate': 'Primate',
1677					'carnivore': 'Carnivore',
1678					'ape': 'Primate' // To check multiple keys for the same submodel; see GH-429
1679				}
1680			});
1681			scope.Primate = scope.Mammal.extend({
1682				subModelTypes: {
1683					'human': 'Human'
1684				}
1685			});
1686			scope.Human = scope.Primate.extend();
1687			scope.Carnivore = scope.Mammal.extend();
1688
1689			var MammalCollection = AnimalCollection.extend({
1690				model: scope.Mammal
1691			});
1692
1693			var mammals = new MammalCollection( [
1694		

Large files files are truncated, but you can click here to view the full file