PageRenderTime 118ms CodeModel.GetById 4ms app.highlight 101ms RepoModel.GetById 2ms app.codeStats 0ms

/test/tests.js

https://github.com/inferno-/Backbone-relational
JavaScript | 2353 lines | 1750 code | 502 blank | 101 comment | 37 complexity | 9c244c8e7058ec8a8287581952852fd0 MD5 | raw file

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

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

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