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

/app/components/backbone-relational/test/tests.js

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