PageRenderTime 32ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

/test/tests.js

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