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

/spec/backbone.associate.spec.js

https://github.com/watsoncj/backbone.associate
JavaScript | 370 lines | 296 code | 74 blank | 0 comment | 11 complexity | f552131ac553641a89219b8e11c0c0c7 MD5 | raw file
  1. (function () {
  2. var Backbone = this.Backbone,
  3. _ = this._;
  4. if (typeof window == 'undefined') {
  5. Backbone = require('backbone');
  6. _ = require('underscore');
  7. require('./helper');
  8. require('../src/backbone.associate');
  9. }
  10. var M = Backbone.Model.extend({}),
  11. N = Backbone.Model.extend({}),
  12. O = Backbone.Model.extend({}),
  13. C = Backbone.Collection.extend({
  14. model: N
  15. });
  16. describe('association', function () {
  17. beforeEach(function () {
  18. this.modelA = M;
  19. this.modelB = N;
  20. this.modelC = O;
  21. this.collectionB = C;
  22. this.associations = {
  23. one: { type: this.modelB },
  24. manies: { type: this.collectionB }
  25. };
  26. Backbone.associate(this.modelA, this.associations);
  27. this.parent = new this.modelA();
  28. });
  29. afterEach(function () {
  30. return Backbone.dissociate(this.modelA);
  31. });
  32. describe('A model with associations', function () {
  33. it('should have accessors for each association', function () {
  34. _.each(this.associations, function (association, key) {
  35. expect(_.isFunction(this.parent[key])).toBeTruthy();
  36. }, this);
  37. });
  38. it('should extend dependent methods', function () {
  39. var methods = ['initialize', 'set', 'toJSON'];
  40. _.each(methods, function (meth, key) {
  41. expect(this.parent[meth]).not.toEqual(Backbone.Model.prototype[meth]);
  42. }, this);
  43. });
  44. });
  45. describe('When model is initialized', function () {
  46. it('should build empty associates if attributes were not provided', function () {
  47. _.each(this.associations, function (association, key) {
  48. expect(this.parent.get(key) instanceof association.type).toBeTruthy();
  49. }, this);
  50. });
  51. it('should not overwrite existing associates', function () {
  52. var expected = { one: { foo: 'bar' } },
  53. model = new this.modelA(expected);
  54. _.each(_.pick(this.associations, _.keys(expected)), function (association, key) {
  55. expect(model[key]().attributes).toEqual(expected[key]);
  56. });
  57. });
  58. it('should use defaults to populate associates', function () {
  59. var model,
  60. expected = { one: { foo: 'bar' } };
  61. this.modelA.prototype.defaults = expected;
  62. model = new this.modelA({});
  63. _.each(_.pick(this.associations, _.keys(expected)), function (association, key) {
  64. expect(model[key]().attributes).toEqual(expected[key]);
  65. });
  66. });
  67. it('should preserves references to associates', function () {
  68. var modelB = new this.modelB(),
  69. modelA = new this.modelA({ one: modelB });
  70. expect(modelA.one()).toBe(modelB);
  71. });
  72. it('should recurse through associated objects', function () {
  73. var fooVal = 'bar',
  74. fixture = {
  75. one: { two: { foo: fooVal } }
  76. };
  77. Backbone.associate(this.modelB, {
  78. two: { type: this.modelC }
  79. });
  80. this.parent = new this.modelA(fixture);
  81. expect(this.parent.one().two().get('foo')).toEqual(fooVal);
  82. return Backbone.dissociate(this.modelB);
  83. });
  84. });
  85. describe('When a model property is set by key and value', function () {
  86. it('should accept a key and its value as arguments', function () {
  87. this.parent.set('a', 'foo');
  88. expect(this.parent.attributes.a).toEqual('foo');
  89. });
  90. it('should set one-to-one relations', function () {
  91. this.parent.set('one', { foo: 'bar' });
  92. expect(this.parent.one().attributes.foo).toEqual('bar');
  93. });
  94. it('should set one-to-many relations', function () {
  95. this.parent.set('manies', [{ id: 'foo' }]);
  96. expect(this.parent.manies().get('foo')).toBeDefined();
  97. });
  98. });
  99. describe('When properties on a model are set by hash', function () {
  100. beforeEach(function () {
  101. this.fixture = {
  102. a: 'foo',
  103. b: 42,
  104. c: { hello: 'world' },
  105. one: { foo: 'bar' },
  106. manies: [{ id: 'foo' }, { id: 'baz' }]
  107. };
  108. });
  109. it('should not affect unrelated keys', function () {
  110. var unrelated = _.omit(this.fixture, _.keys(this.associations));
  111. this.parent.set(this.fixture);
  112. _.each(unrelated, function (expected, key) {
  113. expect(this.parent.attributes[key]).toEqual(expected);
  114. }, this);
  115. });
  116. it('should fill in associations when no attributes are provided', function () {
  117. this.parent.set(this.fixture);
  118. _.each(this.associations, function (association, key) {
  119. expect(this.parent.attributes[key] instanceof association.type).toBeTruthy();
  120. }, this);
  121. });
  122. it('should not change original arguments', function () {
  123. var expected = _.clone(this.fixture);
  124. this.parent.set(expected);
  125. expect(_.isEqual(this.fixture, expected)).toBeTruthy();
  126. });
  127. describe('when relation keys are omitted', function () {
  128. it('should not clobber existing model relations', function () {
  129. var expected, expectedAttr;
  130. this.parent.set(this.fixture);
  131. expected = this.parent.one();
  132. expectedAttr = this.parent.one().get('foo');
  133. this.parent.set({ a: 'foobar' });
  134. expect(this.parent.one()).toEqual(expected);
  135. expect(this.parent.one().get('foo')).toEqual(expectedAttr);
  136. });
  137. it('should not clobber existing collection relations', function () {
  138. var expected, expectedLength;
  139. this.parent.set(this.fixture);
  140. expected = this.parent.manies();
  141. expectedLength = this.parent.manies().length;
  142. this.parent.set({ a: 'foobar' });
  143. expect(this.parent.manies()).toEqual(expected);
  144. expect(this.parent.manies().length).toEqual(expectedLength);
  145. });
  146. });
  147. it('should parse model relations', function () {
  148. this.parent.set(this.fixture);
  149. _.each(this.fixture, function (expected, key) {
  150. if (_.has(this.parent._associations, key) && this.parent[key]() instanceof Backbone.Model) {
  151. expect(this.parent[key]() instanceof this.associations[key].type).toBeTruthy();
  152. expect(this.parent[key]().get('foo')).toEqual(this.fixture[key].foo);
  153. }
  154. }, this);
  155. });
  156. it('should parse collection relations', function () {
  157. this.parent.set(this.fixture);
  158. _.each(this.fixture, function (expected, key) {
  159. if (_.has(this.parent._associations, key) && this.parent[key]() instanceof Backbone.Collection) {
  160. expect(this.parent[key]() instanceof this.associations[key].type).toBeTruthy();
  161. expect(this.parent[key]().first().get('foo')).toEqual(this.fixture[key][0].foo);
  162. }
  163. }, this);
  164. });
  165. it('should preserve passed model references', function () {
  166. var model = new this.modelB();
  167. this.parent.set(_.extend(this.fixture, { one: model }));
  168. expect(this.parent.one() === model).toBeTruthy();
  169. });
  170. it('should preserve passed collection references', function () {
  171. var collection = new this.collectionB();
  172. this.parent.set(_.extend(this.fixture, {
  173. manies: collection
  174. }));
  175. expect(this.parent.manies() === collection).toBeTruthy();
  176. });
  177. it('should preserve references to associated models', function () {
  178. var expected = this.parent.one();
  179. this.parent.set({ one: { 'foo': 'bar' } });
  180. expect(this.parent.one()).toEqual(expected);
  181. });
  182. it('should preserve references to associated collections', function () {
  183. var expected = this.parent.manies();
  184. this.parent.set([{ id: 'mixcoatl', foo: 'bar' }]);
  185. expect(this.parent.manies()).toEqual(expected);
  186. });
  187. it('should recurses for nested models', function () {
  188. var model,
  189. klass = Backbone.Model.extend({}),
  190. fooVal = 'bar',
  191. fixture = {
  192. one: {
  193. two: { foo: fooVal }
  194. }
  195. };
  196. Backbone.associate(this.modelB, { two: { type: klass } });
  197. model = new this.modelA();
  198. model.set(fixture);
  199. expect(model.one().two().get('foo')).toEqual(fooVal);
  200. return Backbone.dissociate(this.modelB);
  201. });
  202. it('should recurse for nested collections', function () {
  203. var model,
  204. fooVal = 'bar',
  205. klass = Backbone.Collection.extend({ model: this.modelC }),
  206. fixture = {
  207. one: {
  208. two: [{ id: 'three', foo: fooVal }]
  209. }
  210. };
  211. Backbone.associate(this.modelB, { two: { type: klass } });
  212. model = new this.modelA();
  213. model.set(fixture);
  214. expect(model.one().two().get('three').get('foo')).toEqual(fooVal);
  215. Backbone.dissociate(this.modelB);
  216. });
  217. });
  218. describe('When serializing model associations', function () {
  219. it('should convert models to attribute hash', function () {
  220. var result,
  221. expected = { foo: 'bar' };
  222. this.parent.one().set(expected);
  223. result = this.parent.toJSON();
  224. expect(result.one).toEqual(expected);
  225. });
  226. it('should convert collections to model arrays', function () {
  227. var result,
  228. expected = [{ id: 'foo' }];
  229. this.parent.manies().reset(expected);
  230. result = this.parent.toJSON();
  231. expect(result.manies).toEqual(expected);
  232. });
  233. });
  234. describe('When an association is inherited', function () {
  235. it('should be defined for the child', function () {
  236. var modelAChild = this.modelA.extend({}),
  237. child = new modelAChild();
  238. expect(child.one() instanceof this.modelB).toBeTruthy();
  239. expect(child.manies() instanceof this.collectionB).toBeTruthy();
  240. });
  241. });
  242. describe('when relation url is provided', function () {
  243. var model, klass;
  244. beforeEach(function () {
  245. klass = Backbone.Collection.extend({
  246. model: this.modelC
  247. });
  248. Backbone.associate(this.modelB, {
  249. two: {
  250. type: klass,
  251. url: '/two' // TODO: functions should work, too!
  252. },
  253. three: {
  254. type: klass,
  255. url: function () {
  256. return '/three';
  257. }
  258. }
  259. });
  260. model = new this.modelB({}, { parse: true });
  261. model.url = '/modelbs/42';
  262. });
  263. it('should build relation url from string', function () {
  264. expect(model.two().url()).toEqual('/modelbs/42/two');
  265. });
  266. it('should evaluate url from function', function () {
  267. expect(model.three().url()).toEqual('/modelbs/42/three');
  268. });
  269. describe('and child is added to parent collection', function () {
  270. beforeEach(function () {
  271. model.two().add({ id: 31 });
  272. });
  273. it('should have correct url', function () {
  274. expect(model.two().last().url()).toEqual('/modelbs/42/two/31');
  275. });
  276. });
  277. });
  278. describe('when relation url is not provided', function () {
  279. var model, klass;
  280. beforeEach(function () {
  281. klass = Backbone.Collection.extend({
  282. model: this.modelC,
  283. url: '/klasses'
  284. });
  285. Backbone.associate(this.modelB, {
  286. two: { type: klass }
  287. });
  288. model = new this.modelB({}, { parse: true });
  289. model.url = '/modelbs/42';
  290. });
  291. it('should revert to related object\'s url', function () {
  292. expect(_.result(model.two(), 'url')).toEqual('/klasses');
  293. });
  294. });
  295. });
  296. })();