PageRenderTime 70ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 1ms

/test/tests.js

https://github.com/sventschui/Backbone-relational
JavaScript | 4888 lines | 3579 code | 1058 blank | 251 comment | 78 complexity | 8f0948fcac86011646b4902c6b28e15f MD5 | raw file
  1. /* vim: set tabstop=4 softtabstop=4 shiftwidth=4 noexpandtab: */
  2. // documentation on writing tests here: http://docs.jquery.com/QUnit
  3. // example tests: https://github.com/jquery/qunit/blob/master/test/same.js
  4. // more examples: https://github.com/jquery/jquery/tree/master/test/unit
  5. // jQueryUI examples: https://github.com/jquery/jquery-ui/tree/master/tests/unit
  6. //sessionStorage.clear();
  7. if ( !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. $(document).ready(function() {
  15. window.requests = [];
  16. Backbone.ajax = function( settings ) {
  17. var callbackContext = settings.context || this,
  18. dfd = new $.Deferred();
  19. dfd = _.extend( settings, dfd );
  20. dfd.respond = function( status, responseText ) {
  21. /**
  22. * Trigger success/error with arguments like jQuery would:
  23. * // Success/Error
  24. * if ( isSuccess ) {
  25. * deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
  26. * } else {
  27. * deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
  28. * }
  29. */
  30. if ( status >= 200 && status < 300 || status === 304 ) {
  31. _.isFunction( settings.success ) && settings.success( responseText, 'success', dfd );
  32. dfd.resolveWith( callbackContext, [ responseText, 'success', dfd ] );
  33. }
  34. else {
  35. _.isFunction( settings.error ) && settings.error( responseText, 'error', 'Internal Server Error' );
  36. dfd.rejectWith( callbackContext, [ dfd, 'error', 'Internal Server Error' ] );
  37. }
  38. };
  39. // Add the request before triggering callbacks that may get us in here again
  40. window.requests.push( dfd );
  41. // If a `response` has been defined, execute it.
  42. // If status < 299, trigger 'success'; otherwise, trigger 'error'
  43. if ( settings.response && settings.response.status ) {
  44. dfd.respond( settings.response.status, settings.response.responseText );
  45. }
  46. return dfd;
  47. };
  48. Backbone.Model.prototype.url = function() {
  49. // Use the 'resource_uri' if possible
  50. var url = this.get( 'resource_uri' );
  51. // Try to have the collection construct a url
  52. if ( !url && this.collection ) {
  53. url = this.collection.url && _.isFunction( this.collection.url ) ? this.collection.url() : this.collection.url;
  54. }
  55. // Fallback to 'urlRoot'
  56. if ( !url && this.urlRoot ) {
  57. url = this.urlRoot + this.id;
  58. }
  59. if ( !url ) {
  60. throw new Error( 'Url could not be determined!' );
  61. }
  62. return url;
  63. };
  64. /**
  65. * 'Zoo'
  66. */
  67. window.Zoo = Backbone.RelationalModel.extend({
  68. urlRoot: '/zoo/',
  69. relations: [
  70. {
  71. type: Backbone.HasMany,
  72. key: 'animals',
  73. relatedModel: 'Animal',
  74. includeInJSON: [ 'id', 'species' ],
  75. collectionType: 'AnimalCollection',
  76. reverseRelation: {
  77. key: 'livesIn',
  78. includeInJSON: [ 'id', 'name' ]
  79. }
  80. },
  81. { // A simple HasMany without reverse relation
  82. type: Backbone.HasMany,
  83. key: 'visitors',
  84. relatedModel: 'Visitor'
  85. }
  86. ],
  87. toString: function() {
  88. return 'Zoo (' + this.id + ')';
  89. }
  90. });
  91. window.Animal = Backbone.RelationalModel.extend({
  92. urlRoot: '/animal/',
  93. relations: [
  94. { // A simple HasOne without reverse relation
  95. type: Backbone.HasOne,
  96. key: 'favoriteFood',
  97. relatedModel: 'Food'
  98. }
  99. ],
  100. // For validation testing. Wikipedia says elephants are reported up to 12.000 kg. Any more, we must've weighted wrong ;).
  101. validate: function( attrs ) {
  102. if ( attrs.species === 'elephant' && attrs.weight && attrs.weight > 12000 ) {
  103. return "Too heavy.";
  104. }
  105. },
  106. toString: function() {
  107. return 'Animal (' + this.id + ')';
  108. }
  109. });
  110. window.AnimalCollection = Backbone.Collection.extend({
  111. model: Animal
  112. });
  113. window.Food = Backbone.RelationalModel.extend({
  114. urlRoot: '/food/'
  115. });
  116. window.Visitor = Backbone.RelationalModel.extend();
  117. /**
  118. * House/Person/Job/Company
  119. */
  120. window.House = Backbone.RelationalModel.extend({
  121. relations: [{
  122. type: Backbone.HasMany,
  123. key: 'occupants',
  124. relatedModel: 'Person',
  125. reverseRelation: {
  126. key: 'livesIn',
  127. includeInJSON: false
  128. }
  129. }],
  130. toString: function() {
  131. return 'House (' + this.id + ')';
  132. }
  133. });
  134. window.User = Backbone.RelationalModel.extend({
  135. urlRoot: '/user/',
  136. toString: function() {
  137. return 'User (' + this.id + ')';
  138. }
  139. });
  140. window.Person = Backbone.RelationalModel.extend({
  141. relations: [
  142. {
  143. // Create a cozy, recursive, one-to-one relationship
  144. type: Backbone.HasOne,
  145. key: 'likesALot',
  146. relatedModel: 'Person',
  147. reverseRelation: {
  148. type: Backbone.HasOne,
  149. key: 'likedALotBy'
  150. }
  151. },
  152. {
  153. type: Backbone.HasOne,
  154. key: 'user',
  155. keyDestination: 'user_id',
  156. relatedModel: 'User',
  157. includeInJSON: Backbone.Model.prototype.idAttribute,
  158. reverseRelation: {
  159. type: Backbone.HasOne,
  160. includeInJSON: 'name',
  161. key: 'person'
  162. }
  163. },
  164. {
  165. type: 'HasMany',
  166. key: 'jobs',
  167. relatedModel: 'Job',
  168. reverseRelation: {
  169. key: 'person'
  170. }
  171. }
  172. ],
  173. toString: function() {
  174. return 'Person (' + this.id + ')';
  175. }
  176. });
  177. window.PersonCollection = Backbone.Collection.extend({
  178. model: Person
  179. });
  180. window.Password = Backbone.RelationalModel.extend({
  181. relations: [{
  182. type: Backbone.HasOne,
  183. key: 'user',
  184. relatedModel: 'User',
  185. reverseRelation: {
  186. type: Backbone.HasOne,
  187. key: 'password'
  188. }
  189. }],
  190. toString: function() {
  191. return 'Password (' + this.id + ')';
  192. }
  193. });
  194. // A link table between 'Person' and 'Company', to achieve many-to-many relations
  195. window.Job = Backbone.RelationalModel.extend({
  196. defaults: {
  197. 'startDate': null,
  198. 'endDate': null
  199. },
  200. toString: function() {
  201. return 'Job (' + this.id + ')';
  202. }
  203. });
  204. window.Company = Backbone.RelationalModel.extend({
  205. relations: [{
  206. type: 'HasMany',
  207. key: 'employees',
  208. relatedModel: 'Job',
  209. reverseRelation: {
  210. key: 'company'
  211. }
  212. },
  213. {
  214. type: 'HasOne',
  215. key: 'ceo',
  216. relatedModel: 'Person',
  217. reverseRelation: {
  218. key: 'runs'
  219. }
  220. }
  221. ],
  222. toString: function() {
  223. return 'Company (' + this.id + ')';
  224. }
  225. });
  226. /**
  227. * Node/NodeList
  228. */
  229. window.Node = Backbone.RelationalModel.extend({
  230. urlRoot: '/node/',
  231. relations: [{
  232. type: Backbone.HasOne,
  233. key: 'parent',
  234. relatedModel: 'Node',
  235. reverseRelation: {
  236. key: 'children'
  237. }
  238. }
  239. ],
  240. toString: function() {
  241. return 'Node (' + this.id + ')';
  242. }
  243. });
  244. window.NodeList = Backbone.Collection.extend({
  245. model: Node
  246. });
  247. /**
  248. * Customer/Address/Shop/Agent
  249. */
  250. window.Customer = Backbone.RelationalModel.extend({
  251. urlRoot: '/customer/',
  252. toString: function() {
  253. return 'Customer (' + this.id + ')';
  254. }
  255. });
  256. window.CustomerCollection = Backbone.Collection.extend({
  257. model: Customer,
  258. initialize: function( models, options ) {
  259. options || (options = {});
  260. this.url = options.url;
  261. }
  262. });
  263. window.Address = Backbone.RelationalModel.extend({
  264. urlRoot: '/address/',
  265. toString: function() {
  266. return 'Address (' + this.id + ')';
  267. }
  268. });
  269. window.Shop = Backbone.RelationalModel.extend({
  270. relations: [
  271. {
  272. type: Backbone.HasMany,
  273. key: 'customers',
  274. collectionType: 'CustomerCollection',
  275. collectionOptions: function( instance ) {
  276. return { 'url': 'shop/' + instance.id + '/customers/' };
  277. },
  278. relatedModel: 'Customer',
  279. autoFetch: true
  280. },
  281. {
  282. type: Backbone.HasOne,
  283. key: 'address',
  284. relatedModel: 'Address',
  285. autoFetch: {
  286. success: function( model, response ) {
  287. response.successOK = true;
  288. },
  289. error: function( model, response ) {
  290. response.errorOK = true;
  291. }
  292. }
  293. }
  294. ],
  295. toString: function() {
  296. return 'Shop (' + this.id + ')';
  297. }
  298. });
  299. window.Agent = Backbone.RelationalModel.extend({
  300. urlRoot: '/agent/',
  301. relations: [
  302. {
  303. type: Backbone.HasMany,
  304. key: 'customers',
  305. relatedModel: 'Customer',
  306. includeInJSON: Backbone.RelationalModel.prototype.idAttribute
  307. },
  308. {
  309. type: Backbone.HasOne,
  310. key: 'address',
  311. relatedModel: 'Address',
  312. autoFetch: false
  313. }
  314. ],
  315. toString: function() {
  316. return 'Agent (' + this.id + ')';
  317. }
  318. });
  319. /**
  320. * Reset variables that are persistent across tests, specifically `window.requests` and the state of
  321. * `Backbone.Relational.store`.
  322. */
  323. function reset() {
  324. // Reset last ajax requests
  325. window.requests = [];
  326. Backbone.Relational.store.reset();
  327. Backbone.Relational.store.addModelScope( window );
  328. Backbone.Relational.eventQueue = new Backbone.BlockingQueue();
  329. }
  330. /**
  331. * Initialize a few models that are used in a large number of tests
  332. */
  333. function initObjects() {
  334. reset();
  335. window.person1 = new Person({
  336. id: 'person-1',
  337. name: 'boy',
  338. likesALot: 'person-2',
  339. resource_uri: 'person-1',
  340. user: { id: 'user-1', login: 'dude', email: 'me@gmail.com', resource_uri: 'user-1' }
  341. });
  342. window.person2 = new Person({
  343. id: 'person-2',
  344. name: 'girl',
  345. likesALot: 'person-1',
  346. resource_uri: 'person-2'
  347. });
  348. window.person3 = new Person({
  349. id: 'person-3',
  350. resource_uri: 'person-3'
  351. });
  352. window.oldCompany = new Company({
  353. id: 'company-1',
  354. name: 'Big Corp.',
  355. ceo: {
  356. name: 'Big Boy'
  357. },
  358. employees: [ { person: 'person-3' } ], // uses the 'Job' link table to achieve many-to-many. No 'id' specified!
  359. resource_uri: 'company-1'
  360. });
  361. window.newCompany = new Company({
  362. id: 'company-2',
  363. name: 'New Corp.',
  364. employees: [ { person: 'person-2' } ],
  365. resource_uri: 'company-2'
  366. });
  367. window.ourHouse = new House({
  368. id: 'house-1',
  369. location: 'in the middle of the street',
  370. occupants: ['person-2'],
  371. resource_uri: 'house-1'
  372. });
  373. window.theirHouse = new House({
  374. id: 'house-2',
  375. location: 'outside of town',
  376. occupants: [],
  377. resource_uri: 'house-2'
  378. });
  379. }
  380. module ( "General / Backbone", { setup: reset } );
  381. test( "Prototypes, constructors and inheritance", function() {
  382. // This stuff makes my brain hurt a bit. So, for reference:
  383. var Model = Backbone.Model.extend(),
  384. i = new Backbone.Model(),
  385. iModel = new Model();
  386. var RelModel= Backbone.RelationalModel.extend(),
  387. iRel = new Backbone.RelationalModel(),
  388. iRelModel = new RelModel();
  389. // Both are functions, so their `constructor` is `Function`
  390. ok( Backbone.Model.constructor === Backbone.RelationalModel.constructor );
  391. ok( Backbone.Model !== Backbone.RelationalModel );
  392. ok( Backbone.Model === Backbone.Model.prototype.constructor );
  393. ok( Backbone.RelationalModel === Backbone.RelationalModel.prototype.constructor );
  394. ok( Backbone.Model.prototype.constructor !== Backbone.RelationalModel.prototype.constructor );
  395. ok( Model.prototype instanceof Backbone.Model );
  396. ok( !( Model.prototype instanceof Backbone.RelationalModel ) );
  397. ok( RelModel.prototype instanceof Backbone.Model );
  398. ok( Backbone.RelationalModel.prototype instanceof Backbone.Model );
  399. ok( RelModel.prototype instanceof Backbone.RelationalModel );
  400. ok( i instanceof Backbone.Model );
  401. ok( !( i instanceof Backbone.RelationalModel ) );
  402. ok( iRel instanceof Backbone.Model );
  403. ok( iRel instanceof Backbone.RelationalModel );
  404. ok( iModel instanceof Backbone.Model );
  405. ok( !( iModel instanceof Backbone.RelationalModel ) );
  406. ok( iRelModel instanceof Backbone.Model );
  407. ok( iRelModel instanceof Backbone.RelationalModel );
  408. });
  409. test('Collection#set', 1, function() {
  410. var a = new Backbone.Model({id: 3, label: 'a'} ),
  411. b = new Backbone.Model({id: 2, label: 'b'} ),
  412. col = new Backbone.Collection([a]);
  413. col.set([a,b], {add: true, merge: false, remove: true});
  414. ok( col.length === 2 );
  415. });
  416. module( "Backbone.Semaphore", { setup: reset } );
  417. test( "Unbounded", 10, function() {
  418. var semaphore = _.extend( {}, Backbone.Semaphore );
  419. ok( !semaphore.isLocked(), 'Semaphore is not locked initially' );
  420. semaphore.acquire();
  421. ok( semaphore.isLocked(), 'Semaphore is locked after acquire' );
  422. semaphore.acquire();
  423. equal( semaphore._permitsUsed, 2 ,'_permitsUsed should be incremented 2 times' );
  424. semaphore.setAvailablePermits( 4 );
  425. equal( semaphore._permitsAvailable, 4 ,'_permitsAvailable should be 4' );
  426. semaphore.acquire();
  427. semaphore.acquire();
  428. equal( semaphore._permitsUsed, 4 ,'_permitsUsed should be incremented 4 times' );
  429. try {
  430. semaphore.acquire();
  431. }
  432. catch( ex ) {
  433. ok( true, 'Error thrown when attempting to acquire too often' );
  434. }
  435. semaphore.release();
  436. equal( semaphore._permitsUsed, 3 ,'_permitsUsed should be decremented to 3' );
  437. semaphore.release();
  438. semaphore.release();
  439. semaphore.release();
  440. equal( semaphore._permitsUsed, 0 ,'_permitsUsed should be decremented to 0' );
  441. ok( !semaphore.isLocked(), 'Semaphore is not locked when all permits are released' );
  442. try {
  443. semaphore.release();
  444. }
  445. catch( ex ) {
  446. ok( true, 'Error thrown when attempting to release too often' );
  447. }
  448. });
  449. module( "Backbone.BlockingQueue", { setup: reset } );
  450. test( "Block", function() {
  451. var queue = new Backbone.BlockingQueue();
  452. var count = 0;
  453. var increment = function() { count++; };
  454. var decrement = function() { count--; };
  455. queue.add( increment );
  456. ok( count === 1, 'Increment executed right away' );
  457. queue.add( decrement );
  458. ok( count === 0, 'Decrement executed right away' );
  459. queue.block();
  460. queue.add( increment );
  461. ok( queue.isLocked(), 'Queue is blocked' );
  462. equal( count, 0, 'Increment did not execute right away' );
  463. queue.block();
  464. queue.block();
  465. equal( queue._permitsUsed, 3 ,'_permitsUsed should be incremented to 3' );
  466. queue.unblock();
  467. queue.unblock();
  468. queue.unblock();
  469. equal( count, 1, 'Increment executed' );
  470. });
  471. module( "Backbone.Store", { setup: initObjects } );
  472. test( "Initialized", function() {
  473. // `initObjects` instantiates models of the following types: `Person`, `Job`, `Company`, `User`, `House` and `Password`.
  474. equal( Backbone.Relational.store._collections.length, 6, "Store contains 6 collections" );
  475. });
  476. test( "getObjectByName", function() {
  477. equal( Backbone.Relational.store.getObjectByName( 'Backbone.RelationalModel' ), Backbone.RelationalModel );
  478. });
  479. test( "Add and remove from store", function() {
  480. var coll = Backbone.Relational.store.getCollection( person1 );
  481. var length = coll.length;
  482. var person = new Person({
  483. id: 'person-10',
  484. name: 'Remi',
  485. resource_uri: 'person-10'
  486. });
  487. ok( coll.length === length + 1, "Collection size increased by 1" );
  488. var request = person.destroy();
  489. // Trigger the 'success' callback to fire the 'destroy' event
  490. request.success();
  491. ok( coll.length === length, "Collection size decreased by 1" );
  492. });
  493. test( "addModelScope", function() {
  494. var models = {};
  495. Backbone.Relational.store.addModelScope( models );
  496. models.Book = Backbone.RelationalModel.extend({
  497. relations: [{
  498. type: Backbone.HasMany,
  499. key: 'pages',
  500. relatedModel: 'Page',
  501. createModels: false,
  502. reverseRelation: {
  503. key: 'book'
  504. }
  505. }]
  506. });
  507. models.Page = Backbone.RelationalModel.extend();
  508. var book = new models.Book();
  509. var page = new models.Page({ book: book });
  510. ok( book.relations.length === 1 );
  511. ok( book.get( 'pages' ).length === 1 );
  512. });
  513. test( "addModelScope with submodels and namespaces", function() {
  514. var ns = {};
  515. ns.People = {};
  516. Backbone.Relational.store.addModelScope( ns );
  517. ns.People.Person = Backbone.RelationalModel.extend({
  518. subModelTypes: {
  519. 'Student': 'People.Student'
  520. },
  521. iam: function() { return "I am an abstract person"; }
  522. });
  523. ns.People.Student = ns.People.Person.extend({
  524. iam: function() { return "I am a student"; }
  525. });
  526. ns.People.PersonCollection = Backbone.Collection.extend({
  527. model: ns.People.Person
  528. });
  529. var people = new ns.People.PersonCollection([{name: "Bob", type: "Student"}]);
  530. ok( people.at(0).iam() === "I am a student" );
  531. });
  532. test( "removeModelScope", function() {
  533. var models = {};
  534. Backbone.Relational.store.addModelScope( models );
  535. models.Page = Backbone.RelationalModel.extend();
  536. ok( Backbone.Relational.store.getObjectByName( 'Page' ) === models.Page );
  537. ok( Backbone.Relational.store.getObjectByName( 'Person' ) === window.Person );
  538. Backbone.Relational.store.removeModelScope( models );
  539. ok( !Backbone.Relational.store.getObjectByName( 'Page' ) );
  540. ok( Backbone.Relational.store.getObjectByName( 'Person' ) === window.Person );
  541. Backbone.Relational.store.removeModelScope( window );
  542. ok( !Backbone.Relational.store.getObjectByName( 'Person' ) );
  543. });
  544. test( "unregister", function() {
  545. var animalStoreColl = Backbone.Relational.store.getCollection( Animal ),
  546. animals = null,
  547. animal = null;
  548. // Single model
  549. animal = new Animal( { id: 'a1' } );
  550. ok( Backbone.Relational.store.find( Animal, 'a1' ) === animal );
  551. Backbone.Relational.store.unregister( animal );
  552. ok( Backbone.Relational.store.find( Animal, 'a1' ) === null );
  553. animal = new Animal( { id: 'a2' } );
  554. ok( Backbone.Relational.store.find( Animal, 'a2' ) === animal );
  555. animal.trigger( 'relational:unregister', animal );
  556. ok( Backbone.Relational.store.find( Animal, 'a2' ) === null );
  557. ok( animalStoreColl.size() === 0 );
  558. // Collection
  559. animals = new AnimalCollection( [ { id: 'a3' }, { id: 'a4' } ] );
  560. animal = animals.first();
  561. ok( Backbone.Relational.store.find( Animal, 'a3' ) === animal );
  562. ok( animalStoreColl.size() === 2 );
  563. Backbone.Relational.store.unregister( animals );
  564. ok( Backbone.Relational.store.find( Animal, 'a3' ) === null );
  565. ok( animalStoreColl.size() === 0 );
  566. // Store collection
  567. animals = new AnimalCollection( [ { id: 'a5' }, { id: 'a6' } ] );
  568. ok( animalStoreColl.size() === 2 );
  569. Backbone.Relational.store.unregister( animalStoreColl );
  570. ok( animalStoreColl.size() === 0 );
  571. // Model type
  572. animals = new AnimalCollection( [ { id: 'a7' }, { id: 'a8' } ] );
  573. ok( animalStoreColl.size() === 2 );
  574. Backbone.Relational.store.unregister( Animal );
  575. ok( animalStoreColl.size() === 0 );
  576. });
  577. test( "`eventQueue` is unblocked again after a duplicate id error", 3, function() {
  578. var node = new Node( { id: 1 } );
  579. ok( Backbone.Relational.eventQueue.isBlocked() === false );
  580. try {
  581. duplicateNode = new Node( { id: 1 } );
  582. }
  583. catch( error ) {
  584. ok( true, "Duplicate id error thrown" );
  585. }
  586. ok( Backbone.Relational.eventQueue.isBlocked() === false );
  587. });
  588. test( "Don't allow setting a duplicate `id`", 4, function() {
  589. var a = new Zoo(); // This object starts with no id.
  590. var b = new Zoo( { 'id': 42 } ); // This object starts with an id of 42.
  591. equal( b.id, 42 );
  592. try {
  593. a.set( 'id', 42 );
  594. }
  595. catch( error ) {
  596. ok( true, "Duplicate id error thrown" );
  597. }
  598. ok( !a.id, "a.id=" + a.id );
  599. equal( b.id, 42 );
  600. });
  601. test( "Models are created from objects, can then be found, destroyed, cannot be found anymore", function() {
  602. var houseId = 'house-10';
  603. var personId = 'person-10';
  604. var anotherHouse = new House({
  605. id: houseId,
  606. location: 'no country for old men',
  607. resource_uri: houseId,
  608. occupants: [{
  609. id: personId,
  610. name: 'Remi',
  611. resource_uri: personId
  612. }]
  613. });
  614. ok( anotherHouse.get('occupants') instanceof Backbone.Collection, "Occupants is a Collection" );
  615. ok( anotherHouse.get('occupants').get( personId ) instanceof Person, "Occupants contains the Person with id='" + personId + "'" );
  616. var person = Backbone.Relational.store.find( Person, personId );
  617. ok( person, "Person with id=" + personId + " is found in the store" );
  618. var request = person.destroy();
  619. // Trigger the 'success' callback to fire the 'destroy' event
  620. request.success();
  621. person = Backbone.Relational.store.find( Person, personId );
  622. ok( !person, personId + " is not found in the store anymore" );
  623. ok( !anotherHouse.get('occupants').get( personId ), "Occupants no longer contains the Person with id='" + personId + "'" );
  624. request = anotherHouse.destroy();
  625. // Trigger the 'success' callback to fire the 'destroy' event
  626. request.success();
  627. var house = Backbone.Relational.store.find( House, houseId );
  628. ok( !house, houseId + " is not found in the store anymore" );
  629. });
  630. test( "Model.collection is the first collection a Model is added to by an end-user (not its Backbone.Store collection!)", function() {
  631. var person = new Person( { id: 5, name: 'New guy' } );
  632. var personColl = new PersonCollection();
  633. personColl.add( person );
  634. ok( person.collection === personColl );
  635. });
  636. test( "Models don't get added to the store until the get an id", function() {
  637. var storeColl = Backbone.Relational.store.getCollection( Node ),
  638. node1 = new Node( { id: 1 } ),
  639. node2 = new Node();
  640. ok( storeColl.contains( node1 ) );
  641. ok( !storeColl.contains( node2 ) );
  642. node2.set( { id: 2 } );
  643. ok( storeColl.contains( node1 ) );
  644. });
  645. test( "All models can be found after adding them to a Collection via 'Collection.reset'", function() {
  646. var nodes = [
  647. { id: 1, parent: null },
  648. { id: 2, parent: 1 },
  649. { id: 3, parent: 4 },
  650. { id: 4, parent: 1 }
  651. ];
  652. var nodeList = new NodeList();
  653. nodeList.reset( nodes );
  654. var storeColl = Backbone.Relational.store.getCollection( Node );
  655. equal( storeColl.length, 4, "Every Node is in Backbone.Relational.store" );
  656. ok( Backbone.Relational.store.find( Node, 1 ) instanceof Node, "Node 1 can be found" );
  657. ok( Backbone.Relational.store.find( Node, 2 ) instanceof Node, "Node 2 can be found" );
  658. ok( Backbone.Relational.store.find( Node, 3 ) instanceof Node, "Node 3 can be found" );
  659. ok( Backbone.Relational.store.find( Node, 4 ) instanceof Node, "Node 4 can be found" );
  660. });
  661. test( "Inheritance creates and uses a separate collection", function() {
  662. var whale = new Animal( { id: 1, species: 'whale' } );
  663. ok( Backbone.Relational.store.find( Animal, 1 ) === whale );
  664. var numCollections = Backbone.Relational.store._collections.length;
  665. var Mammal = Animal.extend({
  666. urlRoot: '/mammal/'
  667. });
  668. var lion = new Mammal( { id: 1, species: 'lion' } );
  669. var donkey = new Mammal( { id: 2, species: 'donkey' } );
  670. equal( Backbone.Relational.store._collections.length, numCollections + 1 );
  671. ok( Backbone.Relational.store.find( Animal, 1 ) === whale );
  672. ok( Backbone.Relational.store.find( Mammal, 1 ) === lion );
  673. ok( Backbone.Relational.store.find( Mammal, 2 ) === donkey );
  674. var Primate = Mammal.extend({
  675. urlRoot: '/primate/'
  676. });
  677. var gorilla = new Primate( { id: 1, species: 'gorilla' } );
  678. equal( Backbone.Relational.store._collections.length, numCollections + 2 );
  679. ok( Backbone.Relational.store.find( Primate, 1 ) === gorilla );
  680. });
  681. test( "Inheritance with `subModelTypes` uses the same collection as the model's super", function() {
  682. var Mammal = Animal.extend({
  683. subModelTypes: {
  684. 'primate': 'Primate',
  685. 'carnivore': 'Carnivore'
  686. }
  687. });
  688. window.Primate = Mammal.extend();
  689. window.Carnivore = Mammal.extend();
  690. var lion = new Carnivore( { id: 1, species: 'lion' } );
  691. var wolf = new Carnivore( { id: 2, species: 'wolf' } );
  692. var numCollections = Backbone.Relational.store._collections.length;
  693. var whale = new Mammal( { id: 3, species: 'whale' } );
  694. equal( Backbone.Relational.store._collections.length, numCollections, "`_collections` should have remained the same" );
  695. ok( Backbone.Relational.store.find( Mammal, 1 ) === lion );
  696. ok( Backbone.Relational.store.find( Mammal, 2 ) === wolf );
  697. ok( Backbone.Relational.store.find( Mammal, 3 ) === whale );
  698. ok( Backbone.Relational.store.find( Carnivore, 1 ) === lion );
  699. ok( Backbone.Relational.store.find( Carnivore, 2 ) === wolf );
  700. ok( Backbone.Relational.store.find( Carnivore, 3 ) !== whale );
  701. var gorilla = new Primate( { id: 4, species: 'gorilla' } );
  702. equal( Backbone.Relational.store._collections.length, numCollections, "`_collections` should have remained the same" );
  703. ok( Backbone.Relational.store.find( Animal, 4 ) !== gorilla );
  704. ok( Backbone.Relational.store.find( Mammal, 4 ) === gorilla );
  705. ok( Backbone.Relational.store.find( Primate, 4 ) === gorilla );
  706. delete window.Primate;
  707. delete window.Carnivore;
  708. });
  709. test( "findOrCreate does not modify attributes hash if parse is used, prior to creating new model", function () {
  710. var model = Backbone.RelationalModel.extend({
  711. parse: function( response ) {
  712. response.id = response.id + 'something';
  713. return response;
  714. }
  715. });
  716. var attributes = {id: 42, foo: "bar"};
  717. var testAttributes = {id: 42, foo: "bar"};
  718. model.findOrCreate( attributes, { parse: true, merge: false, create: false } );
  719. ok( _.isEqual( attributes, testAttributes ), "attributes hash should not be modified" );
  720. });
  721. module( "Backbone.RelationalModel", { setup: initObjects } );
  722. test( "Return values: set returns the Model", function() {
  723. var personId = 'person-10';
  724. var person = new Person({
  725. id: personId,
  726. name: 'Remi',
  727. resource_uri: personId
  728. });
  729. var result = person.set( { 'name': 'Hector' } );
  730. ok( result === person, "Set returns the model" );
  731. });
  732. test( "`clear`", function() {
  733. var person = new Person( { id: 'person-10' } );
  734. ok( person === Person.findOrCreate( 'person-10' ) );
  735. person.clear();
  736. ok( !person.id );
  737. ok( !Person.findOrCreate( 'person-10' ) );
  738. person.set( { id: 'person-10' } );
  739. ok( person === Person.findOrCreate( 'person-10' ) );
  740. });
  741. test( "getRelations", function() {
  742. var relations = person1.getRelations();
  743. equal( relations.length, 6 );
  744. ok( _.every( relations, function( rel ) {
  745. return rel instanceof Backbone.Relation;
  746. })
  747. );
  748. });
  749. test( "getRelation", function() {
  750. var userRel = person1.getRelation( 'user' );
  751. ok( userRel instanceof Backbone.HasOne );
  752. equal( userRel.key, 'user' );
  753. var jobsRel = person1.getRelation( 'jobs' );
  754. ok( jobsRel instanceof Backbone.HasMany );
  755. equal( jobsRel.key, 'jobs' );
  756. ok( person1.getRelation( 'nope' ) == null );
  757. });
  758. test( "getAsync on a HasOne relation", function() {
  759. var errorCount = 0;
  760. var person = new Person({
  761. id: 'person-10',
  762. resource_uri: 'person-10',
  763. user: 'user-10'
  764. });
  765. var idsToFetch = person.getIdsToFetch( 'user' );
  766. deepEqual( idsToFetch, [ 'user-10' ] );
  767. var request = person.getAsync( 'user', { error: function() {
  768. errorCount++;
  769. }
  770. });
  771. ok( _.isObject( request ) && request.always && request.done && request.fail );
  772. equal( window.requests.length, 1, "A single request has been made" );
  773. ok( person.get( 'user' ) instanceof User );
  774. // Triggering the 'error' callback should destroy the model
  775. window.requests[ 0 ].error();
  776. // Trigger the 'success' callback on the `destroy` call to actually fire the 'destroy' event
  777. _.last( window.requests ).success();
  778. ok( !person.get( 'user' ), "User has been destroyed & removed" );
  779. equal( errorCount, 1, "The error callback executed successfully" );
  780. var person2 = new Person({
  781. id: 'person-11',
  782. resource_uri: 'person-11'
  783. });
  784. request = person2.getAsync( 'user' );
  785. equal( window.requests.length, 1, "No request was made" );
  786. });
  787. test( "getAsync on a HasMany relation", function() {
  788. var errorCount = 0;
  789. var zoo = new Zoo({
  790. animals: [ { id: 'monkey-1' }, 'lion-1', 'zebra-1' ]
  791. });
  792. var idsToFetch = zoo.getIdsToFetch( 'animals' );
  793. deepEqual( idsToFetch, [ 'lion-1', 'zebra-1' ] );
  794. //
  795. // Case 1: separate requests for each model
  796. //
  797. window.requests = [];
  798. var request = zoo.getAsync( 'animals', { error: function() { errorCount++; } } );
  799. ok( _.isObject( request ) && request.always && request.done && request.fail );
  800. equal( window.requests.length, 2, "Two requests have been made (a separate one for each animal)" );
  801. equal( zoo.get( 'animals' ).length, 3, "Three animals in the zoo" );
  802. // Triggering the 'error' callback for one request should destroy the model
  803. window.requests[ 0 ].error();
  804. // Trigger the 'success' callback on the `destroy` call to actually fire the 'destroy' event
  805. _.last( window.requests ).success();
  806. equal( zoo.get( 'animals' ).length, 2, "Two animals left in the zoo" );
  807. equal( errorCount, 1, "The error callback executed successfully" );
  808. //
  809. // Case 2: one request per fetch (generated by the collection)
  810. //
  811. // Give 'zoo' a custom url function that builds a url to fetch a set of models from their ids
  812. window.requests = [];
  813. errorCount = 0;
  814. zoo.get( 'animals' ).url = function( models ) {
  815. return '/animal/' + ( models ? 'set/' + _.pluck( models, 'id' ).join(';') + '/' : '' );
  816. };
  817. // Set two new animals to be fetched; both should be fetched in a single request
  818. zoo.set( { animals: [ 'monkey-1', 'lion-2', 'zebra-2' ] } );
  819. equal( zoo.get( 'animals' ).length, 1 );
  820. // `getAsync` creates two placeholder models for the ids present in the relation.
  821. window.requests = [];
  822. request = zoo.getAsync( 'animals', { error: function() { errorCount++; } } );
  823. ok( _.isObject( request ) && request.always && request.done && request.fail );
  824. equal( window.requests.length, 1 );
  825. equal( _.last( window.requests ).url, '/animal/set/lion-2;zebra-2/' );
  826. equal( zoo.get('animals').length, 3, "Three animals in the zoo" );
  827. // Triggering the 'error' callback (some error occured during fetching) should trigger the 'destroy' event
  828. // on both fetched models, but should NOT actually make 'delete' requests to the server!
  829. _.last( window.requests ).error();
  830. equal( window.requests.length, 1, "An error occured when fetching, but no DELETE requests are made to the server while handling local cleanup." );
  831. equal( zoo.get( 'animals' ).length, 1, "Both animals are destroyed" );
  832. equal( errorCount, 2, "The error callback executed successfully for both models" );
  833. // Try to re-fetch; nothing left to get though
  834. window.requests = [];
  835. request = zoo.getAsync( 'animals' );
  836. equal( window.requests.length, 0 );
  837. equal( zoo.get( 'animals' ).length, 1 );
  838. // Re-fetch the existing model
  839. window.requests = [];
  840. request = zoo.getAsync( 'animals', { refresh: true } );
  841. equal( window.requests.length, 1 );
  842. equal( _.last( window.requests ).url, '/animal/set/monkey-1/' );
  843. equal( zoo.get( 'animals' ).length, 1 );
  844. // An error while refreshing an existing model shouldn't affect it
  845. window.requests[ 0 ].error();
  846. equal( zoo.get( 'animals' ).length, 1 );
  847. });
  848. test( "getAsync", 8, function() {
  849. var zoo = Zoo.findOrCreate( { id: 'z-1', animals: [ 'cat-1' ] } );
  850. zoo.on( 'add:animals', function( animal ) {
  851. console.log( 'add:animals=%o', animal );
  852. animal.on( 'change:favoriteFood', function( model, food ) {
  853. console.log( '%s eats %s', animal.get( 'name' ), food.get( 'name' ) );
  854. });
  855. });
  856. zoo.getAsync( 'animals' ).done( function( animals ) {
  857. ok( animals instanceof AnimalCollection );
  858. ok( animals.length === 1 );
  859. var cat = zoo.get( 'animals' ).at( 0 );
  860. equal( cat.get( 'name' ), 'Tiger' );
  861. cat.getAsync( 'favoriteFood' ).done( function( food ) {
  862. equal( food.get( 'name' ), 'Cheese', 'Favorite food is cheese' );
  863. });
  864. });
  865. equal( zoo.get( 'animals' ).length, 1 );
  866. equal( window.requests.length, 1 );
  867. equal( _.last( window.requests ).url, '/animal/cat-1' );
  868. // Declare success
  869. _.last( window.requests ).respond( 200, { id: 'cat-1', name: 'Tiger', favoriteFood: 'f-2' } );
  870. equal( window.requests.length, 2 );
  871. _.last( window.requests ).respond( 200, { id: 'f-2', name: 'Cheese' } );
  872. });
  873. test( "autoFetch a HasMany relation", function() {
  874. var shopOne = new Shop({
  875. id: 'shop-1',
  876. customers: ['customer-1', 'customer-2']
  877. });
  878. equal( requests.length, 2, "Two requests to fetch the users has been made" );
  879. requests.length = 0;
  880. var shopTwo = new Shop({
  881. id: 'shop-2',
  882. customers: ['customer-1', 'customer-3']
  883. });
  884. equal( requests.length, 1, "A request to fetch a user has been made" ); //as customer-1 has already been fetched
  885. });
  886. test( "autoFetch on a HasOne relation (with callbacks)", function() {
  887. var shopThree = new Shop({
  888. id: 'shop-3',
  889. address: 'address-3'
  890. });
  891. equal( requests.length, 1, "A request to fetch the address has been made" );
  892. var res = { successOK: false, errorOK: false };
  893. requests[0].success( res );
  894. equal( res.successOK, true, "The success() callback has been called" );
  895. requests.length = 0;
  896. var shopFour = new Shop({
  897. id: 'shop-4',
  898. address: 'address-4'
  899. });
  900. equal( requests.length, 1, "A request to fetch the address has been made" );
  901. requests[0].error( res );
  902. equal( res.errorOK, true, "The error() callback has been called" );
  903. });
  904. test( "autoFetch false by default", function() {
  905. var agentOne = new Agent({
  906. id: 'agent-1',
  907. customers: ['customer-4', 'customer-5']
  908. });
  909. equal( requests.length, 0, "No requests to fetch the customers has been made as autoFetch was not defined" );
  910. agentOne = new Agent({
  911. id: 'agent-2',
  912. address: 'address-5'
  913. });
  914. equal( requests.length, 0, "No requests to fetch the customers has been made as autoFetch was set to false" );
  915. });
  916. test( "`clone`", function() {
  917. var user = person1.get( 'user' );
  918. // HasOne relations should stay with the original model
  919. var newPerson = person1.clone();
  920. ok( newPerson.get( 'user' ) === null );
  921. ok( person1.get( 'user' ) === user );
  922. });
  923. test( "`save` (with `wait`)", function() {
  924. var node1 = new Node({ id: '1', parent: '3', name: 'First node' } ),
  925. node2 = new Node({ id: '2', name: 'Second node' });
  926. // Set node2's parent to node1 in a request with `wait: true`
  927. var request = node2.save( 'parent', node1, { wait: true } ),
  928. json = JSON.parse( request.data );
  929. ok( _.isObject( json.parent ) );
  930. equal( json.parent.id, '1' );
  931. equal( node2.get( 'parent' ), null );
  932. request.success();
  933. equal( node2.get( 'parent' ), node1 );
  934. // Save a new node as node2's parent, only specified as JSON in the call to save
  935. request = node2.save( 'parent', { id: '3', parent: '2', name: 'Third node' }, { wait: true } );
  936. json = JSON.parse( request.data );
  937. ok( _.isObject( json.parent ) );
  938. equal( json.parent.id, '3' );
  939. equal( node2.get( 'parent' ), node1 );
  940. request.success();
  941. var node3 = node2.get( 'parent' );
  942. ok( node3 instanceof Node );
  943. equal( node3.id, '3' );
  944. // Try to reset node2's parent to node1, but fail the request
  945. request = node2.save( 'parent', node1, { wait: true } );
  946. request.error();
  947. equal( node2.get( 'parent' ), node3 );
  948. // See what happens for different values of `includeInJSON`...
  949. // For `Person.user`, just the `idAttribute` should be serialized to the keyDestination `user_id`
  950. var user1 = person1.get( 'user' );
  951. request = person1.save( 'user', null, { wait: true } );
  952. json = JSON.parse( request.data );
  953. console.log( request, json );
  954. equal( person1.get( 'user' ), user1 );
  955. request.success( json );
  956. equal( person1.get( 'user' ), null );
  957. request = person1.save( 'user', user1, { wait: true } );
  958. json = JSON.parse( request.data );
  959. equal( json.user_id, user1.id );
  960. equal( person1.get( 'user' ), null );
  961. request.success( json );
  962. equal( person1.get( 'user' ), user1 );
  963. // Save a collection with `wait: true`
  964. var zoo = new Zoo( { id: 'z1' } ),
  965. animal1 = new Animal( { id: 'a1', species: 'Goat', name: 'G' } ),
  966. coll = new Backbone.Collection( [ { id: 'a2', species: 'Rabbit', name: 'R' }, animal1 ] );
  967. request = zoo.save( 'animals', coll, { wait: true } );
  968. json = JSON.parse( request.data );
  969. console.log( request, json );
  970. ok( zoo.get( 'animals' ).length === 0 );
  971. request.success( json );
  972. ok( zoo.get( 'animals' ).length === 2 );
  973. console.log( animal1 );
  974. });
  975. test( "`Collection.create` (with `wait`)", function() {
  976. var nodeColl = new NodeList(),
  977. nodesAdded = 0;
  978. nodeColl.on( 'add', function( model, collection, options ) {
  979. nodesAdded++;
  980. });
  981. nodeColl.create({ id: '3', parent: '2', name: 'Third node' }, { wait: true });
  982. ok( nodesAdded === 0 );
  983. requests[ requests.length - 1 ].success();
  984. ok( nodesAdded === 1 );
  985. nodeColl.create({ id: '4', name: 'Third node' }, { wait: true });
  986. ok( nodesAdded === 1 );
  987. requests[ requests.length - 1 ].error();
  988. ok( nodesAdded === 1 );
  989. });
  990. test( "`toJSON`: simple cases", function() {
  991. var node = new Node({ id: '1', parent: '3', name: 'First node' });
  992. new Node({ id: '2', parent: '1', name: 'Second node' });
  993. new Node({ id: '3', parent: '2', name: 'Third node' });
  994. var json = node.toJSON();
  995. ok( json.children.length === 1 );
  996. });
  997. test("'toJSON' should return null for relations that are set to null, even when model is not fetched", function() {
  998. var person = new Person( { user : 'u1' } );
  999. equal( person.toJSON().user_id, 'u1' );
  1000. person.set( 'user', null );
  1001. equal( person.toJSON().user_id, null );
  1002. person = new Person( { user: new User( { id : 'u2' } ) } );
  1003. equal( person.toJSON().user_id, 'u2' );
  1004. person.set( { user: 'unfetched_user_id' } );
  1005. equal( person.toJSON().user_id, 'unfetched_user_id' );
  1006. });
  1007. test( "`toJSON` should include ids for 'unknown' or 'missing' models (if `includeInJSON` is `idAttribute`)", function() {
  1008. // See GH-191
  1009. // `Zoo` shouldn't be affected; `animals.includeInJSON` is not equal to `idAttribute`
  1010. var zoo = new Zoo({ id: 'z1', animals: [ 'a1', 'a2' ] }),
  1011. zooJSON = zoo.toJSON();
  1012. ok( _.isArray( zooJSON.animals ) );
  1013. equal( zooJSON.animals.length, 0, "0 animals in zooJSON; it serializes an array of attributes" );
  1014. var a1 = new Animal( { id: 'a1' } );
  1015. zooJSON = zoo.toJSON();
  1016. equal( zooJSON.animals.length, 1, "1 animals in zooJSON; it serializes an array of attributes" );
  1017. // Agent -> Customer; `idAttribute` on a HasMany
  1018. var agent = new Agent({ id: 'a1', customers: [ 'c1', 'c2' ] } ),
  1019. agentJSON = agent.toJSON();
  1020. ok( _.isArray( agentJSON.customers ) );
  1021. equal( agentJSON.customers.length, 2, "2 customers in agentJSON; it serializes the `idAttribute`" );
  1022. var c1 = new Customer( { id: 'c1' } );
  1023. equal( agent.get( 'customers' ).length, 1, '1 customer in agent' );
  1024. agentJSON = agent.toJSON();
  1025. equal( agentJSON.customers.length, 2, "2 customers in agentJSON; `idAttribute` for 1 missing, other existing" );
  1026. //c1.destroy();
  1027. //agentJSON = agent.toJSON();
  1028. //equal( agentJSON.customers.length, 1, "1 customer in agentJSON; `idAttribute` for 1 missing, other destroyed" );
  1029. agent.set( 'customers', [ 'c1', 'c3' ] );
  1030. var c3 = new Customer( { id: 'c3' } );
  1031. agentJSON = agent.toJSON();
  1032. equal( agentJSON.customers.length, 2, "2 customers in agentJSON; 'c1' already existed, 'c3' created" );
  1033. agent.get( 'customers' ).remove( c1 );
  1034. agentJSON = agent.toJSON();
  1035. equal( agentJSON.customers.length, 1, "1 customer in agentJSON; 'c1' removed, 'c3' still in there" );
  1036. // Person -> User; `idAttribute` on a HasOne
  1037. var person = new Person({ id: 'p1', user: 'u1' } ),
  1038. personJSON = person.toJSON();
  1039. equal( personJSON.user_id, 'u1', "`user_id` gets set in JSON" );
  1040. var u1 = new User( { id: 'u1' } );
  1041. personJSON = person.toJSON();
  1042. ok( u1.get( 'person' ) === person );
  1043. equal( personJSON.user_id, 'u1', "`user_id` gets set in JSON" );
  1044. person.set( 'user', 'u1' );
  1045. personJSON = person.toJSON();
  1046. equal( personJSON.user_id, 'u1', "`user_id` gets set in JSON" );
  1047. u1.destroy();
  1048. personJSON = person.toJSON();
  1049. ok( !u1.get( 'person' ) );
  1050. equal( personJSON.user_id, 'u1', "`user_id` still gets set in JSON" );
  1051. });
  1052. test( "`toJSON` should include ids for unregistered models (if `includeInJSON` is `idAttribute`)", function() {
  1053. // Person -> User; `idAttribute` on a HasOne
  1054. var person = new Person({ id: 'p1', user: 'u1' } ),
  1055. personJSON = person.toJSON();
  1056. equal( personJSON.user_id, 'u1', "`user_id` gets set in JSON even though no user obj exists" );
  1057. var u1 = new User( { id: 'u1' } );
  1058. personJSON = person.toJSON();
  1059. ok( u1.get( 'person' ) === person );
  1060. equal( personJSON.user_id, 'u1', "`user_id` gets set in JSON after matching user obj is created" );
  1061. Backbone.Relational.store.unregister(u1);
  1062. personJSON = person.toJSON();
  1063. equal( personJSON.user_id, 'u1', "`user_id` gets set in JSON after user was unregistered from store" );
  1064. });
  1065. test( "`parse` gets called through `findOrCreate`", function() {
  1066. var parseCalled = 0;
  1067. Zoo.prototype.parse = Animal.prototype.parse = function( resp, options ) {
  1068. parseCalled++;
  1069. return resp;
  1070. };
  1071. var zoo = Zoo.findOrCreate({
  1072. id: '1',
  1073. name: 'San Diego Zoo',
  1074. animals: [ { id: 'a' } ]
  1075. }, { parse: true } );
  1076. var animal = zoo.get( 'animals' ).first();
  1077. ok( animal.get( 'livesIn' ) );
  1078. ok( animal.get( 'livesIn' ) instanceof Zoo );
  1079. ok( animal.get( 'livesIn' ).get( 'animals' ).get( animal ) === animal );
  1080. // `parse` gets called by `findOrCreate` directly when trying to lookup `1`,
  1081. // and the parsed attributes are passed to `build` (called from `findOrCreate`) with `{ parse: false }`,
  1082. // rather than having `parse` called again by the Zoo constructor.
  1083. ok( parseCalled === 1, 'parse called 1 time? ' + parseCalled );
  1084. parseCalled = 0;
  1085. animal = new Animal({ id: 'b' });
  1086. animal.set({
  1087. id: 'b',
  1088. livesIn: {
  1089. id: '2',
  1090. name: 'San Diego Zoo',
  1091. animals: [ 'b' ]
  1092. }
  1093. }, { parse: true } );
  1094. ok( animal.get( 'livesIn' ) );
  1095. ok( animal.get( 'livesIn' ) instanceof Zoo );
  1096. ok( animal.get( 'livesIn' ).get( 'animals' ).get( animal ) === animal );
  1097. ok( parseCalled === 0, 'parse called 0 times? ' + parseCalled );
  1098. // Reset `parse` methods
  1099. Zoo.prototype.parse = Animal.prototype.parse = Backbone.RelationalModel.prototype.parse;
  1100. });
  1101. test( "`Collection#parse` with RelationalModel simple case", function() {
  1102. var Contact = Backbone.RelationalModel.extend({
  1103. parse: function( response ) {
  1104. response.bar = response.foo * 2;
  1105. return response;
  1106. }
  1107. });
  1108. var Contacts = Backbone.Collection.extend({
  1109. model: Contact,
  1110. url: '/contacts',
  1111. parse: function( response ) {
  1112. return response.items;
  1113. }
  1114. });
  1115. var contacts = new Contacts();
  1116. contacts.fetch({
  1117. // fake response for testing
  1118. response: {
  1119. status: 200,
  1120. responseText: { items: [ { foo: 1 }, { foo: 2 } ] }
  1121. }
  1122. });
  1123. equal( contacts.length, 2, 'Collection response was fetched properly' );
  1124. var contact = contacts.first();
  1125. ok( contact , 'Collection has a non-null item' );
  1126. ok( contact instanceof Contact, '... of the type type' );
  1127. equal( contact.get('foo'), 1, '... with correct fetched value' );
  1128. equal( contact.get('bar'), 2, '... with correct parsed value' );
  1129. });
  1130. test( "By default, `parse` should only get called on top-level objects; not for nested models and collections", function() {
  1131. var companyData = {
  1132. 'data': {
  1133. 'id': 'company-1',
  1134. 'contacts': [
  1135. {
  1136. 'id': '1'
  1137. },
  1138. {
  1139. 'id': '2'
  1140. }
  1141. ]
  1142. }
  1143. };
  1144. var Contact = Backbone.RelationalModel.extend();
  1145. var Contacts = Backbone.Collection.extend({
  1146. model: Contact
  1147. });
  1148. var Company = Backbone.RelationalModel.extend({
  1149. urlRoot: '/company/',
  1150. relations: [{
  1151. type: Backbone.HasMany,
  1152. key: 'contacts',
  1153. relatedModel: Contact,
  1154. collectionType: Contacts
  1155. }]
  1156. });
  1157. var parseCalled = 0;
  1158. Company.prototype.parse = Contact.prototype.parse = Contacts.prototype.parse = function( resp, options ) {
  1159. parseCalled++;
  1160. return resp.data || resp;
  1161. };
  1162. var company = new Company( companyData, { parse: true } ),
  1163. contacts = company.get( 'contacts' ),
  1164. contact = contacts.first();
  1165. ok( company.id === 'company-1' );
  1166. ok( contact && contact.id === '1', 'contact exists' );
  1167. ok( parseCalled === 1, 'parse called 1 time? ' + parseCalled );
  1168. // simulate what would happen if company.fetch() was called.
  1169. company.fetch({
  1170. parse: true,
  1171. response: {
  1172. status: 200,
  1173. responseText: _.clone( companyData )
  1174. }
  1175. });
  1176. ok( parseCalled === 2, 'parse called 2 times? ' + parseCalled );
  1177. ok( contacts === company.get( 'contacts' ), 'contacts collection is same instance after fetch' );
  1178. equal( contacts.length, 2, '... with correct length' );
  1179. ok( contact && contact.id === '1', 'contact exists' );
  1180. ok( contact === contacts.first(), '... and same model instances' );
  1181. });
  1182. test( "constructor.findOrCreate", function() {
  1183. var personColl = Backbone.Relational.store.getCollection( person1 ),
  1184. origPersonCollSize = personColl.length;
  1185. // Just find an existing model
  1186. var person = Person.findOrCreate( person1.id );
  1187. ok( person === person1 );
  1188. ok( origPersonCollSize === personColl.length, "Existing person was found (none created)" );
  1189. // Update an existing model
  1190. person = Person.findOrCreate( { id: person1.id, name: 'dude' } );
  1191. equal( person.get( 'name' ), 'dude' );
  1192. equal( person1.get( 'name' ), 'dude' );
  1193. ok( origPersonCollSize === personColl.length, "Existing person was updated (none created)" );
  1194. // Look for a non-existent person; 'options.create' is false
  1195. person = Person.findOrCreate( { id: 5001 }, { create: false } );
  1196. ok( !person );
  1197. ok( origPersonCollSize === personColl.length, "No person was found (none created)" );
  1198. // Create a new model
  1199. person = Person.findOrCreate( { id: 5001 } );
  1200. ok( person instanceof Person );
  1201. ok( origPersonCollSize + 1 === personColl.length, "No person was found (1 created)" );
  1202. // Find when options.merge is false
  1203. person = Person.findOrCreate( { id: person1.id, name: 'phil' }, { merge: false } );
  1204. equal( person.get( 'name' ), 'dude' );
  1205. equal( person1.get( 'name' ), 'dude' );
  1206. });
  1207. test( "constructor.find", function() {
  1208. var personColl = Backbone.Relational.store.getCollection( person1 ),
  1209. origPersonCollSize = personColl.length;
  1210. // Look for a non-existent person
  1211. person = Person.find( { id: 5001 } );
  1212. ok( !person );
  1213. });
  1214. test( "change events in relation can use changedAttributes properly", function() {
  1215. var scope = {};
  1216. Backbone.Relational.store.addModelScope( scope );
  1217. scope.PetAnimal = Backbone.RelationalModel.extend({
  1218. subModelTypes: {
  1219. 'cat': 'Cat',
  1220. 'dog': 'Dog'
  1221. }
  1222. });
  1223. scope.Dog = scope.PetAnimal.extend();
  1224. scope.Cat = scope.PetAnimal.extend();
  1225. scope.PetOwner = Backbone.RelationalModel.extend({
  1226. relations: [{
  1227. type: Backbone.HasMany,
  1228. key: 'pets',
  1229. relatedModel: scope.PetAnimal,
  1230. reverseRelation: {
  1231. key: 'owner'
  1232. }
  1233. }]
  1234. });
  1235. var owner = new scope.PetOwner( { id: 'owner-2354' } );
  1236. var animal = new scope.Dog( { type: 'dog', id: '238902', color: 'blue' } );
  1237. equal( animal.get('color'), 'blue', 'animal starts out blue' );
  1238. var changes = 0, changedAttrs = null;
  1239. animal.on('change', function(model, options) {
  1240. changes++;
  1241. changedAttrs = model.changedAttributes();
  1242. });
  1243. animal.set( { color: 'green' } );
  1244. equal( changes, 1, 'change event gets called after animal.set' );
  1245. equal( changedAttrs.color, 'green', '... with correct properties in "changedAttributes"' );
  1246. owner.set(owner.parse({
  1247. id: 'owner-2354',
  1248. pets: [ { id: '238902', type: 'dog', color: 'red' } ]
  1249. }));
  1250. equal( animal.get('color'), 'red', 'color gets updated properly' );
  1251. equal( changes, 2, 'change event gets called after owner.set' );
  1252. equal( changedAttrs.color, 'red', '... with correct properties in "changedAttributes"' );
  1253. });
  1254. test( 'change events should not fire on new items in Collection#set', function() {
  1255. var modelChangeEvents = 0,
  1256. collectionChangeEvents = 0;
  1257. var Animal2 = Animal.extend({
  1258. initialize: function(options) {
  1259. this.on( 'all', function( name, event ) {
  1260. //console.log( 'Animal2: %o', arguments );
  1261. if ( name.indexOf( 'change' ) === 0 ) {
  1262. modelChangeEvents++;
  1263. }
  1264. });
  1265. }
  1266. });
  1267. var AnimalCollection2 = AnimalCollection.extend({
  1268. model: Animal2,
  1269. initialize: function(options) {
  1270. this.on( 'all', function( name, event ) {
  1271. //console.log( 'AnimalCollection2: %o', arguments );
  1272. if ( name.indexOf('change') === 0 ) {
  1273. collectionChangeEvents++;
  1274. }
  1275. });
  1276. }
  1277. });
  1278. var zoo = new Zoo( { id: 'zoo-1' } );
  1279. var coll = new AnimalCollection2();
  1280. coll.set( [{
  1281. id: 'animal-1',
  1282. livesIn: 'zoo-1'
  1283. }] );
  1284. equal( collectionChangeEvents, 0, 'no change event should be triggered on the collection' );
  1285. modelChangeEvents = collectionChangeEvents = 0;
  1286. coll.at( 0 ).set( 'name', 'Willie' );
  1287. equal( modelChangeEvents, 2, 'change event should be triggered' );
  1288. });
  1289. module( "Backbone.RelationalModel inheritance (`subModelTypes`)", { setup: reset } );
  1290. test( "Object building based on type, when using explicit collections" , function() {
  1291. var scope = {};
  1292. Backbone.Relational.store.addModelScope( scope );
  1293. scope.Mammal = Animal.extend({
  1294. subModelTypes: {
  1295. 'primate': 'Primate',
  1296. 'carnivore': 'Carnivore',
  1297. 'ape': 'Primate' // To check multiple keys for the same submodel; see GH-429
  1298. }
  1299. });
  1300. scope.Primate = scope.Mammal.extend({
  1301. subModelTypes: {
  1302. 'human': 'Human'
  1303. }
  1304. });
  1305. scope.Human = scope.Primate.extend();
  1306. scope.Carnivore = scope.Mammal.extend();
  1307. var MammalCollection = AnimalCollection.extend({
  1308. model: scope.Mammal
  1309. });
  1310. var mammals = new MammalCollection( [
  1311. { id: 5, species: 'chimp', type: 'primate' },
  1312. { id: 6, species: 'panther', type: 'carnivore' },
  1313. { id: 7, species: 'person', type: 'human' },
  1314. { id: 8, species: 'gorilla', type: 'ape' }
  1315. ]);
  1316. ok( mammals.at( 0 ) instanceof scope.Primate );
  1317. ok( mammals.at( 1 ) instanceof scope.Carnivore );
  1318. ok( mammals.at( 2 ) instanceof scope.Human );
  1319. ok( mammals.at( 3 ) instanceof scope.Primate );
  1320. });
  1321. test( "Object building based on type, when used in relations" , function() {
  1322. var scope = {};
  1323. Backbone.Relational.store.addModelScope( scope );
  1324. var PetAnimal = scope.PetAnimal = Backbone.RelationalModel.extend({
  1325. subModelTypes: {
  1326. 'cat': 'Cat',
  1327. 'dog': 'Dog'
  1328. }
  1329. });
  1330. var Dog = scope.Dog = PetAnimal.extend({
  1331. subModelTypes: {
  1332. 'poodle': 'Poodle'
  1333. }
  1334. });
  1335. var Cat = scope.Cat = PetAnimal.extend();
  1336. var Poodle = scope.Poodle = Dog.extend();
  1337. var PetPerson = scope.PetPerson = Backbone.RelationalModel.extend({
  1338. relations: [{
  1339. type: Backbone.HasMany,
  1340. key: 'pets',
  1341. relatedModel: PetAnimal,
  1342. reverseRelation: {
  1343. key: 'owner'
  1344. }
  1345. }]
  1346. });
  1347. var petPerson = new scope.PetPerson({
  1348. pets: [
  1349. {
  1350. type: 'dog',
  1351. name: 'Spot'
  1352. },
  1353. {
  1354. type: 'cat',
  1355. name: 'Whiskers'
  1356. },
  1357. {
  1358. type: 'poodle',
  1359. name: 'Mitsy'
  1360. }
  1361. ]
  1362. });
  1363. ok( petPerson.get( 'pets' ).at( 0 ) instanceof Dog );
  1364. ok( petPerson.get( 'pets' ).at( 1 ) instanceof Cat );
  1365. ok( petPerson.get( 'pets' ).at( 2 ) instanceof Poodle );
  1366. petPerson.get( 'pets' ).add([{
  1367. type: 'dog',
  1368. name: 'Spot II'
  1369. },{
  1370. type: 'poodle',
  1371. name: 'Mitsy II'
  1372. }]);
  1373. ok( petPerson.get( 'pets' ).at( 3 ) instanceof Dog );
  1374. ok( petPerson.get( 'pets' ).at( 4 ) instanceof Poodle );
  1375. });
  1376. test( "Object building based on type in a custom field, when used in relations" , function() {
  1377. var scope = {};
  1378. Backbone.Relational.store.addModelScope( scope );
  1379. var Caveman = scope.Caveman = Backbone.RelationalModel.extend({
  1380. subModelTypes: {
  1381. 'rubble': 'Rubble',
  1382. 'flintstone': 'Flintstone'
  1383. },
  1384. subModelTypeAttribute: "caveman_type"
  1385. });
  1386. var Flintstone = scope.Flintstone = Caveman.extend();
  1387. var Rubble = scope.Rubble = Caveman.extend();
  1388. var Cartoon = scope.Cartoon = Backbone.RelationalModel.extend({
  1389. relations: [{
  1390. type: Backbone.HasMany,
  1391. key: 'cavemen',
  1392. relatedModel: Caveman
  1393. }]
  1394. });
  1395. var captainCaveman = new scope.Cartoon({
  1396. cavemen: [
  1397. {
  1398. type: 'rubble',
  1399. name: 'CaptainCaveman'
  1400. }
  1401. ]
  1402. });
  1403. ok( !(captainCaveman.get( "cavemen" ).at( 0 ) instanceof Rubble) );
  1404. var theFlintstones = new scope.Cartoon({
  1405. cavemen: [
  1406. {
  1407. caveman_type: 'rubble',
  1408. name: 'Barney'
  1409. },
  1410. {
  1411. caveman_type: 'flintstone',
  1412. name: 'Wilma'
  1413. }
  1414. ]
  1415. });
  1416. ok( theFlintstones.get( "cavemen" ).at( 0 ) instanceof Rubble );
  1417. ok( theFlintstones.get( "cavemen" ).at( 1 ) instanceof Flintstone );
  1418. });
  1419. test( "Automatic sharing of 'superModel' relations" , function() {
  1420. var scope = {};
  1421. Backbone.Relational.store.addModelScope( scope );
  1422. scope.PetPerson = Backbone.RelationalModel.extend({});
  1423. scope.PetAnimal = Backbone.RelationalModel.extend({
  1424. subModelTypes: {
  1425. 'dog': 'Dog'
  1426. },
  1427. relations: [{
  1428. type: Backbone.HasOne,
  1429. key: 'owner',
  1430. relatedModel: scope.PetPerson,
  1431. reverseRelation: {
  1432. type: Backbone.HasMany,
  1433. key: 'pets'
  1434. }
  1435. }]
  1436. });
  1437. scope.Flea = Backbone.RelationalModel.extend({});
  1438. scope.Dog = scope.PetAnimal.extend({
  1439. subModelTypes: {
  1440. 'poodle': 'Poodle'
  1441. },
  1442. relations: [{
  1443. type: Backbone.HasMany,
  1444. key: 'fleas',
  1445. relatedModel: scope.Flea,
  1446. reverseRelation: {
  1447. key: 'host'
  1448. }
  1449. }]
  1450. });
  1451. scope.Poodle = scope.Dog.extend();
  1452. var dog = new scope.Dog({
  1453. name: 'Spot'
  1454. });
  1455. var poodle = new scope.Poodle({
  1456. name: 'Mitsy'
  1457. });
  1458. var person = new scope.PetPerson({
  1459. pets: [ dog, poodle ]
  1460. });
  1461. ok( dog.get( 'owner' ) === person, "Dog has a working owner relation." );
  1462. ok( poodle.get( 'owner' ) === person, "Poodle has a working owner relation." );
  1463. var flea = new scope.Flea({
  1464. host: dog
  1465. });
  1466. var flea2 = new scope.Flea({
  1467. host: poodle
  1468. });
  1469. ok( dog.get( 'fleas' ).at( 0 ) === flea, "Dog has a working fleas relation." );
  1470. ok( poodle.get( 'fleas' ).at( 0 ) === flea2, "Poodle has a working fleas relation." );
  1471. });
  1472. test( "Initialization and sharing of 'superModel' reverse relations from a 'leaf' child model" , function() {
  1473. var scope = {};
  1474. Backbone.Relational.store.addModelScope( scope );
  1475. scope.PetAnimal = Backbone.RelationalModel.extend({
  1476. subModelTypes: {
  1477. 'dog': 'Dog'
  1478. }
  1479. });
  1480. scope.Flea = Backbone.RelationalModel.extend({});
  1481. scope.Dog = scope.PetAnimal.extend({
  1482. subModelTypes: {
  1483. 'poodle': 'Poodle'
  1484. },
  1485. relations: [{
  1486. type: Backbone.HasMany,
  1487. key: 'fleas',
  1488. relatedModel: scope.Flea,
  1489. reverseRelation: {
  1490. key: 'host'
  1491. }
  1492. }]
  1493. });
  1494. scope.Poodle = scope.Dog.extend();
  1495. // Define the PetPerson after defining all of the Animal models. Include the 'owner' as a reverse-relation.
  1496. scope.PetPerson = Backbone.RelationalModel.extend({
  1497. relations: [{
  1498. type: Backbone.HasMany,
  1499. key: 'pets',
  1500. relatedModel: scope.PetAnimal,
  1501. reverseRelation: {
  1502. type: Backbone.HasOne,
  1503. key: 'owner'
  1504. }
  1505. }]
  1506. });
  1507. // Initialize the models starting from the deepest descendant and working your way up to the root parent class.
  1508. var poodle = new scope.Poodle({
  1509. name: 'Mitsy'
  1510. });
  1511. var dog = new scope.Dog({
  1512. name: 'Spot'
  1513. });
  1514. var person = new scope.PetPerson({
  1515. pets: [ dog, poodle ]
  1516. });
  1517. ok( dog.get( 'owner' ) === person, "Dog has a working owner relation." );
  1518. ok( poodle.get( 'owner' ) === person, "Poodle has a working owner relation." );
  1519. var flea = new scope.Flea({
  1520. host: dog
  1521. });
  1522. var flea2 = new scope.Flea({
  1523. host: poodle
  1524. });
  1525. ok( dog.get( 'fleas' ).at( 0 ) === flea, "Dog has a working fleas relation." );
  1526. ok( poodle.get( 'fleas' ).at( 0 ) === flea2, "Poodle has a working fleas relation." );
  1527. });
  1528. test( "Initialization and sharing of 'superModel' reverse relations by adding to a polymorphic HasMany" , function() {
  1529. var scope = {};
  1530. Backbone.Relational.store.addModelScope( scope );
  1531. scope.PetAnimal = Backbone.RelationalModel.extend({
  1532. // The order in which these are defined matters for this regression test.
  1533. subModelTypes: {
  1534. 'dog': 'Dog',
  1535. 'fish': 'Fish'
  1536. }
  1537. });
  1538. // This looks unnecessary but it's for this regression test there has to be multiple subModelTypes.
  1539. scope.Fish = scope.PetAnimal.extend({});
  1540. scope.Flea = Backbone.RelationalModel.extend({});
  1541. scope.Dog = scope.PetAnimal.extend({
  1542. subModelTypes: {
  1543. 'poodle': 'Poodle'
  1544. },
  1545. relations: [{
  1546. type: Backbone.HasMany,
  1547. key: 'fleas',
  1548. relatedModel: scope.Flea,
  1549. reverseRelation: {
  1550. key: 'host'
  1551. }
  1552. }]
  1553. });
  1554. scope.Poodle = scope.Dog.extend({});
  1555. // Define the PetPerson after defining all of the Animal models. Include the 'owner' as a reverse-relation.
  1556. scope.PetPerson = Backbone.RelationalModel.extend({
  1557. relations: [{
  1558. type: Backbone.HasMany,
  1559. key: 'pets',
  1560. relatedModel: scope.PetAnimal,
  1561. reverseRelation: {
  1562. type: Backbone.HasOne,
  1563. key: 'owner'
  1564. }
  1565. }]
  1566. });
  1567. // We need to initialize a model through the root-parent-model's build method by adding raw-attributes for a
  1568. // leaf-child-class to a polymorphic HasMany.
  1569. var person = new scope.PetPerson({
  1570. pets: [{
  1571. type: 'poodle',
  1572. name: 'Mitsy'
  1573. }]
  1574. });
  1575. var poodle = person.get('pets').first();
  1576. ok( poodle.get( 'owner' ) === person, "Poodle has a working owner relation." );
  1577. });
  1578. test( "Overriding of supermodel relations", function() {
  1579. var models = {};
  1580. Backbone.Relational.store.addModelScope( models );
  1581. models.URL = Backbone.RelationalModel.extend({});
  1582. models.File = Backbone.RelationalModel.extend({
  1583. subModelTypes: {
  1584. 'video': 'Video',
  1585. 'publication': 'Publication'
  1586. },
  1587. relations: [{
  1588. type: Backbone.HasOne,
  1589. key: 'url',
  1590. relatedModel: models.URL
  1591. }]
  1592. });
  1593. models.Video = models.File.extend({});
  1594. // Publication redefines the `url` relation
  1595. models.Publication = Backbone.RelationalModel.extend({
  1596. relations: [{
  1597. type: Backbone.HasMany,
  1598. key: 'url',
  1599. relatedModel: models.URL
  1600. }]
  1601. });
  1602. models.Project = Backbone.RelationalModel.extend({
  1603. relations: [{
  1604. type: Backbone.HasMany,
  1605. key: 'files',
  1606. relatedModel: models.File,
  1607. reverseRelation: {
  1608. key: 'project'
  1609. }
  1610. }]
  1611. });
  1612. equal( models.File.prototype.relations.length, 2, "2 relations on File" );
  1613. equal( models.Video.prototype.relations.length, 1, "1 relation on Video" );
  1614. equal( models.Publication.prototype.relations.length, 1, "1 relation on Publication" );
  1615. // Instantiating the superModel should instantiate the modelHierarchy, and copy relations over to subModels
  1616. var file = new models.File();
  1617. equal( models.File.prototype.relations.length, 2, "2 relations on File" );
  1618. equal( models.Video.prototype.relations.length, 2, "2 relations on Video" );
  1619. equal( models.Publication.prototype.relations.length, 2, "2 relations on Publication" );
  1620. var projectDecription = {
  1621. name: 'project1',
  1622. files: [
  1623. {
  1624. name: 'file1 - video subclass',
  1625. type: 'video',
  1626. url: {
  1627. location: 'http://www.myurl.com/file1.avi'
  1628. }
  1629. },
  1630. {
  1631. name: 'file2 - file baseclass',
  1632. url: {
  1633. location: 'http://www.myurl.com/file2.jpg'
  1634. }
  1635. },
  1636. {
  1637. name: 'file3 - publication',
  1638. type: 'publication',
  1639. url: [
  1640. { location: 'http://www.myurl.com/file3.pdf' },
  1641. { location: 'http://www.anotherurl.com/file3.doc' }
  1642. ]
  1643. }
  1644. ]
  1645. };
  1646. var project = new models.Project( projectDecription ),
  1647. files = project.get( 'files' ),
  1648. file1 = files.at( 0 ),
  1649. file2 = files.at( 1 ),
  1650. file3 = files.at( 2 );
  1651. equal( models.File.prototype.relations.length, 2, "2 relations on File" );
  1652. equal( models.Video.prototype.relations.length, 2, "2 relations on Video" );
  1653. equal( models.Publication.prototype.relations.length, 2, "2 relations on Publication" );
  1654. equal( _.size( file1._relations ), 2 );
  1655. equal( _.size( file2._relations ), 2 );
  1656. equal( _.size( file3._relations ), 2 );
  1657. ok( file1.get( 'url' ) instanceof Backbone.Model, '`url` on Video is a model' );
  1658. ok( file1.getRelation( 'url' ) instanceof Backbone.HasOne, '`url` relation on Video is HasOne' );
  1659. ok( file3.get( 'url' ) instanceof Backbone.Collection, '`url` on Publication is a collection' );
  1660. ok( file3.getRelation( 'url' ) instanceof Backbone.HasMany, '`url` relation on Publication is HasMany' );
  1661. });
  1662. test( "toJSON includes the type", function() {
  1663. var scope = {};
  1664. Backbone.Relational.store.addModelScope( scope );
  1665. scope.PetAnimal = Backbone.RelationalModel.extend({
  1666. subModelTypes: {
  1667. 'dog': 'Dog'
  1668. }
  1669. });
  1670. scope.Dog = scope.PetAnimal.extend();
  1671. var dog = new scope.Dog({
  1672. name: 'Spot'
  1673. });
  1674. var json = dog.toJSON();
  1675. equal( json.type, 'dog', "The value of 'type' is the pet animal's type." );
  1676. });
  1677. module( "Backbone.Relation options", { setup: initObjects } );
  1678. test( "`includeInJSON` (Person to JSON)", function() {
  1679. var json = person1.toJSON();
  1680. 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)" );
  1681. ok ( json.likesALot instanceof Object, "The value of 'likesALot' is an object ('includeInJSON' is 'true')" );
  1682. equal( json.likesALot.likesALot, 'person-1', "Person is serialized only once" );
  1683. json = person1.get( 'user' ).toJSON();
  1684. equal( json.person, 'boy', "The value of 'person' is the person's name (`includeInJSON` is set to 'name')" );
  1685. json = person2.toJSON();
  1686. ok( person2.get('livesIn') instanceof House, "'person2' has a 'livesIn' relation" );
  1687. equal( json.livesIn, undefined , "The value of 'livesIn' is not serialized (`includeInJSON` is 'false')" );
  1688. json = person3.toJSON();
  1689. ok( json.user_id === null, "The value of 'user_id' is null");
  1690. ok( json.likesALot === null, "The value of 'likesALot' is null");
  1691. });
  1692. test( "`includeInJSON` (Zoo to JSON)", function() {
  1693. var zoo = new Zoo({
  1694. id: 0,
  1695. name: 'Artis',
  1696. city: 'Amsterdam',
  1697. animals: [
  1698. new Animal( { id: 1, species: 'bear', name: 'Baloo' } ),
  1699. new Animal( { id: 2, species: 'tiger', name: 'Shere Khan' } )
  1700. ]
  1701. });
  1702. var jsonZoo = zoo.toJSON(),
  1703. jsonBear = jsonZoo.animals[ 0 ];
  1704. ok( _.isArray( jsonZoo.animals ), "animals is an Array" );
  1705. equal( jsonZoo.animals.length, 2 );
  1706. equal( jsonBear.id, 1, "animal's id has been included in the JSON" );
  1707. equal( jsonBear.species, 'bear', "animal's species has been included in the JSON" );
  1708. ok( !jsonBear.name, "animal's name has not been included in the JSON" );
  1709. var tiger = zoo.get( 'animals' ).get( 1 ),
  1710. jsonTiger = tiger.toJSON();
  1711. ok( _.isObject( jsonTiger.livesIn ) && !_.isArray( jsonTiger.livesIn ), "zoo is an Object" );
  1712. equal( jsonTiger.livesIn.id, 0, "zoo.id is included in the JSON" );
  1713. equal( jsonTiger.livesIn.name, 'Artis', "zoo.name is included in the JSON" );
  1714. ok( !jsonTiger.livesIn.city, "zoo.city is not included in the JSON" );
  1715. });
  1716. test( "'createModels' is false", function() {
  1717. var NewUser = Backbone.RelationalModel.extend({});
  1718. var NewPerson = Backbone.RelationalModel.extend({
  1719. relations: [{
  1720. type: Backbone.HasOne,
  1721. key: 'user',
  1722. relatedModel: NewUser,
  1723. createModels: false
  1724. }]
  1725. });
  1726. var person = new NewPerson({
  1727. id: 'newperson-1',
  1728. resource_uri: 'newperson-1',
  1729. user: { id: 'newuser-1', resource_uri: 'newuser-1' }
  1730. });
  1731. ok( person.get( 'user' ) == null );
  1732. var user = new NewUser( { id: 'newuser-1', name: 'SuperUser' } );
  1733. ok( person.get( 'user' ) === user );
  1734. // Old data gets overwritten by the explicitly created user, since a model was never created from the old data
  1735. ok( person.get( 'user' ).get( 'resource_uri' ) == null );
  1736. });
  1737. test( "Relations load from both `keySource` and `key`", function() {
  1738. var Property = Backbone.RelationalModel.extend({
  1739. idAttribute: 'property_id'
  1740. });
  1741. var View = Backbone.RelationalModel.extend({
  1742. idAttribute: 'id',
  1743. relations: [{
  1744. type: Backbone.HasMany,
  1745. key: 'properties',
  1746. keySource: 'property_ids',
  1747. relatedModel: Property,
  1748. reverseRelation: {
  1749. key: 'view',
  1750. keySource: 'view_id'
  1751. }
  1752. }]
  1753. });
  1754. var property1 = new Property({
  1755. property_id: 1,
  1756. key: 'width',
  1757. value: 500,
  1758. view_id: 5
  1759. });
  1760. var view = new View({
  1761. id: 5,
  1762. property_ids: [ 2 ]
  1763. });
  1764. var property2 = new Property({
  1765. property_id: 2,
  1766. key: 'height',
  1767. value: 400
  1768. });
  1769. // The values from view.property_ids should be loaded into view.properties
  1770. ok( view.get( 'properties' ) && view.get( 'properties' ).length === 2, "'view' has two 'properties'" );
  1771. ok( typeof view.get( 'property_ids' ) === 'undefined', "'view' does not have 'property_ids'" );
  1772. view.set( 'properties', [ property1, property2 ] );
  1773. ok( view.get( 'properties' ) && view.get( 'properties' ).length === 2, "'view' has two 'properties'" );
  1774. view.set( 'property_ids', [ 1, 2 ] );
  1775. ok( view.get( 'properties' ) && view.get( 'properties' ).length === 2, "'view' has two 'properties'" );
  1776. });
  1777. test( "`keySource` is emptied after a set, doesn't get confused by `unset`", function() {
  1778. var SubModel = Backbone.RelationalModel.extend();
  1779. var Model = Backbone.RelationalModel.extend({
  1780. relations: [{
  1781. type: Backbone.HasOne,
  1782. key: 'submodel',
  1783. keySource: 'sub_data',
  1784. relatedModel: SubModel
  1785. }]
  1786. });
  1787. var inst = new Model( {'id': 123} );
  1788. // `set` may be called from fetch
  1789. inst.set({
  1790. 'id': 123,
  1791. 'some_field': 'some_value',
  1792. 'sub_data': {
  1793. 'id': 321,
  1794. 'key': 'value'
  1795. },
  1796. 'to_unset': 'unset value'
  1797. });
  1798. ok( inst.get('submodel').get('key') === 'value', "value of submodule.key should be 'value'" );
  1799. inst.set( { 'to_unset': '' }, { 'unset': true } );
  1800. ok( inst.get('submodel').get('key') === 'value', "after unset value of submodule.key should be still 'value'" );
  1801. ok( typeof inst.get('sub_data') === 'undefined', "keySource field should be removed from model" );
  1802. ok( typeof inst.get('submodel') !== 'undefined', "key field should be added..." );
  1803. ok( inst.get('submodel') instanceof SubModel, "... and should be model instance" );
  1804. // set called from fetch
  1805. inst.set({
  1806. 'sub_data': {
  1807. 'id': 321,
  1808. 'key': 'value2'
  1809. }
  1810. });
  1811. ok( typeof inst.get('sub_data') === 'undefined', "keySource field should be removed from model" );
  1812. ok( typeof inst.get('submodel') !== 'undefined', "key field should be present..." );
  1813. ok( inst.get('submodel').get('key') === 'value2', "... and should be updated" );
  1814. });
  1815. test( "'keyDestination' saves to 'key'", function() {
  1816. var Property = Backbone.RelationalModel.extend({
  1817. idAttribute: 'property_id'
  1818. });
  1819. var View = Backbone.RelationalModel.extend({
  1820. idAttribute: 'id',
  1821. relations: [{
  1822. type: Backbone.HasMany,
  1823. key: 'properties',
  1824. keyDestination: 'properties_attributes',
  1825. relatedModel: Property,
  1826. reverseRelation: {
  1827. key: 'view',
  1828. keyDestination: 'view_attributes',
  1829. includeInJSON: true
  1830. }
  1831. }]
  1832. });
  1833. var property1 = new Property({
  1834. property_id: 1,
  1835. key: 'width',
  1836. value: 500,
  1837. view: 5
  1838. });
  1839. var view = new View({
  1840. id: 5,
  1841. properties: [ 2 ]
  1842. });
  1843. var property2 = new Property({
  1844. property_id: 2,
  1845. key: 'height',
  1846. value: 400
  1847. });
  1848. var viewJSON = view.toJSON();
  1849. ok( viewJSON.properties_attributes && viewJSON.properties_attributes.length === 2, "'viewJSON' has two 'properties_attributes'" );
  1850. ok( typeof viewJSON.properties === 'undefined', "'viewJSON' does not have 'properties'" );
  1851. });
  1852. test( "'collectionOptions' sets the options on the created HasMany Collections", function() {
  1853. var shop = new Shop({ id: 1 });
  1854. equal( shop.get( 'customers' ).url, 'shop/' + shop.id + '/customers/' );
  1855. });
  1856. test( "`parse` with deeply nested relations", function() {
  1857. var collParseCalled = 0,
  1858. modelParseCalled = 0;
  1859. var Job = Backbone.RelationalModel.extend({});
  1860. var JobCollection = Backbone.Collection.extend({
  1861. model: Job,
  1862. parse: function( resp, options ) {
  1863. collParseCalled++;
  1864. return resp.data || resp;
  1865. }
  1866. });
  1867. var Company = Backbone.RelationalModel.extend({
  1868. relations: [{
  1869. type: 'HasMany',
  1870. key: 'employees',
  1871. parse: true,
  1872. relatedModel: Job,
  1873. collectionType: JobCollection,
  1874. reverseRelation: {
  1875. key: 'company'
  1876. }
  1877. }]
  1878. });
  1879. var Person = Backbone.RelationalModel.extend({
  1880. relations: [{
  1881. type: 'HasMany',
  1882. key: 'jobs',
  1883. parse: true,
  1884. relatedModel: Job,
  1885. collectionType: JobCollection,
  1886. reverseRelation: {
  1887. key: 'person',
  1888. parse: false
  1889. }
  1890. }],
  1891. parse: function( resp, options ) {
  1892. modelParseCalled++;
  1893. var data = _.clone( resp.model );
  1894. data.id = data.id.uri;
  1895. return data;
  1896. }
  1897. });
  1898. Company.prototype.parse = Job.prototype.parse = function( resp, options ) {
  1899. modelParseCalled++;
  1900. var data = _.clone( resp.model );
  1901. data.id = data.id.uri;
  1902. return data;
  1903. };
  1904. var data = {
  1905. model: {
  1906. id: { uri: 'c1' },
  1907. employees: [
  1908. {
  1909. model: {
  1910. id: { uri: 'e1' },
  1911. person: {
  1912. /*model: {
  1913. id: { uri: 'p1' },
  1914. jobs: [ 'e1', { model: { id: { uri: 'e3' } } } ]
  1915. }*/
  1916. id: 'p1',
  1917. jobs: [ 'e1', { model: { id: { uri: 'e3' } } } ]
  1918. }
  1919. }
  1920. },
  1921. {
  1922. model: {
  1923. id: { uri: 'e2' },
  1924. person: {
  1925. id: 'p2'
  1926. /*model: {
  1927. id: { uri: 'p2' }
  1928. }*/
  1929. }
  1930. }
  1931. }
  1932. ]
  1933. }
  1934. };
  1935. var company = new Company( data, { parse: true } ),
  1936. employees = company.get( 'employees' ),
  1937. job = employees.first(),
  1938. person = job.get( 'person' );
  1939. ok( job && job.id === 'e1', 'job exists' );
  1940. ok( person && person.id === 'p1', 'person exists' );
  1941. ok( modelParseCalled === 4, 'model.parse called 4 times? ' + modelParseCalled );
  1942. ok( collParseCalled === 0, 'coll.parse called 0 times? ' + collParseCalled );
  1943. });
  1944. module( "Backbone.Relation preconditions", { setup: reset } );
  1945. test( "'type', 'key', 'relatedModel' are required properties", function() {
  1946. var Properties = Backbone.RelationalModel.extend({});
  1947. var View = Backbone.RelationalModel.extend({
  1948. relations: [
  1949. {
  1950. key: 'listProperties',
  1951. relatedModel: Properties
  1952. }
  1953. ]
  1954. });
  1955. var view = new View();
  1956. ok( _.size( view._relations ) === 0 );
  1957. ok( view.getRelations().length === 0 );
  1958. View = Backbone.RelationalModel.extend({
  1959. relations: [
  1960. {
  1961. type: Backbone.HasOne,
  1962. relatedModel: Properties
  1963. }
  1964. ]
  1965. });
  1966. view = new View();
  1967. ok( _.size( view._relations ) === 0 );
  1968. View = Backbone.RelationalModel.extend({
  1969. relations: [
  1970. {
  1971. type: Backbone.HasOne,
  1972. key: 'listProperties'
  1973. }
  1974. ]
  1975. });
  1976. view = new View();
  1977. ok( _.size( view._relations ) === 0 );
  1978. });
  1979. test( "'type' can be a string or an object reference", function() {
  1980. var Properties = Backbone.RelationalModel.extend({});
  1981. var View = Backbone.RelationalModel.extend({
  1982. relations: [
  1983. {
  1984. type: 'Backbone.HasOne',
  1985. key: 'listProperties',
  1986. relatedModel: Properties
  1987. }
  1988. ]
  1989. });
  1990. var view = new View();
  1991. ok( _.size( view._relations ) === 1 );
  1992. View = Backbone.RelationalModel.extend({
  1993. relations: [
  1994. {
  1995. type: 'HasOne',
  1996. key: 'listProperties',
  1997. relatedModel: Properties
  1998. }
  1999. ]
  2000. });
  2001. view = new View();
  2002. ok( _.size( view._relations ) === 1 );
  2003. View = Backbone.RelationalModel.extend({
  2004. relations: [
  2005. {
  2006. type: Backbone.HasOne,
  2007. key: 'listProperties',
  2008. relatedModel: Properties
  2009. }
  2010. ]
  2011. });
  2012. view = new View();
  2013. ok( _.size( view._relations ) === 1 );
  2014. });
  2015. test( "'key' can be a string or an object reference", function() {
  2016. var Properties = Backbone.RelationalModel.extend({});
  2017. var View = Backbone.RelationalModel.extend({
  2018. relations: [
  2019. {
  2020. type: Backbone.HasOne,
  2021. key: 'listProperties',
  2022. relatedModel: Properties
  2023. }
  2024. ]
  2025. });
  2026. var view = new View();
  2027. ok( _.size( view._relations ) === 1 );
  2028. View = Backbone.RelationalModel.extend({
  2029. relations: [
  2030. {
  2031. type: Backbone.HasOne,
  2032. key: 'listProperties',
  2033. relatedModel: Properties
  2034. }
  2035. ]
  2036. });
  2037. view = new View();
  2038. ok( _.size( view._relations ) === 1 );
  2039. });
  2040. test( "HasMany with a reverseRelation HasMany is not allowed", function() {
  2041. var User = Backbone.RelationalModel.extend({});
  2042. var Password = Backbone.RelationalModel.extend({
  2043. relations: [{
  2044. type: 'HasMany',
  2045. key: 'users',
  2046. relatedModel: User,
  2047. reverseRelation: {
  2048. type: 'HasMany',
  2049. key: 'passwords'
  2050. }
  2051. }]
  2052. });
  2053. var password = new Password({
  2054. plaintext: 'qwerty',
  2055. users: [ 'person-1', 'person-2', 'person-3' ]
  2056. });
  2057. ok( _.size( password._relations ) === 0, "No _relations created on Password" );
  2058. });
  2059. test( "Duplicate relations not allowed (two simple relations)", function() {
  2060. var Properties = Backbone.RelationalModel.extend({});
  2061. var View = Backbone.RelationalModel.extend({
  2062. relations: [
  2063. {
  2064. type: Backbone.HasOne,
  2065. key: 'properties',
  2066. relatedModel: Properties
  2067. },
  2068. {
  2069. type: Backbone.HasOne,
  2070. key: 'properties',
  2071. relatedModel: Properties
  2072. }
  2073. ]
  2074. });
  2075. var view = new View();
  2076. view.set( { properties: new Properties() } );
  2077. ok( _.size( view._relations ) === 1 );
  2078. });
  2079. test( "Duplicate relations not allowed (one relation with a reverse relation, one without)", function() {
  2080. var Properties = Backbone.RelationalModel.extend({});
  2081. var View = Backbone.RelationalModel.extend({
  2082. relations: [
  2083. {
  2084. type: Backbone.HasOne,
  2085. key: 'properties',
  2086. relatedModel: Properties,
  2087. reverseRelation: {
  2088. type: Backbone.HasOne,
  2089. key: 'view'
  2090. }
  2091. },
  2092. {
  2093. type: Backbone.HasOne,
  2094. key: 'properties',
  2095. relatedModel: Properties
  2096. }
  2097. ]
  2098. });
  2099. var view = new View();
  2100. view.set( { properties: new Properties() } );
  2101. ok( _.size( view._relations ) === 1 );
  2102. });
  2103. test( "Duplicate relations not allowed (two relations with reverse relations)", function() {
  2104. var Properties = Backbone.RelationalModel.extend({});
  2105. var View = Backbone.RelationalModel.extend({
  2106. relations: [
  2107. {
  2108. type: Backbone.HasOne,
  2109. key: 'properties',
  2110. relatedModel: Properties,
  2111. reverseRelation: {
  2112. type: Backbone.HasOne,
  2113. key: 'view'
  2114. }
  2115. },
  2116. {
  2117. type: Backbone.HasOne,
  2118. key: 'properties',
  2119. relatedModel: Properties,
  2120. reverseRelation: {
  2121. type: Backbone.HasOne,
  2122. key: 'view'
  2123. }
  2124. }
  2125. ]
  2126. });
  2127. var view = new View();
  2128. view.set( { properties: new Properties() } );
  2129. ok( _.size( view._relations ) === 1 );
  2130. });
  2131. test( "Duplicate relations not allowed (different relations, reverse relations)", function() {
  2132. var Properties = Backbone.RelationalModel.extend({});
  2133. var View = Backbone.RelationalModel.extend({
  2134. relations: [
  2135. {
  2136. type: Backbone.HasOne,
  2137. key: 'listProperties',
  2138. relatedModel: Properties,
  2139. reverseRelation: {
  2140. type: Backbone.HasOne,
  2141. key: 'view'
  2142. }
  2143. },
  2144. {
  2145. type: Backbone.HasOne,
  2146. key: 'windowProperties',
  2147. relatedModel: Properties,
  2148. reverseRelation: {
  2149. type: Backbone.HasOne,
  2150. key: 'view'
  2151. }
  2152. }
  2153. ]
  2154. });
  2155. var view = new View(),
  2156. prop1 = new Properties( { name: 'a' } ),
  2157. prop2 = new Properties( { name: 'b' } );
  2158. view.set( { listProperties: prop1, windowProperties: prop2 } );
  2159. ok( _.size( view._relations ) === 2 );
  2160. ok( _.size( prop1._relations ) === 1 );
  2161. ok( view.get( 'listProperties' ).get( 'name' ) === 'a' );
  2162. ok( view.get( 'windowProperties' ).get( 'name' ) === 'b' );
  2163. });
  2164. module( "Backbone.Relation general", { setup: reset } );
  2165. test( "Only valid models (no validation failure) should be added to a relation", function() {
  2166. var zoo = new Zoo();
  2167. zoo.on( 'add:animals', function( animal ) {
  2168. ok( animal instanceof Animal );
  2169. });
  2170. var smallElephant = new Animal( { name: 'Jumbo', species: 'elephant', weight: 2000, livesIn: zoo } );
  2171. equal( zoo.get( 'animals' ).length, 1, "Just 1 elephant in the zoo" );
  2172. // should fail validation, so it shouldn't be added
  2173. zoo.get( 'animals' ).add( { name: 'Big guy', species: 'elephant', weight: 13000 }, { validate: true } );
  2174. equal( zoo.get( 'animals' ).length, 1, "Still just 1 elephant in the zoo" );
  2175. });
  2176. test( "Updating (retrieving) a model keeps relation consistency intact", function() {
  2177. var zoo = new Zoo();
  2178. var lion = new Animal({
  2179. species: 'Lion',
  2180. livesIn: zoo
  2181. });
  2182. equal( zoo.get( 'animals' ).length, 1 );
  2183. lion.set({
  2184. id: 5,
  2185. species: 'Lion',
  2186. livesIn: zoo
  2187. });
  2188. equal( zoo.get( 'animals' ).length, 1 );
  2189. zoo.set({
  2190. name: 'Dierenpark Amersfoort',
  2191. animals: [ 5 ]
  2192. });
  2193. equal( zoo.get( 'animals' ).length, 1 );
  2194. ok( zoo.get( 'animals' ).at( 0 ) === lion, "lion is in zoo" );
  2195. ok( lion.get( 'livesIn' ) === zoo );
  2196. var elephant = new Animal({
  2197. species: 'Elephant',
  2198. livesIn: zoo
  2199. });
  2200. equal( zoo.get( 'animals' ).length, 2 );
  2201. ok( elephant.get( 'livesIn' ) === zoo );
  2202. zoo.set({
  2203. id: 2
  2204. });
  2205. equal( zoo.get( 'animals' ).length, 2 );
  2206. ok( lion.get( 'livesIn' ) === zoo );
  2207. ok( elephant.get( 'livesIn' ) === zoo );
  2208. });
  2209. test( "Setting id on objects with reverse relations updates related collection correctly", function() {
  2210. var zoo1 = new Zoo();
  2211. ok( zoo1.get( 'animals' ).size() === 0, "zoo has no animals" );
  2212. var lion = new Animal( { livesIn: 2 } );
  2213. zoo1.set( 'id', 2 );
  2214. ok( lion.get( 'livesIn' ) === zoo1, "zoo1 connected to lion" );
  2215. ok( zoo1.get( 'animals' ).length === 1, "zoo1 has one Animal" );
  2216. ok( zoo1.get( 'animals' ).at( 0 ) === lion, "lion added to zoo1" );
  2217. ok( zoo1.get( 'animals' ).get( lion ) === lion, "lion can be retrieved from zoo1" );
  2218. lion.set( { id: 5, livesIn: 2 } );
  2219. ok( lion.get( 'livesIn' ) === zoo1, "zoo1 connected to lion" );
  2220. ok( zoo1.get( 'animals' ).length === 1, "zoo1 has one Animal" );
  2221. ok( zoo1.get( 'animals' ).at( 0 ) === lion, "lion added to zoo1" );
  2222. ok( zoo1.get( 'animals' ).get( lion ) === lion, "lion can be retrieved from zoo1" );
  2223. // Other way around
  2224. var elephant = new Animal( { id: 6 } ),
  2225. tiger = new Animal( { id: 7 } ),
  2226. zoo2 = new Zoo( { animals: [ 6 ] } );
  2227. ok( elephant.get( 'livesIn' ) === zoo2, "zoo2 connected to elephant" );
  2228. ok( zoo2.get( 'animals' ).length === 1, "zoo2 has one Animal" );
  2229. ok( zoo2.get( 'animals' ).at( 0 ) === elephant, "elephant added to zoo2" );
  2230. ok( zoo2.get( 'animals' ).get( elephant ) === elephant, "elephant can be retrieved from zoo2" );
  2231. zoo2.set( { id: 5, animals: [ 6, 7 ] } );
  2232. ok( elephant.get( 'livesIn' ) === zoo2, "zoo2 connected to elephant" );
  2233. ok( tiger.get( 'livesIn' ) === zoo2, "zoo2 connected to tiger" );
  2234. ok( zoo2.get( 'animals' ).length === 2, "zoo2 has one Animal" );
  2235. ok( zoo2.get( 'animals' ).at( 0 ) === elephant, "elephant added to zoo2" );
  2236. ok( zoo2.get( 'animals' ).at( 1 ) === tiger, "tiger added to zoo2" );
  2237. ok( zoo2.get( 'animals' ).get( elephant ) === elephant, "elephant can be retrieved from zoo2" );
  2238. ok( zoo2.get( 'animals' ).get( tiger ) === tiger, "tiger can be retrieved from zoo2" );
  2239. });
  2240. test( "Collections can be passed as attributes on creation", function() {
  2241. var animals = new AnimalCollection([
  2242. { id: 1, species: 'Lion' },
  2243. { id: 2 ,species: 'Zebra' }
  2244. ]);
  2245. var zoo = new Zoo( { animals: animals } );
  2246. equal( zoo.get( 'animals' ), animals, "The 'animals' collection has been set as the zoo's animals" );
  2247. equal( zoo.get( 'animals' ).length, 2, "Two animals in 'zoo'" );
  2248. zoo.destroy();
  2249. var newZoo = new Zoo( { animals: animals.models } );
  2250. ok( newZoo.get( 'animals' ).length === 2, "Two animals in the 'newZoo'" );
  2251. });
  2252. test( "Models can be passed as attributes on creation", function() {
  2253. var artis = new Zoo( { name: 'Artis' } );
  2254. var animal = new Animal( { species: 'Hippo', livesIn: artis });
  2255. equal( artis.get( 'animals' ).at( 0 ), animal, "Artis has a Hippo" );
  2256. equal( animal.get( 'livesIn' ), artis, "The Hippo is in Artis" );
  2257. });
  2258. test( "id checking handles `undefined`, `null`, `0` ids properly", function() {
  2259. var parent = new Node();
  2260. var child = new Node( { parent: parent } );
  2261. ok( child.get( 'parent' ) === parent );
  2262. parent.destroy();
  2263. ok( child.get( 'parent' ) === null, child.get( 'parent' ) + ' === null' );
  2264. // It used to be the case that `randomOtherNode` became `child`s parent here, since both the `parent.id`
  2265. // (which is stored as the relation's `keyContents`) and `randomOtherNode.id` were undefined.
  2266. var randomOtherNode = new Node();
  2267. ok( child.get( 'parent' ) === null, child.get( 'parent' ) + ' === null' );
  2268. // Create a child with parent id=0, then create the parent
  2269. child = new Node( { parent: 0 } );
  2270. ok( child.get( 'parent' ) === null, child.get( 'parent' ) + ' === null' );
  2271. parent = new Node( { id: 0 } );
  2272. ok( child.get( 'parent' ) === parent );
  2273. child.destroy();
  2274. parent.destroy();
  2275. // The other way around; create the parent with id=0, then the child
  2276. parent = new Node( { id: 0 } );
  2277. equal( parent.get( 'children' ).length, 0 );
  2278. child = new Node( { parent: 0 } );
  2279. ok( child.get( 'parent' ) === parent );
  2280. });
  2281. test( "Relations are not affected by `silent: true`", function() {
  2282. var ceo = new Person( { id: 1 } );
  2283. var company = new Company( {
  2284. employees: [ { id: 2 }, { id: 3 }, 4 ],
  2285. ceo: 1
  2286. }, { silent: true } ),
  2287. employees = company.get( 'employees' ),
  2288. employee = employees.first();
  2289. ok( company.get( 'ceo' ) === ceo );
  2290. ok( employees instanceof Backbone.Collection );
  2291. equal( employees.length, 2 );
  2292. employee.set( 'company', null, { silent: true } );
  2293. equal( employees.length, 1 );
  2294. employees.add( employee, { silent: true } );
  2295. ok( employee.get( 'company' ) === company );
  2296. ceo.set( 'runs', null, { silent: true } );
  2297. ok( !company.get( 'ceo' ) );
  2298. var employee4 = new Job( { id: 4 } );
  2299. equal( employees.length, 3 );
  2300. });
  2301. test( "Repeated model initialization and a collection should not break existing models", function () {
  2302. var dataCompanyA = {
  2303. id: 'company-a',
  2304. name: 'Big Corp.',
  2305. employees: [ { id: 'job-a' }, { id: 'job-b' } ]
  2306. };
  2307. var dataCompanyB = {
  2308. id: 'company-b',
  2309. name: 'Small Corp.',
  2310. employees: []
  2311. };
  2312. var companyA = new Company( dataCompanyA );
  2313. // Attempting to instantiate another model with the same data will throw an error
  2314. throws( function() { new Company( dataCompanyA ); }, "Can only instantiate one model for a given `id` (per model type)" );
  2315. // init-ed a lead and its nested contacts are a collection
  2316. ok( companyA.get('employees') instanceof Backbone.Collection, "Company's employees should be a collection" );
  2317. equal(companyA.get('employees').length, 2, 'with elements');
  2318. var CompanyCollection = Backbone.Collection.extend({
  2319. model: Company
  2320. });
  2321. var companyCollection = new CompanyCollection( [ dataCompanyA, dataCompanyB ] );
  2322. // After loading a collection with models of the same type
  2323. // the existing company should still have correct collections
  2324. ok( companyCollection.get( dataCompanyA.id ) === companyA );
  2325. ok( companyA.get('employees') instanceof Backbone.Collection, "Company's employees should still be a collection" );
  2326. equal( companyA.get('employees').length, 2, 'with elements' );
  2327. });
  2328. test( "Destroy removes models from (non-reverse) relations", function() {
  2329. var agent = new Agent( { id: 1, customers: [ 2, 3, 4 ], address: { city: 'Utrecht' } } );
  2330. var c2 = new Customer( { id: 2 } );
  2331. var c3 = new Customer( { id: 3 } );
  2332. var c4 = new Customer( { id: 4 } );
  2333. ok( agent.get( 'customers' ).length === 3 );
  2334. c2.destroy();
  2335. ok( agent.get( 'customers' ).length === 2 );
  2336. ok( agent.get( 'customers' ).get( c3 ) === c3 );
  2337. ok( agent.get( 'customers' ).get( c4 ) === c4 );
  2338. agent.get( 'customers' ).remove( c3 );
  2339. ok( agent.get( 'customers' ).length === 1 );
  2340. ok( agent.get( 'address' ) instanceof Address );
  2341. agent.get( 'address' ).destroy();
  2342. ok( !agent.get( 'address' ) );
  2343. agent.destroy();
  2344. equal( agent.get( 'customers' ).length, 0 );
  2345. });
  2346. test( "If keySource is used, don't remove a model that is present in the key attribute", function() {
  2347. var ForumPost = Backbone.RelationalModel.extend({
  2348. // Normally would set something here, not needed for test
  2349. });
  2350. var Forum = Backbone.RelationalModel.extend({
  2351. relations: [{
  2352. type: Backbone.HasMany,
  2353. key: 'posts',
  2354. relatedModel: ForumPost,
  2355. reverseRelation: {
  2356. key: 'forum',
  2357. keySource: 'forum_id'
  2358. }
  2359. }]
  2360. });
  2361. var testPost = new ForumPost({
  2362. id: 1,
  2363. title: 'Hello World',
  2364. forum: { id: 1, title: 'Cupcakes' }
  2365. });
  2366. var testForum = Forum.findOrCreate( 1 );
  2367. notEqual( testPost.get( 'forum' ), null, "The post's forum is not null" );
  2368. equal( testPost.get( 'forum' ).get( 'title' ), "Cupcakes", "The post's forum title is Cupcakes" );
  2369. equal( testForum.get( 'title' ), "Cupcakes", "A forum of id 1 has the title cupcakes" );
  2370. var testPost2 = new ForumPost({
  2371. id: 3,
  2372. title: 'Hello World',
  2373. forum: { id: 2, title: 'Donuts' },
  2374. forum_id: 3
  2375. });
  2376. notEqual( testPost2.get( 'forum' ), null, "The post's forum is not null" );
  2377. equal( testPost2.get( 'forum' ).get( 'title' ), "Donuts", "The post's forum title is Donuts" );
  2378. deepEqual( testPost2.getRelation( 'forum' ).keyContents, { id: 2, title: 'Donuts' }, 'The expected forum is 2' );
  2379. equal( testPost2.getRelation( 'forum' ).keyId, null, "There's no expected forum anymore" );
  2380. var testPost3 = new ForumPost({
  2381. id: 4,
  2382. title: 'Hello World',
  2383. forum: null,
  2384. forum_id: 3
  2385. });
  2386. equal( testPost3.get( 'forum' ), null, "The post's forum is null" );
  2387. equal( testPost3.getRelation( 'forum' ).keyId, 3, 'Forum is expected to have id=3' );
  2388. });
  2389. // GH-187
  2390. test( "Can pass related model in constructor", function() {
  2391. var A = Backbone.RelationalModel.extend();
  2392. var B = Backbone.RelationalModel.extend({
  2393. relations: [{
  2394. type: Backbone.HasOne,
  2395. key: 'a',
  2396. keySource: 'a_id',
  2397. relatedModel: A
  2398. }]
  2399. });
  2400. var a1 = new A({ id: 'a1' });
  2401. var b1 = new B();
  2402. b1.set( 'a', a1 );
  2403. ok( b1.get( 'a' ) instanceof A );
  2404. ok( b1.get( 'a' ).id === 'a1' );
  2405. var a2 = new A({ id: 'a2' });
  2406. var b2 = new B({ a: a2 });
  2407. ok( b2.get( 'a' ) instanceof A );
  2408. ok( b2.get( 'a' ).id === 'a2' );
  2409. });
  2410. module( "Backbone.HasOne", { setup: initObjects } );
  2411. test( "HasOne relations on Person are set up properly", function() {
  2412. ok( person1.get('likesALot') === person2 );
  2413. equal( person1.get('user').id, 'user-1', "The id of 'person1's user is 'user-1'" );
  2414. ok( person2.get('likesALot') === person1 );
  2415. });
  2416. test( "Reverse HasOne relations on Person are set up properly", function() {
  2417. ok( person1.get( 'likedALotBy' ) === person2 );
  2418. ok( person1.get( 'user' ).get( 'person' ) === person1, "The person belonging to 'person1's user is 'person1'" );
  2419. ok( person2.get( 'likedALotBy' ) === person1 );
  2420. });
  2421. test( "'set' triggers 'change' and 'update', on a HasOne relation, for a Model with multiple relations", 9, function() {
  2422. // triggers initialization of the reverse relation from User to Password
  2423. var password = new Password( { plaintext: 'asdf' } );
  2424. person1.on( 'change', function( model, options ) {
  2425. ok( model.get( 'user' ) instanceof User, "In 'change', model.user is an instance of User" );
  2426. equal( model.previous( 'user' ).get( 'login' ), oldLogin, "previousAttributes is available on 'change'" );
  2427. });
  2428. person1.on( 'change:user', function( model, options ) {
  2429. ok( model.get( 'user' ) instanceof User, "In 'change:user', model.user is an instance of User" );
  2430. equal( model.previous( 'user' ).get( 'login' ), oldLogin, "previousAttributes is available on 'change'" );
  2431. });
  2432. person1.on( 'change:user', function( model, attr, options ) {
  2433. ok( model.get( 'user' ) instanceof User, "In 'change:user', model.user is an instance of User" );
  2434. ok( attr.get( 'person' ) === person1, "The user's 'person' is 'person1'" );
  2435. ok( attr.get( 'password' ) instanceof Password, "The user's password attribute is a model of type Password");
  2436. equal( attr.get( 'password' ).get( 'plaintext' ), 'qwerty', "The user's password is ''qwerty'" );
  2437. });
  2438. var user = { login: 'me@hotmail.com', password: { plaintext: 'qwerty' } };
  2439. var oldLogin = person1.get( 'user' ).get( 'login' );
  2440. // Triggers assertions for 'change' and 'change:user'
  2441. person1.set( { user: user } );
  2442. user = person1.get( 'user' ).on( 'change:password', function( model, attr, options ) {
  2443. equal( attr.get( 'plaintext' ), 'asdf', "The user's password is ''qwerty'" );
  2444. });
  2445. // Triggers assertions for 'change:user'
  2446. user.set( { password: password } );
  2447. });
  2448. test( "'set' doesn't triggers 'change' and 'change:' when passed `silent: true`", 2, function() {
  2449. person1.on( 'change', function( model, options ) {
  2450. ok( false, "'change' should not get triggered" );
  2451. });
  2452. person1.on( 'change:user', function( model, attr, options ) {
  2453. ok( false, "'change:user' should not get triggered" );
  2454. });
  2455. person1.on( 'change:user', function( model, attr, options ) {
  2456. ok( false, "'change:user' should not get triggered" );
  2457. });
  2458. ok( person1.get( 'user' ) instanceof User, "person1 has a 'user'" );
  2459. var user = new User({ login: 'me@hotmail.com', password: { plaintext: 'qwerty' } });
  2460. person1.set( 'user', user, { silent: true } );
  2461. equal( person1.get( 'user' ), user );
  2462. });
  2463. test( "'unset' triggers 'change' and 'change:<key>'", 4, function() {
  2464. person1.on( 'change', function( model, options ) {
  2465. equal( model.get('user'), null, "model.user is unset" );
  2466. });
  2467. person1.on( 'change:user', function( model, attr, options ) {
  2468. equal( attr, null, "new value of attr (user) is null" );
  2469. });
  2470. ok( person1.get( 'user' ) instanceof User, "person1 has a 'user'" );
  2471. var user = person1.get( 'user' );
  2472. person1.unset( 'user' );
  2473. equal( user.get( 'person' ), null, "person1 is not set on 'user' anymore" );
  2474. });
  2475. test( "'clear' triggers 'change' and 'change:<key>'", 4, function() {
  2476. person1.on( 'change', function( model, options ) {
  2477. equal( model.get('user'), null, "model.user is unset" );
  2478. });
  2479. person1.on( 'change:user', function( model, attr, options ) {
  2480. equal( attr, null, "new value of attr (user) is null" );
  2481. });
  2482. ok( person1.get( 'user' ) instanceof User, "person1 has a 'user'" );
  2483. var user = person1.get( 'user' );
  2484. person1.clear();
  2485. equal( user.get( 'person' ), null, "person1 is not set on 'user' anymore" );
  2486. });
  2487. module( "Backbone.HasMany", { setup: initObjects } );
  2488. test( "Listeners on 'add'/'remove'", 7, function() {
  2489. ourHouse
  2490. .on( 'add:occupants', function( model, coll ) {
  2491. ok( model === person1, "model === person1" );
  2492. })
  2493. .on( 'remove:occupants', function( model, coll ) {
  2494. ok( model === person1, "model === person1" );
  2495. });
  2496. theirHouse
  2497. .on( 'add:occupants', function( model, coll ) {
  2498. ok( model === person1, "model === person1" );
  2499. })
  2500. .on( 'remove:occupants', function( model, coll ) {
  2501. ok( model === person1, "model === person1" );
  2502. });
  2503. var count = 0;
  2504. person1.on( 'change:livesIn', function( model, attr ) {
  2505. if ( count === 0 ) {
  2506. ok( attr === ourHouse, "model === ourHouse" );
  2507. }
  2508. else if ( count === 1 ) {
  2509. ok( attr === theirHouse, "model === theirHouse" );
  2510. }
  2511. else if ( count === 2 ) {
  2512. ok( attr === null, "model === null" );
  2513. }
  2514. count++;
  2515. });
  2516. ourHouse.get( 'occupants' ).add( person1 );
  2517. person1.set( { 'livesIn': theirHouse } );
  2518. theirHouse.get( 'occupants' ).remove( person1 );
  2519. });
  2520. test( "Listeners for 'add'/'remove', on a HasMany relation, for a Model with multiple relations", function() {
  2521. var job1 = { company: oldCompany };
  2522. var job2 = { company: oldCompany, person: person1 };
  2523. var job3 = { person: person1 };
  2524. var newJob = null;
  2525. newCompany.on( 'add:employees', function( model, coll ) {
  2526. ok( false, "person1 should only be added to 'oldCompany'." );
  2527. });
  2528. // Assert that all relations on a Model are set up, before notifying related models.
  2529. oldCompany.on( 'add:employees', function( model, coll ) {
  2530. newJob = model;
  2531. ok( model instanceof Job );
  2532. ok( model.get('company') instanceof Company && model.get('person') instanceof Person,
  2533. "Both Person and Company are set on the Job instance" );
  2534. });
  2535. person1.on( 'add:jobs', function( model, coll ) {
  2536. ok( model.get( 'company' ) === oldCompany && model.get( 'person' ) === person1,
  2537. "Both Person and Company are set on the Job instance" );
  2538. });
  2539. // Add job1 and job2 to the 'Person' side of the relation
  2540. var jobs = person1.get( 'jobs' );
  2541. jobs.add( job1 );
  2542. ok( jobs.length === 1, "jobs.length is 1" );
  2543. newJob.destroy();
  2544. ok( jobs.length === 0, "jobs.length is 0" );
  2545. jobs.add( job2 );
  2546. ok( jobs.length === 1, "jobs.length is 1" );
  2547. newJob.destroy();
  2548. ok( jobs.length === 0, "jobs.length is 0" );
  2549. // Add job1 and job2 to the 'Company' side of the relation
  2550. var employees = oldCompany.get('employees');
  2551. employees.add( job3 );
  2552. ok( employees.length === 2, "employees.length is 2" );
  2553. newJob.destroy();
  2554. ok( employees.length === 1, "employees.length is 1" );
  2555. employees.add( job2 );
  2556. ok( employees.length === 2, "employees.length is 2" );
  2557. newJob.destroy();
  2558. ok( employees.length === 1, "employees.length is 1" );
  2559. // Create a stand-alone Job ;)
  2560. new Job({
  2561. person: person1,
  2562. company: oldCompany
  2563. });
  2564. ok( jobs.length === 1 && employees.length === 2, "jobs.length is 1 and employees.length is 2" );
  2565. });
  2566. test( "The Collections used for HasMany relations are re-used if possible", function() {
  2567. var collId = ourHouse.get( 'occupants' ).id = 1;
  2568. ourHouse.get( 'occupants' ).add( person1 );
  2569. ok( ourHouse.get( 'occupants' ).id === collId );
  2570. // Set a value on 'occupants' that would cause the relation to be reset.
  2571. // The collection itself should be kept (along with it's properties)
  2572. ourHouse.set( { 'occupants': [ 'person-1' ] } );
  2573. ok( ourHouse.get( 'occupants' ).id === collId );
  2574. ok( ourHouse.get( 'occupants' ).length === 1 );
  2575. // Setting a new collection loses the original collection
  2576. ourHouse.set( { 'occupants': new Backbone.Collection() } );
  2577. ok( ourHouse.get( 'occupants' ).id === undefined );
  2578. });
  2579. test( "On `set`, or creation, accept a collection or an array of ids/objects/models", function() {
  2580. // Handle an array of ids
  2581. var visitor1 = new Visitor( { id: 'visitor-1', name: 'Mr. Pink' } ),
  2582. visitor2 = new Visitor( { id: 'visitor-2' } );
  2583. var zoo = new Zoo( { visitors: [ 'visitor-1', 'visitor-3' ] } ),
  2584. visitors = zoo.get( 'visitors' );
  2585. equal( visitors.length, 1 );
  2586. var visitor3 = new Visitor( { id: 'visitor-3' } );
  2587. equal( visitors.length, 2 );
  2588. zoo.set( 'visitors', [ { name: 'Incognito' } ] );
  2589. equal( visitors.length, 1 );
  2590. zoo.set( 'visitors', [] );
  2591. equal( visitors.length, 0 );
  2592. // Handle an array of objects
  2593. zoo = new Zoo( { visitors: [ { id: 'visitor-1' }, { id: 'visitor-4' } ] } );
  2594. visitors = zoo.get( 'visitors' );
  2595. equal( visitors.length, 2 );
  2596. equal( visitors.get( 'visitor-1' ).get( 'name' ), 'Mr. Pink', 'visitor-1 is Mr. Pink' );
  2597. zoo.set( 'visitors', [ { id: 'visitor-1' }, { id: 'visitor-5' } ] );
  2598. equal( visitors.length, 2 );
  2599. // Handle an array of models
  2600. zoo = new Zoo( { visitors: [ visitor1 ] } );
  2601. visitors = zoo.get( 'visitors' );
  2602. equal( visitors.length, 1 );
  2603. ok( visitors.first() === visitor1 );
  2604. zoo.set( 'visitors', [ visitor2 ] );
  2605. equal( visitors.length, 1 );
  2606. ok( visitors.first() === visitor2 );
  2607. // Handle a Collection
  2608. var visitorColl = new Backbone.Collection( [ visitor1, visitor2 ] );
  2609. zoo = new Zoo( { visitors: visitorColl } );
  2610. visitors = zoo.get( 'visitors' );
  2611. equal( visitors.length, 2 );
  2612. zoo.set( 'visitors', false );
  2613. equal( visitors.length, 0 );
  2614. visitorColl = new Backbone.Collection( [ visitor2 ] );
  2615. zoo.set( 'visitors', visitorColl );
  2616. ok( visitorColl === zoo.get( 'visitors' ) );
  2617. equal( zoo.get( 'visitors' ).length, 1 );
  2618. });
  2619. test( "On `set`, or creation, handle edge-cases where the server supplies a single object/id", function() {
  2620. // Handle single objects
  2621. var zoo = new Zoo({
  2622. animals: { id: 'lion-1' }
  2623. });
  2624. var animals = zoo.get( 'animals' );
  2625. equal( animals.length, 1, "There is 1 animal in the zoo" );
  2626. zoo.set( 'animals', { id: 'lion-2' } );
  2627. equal( animals.length, 1, "There is 1 animal in the zoo" );
  2628. // Handle single models
  2629. var lion3 = new Animal( { id: 'lion-3' } );
  2630. zoo = new Zoo({
  2631. animals: lion3
  2632. });
  2633. animals = zoo.get( 'animals' );
  2634. equal( animals.length, 1, "There is 1 animal in the zoo" );
  2635. zoo.set( 'animals', null );
  2636. equal( animals.length, 0, "No animals in the zoo" );
  2637. zoo.set( 'animals', lion3 );
  2638. equal( animals.length, 1, "There is 1 animal in the zoo" );
  2639. // Handle single ids
  2640. zoo = new Zoo({
  2641. animals: 'lion-4'
  2642. });
  2643. animals = zoo.get( 'animals' );
  2644. equal( animals.length, 0, "No animals in the zoo" );
  2645. var lion4 = new Animal( { id: 'lion-4' } );
  2646. equal( animals.length, 1, "There is 1 animal in the zoo" );
  2647. zoo.set( 'animals', 'lion-5' );
  2648. equal( animals.length, 0, "No animals in the zoo" );
  2649. var lion5 = new Animal( { id: 'lion-5' } );
  2650. equal( animals.length, 1, "There is 1 animal in the zoo" );
  2651. zoo.set( 'animals', null );
  2652. equal( animals.length, 0, "No animals in the zoo" );
  2653. zoo = new Zoo({
  2654. animals: 'lion-4'
  2655. });
  2656. animals = zoo.get( 'animals' );
  2657. equal( animals.length, 1, "There is 1 animal in the zoo" );
  2658. // Bulletproof?
  2659. zoo = new Zoo({
  2660. animals: ''
  2661. });
  2662. animals = zoo.get( 'animals' );
  2663. ok( animals instanceof AnimalCollection );
  2664. equal( animals.length, 0, "No animals in the zoo" );
  2665. });
  2666. test( "Setting a custom collection in 'collectionType' uses that collection for instantiation", function() {
  2667. var zoo = new Zoo();
  2668. // Set values so that the relation gets filled
  2669. zoo.set({
  2670. animals: [
  2671. { species: 'Lion' },
  2672. { species: 'Zebra' }
  2673. ]
  2674. });
  2675. // Check that the animals were created
  2676. ok( zoo.get( 'animals' ).at( 0 ).get( 'species' ) === 'Lion' );
  2677. ok( zoo.get( 'animals' ).at( 1 ).get( 'species' ) === 'Zebra' );
  2678. // Check that the generated collection is of the correct kind
  2679. ok( zoo.get( 'animals' ) instanceof AnimalCollection );
  2680. });
  2681. test( "Setting a new collection maintains that collection's current 'models'", function() {
  2682. var zoo = new Zoo();
  2683. var animals = new AnimalCollection([
  2684. { id: 1, species: 'Lion' },
  2685. { id: 2 ,species: 'Zebra' }
  2686. ]);
  2687. zoo.set( 'animals', animals );
  2688. equal( zoo.get( 'animals' ).length, 2 );
  2689. var newAnimals = new AnimalCollection([
  2690. { id: 2, species: 'Zebra' },
  2691. { id: 3, species: 'Elephant' },
  2692. { id: 4, species: 'Tiger' }
  2693. ]);
  2694. zoo.set( 'animals', newAnimals );
  2695. equal( zoo.get( 'animals' ).length, 3 );
  2696. });
  2697. test( "Models found in 'findRelated' are all added in one go (so 'sort' will only be called once)", function() {
  2698. var count = 0,
  2699. sort = Backbone.Collection.prototype.sort;
  2700. Backbone.Collection.prototype.sort = function() {
  2701. count++;
  2702. };
  2703. AnimalCollection.prototype.comparator = $.noop;
  2704. var zoo = new Zoo({
  2705. animals: [
  2706. { id: 1, species: 'Lion' },
  2707. { id: 2 ,species: 'Zebra' }
  2708. ]
  2709. });
  2710. equal( count, 1, "Sort is called only once" );
  2711. Backbone.Collection.prototype.sort = sort;
  2712. delete AnimalCollection.prototype.comparator;
  2713. });
  2714. test( "Raw-models set to a hasMany relation do trigger an add event in the underlying Collection with a correct index", function() {
  2715. var zoo = new Zoo();
  2716. var indexes = [];
  2717. zoo.get( 'animals' ).on( 'add', function( model, collection, options ) {
  2718. var index = collection.indexOf( model );
  2719. indexes.push(index);
  2720. });
  2721. zoo.set( 'animals', [
  2722. { id : 1, species : 'Lion' },
  2723. { id : 2, species : 'Zebra' }
  2724. ]);
  2725. equal( indexes[0], 0, "First item has index 0" );
  2726. equal( indexes[1], 1, "Second item has index 1" );
  2727. });
  2728. test( "Models set to a hasMany relation do trigger an add event in the underlying Collection with a correct index", function() {
  2729. var zoo = new Zoo();
  2730. var indexes = [];
  2731. zoo.get("animals").on("add", function(model, collection, options) {
  2732. var index = collection.indexOf(model);
  2733. indexes.push(index);
  2734. });
  2735. zoo.set("animals", [
  2736. new Animal({ id : 1, species : 'Lion' }),
  2737. new Animal({ id : 2, species : 'Zebra'})
  2738. ]);
  2739. equal( indexes[0], 0, "First item has index 0" );
  2740. equal( indexes[1], 1, "Second item has index 1" );
  2741. });
  2742. test( "Sort event should be fired after the add event that caused it, even when using 'set'", function() {
  2743. var zoo = new Zoo();
  2744. var animals = zoo.get('animals');
  2745. var events = [];
  2746. animals.comparator = 'id';
  2747. animals.on('add', function() { events.push('add'); });
  2748. animals.on('sort', function() { events.push('sort'); });
  2749. zoo.set('animals' , [
  2750. {id : 'lion-2'},
  2751. {id : 'lion-1'}
  2752. ]);
  2753. equal(animals.at(0).id, 'lion-1');
  2754. deepEqual(events, ['add', 'sort', 'add', 'sort']);
  2755. });
  2756. test( "The 'collectionKey' options is used to create references on generated Collections back to its RelationalModel", function() {
  2757. var zoo = new Zoo({
  2758. animals: [ 'lion-1', 'zebra-1' ]
  2759. });
  2760. equal( zoo.get( 'animals' ).livesIn, zoo );
  2761. equal( zoo.get( 'animals' ).zoo, undefined );
  2762. var FarmAnimal = Backbone.RelationalModel.extend();
  2763. var Barn = Backbone.RelationalModel.extend({
  2764. relations: [{
  2765. type: Backbone.HasMany,
  2766. key: 'animals',
  2767. relatedModel: FarmAnimal,
  2768. collectionKey: 'barn',
  2769. reverseRelation: {
  2770. key: 'livesIn',
  2771. includeInJSON: 'id'
  2772. }
  2773. }]
  2774. });
  2775. var barn = new Barn({
  2776. animals: [ 'chicken-1', 'cow-1' ]
  2777. });
  2778. equal( barn.get( 'animals' ).livesIn, undefined );
  2779. equal( barn.get( 'animals' ).barn, barn );
  2780. FarmAnimal = Backbone.RelationalModel.extend();
  2781. var BarnNoKey = Backbone.RelationalModel.extend({
  2782. relations: [{
  2783. type: Backbone.HasMany,
  2784. key: 'animals',
  2785. relatedModel: FarmAnimal,
  2786. collectionKey: false,
  2787. reverseRelation: {
  2788. key: 'livesIn',
  2789. includeInJSON: 'id'
  2790. }
  2791. }]
  2792. });
  2793. var barnNoKey = new BarnNoKey({
  2794. animals: [ 'chicken-1', 'cow-1' ]
  2795. });
  2796. equal( barnNoKey.get( 'animals' ).livesIn, undefined );
  2797. equal( barnNoKey.get( 'animals' ).barn, undefined );
  2798. });
  2799. test( "Polymorhpic relations", function() {
  2800. var Location = Backbone.RelationalModel.extend();
  2801. var Locatable = Backbone.RelationalModel.extend({
  2802. relations: [
  2803. {
  2804. key: 'locations',
  2805. type: 'HasMany',
  2806. relatedModel: Location,
  2807. reverseRelation: {
  2808. key: 'locatable'
  2809. }
  2810. }
  2811. ]
  2812. });
  2813. var FirstLocatable = Locatable.extend();
  2814. var SecondLocatable = Locatable.extend();
  2815. var firstLocatable = new FirstLocatable();
  2816. var secondLocatable = new SecondLocatable();
  2817. var firstLocation = new Location( { id: 1, locatable: firstLocatable } );
  2818. var secondLocation = new Location( { id: 2, locatable: secondLocatable } );
  2819. ok( firstLocatable.get( 'locations' ).at( 0 ) === firstLocation );
  2820. ok( firstLocatable.get( 'locations' ).at( 0 ).get( 'locatable' ) === firstLocatable );
  2821. ok( secondLocatable.get( 'locations' ).at( 0 ) === secondLocation );
  2822. ok( secondLocatable.get( 'locations' ).at( 0 ).get( 'locatable' ) === secondLocatable );
  2823. });
  2824. test( "Cloned instances of persisted models should not be added to any existing collections", function() {
  2825. var addedModels = 0;
  2826. var zoo = new window.Zoo({
  2827. visitors : [ { name : "Incognito" } ]
  2828. });
  2829. var visitor = new window.Visitor();
  2830. zoo.get( 'visitors' ).on( 'add', function( model, coll ) {
  2831. addedModels++;
  2832. });
  2833. visitor.clone();
  2834. equal( addedModels, 0, "A new visitor should not be forced to go to the zoo!" );
  2835. });
  2836. module( "Reverse relations", { setup: initObjects } );
  2837. test( "Add and remove", function() {
  2838. equal( ourHouse.get( 'occupants' ).length, 1, "ourHouse has 1 occupant" );
  2839. equal( person1.get( 'livesIn' ), null, "Person 1 doesn't live anywhere" );
  2840. ourHouse.get( 'occupants' ).add( person1 );
  2841. equal( ourHouse.get( 'occupants' ).length, 2, "Our House has 2 occupants" );
  2842. equal( person1.get( 'livesIn' ) && person1.get('livesIn').id, ourHouse.id, "Person 1 lives in ourHouse" );
  2843. person1.set( { 'livesIn': theirHouse } );
  2844. equal( theirHouse.get( 'occupants' ).length, 1, "theirHouse has 1 occupant" );
  2845. equal( ourHouse.get( 'occupants' ).length, 1, "ourHouse has 1 occupant" );
  2846. equal( person1.get( 'livesIn' ) && person1.get('livesIn').id, theirHouse.id, "Person 1 lives in theirHouse" );
  2847. });
  2848. test( "Destroy removes models from reverse relations", function() {
  2849. var zoo = new Zoo( { id:1, animals: [ 2, 3, 4 ] } );
  2850. var rhino = new Animal( { id: 2, species: 'rhino' } );
  2851. var baboon = new Animal( { id: 3, species: 'baboon' } );
  2852. var hippo = new Animal( { id: 4, species: 'hippo' } );
  2853. ok( zoo.get( 'animals' ).length === 3 );
  2854. rhino.destroy();
  2855. ok( zoo.get( 'animals' ).length === 2 );
  2856. ok( zoo.get( 'animals' ).get( baboon ) === baboon );
  2857. ok( !rhino.get( 'zoo' ) );
  2858. zoo.get( 'animals' ).remove( hippo );
  2859. ok( zoo.get( 'animals' ).length === 1 );
  2860. ok( !hippo.get( 'zoo' ) );
  2861. zoo.destroy();
  2862. ok( zoo.get( 'animals' ).length === 0 );
  2863. ok( !baboon.get( 'zoo' ) );
  2864. });
  2865. test( "HasOne relations to self (tree stucture)", function() {
  2866. var child1 = new Node({ id: '2', parent: '1', name: 'First child' });
  2867. var parent = new Node({ id: '1', name: 'Parent' });
  2868. var child2 = new Node({ id: '3', parent: '1', name: 'Second child' });
  2869. equal( parent.get( 'children' ).length, 2 );
  2870. ok( parent.get( 'children' ).include( child1 ) );
  2871. ok( parent.get( 'children' ).include( child2 ) );
  2872. ok( child1.get( 'parent' ) === parent );
  2873. equal( child1.get( 'children' ).length, 0 );
  2874. ok( child2.get( 'parent' ) === parent );
  2875. equal( child2.get( 'children' ).length, 0 );
  2876. });
  2877. test( "Models referencing each other in the same relation", function() {
  2878. var parent = new Node({ id: 1 });
  2879. var child = new Node({ id: 2 });
  2880. child.set( 'parent', parent );
  2881. parent.save( { 'parent': child } );
  2882. ok( parent.get( 'parent' ) === child );
  2883. ok( child.get( 'parent' ) === parent );
  2884. });
  2885. test( "HasMany relations to self (tree structure)", function() {
  2886. var child1 = new Node({ id: '2', name: 'First child' });
  2887. var parent = new Node({ id: '1', children: [ '2', '3' ], name: 'Parent' });
  2888. var child2 = new Node({ id: '3', name: 'Second child' });
  2889. equal( parent.get( 'children' ).length, 2 );
  2890. ok( parent.get( 'children' ).include( child1 ) );
  2891. ok( parent.get( 'children' ).include( child2 ) );
  2892. ok( child1.get( 'parent' ) === parent );
  2893. equal( child1.get( 'children' ).length, 0 );
  2894. ok( child2.get( 'parent' ) === parent );
  2895. equal( child2.get( 'children' ).length, 0 );
  2896. });
  2897. test( "HasOne relations to self (cycle, directed graph structure)", function() {
  2898. var node1 = new Node({ id: '1', parent: '3', name: 'First node' });
  2899. var node2 = new Node({ id: '2', parent: '1', name: 'Second node' });
  2900. var node3 = new Node({ id: '3', parent: '2', name: 'Third node' });
  2901. ok( node1.get( 'parent' ) === node3 );
  2902. equal( node1.get( 'children' ).length, 1 );
  2903. ok( node1.get( 'children' ).at(0) === node2 );
  2904. ok( node2.get( 'parent' ) === node1 );
  2905. equal( node2.get( 'children' ).length, 1 );
  2906. ok( node2.get( 'children' ).at(0) === node3 );
  2907. ok( node3.get( 'parent' ) === node2 );
  2908. equal( node3.get( 'children' ).length, 1 );
  2909. ok( node3.get( 'children' ).at(0) === node1 );
  2910. });
  2911. test( "New objects (no 'id' yet) have working relations", function() {
  2912. var person = new Person({
  2913. name: 'Remi'
  2914. });
  2915. person.set( { user: { login: '1', email: '1' } } );
  2916. var user1 = person.get( 'user' );
  2917. ok( user1 instanceof User, "User created on Person" );
  2918. equal( user1.get('login'), '1', "person.user is the correct User" );
  2919. var user2 = new User({
  2920. login: '2',
  2921. email: '2'
  2922. });
  2923. ok( user2.get( 'person' ) === null, "'user' doesn't belong to a 'person' yet" );
  2924. person.set( { user: user2 } );
  2925. ok( user1.get( 'person' ) === null );
  2926. ok( person.get( 'user' ) === user2 );
  2927. ok( user2.get( 'person' ) === person );
  2928. person2.set( { user: user2 } );
  2929. ok( person.get( 'user' ) === null );
  2930. ok( person2.get( 'user' ) === user2 );
  2931. ok( user2.get( 'person' ) === person2 );
  2932. });
  2933. test( "'Save' objects (performing 'set' multiple times without and with id)", 4, function() {
  2934. person3
  2935. .on( 'add:jobs', function( model, coll ) {
  2936. var company = model.get('company');
  2937. ok( company instanceof Company && company.get('ceo').get('name') === 'Lunar boy' && model.get('person') === person3,
  2938. "add:jobs: Both Person and Company are set on the Job instance once the event gets fired" );
  2939. })
  2940. .on( 'remove:jobs', function( model, coll ) {
  2941. ok( false, "remove:jobs: 'person3' should not lose his job" );
  2942. });
  2943. // Create Models from an object. Should trigger `add:jobs` on `person3`
  2944. var company = new Company({
  2945. name: 'Luna Corp.',
  2946. ceo: {
  2947. name: 'Lunar boy'
  2948. },
  2949. employees: [ { person: 'person-3' } ]
  2950. });
  2951. company
  2952. .on( 'add:employees', function( model, coll ) {
  2953. var company = model.get('company');
  2954. ok( company instanceof Company && company.get('ceo').get('name') === 'Lunar boy' && model.get('person') === person3,
  2955. "add:employees: Both Person and Company are set on the Company instance once the event gets fired" );
  2956. })
  2957. .on( 'remove:employees', function( model, coll ) {
  2958. ok( true, "'remove:employees: person3' should lose a job once" );
  2959. });
  2960. // Backbone.save executes "model.set(model.parse(resp), options)". Set a full map over object, but now with ids.
  2961. // Should trigger `remove:employees`, `add:employees`, and `add:jobs`
  2962. company.set({
  2963. id: 'company-3',
  2964. name: 'Big Corp.',
  2965. ceo: {
  2966. id: 'person-4',
  2967. name: 'Lunar boy',
  2968. resource_uri: 'person-4'
  2969. },
  2970. employees: [ { id: 'job-1', person: 'person-3', resource_uri: 'job-1' } ],
  2971. resource_uri: 'company-3'
  2972. });
  2973. // This should not trigger additional `add`/`remove` events
  2974. company.set({
  2975. employees: [ 'job-1' ]
  2976. });
  2977. });
  2978. test( "Set the same value a couple of time, by 'id' and object", function() {
  2979. person1.set( { likesALot: 'person-2' } );
  2980. person1.set( { likesALot: person2 } );
  2981. ok( person1.get('likesALot') === person2 );
  2982. ok( person2.get('likedALotBy' ) === person1 );
  2983. person1.set( { likesALot: 'person-2' } );
  2984. ok( person1.get('likesALot') === person2 );
  2985. ok( person2.get('likedALotBy' ) === person1 );
  2986. });
  2987. test( "Numerical keys", function() {
  2988. var child1 = new Node({ id: 2, name: 'First child' });
  2989. var parent = new Node({ id: 1, children: [2, 3], name: 'Parent' });
  2990. var child2 = new Node({ id: 3, name: 'Second child' });
  2991. equal( parent.get('children').length, 2 );
  2992. ok( parent.get('children').include( child1 ) );
  2993. ok( parent.get('children').include( child2 ) );
  2994. ok( child1.get('parent') === parent );
  2995. equal( child1.get('children').length, 0 );
  2996. ok( child2.get('parent') === parent );
  2997. equal( child2.get('children').length, 0 );
  2998. });
  2999. test( "Relations that use refs to other models (instead of keys)", function() {
  3000. var child1 = new Node({ id: 2, name: 'First child' });
  3001. var parent = new Node({ id: 1, children: [child1, 3], name: 'Parent' });
  3002. var child2 = new Node({ id: 3, name: 'Second child' });
  3003. ok( child1.get('parent') === parent );
  3004. equal( child1.get('children').length, 0 );
  3005. equal( parent.get('children').length, 2 );
  3006. ok( parent.get('children').include( child1 ) );
  3007. ok( parent.get('children').include( child2 ) );
  3008. var child3 = new Node({ id: 4, parent: parent, name: 'Second child' });
  3009. equal( parent.get('children').length, 3 );
  3010. ok( parent.get('children').include( child3 ) );
  3011. ok( child3.get('parent') === parent );
  3012. equal( child3.get('children').length, 0 );
  3013. });
  3014. test( "Add an already existing model (reverseRelation shouldn't exist yet) to a relation as a hash", function() {
  3015. // This test caused a race condition to surface:
  3016. // The 'relation's constructor initializes the 'reverseRelation', which called 'relation.addRelated' in it's 'initialize'.
  3017. // However, 'relation's 'initialize' has not been executed yet, so it doesn't have a 'related' collection yet.
  3018. var Properties = Backbone.RelationalModel.extend({});
  3019. var View = Backbone.RelationalModel.extend({
  3020. relations: [
  3021. {
  3022. type: Backbone.HasMany,
  3023. key: 'properties',
  3024. relatedModel: Properties,
  3025. reverseRelation: {
  3026. type: Backbone.HasOne,
  3027. key: 'view'
  3028. }
  3029. }
  3030. ]
  3031. });
  3032. var props = new Properties( { id: 1, key: 'width', value: '300px', view: 1 } );
  3033. var view = new View({
  3034. id: 1,
  3035. properties: [ { id: 1, key: 'width', value: '300px', view: 1 } ]
  3036. });
  3037. ok( props.get( 'view' ) === view );
  3038. ok( view.get( 'properties' ).include( props ) );
  3039. });
  3040. test( "Reverse relations are found for models that have not been instantiated and use .extend()", function() {
  3041. var View = Backbone.RelationalModel.extend({ });
  3042. var Property = Backbone.RelationalModel.extend({
  3043. relations: [{
  3044. type: Backbone.HasOne,
  3045. key: 'view',
  3046. relatedModel: View,
  3047. reverseRelation: {
  3048. type: Backbone.HasMany,
  3049. key: 'properties'
  3050. }
  3051. }]
  3052. });
  3053. var view = new View({
  3054. id: 1,
  3055. properties: [ { id: 1, key: 'width', value: '300px' } ]
  3056. });
  3057. ok( view.get( 'properties' ) instanceof Backbone.Collection );
  3058. });
  3059. test( "Reverse relations found for models that have not been instantiated and run .setup() manually", function() {
  3060. // Generated from CoffeeScript code:
  3061. // class View extends Backbone.RelationalModel
  3062. //
  3063. // View.setup()
  3064. //
  3065. // class Property extends Backbone.RelationalModel
  3066. // relations: [
  3067. // type: Backbone.HasOne
  3068. // key: 'view'
  3069. // relatedModel: View
  3070. // reverseRelation:
  3071. // type: Backbone.HasMany
  3072. // key: 'properties'
  3073. // ]
  3074. //
  3075. // Property.setup()
  3076. var Property, View,
  3077. __hasProp = {}.hasOwnProperty,
  3078. __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; };
  3079. View = ( function( _super ) {
  3080. __extends(View, _super);
  3081. View.name = 'View';
  3082. function View() {
  3083. return View.__super__.constructor.apply( this, arguments );
  3084. }
  3085. return View;
  3086. })( Backbone.RelationalModel );
  3087. View.setup();
  3088. Property = (function(_super) {
  3089. __extends(Property, _super);
  3090. Property.name = 'Property';
  3091. function Property() {
  3092. return Property.__super__.constructor.apply(this, arguments);
  3093. }
  3094. Property.prototype.relations = [{
  3095. type: Backbone.HasOne,
  3096. key: 'view',
  3097. relatedModel: View,
  3098. reverseRelation: {
  3099. type: Backbone.HasMany,
  3100. key: 'properties'
  3101. }
  3102. }];
  3103. return Property;
  3104. })(Backbone.RelationalModel);
  3105. Property.setup();
  3106. var view = new View({
  3107. id: 1,
  3108. properties: [ { id: 1, key: 'width', value: '300px' } ]
  3109. });
  3110. ok( view.get( 'properties' ) instanceof Backbone.Collection );
  3111. });
  3112. test( "ReverseRelations are applied retroactively", function() {
  3113. // Use brand new Model types, so we can be sure we don't have any reverse relations cached from previous tests
  3114. var NewUser = Backbone.RelationalModel.extend({});
  3115. var NewPerson = Backbone.RelationalModel.extend({
  3116. relations: [{
  3117. type: Backbone.HasOne,
  3118. key: 'user',
  3119. relatedModel: NewUser,
  3120. reverseRelation: {
  3121. type: Backbone.HasOne,
  3122. key: 'person'
  3123. }
  3124. }]
  3125. });
  3126. var user = new NewUser( { id: 'newuser-1' } );
  3127. //var user2 = new NewUser( { id: 'newuser-2', person: 'newperson-1' } );
  3128. var person = new NewPerson( { id: 'newperson-1', user: user } );
  3129. ok( person.get('user') === user );
  3130. ok( user.get('person') === person );
  3131. //console.log( person, user );
  3132. });
  3133. test( "ReverseRelations are applied retroactively (2)", function() {
  3134. var models = {};
  3135. Backbone.Relational.store.addModelScope( models );
  3136. // Use brand new Model types, so we can be sure we don't have any reverse relations cached from previous tests
  3137. models.NewPerson = Backbone.RelationalModel.extend({
  3138. relations: [{
  3139. type: Backbone.HasOne,
  3140. key: 'user',
  3141. relatedModel: 'NewUser',
  3142. reverseRelation: {
  3143. type: Backbone.HasOne,
  3144. key: 'person'
  3145. }
  3146. }]
  3147. });
  3148. models.NewUser = Backbone.RelationalModel.extend({});
  3149. var user = new models.NewUser( { id: 'newuser-1', person: { id: 'newperson-1' } } );
  3150. equal( user.getRelations().length, 1 );
  3151. ok( user.get( 'person' ) instanceof models.NewPerson );
  3152. });
  3153. test( "Deep reverse relation starting from a collection", function() {
  3154. var nodes = new NodeList([
  3155. {
  3156. id: 1,
  3157. children: [
  3158. {
  3159. id: 2,
  3160. children: [
  3161. {
  3162. id: 3,
  3163. children: [ 1 ]
  3164. }
  3165. ]
  3166. }
  3167. ]
  3168. }
  3169. ]);
  3170. var parent = nodes.first();
  3171. ok( parent, 'first item accessible after resetting collection' );
  3172. ok( parent.collection === nodes, '`parent.collection` is set to `nodes`' );
  3173. var child = parent.get( 'children' ).first();
  3174. ok( child, '`child` can be retrieved from `parent`' );
  3175. ok( child.get( 'parent' ), 'reverse relation from `child` to `parent` works');
  3176. var grandchild = child.get( 'children' ).first();
  3177. ok( grandchild, '`grandchild` can be retrieved from `child`' );
  3178. ok( grandchild.get( 'parent' ), 'reverse relation from `grandchild` to `child` works');
  3179. ok( grandchild.get( 'children' ).first() === parent, 'reverse relation from `grandchild` to `parent` works');
  3180. ok( parent.get( 'parent' ) === grandchild, 'circular reference from `grandchild` to `parent` works' );
  3181. });
  3182. test( "Deep reverse relation starting from a collection, with existing model", function() {
  3183. new Node( { id: 1 } );
  3184. var nodes = new NodeList();
  3185. nodes.set([
  3186. {
  3187. id: 1,
  3188. children: [
  3189. {
  3190. id: 2,
  3191. children: [
  3192. {
  3193. id: 3,
  3194. children: [ 1 ]
  3195. }
  3196. ]
  3197. }
  3198. ]
  3199. }
  3200. ]);
  3201. var parent = nodes.first();
  3202. ok( parent && parent.id === 1, 'first item accessible after resetting collection' );
  3203. var child = parent.get( 'children' ).first();
  3204. ok( child, '`child` can be retrieved from `parent`' );
  3205. ok( child.get( 'parent' ), 'reverse relation from `child` to `parent` works');
  3206. var grandchild = child.get( 'children' ).first();
  3207. ok( grandchild, '`grandchild` can be retrieved from `child`' );
  3208. ok( grandchild.get( 'parent' ), 'reverse relation from `grandchild` to `child` works');
  3209. ok( grandchild.get( 'children' ).first() === parent, 'reverse relation from `grandchild` to `parent` works');
  3210. ok( parent.get( 'parent' ) === grandchild, 'circular reference from `grandchild` to `parent` works' );
  3211. });
  3212. module( "Backbone.Collection", { setup: reset } );
  3213. test( "Loading (fetching) multiple times updates the model, and relations's `keyContents`", function() {
  3214. var collA = new Backbone.Collection();
  3215. collA.model = User;
  3216. var collB = new Backbone.Collection();
  3217. collB.model = User;
  3218. // Similar to what happens when calling 'fetch' on collA, updating it, calling 'fetch' on collB
  3219. var name = 'User 1';
  3220. collA.add( { id: '/user/1/', name: name } );
  3221. var user = collA.at( 0 );
  3222. equal( user.get( 'name' ), name );
  3223. // The 'name' of 'user' is updated when adding a new hash to the collection
  3224. name = 'New name';
  3225. collA.add( { id: '/user/1/', name: name }, { merge: true } );
  3226. var updatedUser = collA.at( 0 );
  3227. equal( user.get( 'name' ), name );
  3228. equal( updatedUser.get( 'name' ), name );
  3229. // The 'name' of 'user' is also updated when adding a new hash to another collection
  3230. name = 'Another new name';
  3231. collB.add( { id: '/user/1/', name: name, title: 'Superuser' }, { merge: true } );
  3232. var updatedUser2 = collA.at( 0 );
  3233. equal( user.get( 'name' ), name );
  3234. equal( updatedUser2.get('name'), name );
  3235. //console.log( collA.models, collA.get( '/user/1/' ), user, updatedUser, updatedUser2 );
  3236. ok( collA.get( '/user/1/' ) === updatedUser );
  3237. ok( collA.get( '/user/1/' ) === updatedUser2 );
  3238. ok( collB.get( '/user/1/' ) === user );
  3239. });
  3240. test( "Loading (fetching) a collection multiple times updates related models as well (HasOne)", function() {
  3241. var coll = new PersonCollection();
  3242. coll.add( { id: 'person-10', name: 'Person', user: { id: 'user-10', login: 'User' } } );
  3243. var person = coll.at( 0 );
  3244. var user = person.get( 'user' );
  3245. equal( user.get( 'login' ), 'User' );
  3246. coll.add( { id: 'person-10', name: 'New person', user: { id: 'user-10', login: 'New user' } }, { merge: true } );
  3247. equal( person.get( 'name' ), 'New person' );
  3248. equal( user.get( 'login' ), 'New user' );
  3249. });
  3250. test( "Loading (fetching) a collection multiple times updates related models as well (HasMany)", function() {
  3251. var coll = new Backbone.Collection();
  3252. coll.model = Zoo;
  3253. // Create a 'zoo' with 1 animal in it
  3254. coll.add( { id: 'zoo-1', name: 'Zoo', animals: [ { id: 'lion-1', name: 'Mufasa' } ] } );
  3255. var zoo = coll.at( 0 );
  3256. var lion = zoo.get( 'animals' ) .at( 0 );
  3257. equal( lion.get( 'name' ), 'Mufasa' );
  3258. // Update the name of 'zoo' and 'lion'
  3259. coll.add( { id: 'zoo-1', name: 'Zoo Station', animals: [ { id: 'lion-1', name: 'Simba' } ] }, { merge: true } );
  3260. equal( zoo.get( 'name' ), 'Zoo Station' );
  3261. equal( lion.get( 'name' ), 'Simba' );
  3262. });
  3263. test( "reset should use `merge: true` by default", function() {
  3264. var nodeList = new NodeList();
  3265. nodeList.add( [ { id: 1 }, { id: 2, parent: 1 } ] );
  3266. var node1 = nodeList.get( 1 ),
  3267. node2 = nodeList.get( 2 );
  3268. ok( node2.get( 'parent' ) === node1 );
  3269. ok( !node1.get( 'parent' ) );
  3270. nodeList.reset( [ { id: 1, parent: 2 } ] );
  3271. ok( node1.get( 'parent' ) === node2 );
  3272. });
  3273. test( "Return values for add/remove/reset/set match plain Backbone's", function() {
  3274. var Car = Backbone.RelationalModel.extend(),
  3275. Cars = Backbone.Collection.extend( { model: Car } ),
  3276. cars = new Cars();
  3277. ok( cars.add( { name: 'A' } ) instanceof Car, "Add one model" );
  3278. var added = cars.add( [ { name: 'B' }, { name: 'C' } ] );
  3279. ok( _.isArray( added ), "Added (an array of) two models" );
  3280. ok( added.length === 2 );
  3281. ok( cars.remove( cars.at( 0 ) ) instanceof Car, "Remove one model" );
  3282. var removed = cars.remove( [ cars.at( 0 ), cars.at( 1 ) ] );
  3283. ok( _.isArray( removed ), "Remove (an array of) two models" );
  3284. ok( removed.length === 2 );
  3285. ok( cars.reset( { name: 'D' } ) instanceof Car, "Reset with one model" );
  3286. var reset = cars.reset( [ { name: 'E' }, { name: 'F' } ] );
  3287. ok( _.isArray( reset ), "Reset (an array of) two models" );
  3288. ok( reset.length === 2 );
  3289. ok( cars.length === 2 );
  3290. var e = cars.at(0),
  3291. f = cars.at(1);
  3292. ok( cars.set( e ) instanceof Car, "Set one model" );
  3293. ok( _.isArray( cars.set( [ e, f ] ) ), "Set (an array of) two models" );
  3294. // Check removing `[]`
  3295. var result = cars.remove( [] );
  3296. ok( _.isArray( result ) && !result.length, "Removing `[]` is a noop" );
  3297. ok( cars.length === 2 );
  3298. // Check removing `null`
  3299. result = cars.remove( null );
  3300. ok( _.isUndefined( result ), "Removing `null` is a noop" );
  3301. ok( cars.length === 2 );
  3302. // Check setting to `[]`
  3303. result = cars.set( [] );
  3304. ok( _.isArray( result ) && !result.length, "Set `[]` empties collection" );
  3305. ok( cars.length === 0 );
  3306. cars.set( [ e, f ] );
  3307. ok( cars.length === 2 );
  3308. // Check setting `null`
  3309. ok( _.isUndefined( cars.set( null ) ), "Set `null` empties collection" );
  3310. ok( cars.length === 0 );
  3311. });
  3312. test( "add/remove/set (with `add`, `remove` and `merge` options)", function() {
  3313. var coll = new AnimalCollection();
  3314. /**
  3315. * Add
  3316. */
  3317. coll.add( { id: 1, species: 'giraffe' } );
  3318. ok( coll.length === 1 );
  3319. coll.add( { id: 1, species: 'giraffe' } );
  3320. ok( coll.length === 1 );
  3321. coll.add([
  3322. {
  3323. id: 1, species: 'giraffe'
  3324. },
  3325. {
  3326. id: 2, species: 'gorilla'
  3327. }
  3328. ]);
  3329. var giraffe = coll.get( 1 ),
  3330. gorilla = coll.get( 2 ),
  3331. dolphin = new Animal( { species: 'dolphin' } ),
  3332. hippo = new Animal( { id: 4, species: 'hippo' } );
  3333. ok( coll.length === 2 );
  3334. coll.add( dolphin );
  3335. ok( coll.length === 3 );
  3336. // Update won't do anything
  3337. coll.add( { id: 1, species: 'giraffe', name: 'Long John' } );
  3338. ok( !coll.get( 1 ).get( 'name' ), 'name=' + coll.get( 1 ).get( 'name' ) );
  3339. // Update with `merge: true` will update the animal
  3340. coll.add( { id: 1, species: 'giraffe', name: 'Long John' }, { merge: true } );
  3341. ok( coll.get( 1 ).get( 'name' ) === 'Long John' );
  3342. /**
  3343. * Remove
  3344. */
  3345. coll.remove( 1 );
  3346. ok( coll.length === 2 );
  3347. ok( !coll.get( 1 ), "`giraffe` removed from coll" );
  3348. coll.remove( dolphin );
  3349. ok( coll.length === 1 );
  3350. ok( coll.get( 2 ) === gorilla, "Only `gorilla` is left in coll" );
  3351. /**
  3352. * Update
  3353. */
  3354. coll.add( giraffe );
  3355. // This shouldn't do much at all
  3356. var options = { add: false, merge: false, remove: false };
  3357. coll.set( [ dolphin, { id: 2, name: 'Silverback' } ], options );
  3358. ok( coll.length === 2 );
  3359. ok( coll.get( 2 ) === gorilla, "`gorilla` is left in coll" );
  3360. ok( !coll.get( 2 ).get( 'name' ), "`gorilla` name not updated" );
  3361. // This should remove `giraffe`, add `hippo`, leave `dolphin`, and update `gorilla`.
  3362. options = { add: true, merge: true, remove: true };
  3363. coll.set( [ 4, dolphin, { id: 2, name: 'Silverback' } ], options );
  3364. ok( coll.length === 3 );
  3365. ok( !coll.get( 1 ), "`giraffe` removed from coll" );
  3366. equal( coll.get( 2 ), gorilla );
  3367. ok( !coll.get( 3 ) );
  3368. equal( coll.get( 4 ), hippo );
  3369. equal( coll.get( dolphin ), dolphin );
  3370. equal( gorilla.get( 'name' ), 'Silverback' );
  3371. });
  3372. test( "add/remove/set on a relation (with `add`, `remove` and `merge` options)", function() {
  3373. var zoo = new Zoo(),
  3374. animals = zoo.get( 'animals' ),
  3375. a = new Animal( { id: 'a' } ),
  3376. b = new Animal( { id: 'b' } ),
  3377. c = new Animal( { id: 'c' } );
  3378. // The default is to call `Collection.update` without specifying options explicitly;
  3379. // the defaults are { add: true, merge: true, remove: true }.
  3380. zoo.set( 'animals', [ a ] );
  3381. ok( animals.length === 1, 'animals.length=' + animals.length + ' == 1?' );
  3382. zoo.set( 'animals', [ a, b ], { add: false, merge: true, remove: true } );
  3383. ok( animals.length === 1, 'animals.length=' + animals.length + ' == 1?' );
  3384. zoo.set( 'animals', [ b ], { add: false, merge: false, remove: true } );
  3385. ok( animals.length === 0, 'animals.length=' + animals.length + ' == 0?' );
  3386. zoo.set( 'animals', [ { id: 'a', species: 'a' } ], { add: false, merge: true, remove: false } );
  3387. ok( animals.length === 0, 'animals.length=' + animals.length + ' == 0?' );
  3388. ok( a.get( 'species' ) === 'a', "`a` not added, but attributes did get merged" );
  3389. zoo.set( 'animals', [ { id: 'b', species: 'b' } ], { add: true, merge: false, remove: false } );
  3390. ok( animals.length === 1, 'animals.length=' + animals.length + ' == 1?' );
  3391. ok( !b.get( 'species' ), "`b` added, but attributes did not get merged" );
  3392. zoo.set( 'animals', [ { id: 'c', species: 'c' } ], { add: true, merge: false, remove: true } );
  3393. ok( animals.length === 1, 'animals.length=' + animals.length + ' == 1?' );
  3394. ok( !animals.get( 'b' ), "b removed from animals" );
  3395. ok( animals.get( 'c' ) === c, "c added to animals" );
  3396. ok( !c.get( 'species' ), "`c` added, but attributes did not get merged" );
  3397. zoo.set( 'animals', [ a, { id: 'b', species: 'b' } ] );
  3398. ok( animals.length === 2, 'animals.length=' + animals.length + ' == 2?' );
  3399. ok( b.get( 'species' ) === 'b', "`b` added, attributes got merged" );
  3400. ok( !animals.get( 'c' ), "c removed from animals" );
  3401. zoo.set( 'animals', [ { id: 'c', species: 'c' } ], { add: true, merge: true, remove: false } );
  3402. ok( animals.length === 3, 'animals.length=' + animals.length + ' == 3?' );
  3403. ok( c.get( 'species' ) === 'c', "`c` added, attributes got merged" );
  3404. });
  3405. test( "`merge` on a nested relation", function() {
  3406. var zoo = new Zoo( { id: 1, animals: [ { id: 'a' } ] } ),
  3407. animals = zoo.get( 'animals' ),
  3408. a = animals.get( 'a' );
  3409. ok( a.get( 'livesIn' ) === zoo, "`a` is in `zoo`" );
  3410. // Pass a non-default option to a new model, with an existing nested model
  3411. var zoo2 = new Zoo( { id: 2, animals: [ { id: 'a', species: 'a' } ] }, { merge: false } );
  3412. ok( a.get( 'livesIn' ) === zoo2, "`a` is in `zoo2`" );
  3413. ok( !a.get( 'species' ), "`a` hasn't gotten merged" );
  3414. });
  3415. test( "pop", function() {
  3416. var zoo = new Zoo({
  3417. animals: [ { name: 'a' } ]
  3418. }),
  3419. animals = zoo.get( 'animals' );
  3420. var a = animals.pop(),
  3421. b = animals.pop();
  3422. ok( a && a.get( 'name' ) === 'a' );
  3423. ok( typeof b === 'undefined' );
  3424. });
  3425. module( "Events", { setup: reset } );
  3426. test( "`add:`, `remove:` and `change:` events", function() {
  3427. var zoo = new Zoo(),
  3428. animal = new Animal();
  3429. var addAnimalEventsTriggered = 0,
  3430. removeAnimalEventsTriggered = 0,
  3431. changeEventsTriggered = 0,
  3432. changeLiveInEventsTriggered = 0;
  3433. zoo
  3434. // .on( 'change:animals', function( model, coll ) {
  3435. // console.log( 'change:animals; args=%o', arguments );
  3436. // })
  3437. .on( 'add:animals', function( model, coll ) {
  3438. //console.log( 'add:animals; args=%o', arguments );
  3439. addAnimalEventsTriggered++;
  3440. })
  3441. .on( 'remove:animals', function( model, coll ) {
  3442. //console.log( 'remove:animals; args=%o', arguments );
  3443. removeAnimalEventsTriggered++;
  3444. });
  3445. animal
  3446. .on( 'change', function( model, coll ) {
  3447. console.log( 'change; args=%o', arguments );
  3448. changeEventsTriggered++;
  3449. })
  3450. .on( 'change:livesIn', function( model, coll ) {
  3451. //console.log( 'change:livesIn; args=%o', arguments );
  3452. changeLiveInEventsTriggered++;
  3453. });
  3454. // Directly triggering an event on a model should always fire
  3455. addAnimalEventsTriggered = removeAnimalEventsTriggered = changeEventsTriggered = changeLiveInEventsTriggered = 0;
  3456. animal.trigger( 'change', this.model );
  3457. ok( changeEventsTriggered === 1 );
  3458. ok( changeLiveInEventsTriggered === 0 );
  3459. addAnimalEventsTriggered = removeAnimalEventsTriggered = changeEventsTriggered = changeLiveInEventsTriggered = 0;
  3460. // Should trigger `change:livesIn` and `add:animals`
  3461. animal.set( 'livesIn', zoo );
  3462. zoo.set( 'id', 'z1' );
  3463. animal.set( 'id', 'a1' );
  3464. ok( addAnimalEventsTriggered === 1 );
  3465. ok( removeAnimalEventsTriggered === 0 );
  3466. ok( changeEventsTriggered === 2 );
  3467. ok( changeLiveInEventsTriggered === 1 );
  3468. console.log( changeEventsTriggered );
  3469. // Doing this shouldn't trigger any `add`/`remove`/`update` events
  3470. zoo.set( 'animals', [ 'a1' ] );
  3471. ok( addAnimalEventsTriggered === 1 );
  3472. ok( removeAnimalEventsTriggered === 0 );
  3473. ok( changeEventsTriggered === 2 );
  3474. ok( changeLiveInEventsTriggered === 1 );
  3475. // Doesn't cause an actual state change
  3476. animal.set( 'livesIn', 'z1' );
  3477. ok( addAnimalEventsTriggered === 1 );
  3478. ok( removeAnimalEventsTriggered === 0 );
  3479. ok( changeEventsTriggered === 2 );
  3480. ok( changeLiveInEventsTriggered === 1 );
  3481. // Should trigger a `remove` on zoo and an `update` on animal
  3482. animal.set( 'livesIn', { id: 'z2' } );
  3483. ok( addAnimalEventsTriggered === 1 );
  3484. ok( removeAnimalEventsTriggered === 1 );
  3485. ok( changeEventsTriggered === 3 );
  3486. ok( changeLiveInEventsTriggered === 2 );
  3487. });
  3488. test( "`reset` events", function() {
  3489. var initialize = AnimalCollection.prototype.initialize;
  3490. var resetEvents = 0,
  3491. addEvents = 0,
  3492. removeEvents = 0;
  3493. AnimalCollection.prototype.initialize = function() {
  3494. this
  3495. .on( 'add', function() {
  3496. addEvents++;
  3497. })
  3498. .on( 'reset', function() {
  3499. resetEvents++;
  3500. })
  3501. .on( 'remove', function() {
  3502. removeEvents++;
  3503. });
  3504. };
  3505. var zoo = new Zoo();
  3506. // No events triggered when initializing a HasMany
  3507. ok( zoo.get( 'animals' ) instanceof AnimalCollection );
  3508. ok( resetEvents === 0, "No `reset` event fired" );
  3509. ok( addEvents === 0 );
  3510. ok( removeEvents === 0 );
  3511. zoo.set( 'animals', { id: 1 } );
  3512. ok( addEvents === 1 );
  3513. ok( zoo.get( 'animals' ).length === 1, "animals.length === 1" );
  3514. zoo.get( 'animals' ).reset();
  3515. ok( resetEvents === 1, "`reset` event fired" );
  3516. ok( zoo.get( 'animals' ).length === 0, "animals.length === 0" );
  3517. AnimalCollection.prototype.initialize = initialize;
  3518. });
  3519. test( "Firing of `change` and `change:<key>` events", function() {
  3520. var data = {
  3521. id: 1,
  3522. animals: []
  3523. };
  3524. var zoo = new Zoo( data );
  3525. var change = 0;
  3526. zoo.on( 'change', function() {
  3527. change++;
  3528. });
  3529. var changeAnimals = 0;
  3530. zoo.on( 'change:animals', function() {
  3531. changeAnimals++;
  3532. });
  3533. var animalChange = 0;
  3534. zoo.get( 'animals' ).on( 'change', function() {
  3535. animalChange++;
  3536. });
  3537. // Set the same data
  3538. zoo.set( data );
  3539. ok( change === 0, 'no change event should fire' );
  3540. ok( changeAnimals === 0, 'no change:animals event should fire' );
  3541. ok( animalChange === 0, 'no animals:change event should fire' );
  3542. // Add an `animal`
  3543. change = changeAnimals = animalChange = 0;
  3544. zoo.set( { animals: [ { id: 'a1' } ] } );
  3545. ok( change === 1, 'change event should fire' );
  3546. ok( changeAnimals === 1, 'change:animals event should fire' );
  3547. ok( animalChange === 1, 'animals:change event should fire' );
  3548. // Change an animal
  3549. change = changeAnimals = animalChange = 0;
  3550. zoo.set( { animals: [ { id: 'a1', name: 'a1' } ] } );
  3551. ok( change === 0, 'no change event should fire' );
  3552. ok( changeAnimals === 0, 'no change:animals event should fire' );
  3553. ok( animalChange === 1, 'animals:change event should fire' );
  3554. // Only change the `zoo` itself
  3555. change = changeAnimals = animalChange = 0;
  3556. zoo.set( { name: 'Artis' } );
  3557. ok( change === 1, 'change event should fire' );
  3558. ok( changeAnimals === 0, 'no change:animals event should fire' );
  3559. ok( animalChange === 0, 'no animals:change event should fire' );
  3560. // Replace an `animal`
  3561. change = changeAnimals = animalChange = 0;
  3562. zoo.set( { animals: [ { id: 'a2' } ] } );
  3563. ok( change === 1, 'change event should fire' );
  3564. ok( changeAnimals === 1, 'change:animals event should fire' );
  3565. ok( animalChange === 1, 'animals:change event should fire' );
  3566. // Remove an `animal`
  3567. change = changeAnimals = animalChange = 0;
  3568. zoo.set( { animals: [] } );
  3569. ok( change === 1, 'change event should fire' );
  3570. ok( changeAnimals === 1, 'change:animals event should fire' );
  3571. ok( animalChange === 0, 'no animals:change event should fire' );
  3572. // Operate directly on the HasMany collection
  3573. var animals = zoo.get( 'animals' ),
  3574. a1 = Animal.findOrCreate( 'a1', { create: false } ),
  3575. a2 = Animal.findOrCreate( 'a2', { create: false } );
  3576. ok( a1 instanceof Animal );
  3577. ok( a2 instanceof Animal );
  3578. // Add an animal
  3579. change = changeAnimals = animalChange = 0;
  3580. animals.add( 'a2' );
  3581. ok( change === 0, 'change event not should fire' );
  3582. ok( changeAnimals === 0, 'no change:animals event should fire' );
  3583. ok( animalChange === 0, 'no animals:change event should fire' );
  3584. // Update an animal directly
  3585. change = changeAnimals = animalChange = 0;
  3586. a2.set( 'name', 'a2' );
  3587. ok( change === 0, 'no change event should fire' );
  3588. ok( changeAnimals === 0, 'no change:animals event should fire' );
  3589. ok( animalChange === 1, 'animals:change event should fire' );
  3590. // Remove an animal directly
  3591. change = changeAnimals = animalChange = 0;
  3592. animals.remove( 'a2' );
  3593. ok( change === 0, 'no change event should fire' );
  3594. ok( changeAnimals === 0, 'no change:animals event should fire' );
  3595. ok( animalChange === 0, 'no animals:change event should fire' );
  3596. });
  3597. test( "Does not trigger add / remove events for existing models on bulk assignment", function() {
  3598. var house = new House({
  3599. id: 'house-100',
  3600. location: 'in the middle of the street',
  3601. occupants: [ { id : 'person-5', jobs: [ { id : 'job-22' } ] }, { id : 'person-6' } ]
  3602. });
  3603. var eventsTriggered = 0;
  3604. house
  3605. .on( 'add:occupants', function(model) {
  3606. ok( false, model.id + " should not be added" );
  3607. eventsTriggered++;
  3608. })
  3609. .on( 'remove:occupants', function(model) {
  3610. ok( false, model.id + " should not be removed" );
  3611. eventsTriggered++;
  3612. });
  3613. house.get( 'occupants' ).at( 0 ).on( 'add:jobs', function( model ) {
  3614. ok( false, model.id + " should not be added" );
  3615. eventsTriggered++;
  3616. });
  3617. house.set( house.toJSON() );
  3618. ok( eventsTriggered === 0, "No add / remove events were triggered" );
  3619. });
  3620. test( "triggers appropriate add / remove / change events on bulk assignment", function() {
  3621. var house = new House({
  3622. id: 'house-100',
  3623. location: 'in the middle of the street',
  3624. occupants: [ { id : 'person-5', nickname : 'Jane' }, { id : 'person-6' }, { id : 'person-8', nickname : 'Jon' } ]
  3625. });
  3626. var addEventsTriggered = 0,
  3627. removeEventsTriggered = 0,
  3628. changeEventsTriggered = 0;
  3629. house
  3630. // .on( 'all', function(ev, model) {
  3631. // console.log('all', ev, model);
  3632. // })
  3633. .on( 'add:occupants', function( model ) {
  3634. ok( model.id === 'person-7', "Only person-7 should be added: " + model.id + " being added" );
  3635. addEventsTriggered++;
  3636. })
  3637. .on( 'remove:occupants', function( model ) {
  3638. ok( model.id === 'person-6', "Only person-6 should be removed: " + model.id + " being removed" );
  3639. removeEventsTriggered++;
  3640. });
  3641. house.get( 'occupants' ).on( 'change:nickname', function( model ) {
  3642. ok( model.id === 'person-8', "Only person-8 should have it's nickname updated: " + model.id + " nickname updated" );
  3643. changeEventsTriggered++;
  3644. });
  3645. house.set( { occupants : [ { id : 'person-5', nickname : 'Jane'}, { id : 'person-7' }, { id : 'person-8', nickname : 'Phil' } ] } );
  3646. ok( addEventsTriggered === 1, "Exactly one add event was triggered (triggered " + addEventsTriggered + " events)" );
  3647. ok( removeEventsTriggered === 1, "Exactly one remove event was triggered (triggered " + removeEventsTriggered + " events)" );
  3648. ok( changeEventsTriggered === 1, "Exactly one change event was triggered (triggered " + changeEventsTriggered + " events)" );
  3649. });
  3650. test( "triggers appropriate change events even when callbacks have triggered set with an unchanging value", function() {
  3651. var house = new House({
  3652. id: 'house-100',
  3653. location: 'in the middle of the street'
  3654. });
  3655. var changeEventsTriggered = 0;
  3656. house
  3657. .on('change:location', function() {
  3658. house.set({location: 'somewhere else'});
  3659. })
  3660. .on( 'change', function () {
  3661. changeEventsTriggered++;
  3662. });
  3663. house.set( { location: 'somewhere else' } );
  3664. ok( changeEventsTriggered === 1, 'one change triggered for `house`' );
  3665. var person = new Person({
  3666. id: 1
  3667. });
  3668. changeEventsTriggered = 0;
  3669. person
  3670. .on('change:livesIn', function() {
  3671. //console.log( arguments );
  3672. house.set({livesIn: house});
  3673. })
  3674. .on( 'change', function () {
  3675. //console.log( arguments );
  3676. changeEventsTriggered++;
  3677. });
  3678. person.set({livesIn: house});
  3679. ok( changeEventsTriggered === 2, 'one change each triggered for `house` and `person`' );
  3680. });
  3681. module( "Performance", { setup: reset } );
  3682. test( "Creation and destruction", 0, function() {
  3683. var registerCount = 0,
  3684. unregisterCount = 0,
  3685. register = Backbone.Store.prototype.register,
  3686. unregister = Backbone.Store.prototype.unregister;
  3687. Backbone.Store.prototype.register = function( model ) {
  3688. registerCount++;
  3689. return register.apply( this, arguments );
  3690. };
  3691. Backbone.Store.prototype.unregister = function( model, coll, options ) {
  3692. unregisterCount++;
  3693. return unregister.apply( this, arguments );
  3694. };
  3695. var addHasManyCount = 0,
  3696. addHasOneCount = 0,
  3697. tryAddRelatedHasMany = Backbone.HasMany.prototype.tryAddRelated,
  3698. tryAddRelatedHasOne = Backbone.HasOne.prototype.tryAddRelated;
  3699. Backbone.Store.prototype.tryAddRelated = function( model, coll, options ) {
  3700. addHasManyCount++;
  3701. return tryAddRelatedHasMany.apply( this, arguments );
  3702. };
  3703. Backbone.HasOne.prototype.tryAddRelated = function( model, coll, options ) {
  3704. addHasOneCount++;
  3705. return tryAddRelatedHasOne.apply( this, arguments );
  3706. };
  3707. var removeHasManyCount = 0,
  3708. removeHasOneCount = 0,
  3709. removeRelatedHasMany = Backbone.HasMany.prototype.removeRelated,
  3710. removeRelatedHasOne= Backbone.HasOne.prototype.removeRelated;
  3711. Backbone.HasMany.prototype.removeRelated = function( model, coll, options ) {
  3712. removeHasManyCount++;
  3713. return removeRelatedHasMany.apply( this, arguments );
  3714. };
  3715. Backbone.HasOne.prototype.removeRelated = function( model, coll, options ) {
  3716. removeHasOneCount++;
  3717. return removeRelatedHasOne.apply( this, arguments );
  3718. };
  3719. var Child = Backbone.RelationalModel.extend({
  3720. url: '/child/',
  3721. toString: function() {
  3722. return this.id;
  3723. }
  3724. });
  3725. var Parent = Backbone.RelationalModel.extend({
  3726. relations: [{
  3727. type: Backbone.HasMany,
  3728. key: 'children',
  3729. relatedModel: Child,
  3730. reverseRelation: {
  3731. key: 'parent'
  3732. }
  3733. }],
  3734. toString: function() {
  3735. return this.get( 'name' );
  3736. }
  3737. });
  3738. var Parents = Backbone.Collection.extend({
  3739. model: Parent
  3740. });
  3741. // bootstrap data
  3742. var data = [];
  3743. for ( var i = 1; i <= 300; i++ ) {
  3744. data.push({
  3745. name: 'parent-' + i,
  3746. children: [
  3747. {id: 'p-' + i + '-c1', name: 'child-1'},
  3748. {id: 'p-' + i + '-c2', name: 'child-2'},
  3749. {id: 'p-' + i + '-c3', name: 'child-3'}
  3750. ]
  3751. });
  3752. }
  3753. /**
  3754. * Test 2
  3755. */
  3756. Backbone.Relational.store.reset();
  3757. addHasManyCount = addHasOneCount = 0;
  3758. console.log('loading test 2...');
  3759. var start = new Date();
  3760. var preparedData = _.map( data, function( item ) {
  3761. item = _.clone( item );
  3762. item.children = item.children.map( function( child ) {
  3763. return new Child( child );
  3764. });
  3765. return item;
  3766. });
  3767. var parents = new Parents();
  3768. parents.on('reset', function () {
  3769. var secs = (new Date() - start) / 1000;
  3770. console.log( 'data loaded in %s, addHasManyCount=%o, addHasOneCount=%o', secs, addHasManyCount, addHasOneCount );
  3771. });
  3772. parents.reset( preparedData );
  3773. //_.invoke( _.clone( parents.models ), 'destroy' );
  3774. /**
  3775. * Test 1
  3776. */
  3777. Backbone.Relational.store.reset();
  3778. addHasManyCount = addHasOneCount = 0;
  3779. console.log('loading test 1...');
  3780. var start = new Date();
  3781. var parents = new Parents();
  3782. parents.on('reset', function () {
  3783. var secs = (new Date() - start) / 1000;
  3784. console.log( 'data loaded in %s, addHasManyCount=%o, addHasOneCount=%o', secs, addHasManyCount, addHasOneCount );
  3785. });
  3786. parents.reset( data );
  3787. //_.invoke( _.clone( parents.models ), 'destroy' );
  3788. /**
  3789. * Test 2 (again)
  3790. */
  3791. Backbone.Relational.store.reset();
  3792. addHasManyCount = addHasOneCount = removeHasManyCount = removeHasOneCount = 0;
  3793. console.log('loading test 2...');
  3794. var start = new Date();
  3795. var parents = new Parents();
  3796. parents.on('reset', function () {
  3797. var secs = (new Date() - start) / 1000;
  3798. console.log( 'data loaded in %s, addHasManyCount=%o, addHasOneCount=%o', secs, addHasManyCount, addHasOneCount );
  3799. });
  3800. parents.reset( preparedData );
  3801. start = new Date();
  3802. parents.each( function( parent ) {
  3803. var children = _.clone( parent.get( 'children' ).models );
  3804. _.each( children, function( child ) {
  3805. child.destroy();
  3806. });
  3807. });
  3808. var secs = (new Date() - start) / 1000;
  3809. console.log( 'data loaded in %s, removeHasManyCount=%o, removeHasOneCount=%o', secs, removeHasManyCount, removeHasOneCount );
  3810. //_.invoke( _.clone( parents.models ), 'destroy' );
  3811. /**
  3812. * Test 1 (again)
  3813. */
  3814. Backbone.Relational.store.reset();
  3815. addHasManyCount = addHasOneCount = removeHasManyCount = removeHasOneCount = 0;
  3816. console.log('loading test 1...');
  3817. var start = new Date();
  3818. var parents = new Parents();
  3819. parents.on('reset', function () {
  3820. var secs = (new Date() - start) / 1000;
  3821. console.log( 'data loaded in %s, addHasManyCount=%o, addHasOneCount=%o', secs, addHasManyCount, addHasOneCount );
  3822. });
  3823. parents.reset(data);
  3824. start = new Date();
  3825. parents.remove( parents.models );
  3826. var secs = (new Date() - start) / 1000;
  3827. console.log( 'data removed in %s, removeHasManyCount=%o, removeHasOneCount=%o', secs, removeHasManyCount, removeHasOneCount );
  3828. console.log( 'registerCount=%o, unregisterCount=%o', registerCount, unregisterCount );
  3829. });
  3830. });