PageRenderTime 59ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/test/tests.js

https://github.com/sventschui/Backbone-relational
JavaScript | 4888 lines | 3579 code | 1058 blank | 251 comment | 78 complexity | 8f0948fcac86011646b4902c6b28e15f MD5 | raw file

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

  1. /* vim: set tabstop=4 softtabstop=4 shiftwidth=4 noexpandtab: */
  2. // documentation on writing tests here: http://docs.jquery.com/QUnit
  3. // example tests: https://github.com/jquery/qunit/blob/master/test/same.js
  4. // more examples: https://github.com/jquery/jquery/tree/master/test/unit
  5. // jQueryUI examples: https://github.com/jquery/jquery-ui/tree/master/tests/unit
  6. //sessionStorage.clear();
  7. if ( !window.console ) {
  8. var names = [ 'log', 'debug', 'info', 'warn', 'error', 'assert', 'dir', 'dirxml',
  9. 'group', 'groupEnd', 'time', 'timeEnd', 'count', 'trace', 'profile', 'profileEnd' ];
  10. window.console = {};
  11. for ( var i = 0; i < names.length; ++i )
  12. window.console[ names[i] ] = function() {};
  13. }
  14. $(document).ready(function() {
  15. window.requests = [];
  16. Backbone.ajax = function( settings ) {
  17. var callbackContext = settings.context || this,
  18. dfd = new $.Deferred();
  19. dfd = _.extend( settings, dfd );
  20. dfd.respond = function( status, responseText ) {
  21. /**
  22. * Trigger success/error with arguments like jQuery would:
  23. * // Success/Error
  24. * if ( isSuccess ) {
  25. * deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
  26. * } else {
  27. * deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
  28. * }
  29. */
  30. if ( status >= 200 && status < 300 || status === 304 ) {
  31. _.isFunction( settings.success ) && settings.success( responseText, 'success', dfd );
  32. dfd.resolveWith( callbackContext, [ responseText, 'success', dfd ] );
  33. }
  34. else {
  35. _.isFunction( settings.error ) && settings.error( responseText, 'error', 'Internal Server Error' );
  36. dfd.rejectWith( callbackContext, [ dfd, 'error', 'Internal Server Error' ] );
  37. }
  38. };
  39. // Add the request before triggering callbacks that may get us in here again
  40. window.requests.push( dfd );
  41. // If a `response` has been defined, execute it.
  42. // If status < 299, trigger 'success'; otherwise, trigger 'error'
  43. if ( settings.response && settings.response.status ) {
  44. dfd.respond( settings.response.status, settings.response.responseText );
  45. }
  46. return dfd;
  47. };
  48. Backbone.Model.prototype.url = function() {
  49. // Use the 'resource_uri' if possible
  50. var url = this.get( 'resource_uri' );
  51. // Try to have the collection construct a url
  52. if ( !url && this.collection ) {
  53. url = this.collection.url && _.isFunction( this.collection.url ) ? this.collection.url() : this.collection.url;
  54. }
  55. // Fallback to 'urlRoot'
  56. if ( !url && this.urlRoot ) {
  57. url = this.urlRoot + this.id;
  58. }
  59. if ( !url ) {
  60. throw new Error( 'Url could not be determined!' );
  61. }
  62. return url;
  63. };
  64. /**
  65. * 'Zoo'
  66. */
  67. window.Zoo = Backbone.RelationalModel.extend({
  68. urlRoot: '/zoo/',
  69. relations: [
  70. {
  71. type: Backbone.HasMany,
  72. key: 'animals',
  73. relatedModel: 'Animal',
  74. includeInJSON: [ 'id', 'species' ],
  75. collectionType: 'AnimalCollection',
  76. reverseRelation: {
  77. key: 'livesIn',
  78. includeInJSON: [ 'id', 'name' ]
  79. }
  80. },
  81. { // A simple HasMany without reverse relation
  82. type: Backbone.HasMany,
  83. key: 'visitors',
  84. relatedModel: 'Visitor'
  85. }
  86. ],
  87. toString: function() {
  88. return 'Zoo (' + this.id + ')';
  89. }
  90. });
  91. window.Animal = Backbone.RelationalModel.extend({
  92. urlRoot: '/animal/',
  93. relations: [
  94. { // A simple HasOne without reverse relation
  95. type: Backbone.HasOne,
  96. key: 'favoriteFood',
  97. relatedModel: 'Food'
  98. }
  99. ],
  100. // For validation testing. Wikipedia says elephants are reported up to 12.000 kg. Any more, we must've weighted wrong ;).
  101. validate: function( attrs ) {
  102. if ( attrs.species === 'elephant' && attrs.weight && attrs.weight > 12000 ) {
  103. return "Too heavy.";
  104. }
  105. },
  106. toString: function() {
  107. return 'Animal (' + this.id + ')';
  108. }
  109. });
  110. window.AnimalCollection = Backbone.Collection.extend({
  111. model: Animal
  112. });
  113. window.Food = Backbone.RelationalModel.extend({
  114. urlRoot: '/food/'
  115. });
  116. window.Visitor = Backbone.RelationalModel.extend();
  117. /**
  118. * House/Person/Job/Company
  119. */
  120. window.House = Backbone.RelationalModel.extend({
  121. relations: [{
  122. type: Backbone.HasMany,
  123. key: 'occupants',
  124. relatedModel: 'Person',
  125. reverseRelation: {
  126. key: 'livesIn',
  127. includeInJSON: false
  128. }
  129. }],
  130. toString: function() {
  131. return 'House (' + this.id + ')';
  132. }
  133. });
  134. window.User = Backbone.RelationalModel.extend({
  135. urlRoot: '/user/',
  136. toString: function() {
  137. return 'User (' + this.id + ')';
  138. }
  139. });
  140. window.Person = Backbone.RelationalModel.extend({
  141. relations: [
  142. {
  143. // Create a cozy, recursive, one-to-one relationship
  144. type: Backbone.HasOne,
  145. key: 'likesALot',
  146. relatedModel: 'Person',
  147. reverseRelation: {
  148. type: Backbone.HasOne,
  149. key: 'likedALotBy'
  150. }
  151. },
  152. {
  153. type: Backbone.HasOne,
  154. key: 'user',
  155. keyDestination: 'user_id',
  156. relatedModel: 'User',
  157. includeInJSON: Backbone.Model.prototype.idAttribute,
  158. reverseRelation: {
  159. type: Backbone.HasOne,
  160. includeInJSON: 'name',
  161. key: 'person'
  162. }
  163. },
  164. {
  165. type: 'HasMany',
  166. key: 'jobs',
  167. relatedModel: 'Job',
  168. reverseRelation: {
  169. key: 'person'
  170. }
  171. }
  172. ],
  173. toString: function() {
  174. return 'Person (' + this.id + ')';
  175. }
  176. });
  177. window.PersonCollection = Backbone.Collection.extend({
  178. model: Person
  179. });
  180. window.Password = Backbone.RelationalModel.extend({
  181. relations: [{
  182. type: Backbone.HasOne,
  183. key: 'user',
  184. relatedModel: 'User',
  185. reverseRelation: {
  186. type: Backbone.HasOne,
  187. key: 'password'
  188. }
  189. }],
  190. toString: function() {
  191. return 'Password (' + this.id + ')';
  192. }
  193. });
  194. // A link table between 'Person' and 'Company', to achieve many-to-many relations
  195. window.Job = Backbone.RelationalModel.extend({
  196. defaults: {
  197. 'startDate': null,
  198. 'endDate': null
  199. },
  200. toString: function() {
  201. return 'Job (' + this.id + ')';
  202. }
  203. });
  204. window.Company = Backbone.RelationalModel.extend({
  205. relations: [{
  206. type: 'HasMany',
  207. key: 'employees',
  208. relatedModel: 'Job',
  209. reverseRelation: {
  210. key: 'company'
  211. }
  212. },
  213. {
  214. type: 'HasOne',
  215. key: 'ceo',
  216. relatedModel: 'Person',
  217. reverseRelation: {
  218. key: 'runs'
  219. }
  220. }
  221. ],
  222. toString: function() {
  223. return 'Company (' + this.id + ')';
  224. }
  225. });
  226. /**
  227. * Node/NodeList
  228. */
  229. window.Node = Backbone.RelationalModel.extend({
  230. urlRoot: '/node/',
  231. relations: [{
  232. type: Backbone.HasOne,
  233. key: 'parent',
  234. relatedModel: 'Node',
  235. reverseRelation: {
  236. key: 'children'
  237. }
  238. }
  239. ],
  240. toString: function() {
  241. return 'Node (' + this.id + ')';
  242. }
  243. });
  244. window.NodeList = Backbone.Collection.extend({
  245. model: Node
  246. });
  247. /**
  248. * Customer/Address/Shop/Agent
  249. */
  250. window.Customer = Backbone.RelationalModel.extend({
  251. urlRoot: '/customer/',
  252. toString: function() {
  253. return 'Customer (' + this.id + ')';
  254. }
  255. });
  256. window.CustomerCollection = Backbone.Collection.extend({
  257. model: Customer,
  258. initialize: function( models, options ) {
  259. options || (options = {});
  260. this.url = options.url;
  261. }
  262. });
  263. window.Address = Backbone.RelationalModel.extend({
  264. urlRoot: '/address/',
  265. toString: function() {
  266. return 'Address (' + this.id + ')';
  267. }
  268. });
  269. window.Shop = Backbone.RelationalModel.extend({
  270. relations: [
  271. {
  272. type: Backbone.HasMany,
  273. key: 'customers',
  274. collectionType: 'CustomerCollection',
  275. collectionOptions: function( instance ) {
  276. return { 'url': 'shop/' + instance.id + '/customers/' };
  277. },
  278. relatedModel: 'Customer',
  279. autoFetch: true
  280. },
  281. {
  282. type: Backbone.HasOne,
  283. key: 'address',
  284. relatedModel: 'Address',
  285. autoFetch: {
  286. success: function( model, response ) {
  287. response.successOK = true;
  288. },
  289. error: function( model, response ) {
  290. response.errorOK = true;
  291. }
  292. }
  293. }
  294. ],
  295. toString: function() {
  296. return 'Shop (' + this.id + ')';
  297. }
  298. });
  299. window.Agent = Backbone.RelationalModel.extend({
  300. urlRoot: '/agent/',
  301. relations: [
  302. {
  303. type: Backbone.HasMany,
  304. key: 'customers',
  305. relatedModel: 'Customer',
  306. includeInJSON: Backbone.RelationalModel.prototype.idAttribute
  307. },
  308. {
  309. type: Backbone.HasOne,
  310. key: 'address',
  311. relatedModel: 'Address',
  312. autoFetch: false
  313. }
  314. ],
  315. toString: function() {
  316. return 'Agent (' + this.id + ')';
  317. }
  318. });
  319. /**
  320. * Reset variables that are persistent across tests, specifically `window.requests` and the state of
  321. * `Backbone.Relational.store`.
  322. */
  323. function reset() {
  324. // Reset last ajax requests
  325. window.requests = [];
  326. Backbone.Relational.store.reset();
  327. Backbone.Relational.store.addModelScope( window );
  328. Backbone.Relational.eventQueue = new Backbone.BlockingQueue();
  329. }
  330. /**
  331. * Initialize a few models that are used in a large number of tests
  332. */
  333. function initObjects() {
  334. reset();
  335. window.person1 = new Person({
  336. id: 'person-1',
  337. name: 'boy',
  338. likesALot: 'person-2',
  339. resource_uri: 'person-1',
  340. user: { id: 'user-1', login: 'dude', email: 'me@gmail.com', resource_uri: 'user-1' }
  341. });
  342. window.person2 = new Person({
  343. id: 'person-2',
  344. name: 'girl',
  345. likesALot: 'person-1',
  346. resource_uri: 'person-2'
  347. });
  348. window.person3 = new Person({
  349. id: 'person-3',
  350. resource_uri: 'person-3'
  351. });
  352. window.oldCompany = new Company({
  353. id: 'company-1',
  354. name: 'Big Corp.',
  355. ceo: {
  356. name: 'Big Boy'
  357. },
  358. employees: [ { person: 'person-3' } ], // uses the 'Job' link table to achieve many-to-many. No 'id' specified!
  359. resource_uri: 'company-1'
  360. });
  361. window.newCompany = new Company({
  362. id: 'company-2',
  363. name: 'New Corp.',
  364. employees: [ { person: 'person-2' } ],
  365. resource_uri: 'company-2'
  366. });
  367. window.ourHouse = new House({
  368. id: 'house-1',
  369. location: 'in the middle of the street',
  370. occupants: ['person-2'],
  371. resource_uri: 'house-1'
  372. });
  373. window.theirHouse = new House({
  374. id: 'house-2',
  375. location: 'outside of town',
  376. occupants: [],
  377. resource_uri: 'house-2'
  378. });
  379. }
  380. module ( "General / Backbone", { setup: reset } );
  381. test( "Prototypes, constructors and inheritance", function() {
  382. // This stuff makes my brain hurt a bit. So, for reference:
  383. var Model = Backbone.Model.extend(),
  384. i = new Backbone.Model(),
  385. iModel = new Model();
  386. var RelModel= Backbone.RelationalModel.extend(),
  387. iRel = new Backbone.RelationalModel(),
  388. iRelModel = new RelModel();
  389. // Both are functions, so their `constructor` is `Function`
  390. ok( Backbone.Model.constructor === Backbone.RelationalModel.constructor );
  391. ok( Backbone.Model !== Backbone.RelationalModel );
  392. ok( Backbone.Model === Backbone.Model.prototype.constructor );
  393. ok( Backbone.RelationalModel === Backbone.RelationalModel.prototype.constructor );
  394. ok( Backbone.Model.prototype.constructor !== Backbone.RelationalModel.prototype.constructor );
  395. ok( Model.prototype instanceof Backbone.Model );
  396. ok( !( Model.prototype instanceof Backbone.RelationalModel ) );
  397. ok( RelModel.prototype instanceof Backbone.Model );
  398. ok( Backbone.RelationalModel.prototype instanceof Backbone.Model );
  399. ok( RelModel.prototype instanceof Backbone.RelationalModel );
  400. ok( i instanceof Backbone.Model );
  401. ok( !( i instanceof Backbone.RelationalModel ) );
  402. ok( iRel instanceof Backbone.Model );
  403. ok( iRel instanceof Backbone.RelationalModel );
  404. ok( iModel instanceof Backbone.Model );
  405. ok( !( iModel instanceof Backbone.RelationalModel ) );
  406. ok( iRelModel instanceof Backbone.Model );
  407. ok( iRelModel instanceof Backbone.RelationalModel );
  408. });
  409. test('Collection#set', 1, function() {
  410. var a = new Backbone.Model({id: 3, label: 'a'} ),
  411. b = new Backbone.Model({id: 2, label: 'b'} ),
  412. col = new Backbone.Collection([a]);
  413. col.set([a,b], {add: true, merge: false, remove: true});
  414. ok( col.length === 2 );
  415. });
  416. module( "Backbone.Semaphore", { setup: reset } );
  417. test( "Unbounded", 10, function() {
  418. var semaphore = _.extend( {}, Backbone.Semaphore );
  419. ok( !semaphore.isLocked(), 'Semaphore is not locked initially' );
  420. semaphore.acquire();
  421. ok( semaphore.isLocked(), 'Semaphore is locked after acquire' );
  422. semaphore.acquire();
  423. equal( semaphore._permitsUsed, 2 ,'_permitsUsed should be incremented 2 times' );
  424. semaphore.setAvailablePermits( 4 );
  425. equal( semaphore._permitsAvailable, 4 ,'_permitsAvailable should be 4' );
  426. semaphore.acquire();
  427. semaphore.acquire();
  428. equal( semaphore._permitsUsed, 4 ,'_permitsUsed should be incremented 4 times' );
  429. try {
  430. semaphore.acquire();
  431. }
  432. catch( ex ) {
  433. ok( true, 'Error thrown when attempting to acquire too often' );
  434. }
  435. semaphore.release();
  436. equal( semaphore._permitsUsed, 3 ,'_permitsUsed should be decremented to 3' );
  437. semaphore.release();
  438. semaphore.release();
  439. semaphore.release();
  440. equal( semaphore._permitsUsed, 0 ,'_permitsUsed should be decremented to 0' );
  441. ok( !semaphore.isLocked(), 'Semaphore is not locked when all permits are released' );
  442. try {
  443. semaphore.release();
  444. }
  445. catch( ex ) {
  446. ok( true, 'Error thrown when attempting to release too often' );
  447. }
  448. });
  449. module( "Backbone.BlockingQueue", { setup: reset } );
  450. test( "Block", function() {
  451. var queue = new Backbone.BlockingQueue();
  452. var count = 0;
  453. var increment = function() { count++; };
  454. var decrement = function() { count--; };
  455. queue.add( increment );
  456. ok( count === 1, 'Increment executed right away' );
  457. queue.add( decrement );
  458. ok( count === 0, 'Decrement executed right away' );
  459. queue.block();
  460. queue.add( increment );
  461. ok( queue.isLocked(), 'Queue is blocked' );
  462. equal( count, 0, 'Increment did not execute right away' );
  463. queue.block();
  464. queue.block();
  465. equal( queue._permitsUsed, 3 ,'_permitsUsed should be incremented to 3' );
  466. queue.unblock();
  467. queue.unblock();
  468. queue.unblock();
  469. equal( count, 1, 'Increment executed' );
  470. });
  471. module( "Backbone.Store", { setup: initObjects } );
  472. test( "Initialized", function() {
  473. // `initObjects` instantiates models of the following types: `Person`, `Job`, `Company`, `User`, `House` and `Password`.
  474. equal( Backbone.Relational.store._collections.length, 6, "Store contains 6 collections" );
  475. });
  476. test( "getObjectByName", function() {
  477. equal( Backbone.Relational.store.getObjectByName( 'Backbone.RelationalModel' ), Backbone.RelationalModel );
  478. });
  479. test( "Add and remove from store", function() {
  480. var coll = Backbone.Relational.store.getCollection( person1 );
  481. var length = coll.length;
  482. var person = new Person({
  483. id: 'person-10',
  484. name: 'Remi',
  485. resource_uri: 'person-10'
  486. });
  487. ok( coll.length === length + 1, "Collection size increased by 1" );
  488. var request = person.destroy();
  489. // Trigger the 'success' callback to fire the 'destroy' event
  490. request.success();
  491. ok( coll.length === length, "Collection size decreased by 1" );
  492. });
  493. test( "addModelScope", function() {
  494. var models = {};
  495. Backbone.Relational.store.addModelScope( models );
  496. models.Book = Backbone.RelationalModel.extend({
  497. relations: [{
  498. type: Backbone.HasMany,
  499. key: 'pages',
  500. relatedModel: 'Page',
  501. createModels: false,
  502. reverseRelation: {
  503. key: 'book'
  504. }
  505. }]
  506. });
  507. models.Page = Backbone.RelationalModel.extend();
  508. var book = new models.Book();
  509. var page = new models.Page({ book: book });
  510. ok( book.relations.length === 1 );
  511. ok( book.get( 'pages' ).length === 1 );
  512. });
  513. test( "addModelScope with submodels and namespaces", function() {
  514. var ns = {};
  515. ns.People = {};
  516. Backbone.Relational.store.addModelScope( ns );
  517. ns.People.Person = Backbone.RelationalModel.extend({
  518. subModelTypes: {
  519. 'Student': 'People.Student'
  520. },
  521. iam: function() { return "I am an abstract person"; }
  522. });
  523. ns.People.Student = ns.People.Person.extend({
  524. iam: function() { return "I am a student"; }
  525. });
  526. ns.People.PersonCollection = Backbone.Collection.extend({
  527. model: ns.People.Person
  528. });
  529. var people = new ns.People.PersonCollection([{name: "Bob", type: "Student"}]);
  530. ok( people.at(0).iam() === "I am a student" );
  531. });
  532. test( "removeModelScope", function() {
  533. var models = {};
  534. Backbone.Relational.store.addModelScope( models );
  535. models.Page = Backbone.RelationalModel.extend();
  536. ok( Backbone.Relational.store.getObjectByName( 'Page' ) === models.Page );
  537. ok( Backbone.Relational.store.getObjectByName( 'Person' ) === window.Person );
  538. Backbone.Relational.store.removeModelScope( models );
  539. ok( !Backbone.Relational.store.getObjectByName( 'Page' ) );
  540. ok( Backbone.Relational.store.getObjectByName( 'Person' ) === window.Person );
  541. Backbone.Relational.store.removeModelScope( window );
  542. ok( !Backbone.Relational.store.getObjectByName( 'Person' ) );
  543. });
  544. test( "unregister", function() {
  545. var animalStoreColl = Backbone.Relational.store.getCollection( Animal ),
  546. animals = null,
  547. animal = null;
  548. // Single model
  549. animal = new Animal( { id: 'a1' } );
  550. ok( Backbone.Relational.store.find( Animal, 'a1' ) === animal );
  551. Backbone.Relational.store.unregister( animal );
  552. ok( Backbone.Relational.store.find( Animal, 'a1' ) === null );
  553. animal = new Animal( { id: 'a2' } );
  554. ok( Backbone.Relational.store.find( Animal, 'a2' ) === animal );
  555. animal.trigger( 'relational:unregister', animal );
  556. ok( Backbone.Relational.store.find( Animal, 'a2' ) === null );
  557. ok( animalStoreColl.size() === 0 );
  558. // Collection
  559. animals = new AnimalCollection( [ { id: 'a3' }, { id: 'a4' } ] );
  560. animal = animals.first();
  561. ok( Backbone.Relational.store.find( Animal, 'a3' ) === animal );
  562. ok( animalStoreColl.size() === 2 );
  563. Backbone.Relational.store.unregister( animals );
  564. ok( Backbone.Relational.store.find( Animal, 'a3' ) === null );
  565. ok( animalStoreColl.size() === 0 );
  566. // Store collection
  567. animals = new AnimalCollection( [ { id: 'a5' }, { id: 'a6' } ] );
  568. ok( animalStoreColl.size() === 2 );
  569. Backbone.Relational.store.unregister( animalStoreColl );
  570. ok( animalStoreColl.size() === 0 );
  571. // Model type
  572. animals = new AnimalCollection( [ { id: 'a7' }, { id: 'a8' } ] );
  573. ok( animalStoreColl.size() === 2 );
  574. Backbone.Relational.store.unregister( Animal );
  575. ok( animalStoreColl.size() === 0 );
  576. });
  577. test( "`eventQueue` is unblocked again after a duplicate id error", 3, function() {
  578. var node = new Node( { id: 1 } );
  579. ok( Backbone.Relational.eventQueue.isBlocked() === false );
  580. try {
  581. duplicateNode = new Node( { id: 1 } );
  582. }
  583. catch( error ) {
  584. ok( true, "Duplicate id error thrown" );
  585. }
  586. ok( Backbone.Relational.eventQueue.isBlocked() === false );
  587. });
  588. test( "Don't allow setting a duplicate `id`", 4, function() {
  589. var a = new Zoo(); // This object starts with no id.
  590. var b = new Zoo( { 'id': 42 } ); // This object starts with an id of 42.
  591. equal( b.id, 42 );
  592. try {
  593. a.set( 'id', 42 );
  594. }
  595. catch( error ) {
  596. ok( true, "Duplicate id error thrown" );
  597. }
  598. ok( !a.id, "a.id=" + a.id );
  599. equal( b.id, 42 );
  600. });
  601. test( "Models are created from objects, can then be found, destroyed, cannot be found anymore", function() {
  602. var houseId = 'house-10';
  603. var personId = 'person-10';
  604. var anotherHouse = new House({
  605. id: houseId,
  606. location: 'no country for old men',
  607. resource_uri: houseId,
  608. occupants: [{
  609. id: personId,
  610. name: 'Remi',
  611. resource_uri: personId
  612. }]
  613. });
  614. ok( anotherHouse.get('occupants') instanceof Backbone.Collection, "Occupants is a Collection" );
  615. ok( anotherHouse.get('occupants').get( personId ) instanceof Person, "Occupants contains the Person with id='" + personId + "'" );
  616. var person = Backbone.Relational.store.find( Person, personId );
  617. ok( person, "Person with id=" + personId + " is found in the store" );
  618. var request = person.destroy();
  619. // Trigger the 'success' callback to fire the 'destroy' event
  620. request.success();
  621. person = Backbone.Relational.store.find( Person, personId );
  622. ok( !person, personId + " is not found in the store anymore" );
  623. ok( !anotherHouse.get('occupants').get( personId ), "Occupants no longer contains the Person with id='" + personId + "'" );
  624. request = anotherHouse.destroy();
  625. // Trigger the 'success' callback to fire the 'destroy' event
  626. request.success();
  627. var house = Backbone.Relational.store.find( House, houseId );
  628. ok( !house, houseId + " is not found in the store anymore" );
  629. });
  630. test( "Model.collection is the first collection a Model is added to by an end-user (not its Backbone.Store collection!)", function() {
  631. var person = new Person( { id: 5, name: 'New guy' } );
  632. var personColl = new PersonCollection();
  633. personColl.add( person );
  634. ok( person.collection === personColl );
  635. });
  636. test( "Models don't get added to the store until the get an id", function() {
  637. var storeColl = Backbone.Relational.store.getCollection( Node ),
  638. node1 = new Node( { id: 1 } ),
  639. node2 = new Node();
  640. ok( storeColl.contains( node1 ) );
  641. ok( !storeColl.contains( node2 ) );
  642. node2.set( { id: 2 } );
  643. ok( storeColl.contains( node1 ) );
  644. });
  645. test( "All models can be found after adding them to a Collection via 'Collection.reset'", function() {
  646. var nodes = [
  647. { id: 1, parent: null },
  648. { id: 2, parent: 1 },
  649. { id: 3, parent: 4 },
  650. { id: 4, parent: 1 }
  651. ];
  652. var nodeList = new NodeList();
  653. nodeList.reset( nodes );
  654. var storeColl = Backbone.Relational.store.getCollection( Node );
  655. equal( storeColl.length, 4, "Every Node is in Backbone.Relational.store" );
  656. ok( Backbone.Relational.store.find( Node, 1 ) instanceof Node, "Node 1 can be found" );
  657. ok( Backbone.Relational.store.find( Node, 2 ) instanceof Node, "Node 2 can be found" );
  658. ok( Backbone.Relational.store.find( Node, 3 ) instanceof Node, "Node 3 can be found" );
  659. ok( Backbone.Relational.store.find( Node, 4 ) instanceof Node, "Node 4 can be found" );
  660. });
  661. test( "Inheritance creates and uses a separate collection", function() {
  662. var whale = new Animal( { id: 1, species: 'whale' } );
  663. ok( Backbone.Relational.store.find( Animal, 1 ) === whale );
  664. var numCollections = Backbone.Relational.store._collections.length;
  665. var Mammal = Animal.extend({
  666. urlRoot: '/mammal/'
  667. });
  668. var lion = new Mammal( { id: 1, species: 'lion' } );
  669. var donkey = new Mammal( { id: 2, species: 'donkey' } );
  670. equal( Backbone.Relational.store._collections.length, numCollections + 1 );
  671. ok( Backbone.Relational.store.find( Animal, 1 ) === whale );
  672. ok( Backbone.Relational.store.find( Mammal, 1 ) === lion );
  673. ok( Backbone.Relational.store.find( Mammal, 2 ) === donkey );
  674. var Primate = Mammal.extend({
  675. urlRoot: '/primate/'
  676. });
  677. var gorilla = new Primate( { id: 1, species: 'gorilla' } );
  678. equal( Backbone.Relational.store._collections.length, numCollections + 2 );
  679. ok( Backbone.Relational.store.find( Primate, 1 ) === gorilla );
  680. });
  681. test( "Inheritance with `subModelTypes` uses the same collection as the model's super", function() {
  682. var Mammal = Animal.extend({
  683. subModelTypes: {
  684. 'primate': 'Primate',
  685. 'carnivore': 'Carnivore'
  686. }
  687. });
  688. window.Primate = Mammal.extend();
  689. window.Carnivore = Mammal.extend();
  690. var lion = new Carnivore( { id: 1, species: 'lion' } );
  691. var wolf = new Carnivore( { id: 2, species: 'wolf' } );
  692. var numCollections = Backbone.Relational.store._collections.length;
  693. var whale = new Mammal( { id: 3, species: 'whale' } );
  694. equal( Backbone.Relational.store._collections.length, numCollections, "`_collections` should have remained the same" );
  695. ok( Backbone.Relational.store.find( Mammal, 1 ) === lion );
  696. ok( Backbone.Relational.store.find( Mammal, 2 ) === wolf );
  697. ok( Backbone.Relational.store.find( Mammal, 3 ) === whale );
  698. ok( Backbone.Relational.store.find( Carnivore, 1 ) === lion );
  699. ok( Backbone.Relational.store.find( Carnivore, 2 ) === wolf );
  700. ok( Backbone.Relational.store.find( Carnivore, 3 ) !== whale );
  701. var gorilla = new Primate( { id: 4, species: 'gorilla' } );
  702. equal( Backbone.Relational.store._collections.length, numCollections, "`_collections` should have remained the same" );
  703. ok( Backbone.Relational.store.find( Animal, 4 ) !== gorilla );
  704. ok( Backbone.Relational.store.find( Mammal, 4 ) === gorilla );
  705. ok( Backbone.Relational.store.find( Primate, 4 ) === gorilla );
  706. delete window.Primate;
  707. delete window.Carnivore;
  708. });
  709. test( "findOrCreate does not modify attributes hash if parse is used, prior to creating new model", function () {
  710. var model = Backbone.RelationalModel.extend({
  711. parse: function( response ) {
  712. response.id = response.id + 'something';
  713. return response;
  714. }
  715. });
  716. var attributes = {id: 42, foo: "bar"};
  717. var testAttributes = {id: 42, foo: "bar"};
  718. model.findOrCreate( attributes, { parse: true, merge: false, create: false } );
  719. ok( _.isEqual( attributes, testAttributes ), "attributes hash should not be modified" );
  720. });
  721. module( "Backbone.RelationalModel", { setup: initObjects } );
  722. test( "Return values: set returns the Model", function() {
  723. var personId = 'person-10';
  724. var person = new Person({
  725. id: personId,
  726. name: 'Remi',
  727. resource_uri: personId
  728. });
  729. var result = person.set( { 'name': 'Hector' } );
  730. ok( result === person, "Set returns the model" );
  731. });
  732. test( "`clear`", function() {
  733. var person = new Person( { id: 'person-10' } );
  734. ok( person === Person.findOrCreate( 'person-10' ) );
  735. person.clear();
  736. ok( !person.id );
  737. ok( !Person.findOrCreate( 'person-10' ) );
  738. person.set( { id: 'person-10' } );
  739. ok( person === Person.findOrCreate( 'person-10' ) );
  740. });
  741. test( "getRelations", function() {
  742. var relations = person1.getRelations();
  743. equal( relations.length, 6 );
  744. ok( _.every( relations, function( rel ) {
  745. return rel instanceof Backbone.Relation;
  746. })
  747. );
  748. });
  749. test( "getRelation", function() {
  750. var userRel = person1.getRelation( 'user' );
  751. ok( userRel instanceof Backbone.HasOne );
  752. equal( userRel.key, 'user' );
  753. var jobsRel = person1.getRelation( 'jobs' );
  754. ok( jobsRel instanceof Backbone.HasMany );
  755. equal( jobsRel.key, 'jobs' );
  756. ok( person1.getRelation( 'nope' ) == null );
  757. });
  758. test( "getAsync on a HasOne relation", function() {
  759. var errorCount = 0;
  760. var person = new Person({
  761. id: 'person-10',
  762. resource_uri: 'person-10',
  763. user: 'user-10'
  764. });
  765. var idsToFetch = person.getIdsToFetch( 'user' );
  766. deepEqual( idsToFetch, [ 'user-10' ] );
  767. var request = person.getAsync( 'user', { error: function() {
  768. errorCount++;
  769. }
  770. });
  771. ok( _.isObject( request ) && request.always && request.done && request.fail );
  772. equal( window.requests.length, 1, "A single request has been made" );
  773. ok( person.get( 'user' ) instanceof User );
  774. // Triggering the 'error' callback should destroy the model
  775. window.requests[ 0 ].error();
  776. // Trigger the 'success' callback on the `destroy` call to actually fire the 'destroy' event
  777. _.last( window.requests ).success();
  778. ok( !person.get( 'user' ), "User has been destroyed & removed" );
  779. equal( errorCount, 1, "The error callback executed successfully" );
  780. var person2 = new Person({
  781. id: 'person-11',
  782. resource_uri: 'person-11'
  783. });
  784. request = person2.getAsync( 'user' );
  785. equal( window.requests.length, 1, "No request was made" );
  786. });
  787. test( "getAsync on a HasMany relation", function() {
  788. var errorCount = 0;
  789. var zoo = new Zoo({
  790. animals: [ { id: 'monkey-1' }, 'lion-1', 'zebra-1' ]
  791. });
  792. var idsToFetch = zoo.getIdsToFetch( 'animals' );
  793. deepEqual( idsToFetch, [ 'lion-1', 'zebra-1' ] );
  794. //
  795. // Case 1: separate requests for each model
  796. //
  797. window.requests = [];
  798. var request = zoo.getAsync( 'animals', { error: function() { errorCount++; } } );
  799. ok( _.isObject( request ) && request.always && request.done && request.fail );
  800. equal( window.requests.length, 2, "Two requests have been made (a separate one for each animal)" );
  801. equal( zoo.get( 'animals' ).length, 3, "Three animals in the zoo" );
  802. // Triggering the 'error' callback for one request should destroy the model
  803. window.requests[ 0 ].error();
  804. // Trigger the 'success' callback on the `destroy` call to actually fire the 'destroy' event
  805. _.last( window.requests ).success();
  806. equal( zoo.get( 'animals' ).length, 2, "Two animals left in the zoo" );
  807. equal( errorCount, 1, "The error callback executed successfully" );
  808. //
  809. // Case 2: one request per fetch (generated by the collection)
  810. //
  811. // Give 'zoo' a custom url function that builds a url to fetch a set of models from their ids
  812. window.requests = [];
  813. errorCount = 0;
  814. zoo.get( 'animals' ).url = function( models ) {
  815. return '/animal/' + ( models ? 'set/' + _.pluck( models, 'id' ).join(';') + '/' : '' );
  816. };
  817. // Set two new animals to be fetched; both should be fetched in a single request
  818. zoo.set( { animals: [ 'monkey-1', 'lion-2', 'zebra-2' ] } );
  819. equal( zoo.get( 'animals' ).length, 1 );
  820. // `getAsync` creates two placeholder models for the ids present in the relation.
  821. window.requests = [];
  822. request = zoo.getAsync( 'animals', { error: function() { errorCount++; } } );
  823. ok( _.isObject( request ) && request.always && request.done && request.fail );
  824. equal( window.requests.length, 1 );
  825. equal( _.last( window.requests ).url, '/animal/set/lion-2;zebra-2/' );
  826. equal( zoo.get('animals').length, 3, "Three animals in the zoo" );
  827. // Triggering the 'error' callback (some error occured during fetching) should trigger the 'destroy' event
  828. // on both fetched models, but should NOT actually make 'delete' requests to the server!
  829. _.last( window.requests ).error();
  830. equal( window.requests.length, 1, "An error occured when fetching, but no DELETE requests are made to the server while handling local cleanup." );
  831. equal( zoo.get( 'animals' ).length, 1, "Both animals are destroyed" );
  832. equal( errorCount, 2, "The error callback executed successfully for both models" );
  833. // Try to re-fetch; nothing left to get though
  834. window.requests = [];
  835. request = zoo.getAsync( 'animals' );
  836. equal( window.requests.length, 0 );
  837. equal( zoo.get( 'animals' ).length, 1 );
  838. // Re-fetch the existing model
  839. window.requests = [];
  840. request = zoo.getAsync( 'animals', { refresh: true } );
  841. equal( window.requests.length, 1 );
  842. equal( _.last( window.requests ).url, '/animal/set/monkey-1/' );
  843. equal( zoo.get( 'animals' ).length, 1 );
  844. // An error while refreshing an existing model shouldn't affect it
  845. window.requests[ 0 ].error();
  846. equal( zoo.get( 'animals' ).length, 1 );
  847. });
  848. test( "getAsync", 8, function() {
  849. var zoo = Zoo.findOrCreate( { id: 'z-1', animals: [ 'cat-1' ] } );
  850. zoo.on( 'add:animals', function( animal ) {
  851. console.log( 'add:animals=%o', animal );
  852. animal.on( 'change:favoriteFood', function( model, food ) {
  853. console.log( '%s eats %s', animal.get( 'name' ), food.get( 'name' ) );
  854. });
  855. });
  856. zoo.getAsync( 'animals' ).done( function( animals ) {
  857. ok( animals instanceof AnimalCollection );
  858. ok( animals.length === 1 );
  859. var cat = zoo.get( 'animals' ).at( 0 );
  860. equal( cat.get( 'name' ), 'Tiger' );
  861. cat.getAsync( 'favoriteFood' ).done( function( food ) {
  862. equal( food.get( 'name' ), 'Cheese', 'Favorite food is cheese' );
  863. });
  864. });
  865. equal( zoo.get( 'animals' ).length, 1 );
  866. equal( window.requests.length, 1 );
  867. equal( _.last( window.requests ).url, '/animal/cat-1' );
  868. // Declare success
  869. _.last( window.requests ).respond( 200, { id: 'cat-1', name: 'Tiger', favoriteFood: 'f-2' } );
  870. equal( window.requests.length, 2 );
  871. _.last( window.requests ).respond( 200, { id: 'f-2', name: 'Cheese' } );
  872. });
  873. test( "autoFetch a HasMany relation", function() {
  874. var shopOne = new Shop({
  875. id: 'shop-1',
  876. customers: ['customer-1', 'customer-2']
  877. });
  878. equal( requests.length, 2, "Two requests to fetch the users has been made" );
  879. requests.length = 0;
  880. var shopTwo = new Shop({
  881. id: 'shop-2',
  882. customers: ['customer-1', 'customer-3']
  883. });
  884. equal( requests.length, 1, "A request to fetch a user has been made" ); //as customer-1 has already been fetched
  885. });
  886. test( "autoFetch on a HasOne relation (with callbacks)", function() {
  887. var shopThree = new Shop({
  888. id: 'shop-3',
  889. address: 'address-3'
  890. });
  891. equal( requests.length, 1, "A request to fetch the address has been made" );
  892. var res = { successOK: false, errorOK: false };
  893. requests[0].success( res );
  894. equal( res.successOK, true, "The success() callback has been called" );
  895. requests.length = 0;
  896. var shopFour = new Shop({
  897. id: 'shop-4',
  898. address: 'address-4'
  899. });
  900. equal( requests.length, 1, "A request to fetch the address has been made" );
  901. requests[0].error( res );
  902. equal( res.errorOK, true, "The error() callback has been called" );
  903. });
  904. test( "autoFetch false by default", function() {
  905. var agentOne = new Agent({
  906. id: 'agent-1',
  907. customers: ['customer-4', 'customer-5']
  908. });
  909. equal( requests.length, 0, "No requests to fetch the customers has been made as autoFetch was not defined" );
  910. agentOne = new Agent({
  911. id: 'agent-2',
  912. address: 'address-5'
  913. });
  914. equal( requests.length, 0, "No requests to fetch the customers has been made as autoFetch was set to false" );
  915. });
  916. test( "`clone`", function() {
  917. var user = person1.get( 'user' );
  918. // HasOne relations should stay with the original model
  919. var newPerson = person1.clone();
  920. ok( newPerson.get( 'user' ) === null );
  921. ok( person1.get( 'user' ) === user );
  922. });
  923. test( "`save` (with `wait`)", function() {
  924. var node1 = new Node({ id: '1', parent: '3', name: 'First node' } ),
  925. node2 = new Node({ id: '2', name: 'Second node' });
  926. // Set node2's parent to node1 in a request with `wait: true`
  927. var request = node2.save( 'parent', node1, { wait: true } ),
  928. json = JSON.parse( request.data );
  929. ok( _.isObject( json.parent ) );
  930. equal( json.parent.id, '1' );
  931. equal( node2.get( 'parent' ), null );
  932. request.success();
  933. equal( node2.get( 'parent' ), node1 );
  934. // Save a new node as node2's parent, only specified as JSON in the call to save
  935. request = node2.save( 'parent', { id: '3', parent: '2', name: 'Third node' }, { wait: true } );
  936. json = JSON.parse( request.data );
  937. ok( _.isObject( json.parent ) );
  938. equal( json.parent.id, '3' );
  939. equal( node2.get( 'parent' ), node1 );
  940. request.success();
  941. var node3 = node2.get( 'parent' );
  942. ok( node3 instanceof Node );
  943. equal( node3.id, '3' );
  944. // Try to reset node2's parent to node1, but fail the request
  945. request = node2.save( 'parent', node1, { wait: true } );
  946. request.error();
  947. equal( node2.get( 'parent' ), node3 );
  948. // See what happens for different values of `includeInJSON`...
  949. // For `Person.user`, just the `idAttribute` should be serialized to the keyDestination `user_id`
  950. var user1 = person1.get( 'user' );
  951. request = person1.save( 'user', null, { wait: true } );
  952. json = JSON.parse( request.data );
  953. console.log( request, json );
  954. equal( person1.get( 'user' ), user1 );
  955. request.success( json );
  956. equal( person1.get( 'user' ), null );
  957. request = person1.save( 'user', user1, { wait: true } );
  958. json = JSON.parse( request.data );
  959. equal( json.user_id, user1.id );
  960. equal( person1.get( 'user' ), null );
  961. request.success( json );
  962. equal( person1.get( 'user' ), user1 );
  963. // Save a collection with `wait: true`
  964. var zoo = new Zoo( { id: 'z1' } ),
  965. animal1 = new Animal( { id: 'a1', species: 'Goat', name: 'G' } ),
  966. coll = new Backbone.Collection( [ { id: 'a2', species: 'Rabbit', name: 'R' }, animal1 ] );
  967. request = zoo.save( 'animals', coll, { wait: true } );
  968. json = JSON.parse( request.data );
  969. console.log( request, json );
  970. ok( zoo.get( 'animals' ).length === 0 );
  971. request.success( json );
  972. ok( zoo.get( 'animals' ).length === 2 );
  973. console.log( animal1 );
  974. });
  975. test( "`Collection.create` (with `wait`)", function() {
  976. var nodeColl = new NodeList(),
  977. nodesAdded = 0;
  978. nodeColl.on( 'add', function( model, collection, options ) {
  979. nodesAdded++;
  980. });
  981. nodeColl.create({ id: '3', parent: '2', name: 'Third node' }, { wait: true });
  982. ok( nodesAdded === 0 );
  983. requests[ requests.length - 1 ].success();
  984. ok( nodesAdded === 1 );
  985. nodeColl.create({ id: '4', name: 'Third node' }, { wait: true });
  986. ok( nodesAdded === 1 );
  987. requests[ requests.length - 1 ].error();
  988. ok( nodesAdded === 1 );
  989. });
  990. test( "`toJSON`: simple cases", function() {
  991. var node = new Node({ id: '1', parent: '3', name: 'First node' });
  992. new Node({ id: '2', parent: '1', name: 'Second node' });
  993. new Node({ id: '3', parent: '2', name: 'Third node' });
  994. var json = node.toJSON();
  995. ok( json.children.length === 1 );
  996. });
  997. test("'toJSON' should return null for relations that are set to null, even when model is not fetched", function() {
  998. var person = new Person( { user : 'u1' } );
  999. equal( person.toJSON().user_id, 'u1' );
  1000. person.set( 'user', null );
  1001. equal( person.toJSON().user_id, null );
  1002. person = new Person( { user: new User( { id : 'u2' } ) } );
  1003. equal( person.toJSON().user_id, 'u2' );
  1004. person.set( { user: 'unfetched_user_id' } );
  1005. equal( person.toJSON().user_id, 'unfetched_user_id' );
  1006. });
  1007. test( "`toJSON` should include ids for 'unknown' or 'missing' models (if `includeInJSON` is `idAttribute`)", function() {
  1008. // See GH-191
  1009. // `Zoo` shouldn't be affected; `animals.includeInJSON` is not equal to `idAttribute`
  1010. var zoo = new Zoo({ id: 'z1', animals: [ 'a1', 'a2' ] }),
  1011. zooJSON = zoo.toJSON();
  1012. ok( _.isArray( zooJSON.animals ) );
  1013. equal( zooJSON.animals.length, 0, "0 animals in zooJSON; it serializes an array of attributes" );
  1014. var a1 = new Animal( { id: 'a1' } );
  1015. zooJSON = zoo.toJSON();
  1016. equal( zooJSON.animals.length, 1, "1 animals in zooJSON; it serializes an array of attributes" );
  1017. // Agent -> Customer; `idAttribute` on a HasMany
  1018. var agent = new Agent({ id: 'a1', customers: [ 'c1', 'c2' ] } ),
  1019. agentJSON = agent.toJSON();
  1020. ok( _.isArray( agentJSON.customers ) );
  1021. equal( agentJSON.customers.length, 2, "2 customers in agentJSON; it serializes the `idAttribute`" );
  1022. var c1 = new Customer( { id: 'c1' } );
  1023. equal( agent.get( 'customers' ).length, 1, '1 customer in agent' );
  1024. agentJSON = agent.toJSON();
  1025. equal( agentJSON.customers.length, 2, "2 customers in agentJSON; `idAttribute` for 1 missing, other existing" );
  1026. //c1.destroy();
  1027. //agentJSON = agent.toJSON();
  1028. //equal( agentJSON.customers.length, 1, "1 customer in agentJSON; `idAttribute` for 1 missing, other destroyed" );
  1029. agent.set( 'customers', [ 'c1', 'c3' ] );
  1030. var c3 = new Customer( { id: 'c3' } );
  1031. agentJSON = agent.toJSON();
  1032. equal( agentJSON.customers.length, 2, "2 customers in agentJSON; 'c1' already existed, 'c3' created" );
  1033. agent.get( 'customers' ).remove( c1 );
  1034. agentJSON = agent.toJSON();
  1035. equal( agentJSON.customers.length, 1, "1 customer in agentJSON; 'c1' removed, 'c3' still in there" );
  1036. // Person -> User; `idAttribute` on a HasOne
  1037. var person = new Person({ id: 'p1', user: 'u1' } ),
  1038. personJSON = person.toJSON();
  1039. equal( personJSON.user_id, 'u1', "`user_id` gets set in JSON" );
  1040. var u1 = new User( { id: 'u1' } );
  1041. personJSON = person.toJSON();
  1042. ok( u1.get( 'person' ) === person );
  1043. equal( personJSON.user_id, 'u1', "`user_id` gets set in JSON" );
  1044. person.set( 'user', 'u1' );
  1045. personJSON = person.toJSON();
  1046. equal( personJSON.user_id, 'u1', "`user_id` gets set in JSON" );
  1047. u1.destroy();
  1048. personJSON = person.toJSON();
  1049. ok( !u1.get( 'person' ) );
  1050. equal( personJSON.user_id, 'u1', "`user_id` still gets set in JSON" );
  1051. });
  1052. test( "`toJSON` should include ids for unregistered models (if `includeInJSON` is `idAttribute`)", function() {
  1053. // Person -> User; `idAttribute` on a HasOne
  1054. var person = new Person({ id: 'p1', user: 'u1' } ),
  1055. personJSON = person.toJSON();
  1056. equal( personJSON.user_id, 'u1', "`user_id` gets set in JSON even though no user obj exists" );
  1057. var u1 = new User( { id: 'u1' } );
  1058. personJSON = person.toJSON();
  1059. ok( u1.get( 'person' ) === person );
  1060. equal( personJSON.user_id, 'u1', "`user_id` gets set in JSON after matching user obj is created" );
  1061. Backbone.Relational.store.unregister(u1);
  1062. personJSON = person.toJSON();
  1063. equal( personJSON.user_id, 'u1', "`user_id` gets set in JSON after user was unregistered from store" );
  1064. });
  1065. test( "`parse` gets called through `findOrCreate`", function() {
  1066. var parseCalled = 0;
  1067. Zoo.prototype.parse = Animal.prototype.parse = function( resp, options ) {
  1068. parseCalled++;
  1069. return resp;
  1070. };
  1071. var zoo = Zoo.findOrCreate({
  1072. id: '1',
  1073. name: 'San Diego Zoo',
  1074. animals: [ { id: 'a' } ]
  1075. }, { parse: true } );
  1076. var animal = zoo.get( 'animals' ).first();
  1077. ok( animal.get( 'livesIn' ) );
  1078. ok( animal.get( 'livesIn' ) instanceof Zoo );
  1079. ok( animal.get( 'livesIn' ).get( 'animals' ).get( animal ) === animal );
  1080. // `parse` gets called by `findOrCreate` directly when trying to lookup `1`,
  1081. // and the parsed attributes are passed to `build` (called from `findOrCreate`) with `{ parse: false }`,
  1082. // rather than having `parse` called again by the Zoo constructor.
  1083. ok( parseCalled === 1, 'parse called 1 time? ' + parseCalled );
  1084. parseCalled = 0;
  1085. animal = new Animal({ id: 'b' });
  1086. animal.set({
  1087. id: 'b',
  1088. livesIn: {
  1089. id: '2',
  1090. name: 'San Diego Zoo',
  1091. animals: [ 'b' ]
  1092. }
  1093. }, { parse: true } );
  1094. ok( animal.get( 'livesIn' ) );
  1095. ok( animal.get( 'livesIn' ) instanceof Zoo );
  1096. ok( animal.get( 'livesIn' ).get( 'animals' ).get( animal ) === animal );
  1097. ok( parseCalled === 0, 'parse called 0 times? ' + parseCalled );
  1098. // Reset `parse` methods
  1099. Zoo.prototype.parse = Animal.prototype.parse = Backbone.RelationalModel.prototype.parse;
  1100. });
  1101. test( "`Collection#parse` with RelationalModel simple case", function() {
  1102. var Contact = Backbone.RelationalModel.extend({
  1103. parse: function( response ) {
  1104. response.bar = response.foo * 2;
  1105. return response;
  1106. }
  1107. });
  1108. var Contacts = Backbone.Collection.extend({
  1109. model: Contact,
  1110. url: '/contacts',
  1111. parse: function( response ) {
  1112. return response.items;
  1113. }
  1114. });
  1115. var contacts = new Contacts();
  1116. contacts.fetch({
  1117. // fake response for testing
  1118. response: {
  1119. status: 200,
  1120. responseText: { items: [ { foo: 1 }, { foo: 2 } ] }
  1121. }
  1122. });
  1123. equal( contacts.length, 2, 'Collection response was fetched properly' );
  1124. var contact = contacts.first();
  1125. ok( contact , 'Collection has a non-null item' );
  1126. ok( contact instanceof Contact, '... of the type type' );
  1127. equal( contact.get('foo'), 1, '... with correct fetched value' );
  1128. equal( contact.get('bar'), 2, '... with correct parsed value' );
  1129. });
  1130. test( "By default, `parse` should only get called on top-level objects; not for nested models and collections", function() {
  1131. var companyData = {
  1132. 'data': {
  1133. 'id': 'company-1',
  1134. 'contacts': [
  1135. {
  1136. 'id': '1'
  1137. },
  1138. {
  1139. 'id': '2'
  1140. }
  1141. ]
  1142. }
  1143. };
  1144. var Contact = Backbone.RelationalModel.extend();
  1145. var Contacts = Backbone.Collection.extend({
  1146. model: Contact
  1147. });
  1148. var Company = Backbone.RelationalModel.extend({
  1149. urlRoot: '/company/',
  1150. relations: [{
  1151. type: Backbone.HasMany,
  1152. key: 'contacts',
  1153. relatedModel: Contact,
  1154. collectionType: Contacts
  1155. }]
  1156. });
  1157. var parseCalled = 0;
  1158. Company.prototype.parse = Contact.prototype.parse = Contacts.prototype.parse = function( resp, options ) {
  1159. parseCalled++;
  1160. return resp.data || resp;
  1161. };
  1162. var company = new Company( companyData, { parse: true } ),
  1163. contacts = company.get( 'contacts' ),
  1164. contact = contacts.first();
  1165. ok( company.id === 'company-1' );
  1166. ok( contact && contact.id === '1', 'contact exists' );
  1167. ok( parseCalled === 1, 'parse called 1 time? ' + parseCalled );
  1168. // simulate what would happen if company.fetch() was called.
  1169. company.fetch({
  1170. parse: true,
  1171. response: {
  1172. status: 200,
  1173. responseText: _.clone( companyData )
  1174. }
  1175. });
  1176. ok( parseCalled === 2, 'parse called 2 times? ' + parseCalled );
  1177. ok( contacts === company.get( 'contacts' ), 'contacts collection is same instance after fetch' );
  1178. equal( contacts.length, 2, '... with correct length' );
  1179. ok( contact && contact.id === '1', 'contact exists' );
  1180. ok( contact === contacts.first(), '... and same model instances' );
  1181. });
  1182. test( "constructor.findOrCreate", function() {
  1183. var personColl = Backbone.Relational.store.getCollection( person1 ),
  1184. origPersonCollSize = personColl.length;
  1185. // Just find an existing model
  1186. var person = Person.findOrCreate( person1.id );
  1187. ok( person === person1 );
  1188. ok( origPersonCollSize === personColl.length, "Existing person was found (none created)" );
  1189. // Update an existing model
  1190. person = Person.findOrCreate( { id: person1.id, name: 'dude' } );
  1191. equal( person.get( 'name' ), 'dude' );
  1192. equal( person1.get( 'name' ), 'dude' );
  1193. ok( origPersonCollSize === personColl.length, "Existing person was updated (none created)" );
  1194. // Look for a non-existent person; 'options.create' is false
  1195. person = Person.findOrCreate( { id: 5001 }, { create: false } );
  1196. ok( !person );
  1197. ok( origPersonCollSize === personColl.length, "No person was found (none created)" );
  1198. // Create a new model
  1199. person = Person.findOrCreate( { id: 5001 } );
  1200. ok( person instanceof Person );
  1201. ok( origPersonCollSize + 1 === personColl.length, "No person was found (1 created)" );
  1202. // Find when options.merge is false
  1203. person = Person.findOrCreate( { id: person1.id, name: 'phil' }, { merge: false } );
  1204. equal( person.get( 'name' ), 'dude' );
  1205. equal( person1.get( 'name' ), 'dude' );
  1206. });
  1207. test( "constructor.find", function() {
  1208. var personColl = Backbone.Relational.store.getCollection( person1 ),
  1209. origPersonCollSize = personColl.length;
  1210. // Look for a non-existent person
  1211. person = Person.find( { id: 5001 } );
  1212. ok( !person );
  1213. });
  1214. test( "change events in relation can use changedAttributes properly", function() {
  1215. var scope = {};
  1216. Backbone.Relational.store.addModelScope( scope );
  1217. scope.PetAnimal = Backbone.RelationalModel.extend({
  1218. subModelTypes: {
  1219. 'cat': 'Cat',
  1220. 'dog': 'Dog'
  1221. }
  1222. });
  1223. scope.Dog = scope.PetAnimal.extend();
  1224. scope.Cat = scope.PetAnimal.extend();
  1225. scope.PetOwner = Backbone.RelationalModel.extend({
  1226. relations: [{
  1227. type: Backbone.HasMany,
  1228. key: 'pets',
  1229. relatedModel: scope.PetAnimal,
  1230. reverseRelation: {
  1231. key: 'owner'
  1232. }
  1233. }]
  1234. });
  1235. var owner = new scope.PetOwner( { id: 'owner-2354' } );
  1236. var animal = new scope.Dog( { type: 'dog', id: '238902', color: 'blue' } );
  1237. equal( animal.get('color'), 'blue', 'animal starts out blue' );
  1238. var changes = 0, changedAttrs = null;
  1239. animal.on('change', function(model, options) {
  1240. changes++;
  1241. changedAttrs = model.changedAttributes();
  1242. });
  1243. animal.set( { color: 'green' } );
  1244. equal( changes, 1, 'change event gets called after animal.set' );
  1245. equal( changedAttrs.color, 'green', '... with correct properties in "changedAttributes"' );
  1246. owner.set(owner.parse({
  1247. id: 'owner-2354',
  1248. pets: [ { id: '238902', type: 'dog', color: 'red' } ]
  1249. }));
  1250. equal( animal.get('color'), 'red', 'color gets updated properly' );
  1251. equal( changes, 2, 'change event gets called after owner.set' );
  1252. equal( changedAttrs.color, 'red', '... with correct properties in "changedAttributes"' );
  1253. });
  1254. test( 'change events should not fire on new items in Collection#set', function() {
  1255. var modelChangeEvents = 0,
  1256. collectionChangeEvents = 0;
  1257. var Animal2 = Animal.extend({
  1258. initialize: function(options) {
  1259. this.on( 'all', function( name, event ) {
  1260. //console.log( 'Animal2: %o', arguments );
  1261. if ( name.indexOf( 'change' ) === 0 ) {
  1262. modelChangeEvents++;
  1263. }
  1264. });
  1265. }
  1266. });
  1267. var AnimalCollection2 = AnimalCollection.extend({
  1268. model: Animal2,
  1269. initialize: function(options) {
  1270. this.on( 'all', function( name, event ) {
  1271. //console.log( 'AnimalCollection2: %o', arguments );
  1272. if ( name.indexOf('change') === 0 ) {
  1273. collectionChangeEvents++;
  1274. }
  1275. });
  1276. }
  1277. });
  1278. var zoo = new Zoo( { id: 'zoo-1' } );
  1279. var coll = new AnimalCollection2();
  1280. coll.set( [{
  1281. id: 'animal-1',
  1282. livesIn: 'zoo-1'
  1283. }] );
  1284. equal( collectionChangeEvents, 0, 'no change event should be triggered on the collection' );
  1285. modelChangeEvents = collectionChangeEvents = 0;
  1286. coll.at( 0 ).set( 'name', 'Willie' );
  1287. equal( modelChangeEvents, 2, 'change event should be triggered' );
  1288. });
  1289. module( "Backbone.RelationalModel inheritance (`subModelTypes`)", { setup: reset } );
  1290. test( "Object building based on type, when using explicit collections" , function() {
  1291. var scope = {};
  1292. Backbone.Relational.store.addModelScope( scope );
  1293. scope.Mammal = Animal.extend({
  1294. subModelTypes: {
  1295. 'primate': 'Primate',
  1296. 'carnivore': 'Carnivore',
  1297. 'ape': 'Primate' // To check multiple keys for the same submodel; see GH-429
  1298. }
  1299. });
  1300. scope.Primate = scope.Mammal.extend({
  1301. subModelTypes: {
  1302. 'human': 'Human'
  1303. }
  1304. });
  1305. scope.Human = scope.Primate.extend();
  1306. scope.Carnivore = scope.Mammal.extend();
  1307. var MammalCollection = AnimalCollection.extend({
  1308. model: scope.Mammal
  1309. });
  1310. var mammals = new MammalCollection( [

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