PageRenderTime 657ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

/test/tests.js

https://github.com/jeffmess/Backbone-relational
JavaScript | 1373 lines | 1033 code | 282 blank | 58 comment | 23 complexity | 0f9da0ebdc156046f383d1fcc2f5484f MD5 | raw file
  1. // documentation on writing tests here: http://docs.jquery.com/QUnit
  2. // example tests: https://github.com/jquery/qunit/blob/master/test/same.js
  3. // more examples: https://github.com/jquery/jquery/tree/master/test/unit
  4. // jQueryUI examples: https://github.com/jquery/jquery-ui/tree/master/tests/unit
  5. //sessionStorage.clear();
  6. if ( !window.console ) {
  7. var names = [ 'log', 'debug', 'info', 'warn', 'error', 'assert', 'dir', 'dirxml',
  8. 'group', 'groupEnd', 'time', 'timeEnd', 'count', 'trace', 'profile', 'profileEnd' ];
  9. window.console = {};
  10. for ( var i = 0; i < names.length; ++i )
  11. window.console[ names[i] ] = function() {};
  12. }
  13. $(document).ready(function() {
  14. $.ajax = function( obj ) {
  15. window.requests.push( obj );
  16. return obj;
  17. };
  18. Backbone.Model.prototype.url = function() {
  19. // Use the 'resource_uri' if possible
  20. var url = this.get( 'resource_uri' );
  21. // Try to have the collection construct a url
  22. if ( !url && this.collection ) {
  23. url = this.collection.url && _.isFunction( this.collection.url ) ? this.collection.url() : this.collection.url;
  24. }
  25. // Fallback to 'urlRoot'
  26. if ( !url && this.urlRoot ) {
  27. url = this.urlRoot + this.id;
  28. }
  29. if ( !url ) {
  30. throw new Error( 'Url could not be determined!' );
  31. }
  32. return url;
  33. };
  34. Zoo = Backbone.RelationalModel.extend({
  35. relations: [{
  36. type: Backbone.HasMany,
  37. key: 'animals',
  38. relatedModel: 'Animal',
  39. collectionType: 'AnimalCollection',
  40. reverseRelation: {
  41. key: 'livesIn',
  42. includeInJSON: 'id'
  43. }
  44. }]
  45. });
  46. Animal = Backbone.RelationalModel.extend({
  47. urlRoot: '/animal/'
  48. });
  49. AnimalCollection = Backbone.Collection.extend({
  50. model: Animal
  51. });
  52. House = Backbone.RelationalModel.extend({
  53. relations: [{
  54. type: Backbone.HasMany,
  55. key: 'occupants',
  56. relatedModel: 'Person',
  57. reverseRelation: {
  58. key: 'livesIn',
  59. includeInJSON: false
  60. }
  61. }]
  62. });
  63. User = Backbone.RelationalModel.extend({
  64. urlRoot: '/user/'
  65. });
  66. Person = Backbone.RelationalModel.extend({
  67. relations: [{
  68. // Create a cozy, recursive, one-to-one relationship
  69. type: Backbone.HasOne,
  70. key: 'likesALot',
  71. relatedModel: 'Person',
  72. reverseRelation: {
  73. type: Backbone.HasOne,
  74. key: 'likedALotBy'
  75. }
  76. },
  77. {
  78. type: Backbone.HasOne,
  79. key: 'user',
  80. relatedModel: 'User',
  81. includeInJSON: Backbone.Model.prototype.idAttribute,
  82. reverseRelation: {
  83. type: Backbone.HasOne,
  84. includeInJSON: 'name',
  85. key: 'person'
  86. }
  87. },
  88. {
  89. type: 'HasMany',
  90. key: 'jobs',
  91. relatedModel: 'Job',
  92. reverseRelation: {
  93. key: 'person'
  94. }
  95. }
  96. ]
  97. });
  98. PersonCollection = Backbone.Collection.extend({
  99. model: Person
  100. });
  101. // A link table between 'Person' and 'Company', to achieve many-to-many relations
  102. Job = Backbone.RelationalModel.extend({
  103. defaults: {
  104. 'startDate': null,
  105. 'endDate': null
  106. }
  107. });
  108. Company = Backbone.RelationalModel.extend({
  109. relations: [{
  110. type: 'HasMany',
  111. key: 'employees',
  112. relatedModel: 'Job',
  113. reverseRelation: {
  114. key: 'company'
  115. }
  116. },
  117. {
  118. type: 'HasOne',
  119. key: 'ceo',
  120. relatedModel: 'Person',
  121. reverseRelation: {
  122. key: 'runs'
  123. }
  124. }
  125. ]
  126. });
  127. Node = Backbone.RelationalModel.extend({
  128. relations: [{
  129. type: Backbone.HasOne,
  130. key: 'parent',
  131. relatedModel: 'Node',
  132. includeInJSON: false,
  133. reverseRelation: {
  134. key: 'children'
  135. }
  136. }
  137. ]
  138. });
  139. NodeList = Backbone.Collection.extend({
  140. model: Node
  141. });
  142. function initObjects() {
  143. // Reset last ajax requests
  144. window.requests = [];
  145. // save _reverseRelations, otherwise we'll get a lot of warnings about existing relations
  146. var oldReverseRelations = Backbone.Relational.store._reverseRelations;
  147. Backbone.Relational.store = new Backbone.Store();
  148. Backbone.Relational.store._reverseRelations = oldReverseRelations;
  149. Backbone.Relational.eventQueue = new Backbone.BlockingQueue();
  150. person1 = new Person({
  151. id: 'person-1',
  152. name: 'boy',
  153. likesALot: 'person-2',
  154. resource_uri: 'person-1',
  155. user: { id: 'user-1', login: 'dude', email: 'me@gmail.com', resource_uri: 'user-1' }
  156. });
  157. person2 = new Person({
  158. id: 'person-2',
  159. name: 'girl',
  160. likesALot: 'person-1',
  161. resource_uri: 'person-2'
  162. });
  163. person3 = new Person({
  164. id: 'person-3',
  165. resource_uri: 'person-3'
  166. });
  167. oldCompany = new Company({
  168. id: 'company-1',
  169. name: 'Big Corp.',
  170. ceo: {
  171. name: 'Big Boy'
  172. },
  173. employees: [ { person: 'person-3' } ], // uses the 'Job' link table to achieve many-to-many. No 'id' specified!
  174. resource_uri: 'company-1'
  175. });
  176. newCompany = new Company({
  177. id: 'company-2',
  178. name: 'New Corp.',
  179. employees: [ { person: 'person-2' } ],
  180. resource_uri: 'company-2'
  181. });
  182. ourHouse = new House({
  183. id: 'house-1',
  184. location: 'in the middle of the street',
  185. occupants: ['person-2'],
  186. resource_uri: 'house-1'
  187. });
  188. theirHouse = new House({
  189. id: 'house-2',
  190. location: 'outside of town',
  191. occupants: [],
  192. resource_uri: 'house-2'
  193. });
  194. }
  195. module("Backbone.Semaphore");
  196. test("Unbounded", function() {
  197. expect( 10 );
  198. var semaphore = _.extend( {}, Backbone.Semaphore );
  199. ok( !semaphore.isLocked(), 'Semaphore is not locked initially' );
  200. semaphore.acquire();
  201. ok( semaphore.isLocked(), 'Semaphore is locked after acquire' );
  202. semaphore.acquire();
  203. equal( semaphore._permitsUsed, 2 ,'_permitsUsed should be incremented 2 times' );
  204. semaphore.setAvailablePermits( 4 );
  205. equal( semaphore._permitsAvailable, 4 ,'_permitsAvailable should be 4' );
  206. semaphore.acquire();
  207. semaphore.acquire();
  208. equal( semaphore._permitsUsed, 4 ,'_permitsUsed should be incremented 4 times' );
  209. try {
  210. semaphore.acquire();
  211. }
  212. catch( ex ) {
  213. ok( true, 'Error thrown when attempting to acquire too often' );
  214. }
  215. semaphore.release();
  216. equal( semaphore._permitsUsed, 3 ,'_permitsUsed should be decremented to 3' );
  217. semaphore.release();
  218. semaphore.release();
  219. semaphore.release();
  220. equal( semaphore._permitsUsed, 0 ,'_permitsUsed should be decremented to 0' );
  221. ok( !semaphore.isLocked(), 'Semaphore is not locked when all permits are released' );
  222. try {
  223. semaphore.release();
  224. }
  225. catch( ex ) {
  226. ok( true, 'Error thrown when attempting to release too often' );
  227. }
  228. });
  229. module( "Backbone.BlockingQueue" );
  230. test( "Block", function() {
  231. var queue = new Backbone.BlockingQueue();
  232. var count = 0;
  233. var increment = function() { count++; };
  234. var decrement = function() { count--; };
  235. queue.add( increment );
  236. ok( count === 1, 'Increment executed right away' );
  237. queue.add( decrement );
  238. ok( count === 0, 'Decrement executed right away' );
  239. queue.block();
  240. queue.add( increment );
  241. ok( queue.isLocked(), 'Queue is blocked' );
  242. equal( count, 0, 'Increment did not execute right away' );
  243. queue.block();
  244. queue.block();
  245. equal( queue._permitsUsed, 3 ,'_permitsUsed should be incremented to 3' );
  246. queue.unblock();
  247. queue.unblock();
  248. queue.unblock();
  249. equal( count, 1, 'Increment executed' );
  250. });
  251. module( "Backbone.Store", { setup: initObjects } );
  252. test( "Initialized", function() {
  253. equal( Backbone.Relational.store._collections.length, 5, "Store contains 5 collections" );
  254. });
  255. test( "getObjectByName", function() {
  256. equal( Backbone.Relational.store.getObjectByName( 'Backbone' ), Backbone );
  257. equal( Backbone.Relational.store.getObjectByName( 'Backbone.RelationalModel' ), Backbone.RelationalModel );
  258. });
  259. test( "Add and remove from store", function() {
  260. var coll = Backbone.Relational.store.getCollection( person1 );
  261. var length = coll.length;
  262. var person = new Person({
  263. id: 'person-10',
  264. name: 'Remi',
  265. resource_uri: 'person-10'
  266. });
  267. ok( coll.length === length + 1, "Collection size increased by 1" );
  268. var request = person.destroy();
  269. // Trigger the 'success' callback to fire the 'destroy' event
  270. request.success();
  271. ok( coll.length === length, "Collection size decreased by 1" );
  272. });
  273. test( "Models are created from objects, can then be found, destroyed, cannot be found anymore", function() {
  274. var houseId = 'house-10';
  275. var personId = 'person-10';
  276. var anotherHouse = new House({
  277. id: houseId,
  278. location: 'no country for old men',
  279. resource_uri: houseId,
  280. occupants: [{
  281. id: personId,
  282. name: 'Remi',
  283. resource_uri: personId
  284. }]
  285. });
  286. ok( anotherHouse.get('occupants') instanceof Backbone.Collection, "Occupants is a Collection" );
  287. ok( anotherHouse.get('occupants').get( personId ) instanceof Person, "Occupants contains the Person with id='" + personId + "'" );
  288. var person = Backbone.Relational.store.find( Person, personId );
  289. ok( person, "Person with id=" + personId + " is found in the store" );
  290. var request = person.destroy();
  291. // Trigger the 'success' callback to fire the 'destroy' event
  292. request.success();
  293. person = Backbone.Relational.store.find( Person, personId );
  294. ok( !person, personId + " is not found in the store anymore" );
  295. ok( !anotherHouse.get('occupants').get( personId ), "Occupants no longer contains the Person with id='" + personId + "'" );
  296. var request = anotherHouse.destroy();
  297. // Trigger the 'success' callback to fire the 'destroy' event
  298. request.success();
  299. var house = Backbone.Relational.store.find( House, houseId );
  300. ok( !house, houseId + " is not found in the store anymore" );
  301. });
  302. test( "Model.collection is the first collection a Model is added to by an end-user (not it's Backbone.Store collection!)", function() {
  303. var person = new Person( { name: 'New guy' } );
  304. var personColl = new PersonCollection();
  305. personColl.add( person );
  306. ok( person.collection === personColl );
  307. });
  308. test( "All models can be found after adding them to a Collection via 'Collection.reset'", function() {
  309. var nodes = [
  310. { id: 1, parent: null },
  311. { id: 2, parent: 1 },
  312. { id: 3, parent: 4 },
  313. { id: 4, parent: 1 }
  314. ];
  315. var nodeList = new NodeList();
  316. nodeList.reset( nodes );
  317. var storeColl = Backbone.Relational.store.getCollection( Node );
  318. equals( storeColl.length, 4, "Every Node is in Backbone.Relational.store" );
  319. ok( Backbone.Relational.store.find( Node, 1 ) instanceof Node, "Node 1 can be found" );
  320. ok( Backbone.Relational.store.find( Node, 2 ) instanceof Node, "Node 2 can be found" );
  321. ok( Backbone.Relational.store.find( Node, 3 ) instanceof Node, "Node 3 can be found" );
  322. ok( Backbone.Relational.store.find( Node, 4 ) instanceof Node, "Node 4 can be found" );
  323. });
  324. module( "Backbone.RelationalModel", { setup: initObjects } );
  325. test( "Return values: set returns the Model", function() {
  326. var personId = 'person-10';
  327. var person = new Person({
  328. id: personId,
  329. name: 'Remi',
  330. resource_uri: personId
  331. });
  332. var result = person.set( { 'name': 'Hector' } );
  333. ok( result === person, "Set returns the model" );
  334. });
  335. test( "getRelations", function() {
  336. equal( person1.getRelations().length, 6 );
  337. });
  338. test( "getRelation", function() {
  339. var rel = person1.getRelation( 'user' );
  340. equal( rel.key, 'user' );
  341. });
  342. test( "fetchRelated on a HasOne relation", function() {
  343. var errorCount = 0;
  344. var person = new Person({
  345. id: 'person-10',
  346. resource_uri: 'person-10',
  347. user: 'user-10'
  348. });
  349. var requests = person.fetchRelated( 'user', { error: function() {
  350. errorCount++;
  351. }
  352. });
  353. ok( _.isArray( requests ) );
  354. equal( requests.length, 1, "A request has been made" );
  355. ok( person.get( 'user' ) instanceof User );
  356. // Triggering the 'error' callback should destroy the model
  357. requests[ 0 ].error();
  358. // Trigger the 'success' callback to fire the 'destroy' event
  359. window.requests[ window.requests.length - 1 ].success();
  360. equal( person.get( 'user' ), null );
  361. ok( errorCount, 1, "The error callback executed successfully" );
  362. var person2 = new Person({
  363. id: 'person-10',
  364. resource_uri: 'person-10'
  365. });
  366. requests = person2.fetchRelated( 'user' );
  367. equal( requests.length, 0, "No request was made" );
  368. });
  369. test( "fetchRelated on a HasMany relation", function() {
  370. var errorCount = 0;
  371. var zoo = new Zoo({
  372. animals: [ 'lion-1', 'zebra-1' ]
  373. });
  374. //
  375. // Case 1: separate requests for each model
  376. //
  377. var requests = zoo.fetchRelated( 'animals', { error: function() { errorCount++; } } );
  378. ok( _.isArray( requests ) );
  379. equal( requests.length, 2, "Two requests have been made (a separate one for each animal)" );
  380. equal( zoo.get( 'animals' ).length, 2, "Two animals in the zoo" );
  381. // Triggering the 'error' callback for either request should destroy the model
  382. requests[ 0 ].error();
  383. // Trigger the 'success' callback to fire the 'destroy' event
  384. window.requests[ window.requests.length - 1 ].success();
  385. equal( zoo.get( 'animals' ).length, 1, "One animal left in the zoo" );
  386. ok( errorCount, 1, "The error callback executed successfully" );
  387. //
  388. // Case 2: one request per fetch (generated by the collection)
  389. //
  390. // Give 'zoo' a custom url function that builds a url to fetch a set of models from their ids
  391. errorCount = 0;
  392. zoo.get( 'animals' ).url = function( models ) {
  393. return '/animal/' + ( models ? 'set/' + _.pluck( models, 'id' ).join(';') + '/' : '' );
  394. };
  395. // Set two new animals to be fetched; both should be fetched in a single request
  396. zoo.set( { animals: [ 'lion-2', 'zebra-2' ] } );
  397. equal( zoo.get( 'animals' ).length, 0 );
  398. requests = zoo.fetchRelated( 'animals', { error: function() { errorCount++; } } );
  399. ok( _.isArray( requests ) );
  400. equal( requests.length, 1 );
  401. ok( requests[ 0 ].url === '/animal/set/lion-2;zebra-2/' );
  402. equal( zoo.get('animals').length, 2 );
  403. // Triggering the 'error' callback should destroy both of the fetched models
  404. requests[ 0 ].error();
  405. // Trigger the 'success' callback for both 'delete' calls to fire the 'destroy' event
  406. window.requests[ window.requests.length - 1 ].success();
  407. window.requests[ window.requests.length - 2 ].success();
  408. equal( zoo.get( 'animals' ).length, 0, "Both animals are destroyed" );
  409. ok( errorCount, 2, "The error callback executed successfully for both models" );
  410. // Re-fetch them
  411. requests = zoo.fetchRelated( 'animals' );
  412. equal( requests.length, 1 );
  413. equal( zoo.get( 'animals' ).length, 2 );
  414. // No more animals to fetch!
  415. requests = zoo.fetchRelated( 'animals' );
  416. ok( _.isArray( requests ) );
  417. equal( requests.length, 0 );
  418. equal( zoo.get( 'animals' ).length, 2 );
  419. });
  420. module( "Backbone.Relation options", { setup: initObjects } );
  421. test( "includeInJSON (Person to JSON)", function() {
  422. var json = person1.toJSON();
  423. equal( json.user, 'user-1', "The value 'user' is the user's id (not an object, since 'includeInJSON' is set to the idAttribute)" );
  424. ok ( json.likesALot instanceof Object, "The value of 'likesALot' is an object ('includeInJSON' is 'true')" );
  425. equal( json.likesALot.likesALot, 'person-1', "Person is serialized only once" );
  426. json = person1.get( 'user' ).toJSON();
  427. equal( json.person, 'boy', "The value of 'person' is the person's name ('includeInJSON is set to 'name')" );
  428. json = person2.toJSON();
  429. ok( person2.get('livesIn') instanceof House, "'person2' has a 'livesIn' relation" );
  430. equal( json.livesIn, undefined , "The value of 'livesIn' is not serialized ('includeInJSON is 'false')" );
  431. });
  432. test( "createModels is false", function() {
  433. var NewUser = Backbone.RelationalModel.extend({});
  434. var NewPerson = Backbone.RelationalModel.extend({
  435. relations: [{
  436. type: Backbone.HasOne,
  437. key: 'user',
  438. relatedModel: NewUser,
  439. createModels: false
  440. }]
  441. });
  442. var person = new NewPerson({
  443. id: 'newperson-1',
  444. resource_uri: 'newperson-1',
  445. user: { id: 'newuser-1', resource_uri: 'newuser-1' }
  446. });
  447. ok( person.get( 'user' ) == null );
  448. var user = new NewUser( { id: 'newuser-1', name: 'SuperUser' } );
  449. ok( person.get( 'user' ) === user );
  450. // Old data gets overwritten by the explicitly created user, since a model was never created from the old data
  451. ok( person.get( 'user' ).get( 'resource_uri' ) == null );
  452. });
  453. module( "Backbone.Relation preconditions" );
  454. test( "'type', 'key', 'relatedModel' are required properties", function() {
  455. var Properties = Backbone.RelationalModel.extend({});
  456. var View = Backbone.RelationalModel.extend({
  457. relations: [
  458. {
  459. key: 'listProperties',
  460. relatedModel: Properties
  461. }
  462. ]
  463. });
  464. var view = new View();
  465. ok( view._relations.length === 0 );
  466. View = Backbone.RelationalModel.extend({
  467. relations: [
  468. {
  469. type: Backbone.HasOne,
  470. relatedModel: Properties
  471. }
  472. ]
  473. });
  474. view = new View();
  475. ok( view._relations.length === 0 );
  476. View = Backbone.RelationalModel.extend({
  477. relations: [
  478. {
  479. type: Backbone.HasOne,
  480. key: 'listProperties'
  481. }
  482. ]
  483. });
  484. view = new View();
  485. ok( view._relations.length === 0 );
  486. });
  487. test( "'type' can be a string or an object reference", function() {
  488. var Properties = Backbone.RelationalModel.extend({});
  489. var View = Backbone.RelationalModel.extend({
  490. relations: [
  491. {
  492. type: 'Backbone.HasOne',
  493. key: 'listProperties',
  494. relatedModel: Properties
  495. }
  496. ]
  497. });
  498. var view = new View();
  499. ok( view._relations.length === 1 );
  500. View = Backbone.RelationalModel.extend({
  501. relations: [
  502. {
  503. type: 'HasOne',
  504. key: 'listProperties',
  505. relatedModel: Properties
  506. }
  507. ]
  508. });
  509. view = new View();
  510. ok( view._relations.length === 1 );
  511. View = Backbone.RelationalModel.extend({
  512. relations: [
  513. {
  514. type: Backbone.HasOne,
  515. key: 'listProperties',
  516. relatedModel: Properties
  517. }
  518. ]
  519. });
  520. view = new View();
  521. ok( view._relations.length === 1 );
  522. });
  523. test( "'key' can be a string or an object reference", function() {
  524. Properties = Backbone.RelationalModel.extend({});
  525. var View = Backbone.RelationalModel.extend({
  526. relations: [
  527. {
  528. type: Backbone.HasOne,
  529. key: 'listProperties',
  530. relatedModel: 'Properties'
  531. }
  532. ]
  533. });
  534. var view = new View();
  535. ok( view._relations.length === 1 );
  536. View = Backbone.RelationalModel.extend({
  537. relations: [
  538. {
  539. type: Backbone.HasOne,
  540. key: 'listProperties',
  541. relatedModel: Properties
  542. }
  543. ]
  544. });
  545. view = new View();
  546. ok( view._relations.length === 1 );
  547. delete Properties;
  548. });
  549. test( "HasMany with a reverseRelation HasMany is not allowed", function() {
  550. Password = Backbone.RelationalModel.extend({
  551. relations: [{
  552. type: 'HasMany',
  553. key: 'users',
  554. relatedModel: 'User',
  555. reverseRelation: {
  556. type: 'HasMany',
  557. key: 'passwords'
  558. }
  559. }]
  560. });
  561. var password = new Password({
  562. plaintext: 'qwerty',
  563. users: [ 'person-1', 'person-2', 'person-3' ]
  564. });
  565. ok( password._relations.length === 0, "No _relations created on Password" );
  566. });
  567. test( "Duplicate relations not allowed (two simple relations)", function() {
  568. var Properties = Backbone.RelationalModel.extend({});
  569. var View = Backbone.RelationalModel.extend({
  570. relations: [
  571. {
  572. type: Backbone.HasOne,
  573. key: 'properties',
  574. relatedModel: Properties
  575. },
  576. {
  577. type: Backbone.HasOne,
  578. key: 'properties',
  579. relatedModel: Properties
  580. }
  581. ]
  582. });
  583. var view = new View();
  584. view.set( { properties: new Properties() } );
  585. ok( view._relations.length === 1 );
  586. });
  587. test( "Duplicate relations not allowed (one relation with a reverse relation, one without)", function() {
  588. var Properties = Backbone.RelationalModel.extend({});
  589. var View = Backbone.RelationalModel.extend({
  590. relations: [
  591. {
  592. type: Backbone.HasOne,
  593. key: 'properties',
  594. relatedModel: Properties,
  595. reverseRelation: {
  596. type: Backbone.HasOne,
  597. key: 'view'
  598. }
  599. },
  600. {
  601. type: Backbone.HasOne,
  602. key: 'properties',
  603. relatedModel: Properties
  604. }
  605. ]
  606. });
  607. var view = new View();
  608. view.set( { properties: new Properties() } );
  609. ok( view._relations.length === 1 );
  610. });
  611. test( "Duplicate relations not allowed (two relations with reverse relations)", function() {
  612. var Properties = Backbone.RelationalModel.extend({});
  613. var View = Backbone.RelationalModel.extend({
  614. relations: [
  615. {
  616. type: Backbone.HasOne,
  617. key: 'properties',
  618. relatedModel: Properties,
  619. reverseRelation: {
  620. type: Backbone.HasOne,
  621. key: 'view'
  622. }
  623. },
  624. {
  625. type: Backbone.HasOne,
  626. key: 'properties',
  627. relatedModel: Properties,
  628. reverseRelation: {
  629. type: Backbone.HasOne,
  630. key: 'view'
  631. }
  632. }
  633. ]
  634. });
  635. var view = new View();
  636. view.set( { properties: new Properties() } );
  637. ok( view._relations.length === 1 );
  638. });
  639. test( "Duplicate relations not allowed (different relations, reverse relations)", function() {
  640. var Properties = Backbone.RelationalModel.extend({});
  641. var View = Backbone.RelationalModel.extend({
  642. relations: [
  643. {
  644. type: Backbone.HasOne,
  645. key: 'listProperties',
  646. relatedModel: Properties,
  647. reverseRelation: {
  648. type: Backbone.HasOne,
  649. key: 'view'
  650. }
  651. },
  652. {
  653. type: Backbone.HasOne,
  654. key: 'windowProperties',
  655. relatedModel: Properties,
  656. reverseRelation: {
  657. type: Backbone.HasOne,
  658. key: 'view'
  659. }
  660. }
  661. ]
  662. });
  663. var view = new View();
  664. var prop1 = new Properties( { name: 'a' } );
  665. var prop2 = new Properties( { name: 'b' } );
  666. view.set( { listProperties: prop1, windowProperties: prop2 } );
  667. ok( view._relations.length === 2 );
  668. ok( prop1._relations.length === 2 );
  669. ok( view.get( 'listProperties' ).get( 'name' ) === 'a' );
  670. ok( view.get( 'windowProperties' ).get( 'name' ) === 'b' );
  671. });
  672. module( "Backbone.HasOne", { setup: initObjects } );
  673. test( "HasOne relations on Person are set up properly", function() {
  674. ok( person1.get('likesALot') === person2 );
  675. equal( person1.get('user').id, 'user-1', "The id of 'person1's user is 'user-1'" );
  676. ok( person2.get('likesALot') === person1 );
  677. });
  678. test( "Reverse HasOne relations on Person are set up properly", function() {
  679. ok( person1.get( 'likedALotBy' ) === person2 );
  680. ok( person1.get( 'user' ).get( 'person' ) === person1, "The person belonging to 'person1's user is 'person1'" );
  681. ok( person2.get( 'likedALotBy' ) === person1 );
  682. });
  683. test( "'set' triggers 'change' and 'update', on a HasOne relation, for a Model with multiple relations", function() {
  684. expect( 9 );
  685. Password = Backbone.RelationalModel.extend({
  686. relations: [{
  687. type: Backbone.HasOne,
  688. key: 'user',
  689. relatedModel: 'User',
  690. reverseRelation: {
  691. type: Backbone.HasOne,
  692. key: 'password',
  693. }
  694. }]
  695. });
  696. // triggers initialization of the reverse relation from User to Password
  697. password = new Password( { plaintext: 'asdf' } );
  698. person1.bind( 'change', function( model, options ) {
  699. ok( model.get( 'user' ) instanceof User, "model.user is an instance of User" );
  700. equals( model.previous( 'user' ).get( 'login' ), oldLogin, "previousAttributes is available on 'change'" );
  701. });
  702. person1.bind( 'change:user', function( model, options ) {
  703. ok( model.get( 'user' ) instanceof User, "model.user is an instance of User" );
  704. equals( model.previous( 'user' ).get( 'login' ), oldLogin, "previousAttributes is available on 'change'" );
  705. });
  706. person1.bind( 'update:user', function( model, attr, options ) {
  707. ok( model.get( 'user' ) instanceof User, "model.user is an instance of User" );
  708. ok( attr.get( 'person' ) === person1, "The user's 'person' is 'person1'" );
  709. ok( attr.get( 'password' ) instanceof Password, "The user's password attribute is a model of type Password");
  710. equal( attr.get( 'password' ).get( 'plaintext' ), 'qwerty', "The user's password is ''qwerty'" );
  711. });
  712. var user = { login: 'me@hotmail.com', password: { plaintext: 'qwerty' } };
  713. var oldLogin = person1.get('user').get( 'login' );
  714. // Triggers first # assertions
  715. person1.set( { user: user } );
  716. user = person1.get( 'user' ).bind( 'update:password', function( model, attr, options ) {
  717. equal( attr.get( 'plaintext' ), 'asdf', "The user's password is ''qwerty'" );
  718. });
  719. // Triggers last assertion
  720. user.set( { password: password } );
  721. });
  722. test( "'unset' triggers 'change' and 'update:'", function() {
  723. expect( 4 );
  724. person1.bind( 'change', function( model, options ) {
  725. equals( model.get('user'), null, "model.user is unset" );
  726. });
  727. person1.bind( 'update:user', function( model, attr, options ) {
  728. equals( attr, null, "new value of attr (user) is null" );
  729. });
  730. ok( person1.get( 'user' ) instanceof User, "person1 has a 'user'" );
  731. var user = person1.get( 'user' );
  732. person1.unset( 'user' );
  733. equals( user.get( 'person' ), null, "person1 is not set on 'user' anymore" );
  734. });
  735. test( "'clear' triggers 'change' and 'update:'", function() {
  736. expect( 4 );
  737. person1.bind( 'change', function( model, options ) {
  738. equals( model.get('user'), null, "model.user is unset" );
  739. });
  740. person1.bind( 'update:user', function( model, attr, options ) {
  741. equals( attr, null, "new value of attr (user) is null" );
  742. });
  743. ok( person1.get( 'user' ) instanceof User, "person1 has a 'user'" );
  744. var user = person1.get( 'user' );
  745. person1.clear();
  746. equals( user.get( 'person' ), null, "person1 is not set on 'user' anymore" );
  747. });
  748. module( "Backbone.HasMany", { setup: initObjects } );
  749. test( "Listeners on 'add'/'remove'", function() {
  750. expect( 7 );
  751. ourHouse
  752. .bind( 'add:occupants', function( model, coll ) {
  753. ok( model === person1, "model === person1" );
  754. })
  755. .bind( 'remove:occupants', function( model, coll ) {
  756. ok( model === person1, "model === person1" );
  757. });
  758. theirHouse
  759. .bind( 'add:occupants', function( model, coll ) {
  760. ok( model === person1, "model === person1" );
  761. })
  762. .bind( 'remove:occupants', function( model, coll ) {
  763. ok( model === person1, "model === person1" );
  764. });
  765. var count = 0;
  766. person1.bind( 'update:livesIn', function( model, attr ) {
  767. if ( count === 0 ) {
  768. ok( attr === ourHouse, "model === ourHouse" );
  769. }
  770. else if ( count === 1 ) {
  771. ok( attr === theirHouse, "model === theirHouse" );
  772. }
  773. else if ( count === 2 ) {
  774. ok( attr === null, "model === null" );
  775. }
  776. count++;
  777. });
  778. ourHouse.get( 'occupants' ).add( person1 );
  779. person1.set( { 'livesIn': theirHouse } );
  780. theirHouse.get( 'occupants' ).remove( person1 );
  781. });
  782. test( "Listeners for 'add'/'remove', on a HasMany relation, for a Model with multiple relations", function() {
  783. var job1 = { company: oldCompany };
  784. var job2 = { company: oldCompany, person: person1 };
  785. var job3 = { person: person1 };
  786. var newJob = null;
  787. newCompany.bind( 'add:employees', function( model, coll ) {
  788. ok( false, "person1 should only be added to 'oldCompany'." );
  789. });
  790. // Assert that all relations on a Model are set up, before notifying related models.
  791. oldCompany.bind( 'add:employees', function( model, coll ) {
  792. newJob = model;
  793. ok( model instanceof Job );
  794. ok( model.get('company') instanceof Company && model.get('person') instanceof Person,
  795. "Both Person and Company are set on the Job instance" );
  796. });
  797. person1.bind( 'add:jobs', function( model, coll ) {
  798. ok( model.get( 'company' ) === oldCompany && model.get( 'person' ) === person1,
  799. "Both Person and Company are set on the Job instance" );
  800. });
  801. // Add job1 and job2 to the 'Person' side of the relation
  802. var jobs = person1.get('jobs');
  803. jobs.add( job1 );
  804. ok( jobs.length === 1, "jobs.length is 1" );
  805. newJob.destroy();
  806. ok( jobs.length === 0, "jobs.length is 0" );
  807. jobs.add( job2 );
  808. ok( jobs.length === 1, "jobs.length is 1" );
  809. newJob.destroy();
  810. ok( jobs.length === 0, "jobs.length is 0" );
  811. // Add job1 and job2 to the 'Company' side of the relation
  812. var employees = oldCompany.get('employees');
  813. employees.add( job3 );
  814. ok( employees.length === 2, "employees.length is 2" );
  815. newJob.destroy();
  816. ok( employees.length === 1, "employees.length is 1" );
  817. employees.add( job2 );
  818. ok( employees.length === 2, "employees.length is 2" );
  819. newJob.destroy();
  820. ok( employees.length === 1, "employees.length is 1" );
  821. // Create a stand-alone Job ;)
  822. new Job({
  823. person: person1,
  824. company: oldCompany
  825. });
  826. ok( jobs.length === 1 && employees.length === 2, "jobs.length is 1 and employees.length is 2" );
  827. });
  828. test( "The Collections used for HasMany relations are re-used if possible", function() {
  829. var collId = ourHouse.get( 'occupants' ).id = 1;
  830. ourHouse.get( 'occupants' ).add( person1 );
  831. ok( ourHouse.get( 'occupants' ).id === collId );
  832. // Set a value on 'occupants' that would cause the relation to be reset.
  833. // The collection itself should be kept (along with it's properties)
  834. ourHouse.set( { 'occupants': [ 'person-1' ] } );
  835. ok( ourHouse.get( 'occupants' ).id === collId );
  836. ok( ourHouse.get( 'occupants' ).length === 1 );
  837. // Setting a new collection loses the original collection
  838. ourHouse.set( { 'occupants': new Backbone.Collection() } );
  839. ok( ourHouse.get( 'occupants' ).id === undefined );
  840. });
  841. test( "Setting a custom collection in relatedCollection uses that collection for instantiation", function() {
  842. var zoo = new Zoo();
  843. // Set values so that the relation gets filled
  844. zoo.set({
  845. animals: [
  846. { race: 'Lion' },
  847. { race: 'Zebra' }
  848. ]
  849. });
  850. // Check that the animals were created
  851. ok( zoo.get( 'animals' ).at( 0 ).get( 'race' ) === 'Lion' );
  852. ok( zoo.get( 'animals' ).at( 1 ).get( 'race' ) === 'Zebra' );
  853. // Check that the generated collection is of the correct kind
  854. ok( zoo.get( 'animals' ) instanceof AnimalCollection );
  855. });
  856. module( "Reverse relationships", { setup: initObjects } );
  857. test( "Add and remove", function() {
  858. equal( ourHouse.get( 'occupants' ).length, 1, "ourHouse has 1 occupant" );
  859. equal( person1.get( 'livesIn' ), null, "Person 1 doesn't live anywhere" );
  860. ourHouse.get( 'occupants' ).add( person1 );
  861. equal( ourHouse.get( 'occupants' ).length, 2, "Our House has 2 occupants" );
  862. equal( person1.get( 'livesIn' ) && person1.get('livesIn').id, ourHouse.id, "Person 1 lives in ourHouse" );
  863. person1.set( { 'livesIn': theirHouse } );
  864. equal( theirHouse.get( 'occupants' ).length, 1, "theirHouse has 1 occupant" );
  865. equal( ourHouse.get( 'occupants' ).length, 1, "ourHouse has 1 occupant" );
  866. equal( person1.get( 'livesIn' ) && person1.get('livesIn').id, theirHouse.id, "Person 1 lives in theirHouse" );
  867. });
  868. test( "HasOne relations to self (tree stucture)", function() {
  869. var child1 = new Node({ id: '2', parent: '1', name: 'First child' });
  870. var parent = new Node({ id: '1', name: 'Parent' });
  871. var child2 = new Node({ id: '3', parent: '1', name: 'Second child' });
  872. equal( parent.get( 'children' ).length, 2 );
  873. ok( parent.get( 'children' ).include( child1 ) );
  874. ok( parent.get( 'children' ).include( child2 ) );
  875. ok( child1.get( 'parent' ) === parent );
  876. equal( child1.get( 'children' ).length, 0 );
  877. ok( child2.get( 'parent' ) === parent );
  878. equal( child2.get( 'children' ).length, 0 );
  879. });
  880. test( "HasMany relations to self (tree structure)", function() {
  881. var child1 = new Node({ id: '2', name: 'First child' });
  882. var parent = new Node({ id: '1', children: [ '2', '3' ], name: 'Parent' });
  883. var child2 = new Node({ id: '3', name: 'Second child' });
  884. equal( parent.get( 'children' ).length, 2 );
  885. ok( parent.get( 'children' ).include( child1 ) );
  886. ok( parent.get( 'children' ).include( child2 ) );
  887. ok( child1.get( 'parent' ) === parent );
  888. equal( child1.get( 'children' ).length, 0 );
  889. ok( child2.get( 'parent' ) === parent );
  890. equal( child2.get( 'children' ).length, 0 );
  891. });
  892. test( "HasOne relations to self (cycle, directed graph structure)", function() {
  893. var node1 = new Node({ id: '1', parent: '3', name: 'First node' });
  894. var node2 = new Node({ id: '2', parent: '1', name: 'Second node' });
  895. var node3 = new Node({ id: '3', parent: '2', name: 'Third node' });
  896. ok( node1.get( 'parent' ) === node3 );
  897. equal( node1.get( 'children' ).length, 1 );
  898. ok( node1.get( 'children' ).at(0) === node2 );
  899. ok( node2.get( 'parent' ) === node1 );
  900. equal( node2.get( 'children' ).length, 1 );
  901. ok( node2.get( 'children' ).at(0) === node3 );
  902. ok( node3.get( 'parent' ) === node2 );
  903. equal( node3.get( 'children' ).length, 1 );
  904. ok( node3.get( 'children' ).at(0) === node1 );
  905. });
  906. test("New objects (no 'id' yet) have working relations", function() {
  907. var person = new Person({
  908. name: 'Remi'
  909. });
  910. person.set( { user: { login: '1', email: '1' } } );
  911. var user1 = person.get( 'user' );
  912. ok( user1 instanceof User, "User created on Person" );
  913. equal( user1.get('login'), '1', "person.user is the correct User" );
  914. var user2 = new User({
  915. login: '2',
  916. email: '2'
  917. });
  918. ok( user2.get( 'person' ) === null, "'user' doesn't belong to a 'person' yet" );
  919. person.set( { user: user2 } );
  920. ok( user1.get( 'person' ) === null );
  921. ok( person.get( 'user' ) === user2 );
  922. ok( user2.get( 'person' ) === person );
  923. person2.set( { user: user2 } );
  924. ok( person.get( 'user' ) === null );
  925. ok( person2.get( 'user' ) === user2 );
  926. ok( user2.get( 'person' ) === person2 );
  927. });
  928. test("'Save' objects (performing 'set' multiple times without and with id)", function() {
  929. expect( 2 );
  930. person3
  931. .bind( 'add:jobs', function( model, coll ) {
  932. var company = model.get('company');
  933. ok( company instanceof Company && company.get('ceo').get('name') === 'Lunar boy' && model.get('person') === person3,
  934. "Both Person and Company are set on the Job instance" );
  935. })
  936. .bind( 'remove:jobs', function( model, coll ) {
  937. ok( false, "'person3' should not lose his job" );
  938. });
  939. // Create Models from an object
  940. var company = new Company({
  941. name: 'Luna Corp.',
  942. ceo: {
  943. name: 'Lunar boy'
  944. },
  945. employees: [ { person: 'person-3' } ],
  946. });
  947. // Backbone.save executes "model.set(model.parse(resp), options)". Set a full map over object, but now with ids.
  948. company.set({
  949. id: 'company-3',
  950. name: 'Big Corp.',
  951. ceo: {
  952. id: 'person-4',
  953. name: 'Lunar boy',
  954. resource_uri: 'person-4'
  955. },
  956. employees: [ { id: 'job-1', person: 'person-3', resource_uri: 'job-1' } ],
  957. resource_uri: 'company-3'
  958. });
  959. });
  960. test("Set the same value a couple of time, by 'id' and object", function() {
  961. person1.set( { likesALot: 'person-2' } );
  962. person1.set( { likesALot: person2 } );
  963. ok( person1.get('likesALot') === person2 );
  964. ok( person2.get('likedALotBy' ) === person1 );
  965. person1.set( { likesALot: 'person-2' } );
  966. ok( person1.get('likesALot') === person2 );
  967. ok( person2.get('likedALotBy' ) === person1 );
  968. });
  969. test("Numerical keys", function() {
  970. var child1 = new Node({ id: 2, name: 'First child' });
  971. var parent = new Node({ id: 1, children: [2, 3], name: 'Parent' });
  972. var child2 = new Node({ id: 3, name: 'Second child' });
  973. equal( parent.get('children').length, 2 );
  974. ok( parent.get('children').include( child1 ) );
  975. ok( parent.get('children').include( child2 ) );
  976. ok( child1.get('parent') === parent );
  977. equal( child1.get('children').length, 0 );
  978. ok( child2.get('parent') === parent );
  979. equal( child2.get('children').length, 0 );
  980. });
  981. test("Relations that use refs to other models (instead of keys)", function() {
  982. var child1 = new Node({ id: 2, name: 'First child' });
  983. var parent = new Node({ id: 1, children: [child1, 3], name: 'Parent' });
  984. var child2 = new Node({ id: 3, name: 'Second child' });
  985. ok( child1.get('parent') === parent );
  986. equal( child1.get('children').length, 0 );
  987. equal( parent.get('children').length, 2 );
  988. ok( parent.get('children').include( child1 ) );
  989. ok( parent.get('children').include( child2 ) );
  990. var child3 = new Node({ id: 4, parent: parent, name: 'Second child' });
  991. equal( parent.get('children').length, 3 );
  992. ok( parent.get('children').include( child3 ) );
  993. ok( child3.get('parent') === parent );
  994. equal( child3.get('children').length, 0 );
  995. });
  996. test("Add an already existing model (reverseRelation shouldn't exist yet) to a relation as a hash", function() {
  997. // This test caused a race condition to surface:
  998. // The 'relation's constructor initializes the 'reverseRelation', which called 'relation.addRelated' in it's 'initialize'.
  999. // However, 'relation's 'initialize' has not been executed yet, so it doesn't have a 'related' collection yet.
  1000. var Properties = Backbone.RelationalModel.extend({});
  1001. var View = Backbone.RelationalModel.extend({
  1002. relations: [
  1003. {
  1004. type: Backbone.HasMany,
  1005. key: 'properties',
  1006. relatedModel: Properties,
  1007. reverseRelation: {
  1008. type: Backbone.HasOne,
  1009. key: 'view'
  1010. }
  1011. }
  1012. ]
  1013. });
  1014. var props = new Properties( { id: 1, key: 'width', value: '300px', view: 1 } );
  1015. var view = new View({
  1016. id: 1,
  1017. properties: [ { id: 1, key: 'width', value: '300px', view: 1 } ]
  1018. });
  1019. ok( props.get( 'view' ) === view );
  1020. ok( view.get( 'properties' ).include( props ) );
  1021. });
  1022. test("ReverseRelations are applied retroactively", function() {
  1023. // Use brand new Model types, so we can be sure we don't have any reverse relations cached from previous tests
  1024. var NewUser = Backbone.RelationalModel.extend({});
  1025. var NewPerson = Backbone.RelationalModel.extend({
  1026. relations: [{
  1027. type: Backbone.HasOne,
  1028. key: 'user',
  1029. relatedModel: NewUser,
  1030. reverseRelation: {
  1031. type: Backbone.HasOne,
  1032. key: 'person'
  1033. }
  1034. }]
  1035. });
  1036. var user = new NewUser( { id: 'newuser-1' } );
  1037. //var user2 = new NewUser( { id: 'newuser-2', person: 'newperson-1' } );
  1038. var person = new NewPerson( { id: 'newperson-1', user: user } );
  1039. ok( person.get('user') === user );
  1040. ok( user.get('person') === person );
  1041. //console.debug( person, user );
  1042. });
  1043. module("Model loading", { setup: initObjects } );
  1044. test("Loading (fetching) multiple times updates the model", function() {
  1045. var collA = new Backbone.Collection();
  1046. collA.model = User;
  1047. var collB = new Backbone.Collection();
  1048. collB.model = User;
  1049. // Similar to what happens when calling 'fetch' on collA, updating it, calling 'fetch' on collB
  1050. var name = 'User 1';
  1051. var user = collA._add( { id: '/user/1/', name: name } );
  1052. equal( user.get( 'name' ), name );
  1053. // The 'name' of 'user' is updated when adding a new hash to the collection
  1054. name = 'New name';
  1055. var updatedUser = collA._add( { id: '/user/1/', name: name } );
  1056. equal( user.get( 'name' ), name );
  1057. equal( updatedUser.get( 'name' ), name );
  1058. // The 'name' of 'user' is also updated when adding a new hash to another collection
  1059. name = 'Another new name';
  1060. var updatedUser2 = collB._add( { id: '/user/1/', name: name, title: 'Superuser' } );
  1061. equal( user.get( 'name' ), name );
  1062. equal( updatedUser2.get('name'), name );
  1063. ok( collA.get('/user/1/') === updatedUser );
  1064. ok( collA.get('/user/1/') === updatedUser2 );
  1065. ok( collB.get('/user/1/') === user );
  1066. ok( collB.get('/user/1/') === updatedUser );
  1067. });
  1068. test("Loading (fetching) multiple times updates related models as well (HasOne)", function() {
  1069. var coll = new PersonCollection();
  1070. coll.add( { id: 'person-10', name: 'Person', user: { id: 'user-10', login: 'User' } } );
  1071. var person = coll.at( 0 );
  1072. var user = person.get( 'user' );
  1073. equals( user.get( 'login' ), 'User' );
  1074. coll.add( { id: 'person-10', name: 'New person', user: { id: 'user-10', login: 'New user' } } );
  1075. equals( person.get( 'name' ), 'New person' );
  1076. equals( user.get( 'login' ), 'New user' );
  1077. });
  1078. test("Loading (fetching) multiple times updates related models as well (HasMany)", function() {
  1079. var coll = new Backbone.Collection();
  1080. coll.model = Zoo;
  1081. // Create a 'zoo' with 1 animal in it
  1082. coll.add( { id: 'zoo-1', name: 'Zoo', animals: [ { id: 'lion-1', name: 'Mufasa' } ] } );
  1083. var zoo = coll.at( 0 );
  1084. var lion = zoo.get( 'animals' ) .at( 0 );
  1085. equals( lion.get( 'name' ), 'Mufasa' );
  1086. // Update the name of 'zoo' and 'lion'
  1087. coll.add( { id: 'zoo-1', name: 'Zoo Station', animals: [ { id: 'lion-1', name: 'Simba' } ] } );
  1088. equals( zoo.get( 'name' ), 'Zoo Station' );
  1089. equals( lion.get( 'name' ), 'Simba' );
  1090. });
  1091. });