brainstem-js 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. data/.travis.yml +3 -6
  2. data/Gemfile.lock +1 -1
  3. data/README.md +3 -1
  4. data/Rakefile +2 -0
  5. data/lib/brainstem/js/version.rb +1 -1
  6. data/spec/brainstem-collection-spec.coffee +25 -0
  7. data/spec/{brianstem-expectation-spec.coffee → brainstem-expectation-spec.coffee} +132 -17
  8. data/spec/brainstem-model-spec.coffee +67 -27
  9. data/spec/brainstem-sync-spec.coffee +29 -6
  10. data/spec/brainstem-utils-spec.coffee +11 -1
  11. data/spec/helpers/builders.coffee +2 -2
  12. data/spec/helpers/models/post.coffee +1 -1
  13. data/spec/helpers/models/project.coffee +1 -1
  14. data/spec/helpers/models/task.coffee +2 -2
  15. data/spec/helpers/models/time-entry.coffee +1 -1
  16. data/spec/helpers/models/user.coffee +1 -1
  17. data/spec/helpers/spec-helper.coffee +21 -0
  18. data/spec/loaders/abstract-loader-shared-behavior.coffee +604 -0
  19. data/spec/loaders/abstract-loader-spec.coffee +3 -0
  20. data/spec/loaders/collection-loader-spec.coffee +146 -0
  21. data/spec/loaders/model-loader-spec.coffee +99 -0
  22. data/spec/storage-manager-spec.coffee +242 -56
  23. data/vendor/assets/javascripts/brainstem/brainstem-collection.coffee +16 -6
  24. data/vendor/assets/javascripts/brainstem/brainstem-expectation.coffee +70 -20
  25. data/vendor/assets/javascripts/brainstem/brainstem-model.coffee +13 -13
  26. data/vendor/assets/javascripts/brainstem/brainstem-sync.coffee +8 -3
  27. data/vendor/assets/javascripts/brainstem/loaders/abstract-loader.coffee +289 -0
  28. data/vendor/assets/javascripts/brainstem/loaders/collection-loader.coffee +68 -0
  29. data/vendor/assets/javascripts/brainstem/loaders/model-loader.coffee +35 -0
  30. data/vendor/assets/javascripts/brainstem/loading-mixin.coffee +1 -7
  31. data/vendor/assets/javascripts/brainstem/storage-manager.coffee +79 -196
  32. data/vendor/assets/javascripts/brainstem/utils.coffee +18 -4
  33. metadata +17 -6
@@ -1,5 +1,5 @@
1
1
  describe 'Brainstem Utils', ->
2
- describe "matches", ->
2
+ describe ".matches", ->
3
3
  it "should recursively compare objects and arrays", ->
4
4
  expect(Brainstem.Utils.matches(2, 2)).toBe true
5
5
  expect(Brainstem.Utils.matches([2], [2])).toBe true, '[2], [2]'
@@ -10,3 +10,13 @@ describe 'Brainstem Utils', ->
10
10
  expect(Brainstem.Utils.matches([2, { hi: "there" }], [2, { hi: "there" }])).toBe true, '[2, { hi: "there" }], [2, { hi: "there" }]'
11
11
  expect(Brainstem.Utils.matches([2, { hi: ["there", 3] }], [2, { hi: ["there", 2] }])).toBe false
12
12
  expect(Brainstem.Utils.matches([2, { hi: ["there", 2] }], [2, { hi: ["there", 2] }])).toBe true, '[2, { hi: ["there", 2] }], [2, { hi: ["there", 2] }]'
13
+
14
+ describe ".wrapObjects", ->
15
+ it "wraps elements in an array with objects unless they are already objects", ->
16
+ expect(Brainstem.Utils.wrapObjects([])).toEqual []
17
+ expect(Brainstem.Utils.wrapObjects(['a', 'b'])).toEqual [{a: []}, {b: []}]
18
+ expect(Brainstem.Utils.wrapObjects(['a', 'b': []])).toEqual [{a: []}, {b: []}]
19
+ expect(Brainstem.Utils.wrapObjects(['a', 'b': 'c'])).toEqual [{a: []}, {b: [{c: []}]}]
20
+ expect(Brainstem.Utils.wrapObjects([{'a':[], b: 'c', d: 'e' }])).toEqual [{a: []}, {b: [{c: []}]}, {d: [{e: []}]}]
21
+ expect(Brainstem.Utils.wrapObjects(['a', { b: 'c', d: 'e' }])).toEqual [{a: []}, {b: [{c: []}]}, {d: [{e: []}]}]
22
+ expect(Brainstem.Utils.wrapObjects([{'a': []}, {'b': ['c', d: []]}])).toEqual [{a: []}, {b: [{c: []}, {d: []}]}]
@@ -25,8 +25,8 @@ spec.defineBuilders = ->
25
25
  window.base.data.storage(storageName).add obj if window.base.data.collectionExists(storageName)
26
26
  obj
27
27
 
28
- eval("window.#{"build_#{name.underscore()}".camelize(true)} = builder")
29
- eval("window.#{"build_and_cache_#{name.underscore()}".camelize(true)} = creator")
28
+ window["build_#{name.underscore()}".camelize(true)] = builder
29
+ window["build_and_cache_#{name.underscore()}".camelize(true)] = creator
30
30
 
31
31
  isIdAttr = (attrName) ->
32
32
  attrName == 'id' || attrName.match(/_id$/) || (attrName.match(/_ids$/))
@@ -1,7 +1,7 @@
1
1
  class App.Models.Post extends Brainstem.Model
2
2
  brainstemKey: "posts"
3
3
  paramRoot: 'post'
4
- url: '/api/posts'
4
+ urlRoot: '/api/posts'
5
5
 
6
6
  @associations:
7
7
  replies: ["posts"]
@@ -1,7 +1,7 @@
1
1
  class App.Models.Project extends Brainstem.Model
2
2
  brainstemKey: "projects"
3
3
  paramRoot: 'project'
4
- url: '/api/projects'
4
+ urlRoot: '/api/projects'
5
5
 
6
6
  @associations:
7
7
  tasks: ["tasks"]
@@ -1,7 +1,7 @@
1
1
  class App.Models.Task extends Brainstem.Model
2
2
  brainstemKey: "tasks"
3
3
  paramRoot: 'task'
4
- url: '/api/tasks.json'
4
+ urlRoot: '/api/tasks'
5
5
 
6
6
  @associations:
7
7
  project: "projects"
@@ -11,4 +11,4 @@ class App.Models.Task extends Brainstem.Model
11
11
 
12
12
  class App.Collections.Tasks extends Brainstem.Collection
13
13
  model: App.Models.Task
14
- url: '/api/tasks.json'
14
+ url: '/api/tasks'
@@ -1,7 +1,7 @@
1
1
  class App.Models.TimeEntry extends Brainstem.Model
2
2
  brainstemKey: "time_entries"
3
3
  paramRoot: 'time_entry'
4
- url: '/api/time_entries'
4
+ urlRoot: '/api/time_entries'
5
5
 
6
6
  @associations:
7
7
  project: "projects"
@@ -1,7 +1,7 @@
1
1
  class App.Models.User extends Brainstem.Model
2
2
  brainstemKey: "users"
3
3
  paramRoot: 'user'
4
- url: "/api/users"
4
+ urlRoot: "/api/users"
5
5
 
6
6
  class App.Collections.Users extends Brainstem.Collection
7
7
  model: App.Models.User
@@ -77,3 +77,24 @@ window.clearLiveEventBindings = ->
77
77
 
78
78
  window.context = describe
79
79
  window.xcontext = xdescribe
80
+
81
+ # Shared Behaviors
82
+ window.SharedBehaviors ?= {};
83
+
84
+ window.registerSharedBehavior = (behaviorName, funct) ->
85
+ if not behaviorName
86
+ throw "Invalid shared behavior name"
87
+
88
+ if typeof funct != 'function'
89
+ throw "Invalid shared behavior, it must be a function"
90
+
91
+ window.SharedBehaviors[behaviorName] = funct
92
+
93
+ window.itShouldBehaveLike = (behaviorName, context) ->
94
+ behavior = window.SharedBehaviors[behaviorName];
95
+ context ?= {}
96
+
97
+ if not behavior || typeof behavior != 'function'
98
+ throw "Shared behavior #{behaviorName} not found."
99
+ else
100
+ jasmine.getEnv().describe "#{behaviorName} (shared behavior)", -> behavior.call(this, context)
@@ -0,0 +1,604 @@
1
+ registerSharedBehavior "AbstractLoaderSharedBehavior", (sharedContext) ->
2
+ loader = loaderClass = null
3
+
4
+ beforeEach ->
5
+ loaderClass = sharedContext.loaderClass
6
+
7
+ fakeNestedInclude = ['parent', { project: ['participants'] }, { assignees: ['something_else'] }]
8
+
9
+ defaultLoadOptions = ->
10
+ name: 'tasks'
11
+
12
+ createLoader = (opts = {}) ->
13
+ storageManager = new Brainstem.StorageManager()
14
+ storageManager.addCollection('tasks', App.Collections.Tasks)
15
+
16
+ defaults =
17
+ storageManager: storageManager
18
+
19
+ loader = new loaderClass(_.extend {}, defaults, opts)
20
+ loader._getCollectionName = -> 'tasks'
21
+ loader._createObjects = ->
22
+ @internalObject = bar: 'foo'
23
+ @externalObject = foo: 'bar'
24
+
25
+ loader._getModelsForAssociation = -> [{ id: 5 }, { id: 2 }, { id: 1 }, { id: 4 }, { id: 1 }, [{ id: 6 }], { id: null }]
26
+ loader._getModel = -> App.Collections.Tasks::model
27
+ loader._updateStorageManagerFromResponse = jasmine.createSpy()
28
+ loader._updateObjects = (obj, data, silent) ->
29
+ obj.setLoaded true unless silent
30
+
31
+ spyOn(loader, '_updateObjects')
32
+
33
+ loader
34
+
35
+ describe '#constructor', ->
36
+ it 'saves off a reference to the passed in StorageManager', ->
37
+ storageManager = new Brainstem.StorageManager()
38
+ loader = createLoader(storageManager: storageManager)
39
+ expect(loader.storageManager).toEqual storageManager
40
+
41
+ it 'creates a deferred object and turns the loader into a promise', ->
42
+ spy = jasmine.createSpy('promise spy')
43
+
44
+ loader = createLoader()
45
+ expect(loader._deferred).not.toBeUndefined()
46
+ loader.then(spy)
47
+
48
+ loader._deferred.resolve()
49
+ expect(spy).toHaveBeenCalled()
50
+
51
+ describe 'options.loadOptions', ->
52
+ it 'calls #setup with loadOptions if loadOptions were passed in', ->
53
+ spy = spyOn(loaderClass.prototype, 'setup')
54
+
55
+ loader = createLoader(loadOptions: defaultLoadOptions())
56
+ expect(spy).toHaveBeenCalledWith defaultLoadOptions()
57
+
58
+ it 'does not call #setup if loadOptions were not passed in', ->
59
+ spy = spyOn(loaderClass.prototype, 'setup')
60
+
61
+ loader = createLoader()
62
+ expect(spy).not.toHaveBeenCalled()
63
+
64
+ describe '#setup', ->
65
+ it 'calls #_parseLoadOptions with the loadOptions', ->
66
+ loader = createLoader()
67
+ spyOn(loader, '_parseLoadOptions')
68
+
69
+ opts = foo: 'bar'
70
+
71
+ loader.setup(opts)
72
+ expect(loader._parseLoadOptions).toHaveBeenCalledWith(opts)
73
+
74
+ it 'calls _createObjects', ->
75
+ loader = createLoader()
76
+ spyOn(loader, '_createObjects')
77
+
78
+ loader.setup()
79
+ expect(loader._createObjects).toHaveBeenCalled()
80
+
81
+ it 'returns the externalObject', ->
82
+ loader = createLoader()
83
+ spyOn(loader, '_parseLoadOptions')
84
+
85
+ externalObject = loader.setup()
86
+ expect(externalObject).toEqual(loader.externalObject)
87
+
88
+ describe '#load', ->
89
+ describe 'sanity checking loadOptions', ->
90
+ funct = null
91
+
92
+ beforeEach ->
93
+ loader = createLoader()
94
+ spyOn(loader, '_checkCacheForData')
95
+ spyOn(loader, '_loadFromServer')
96
+ funct = -> loader.load()
97
+
98
+ it 'throws if there are no loadOptions', ->
99
+ expect(funct).toThrow()
100
+
101
+ it 'does not throw if there are loadOptions', ->
102
+ loader.loadOptions = {}
103
+ expect(funct).not.toThrow()
104
+
105
+ describe 'checking the cache', ->
106
+ beforeEach ->
107
+ loader = createLoader()
108
+ spyOn(loader, '_checkCacheForData')
109
+ spyOn(loader, '_loadFromServer')
110
+
111
+ context 'loadOptions.cache is true', ->
112
+ it 'calls #_checkCacheForData', ->
113
+ loader.setup()
114
+ expect(loader.loadOptions.cache).toEqual(true)
115
+
116
+ loader.load()
117
+ expect(loader._checkCacheForData).toHaveBeenCalled()
118
+
119
+ context '#_checkCacheForData returns data', ->
120
+ it 'returns the data', ->
121
+ fakeData = ['some', 'stuff']
122
+ loader._checkCacheForData.andReturn(fakeData)
123
+
124
+ loader.setup()
125
+ expect(loader.load()).toEqual(fakeData)
126
+
127
+ context '#_checkCacheForData does not return data', ->
128
+ it 'calls #_loadFromServer', ->
129
+ loader.setup()
130
+ loader.load()
131
+ expect(loader._loadFromServer).toHaveBeenCalled()
132
+
133
+ context 'loadOptions.cache is false', ->
134
+ it 'does not call #_checkCacheForData', ->
135
+ loader.setup(cache: false)
136
+
137
+ loader.load()
138
+ expect(loader._checkCacheForData).not.toHaveBeenCalled()
139
+
140
+ it 'calls #_loadFromServer', ->
141
+ loader.setup()
142
+ loader.load()
143
+ expect(loader._loadFromServer).toHaveBeenCalled()
144
+
145
+ describe '#_parseLoadOptions', ->
146
+ opts = null
147
+
148
+ beforeEach ->
149
+ loader = createLoader()
150
+ opts = defaultLoadOptions()
151
+
152
+ it 'saves off a reference of the loadOptions as originalOptions', ->
153
+ loader._parseLoadOptions(defaultLoadOptions())
154
+ expect(loader.originalOptions).toEqual(defaultLoadOptions())
155
+
156
+ it 'parses the include options', ->
157
+ opts.include = ['foo': ['bar'], 'toad', 'stool']
158
+ loadOptions = loader._parseLoadOptions(opts)
159
+
160
+ expect(loadOptions.include).toEqual [
161
+ { foo: [{ bar: [ ]}] }
162
+ { toad: [] }
163
+ { stool: [] }
164
+ ]
165
+
166
+ describe 'only parsing', ->
167
+ context 'only is present', ->
168
+ it 'sets only as an array of strings from the original only', ->
169
+ opts.only = [1, 2, 3, 4]
170
+ loadOptions = loader._parseLoadOptions(opts)
171
+
172
+ expect(loadOptions.only).toEqual ['1', '2', '3', '4']
173
+
174
+ context 'only is not present', ->
175
+ it 'sets only as null', ->
176
+ loadOptions = loader._parseLoadOptions(opts)
177
+ expect(loadOptions.only).toEqual(null)
178
+
179
+ it 'defaults filters to an empty object', ->
180
+ loadOptions = loader._parseLoadOptions(opts)
181
+ expect(loadOptions.filters).toEqual {}
182
+
183
+ # make sure it leaves them alone if they are present
184
+ opts.filters = filters = foo: 'bar'
185
+ loadOptions = loader._parseLoadOptions(opts)
186
+ expect(loadOptions.filters).toEqual filters
187
+
188
+ it 'pulls of the top layer of includes and sets them as thisLayerInclude', ->
189
+ opts.include = ['foo': ['bar'], 'toad': ['stool'], 'mushroom']
190
+ loadOptions = loader._parseLoadOptions(opts)
191
+ expect(loadOptions.thisLayerInclude).toEqual ['foo', 'toad', 'mushroom']
192
+
193
+ it 'defaults cache to true', ->
194
+ loadOptions = loader._parseLoadOptions(opts)
195
+ expect(loadOptions.cache).toEqual true
196
+
197
+ # make sure it leaves cache alone if it is present
198
+ opts.cache = false
199
+ loadOptions = loader._parseLoadOptions(opts)
200
+ expect(loadOptions.cache).toEqual false
201
+
202
+ it 'sets cache to false if search is present', ->
203
+ opts = _.extend opts, cache: true, search: 'term'
204
+
205
+ loadOptions = loader._parseLoadOptions(opts)
206
+ expect(loadOptions.cache).toEqual false
207
+
208
+ it 'builds a cache key', ->
209
+ # order, filterKeys, page, perPage, limit, offset
210
+ myOpts =
211
+ order: 'myOrder'
212
+ filters:
213
+ key1: 'value1'
214
+ key2: 'value2'
215
+ page: 1
216
+ perPage: 200
217
+ limit: 50
218
+ offset: 0
219
+
220
+ opts = _.extend(opts, myOpts)
221
+ loadOptions = loader._parseLoadOptions(opts)
222
+ expect(loadOptions.cacheKey).toEqual 'myOrder|key1:value1,key2:value2|1|200|50|0'
223
+
224
+ it 'sets the cachedCollection on the loader from the storageManager', ->
225
+ loader._parseLoadOptions(opts)
226
+ expect(loader.cachedCollection).toEqual loader.storageManager.storage(loader.loadOptions.name)
227
+
228
+ describe '#_checkCacheForData', ->
229
+ opts = null
230
+ taskOne = taskTwo = null
231
+
232
+ beforeEach ->
233
+ loader = createLoader()
234
+ opts = defaultLoadOptions()
235
+ spyOn(loader, '_onLoadSuccess')
236
+
237
+ taskOne = buildTask(id: 2)
238
+ taskTwo = buildTask(id: 3)
239
+
240
+ notFound = (loader, opts) ->
241
+ loader.setup(opts)
242
+ ret = loader._checkCacheForData()
243
+
244
+ expect(ret).toEqual false
245
+ expect(loader._onLoadSuccess).not.toHaveBeenCalled()
246
+
247
+ context 'only query', ->
248
+ beforeEach ->
249
+ opts.only = ['2', '3']
250
+
251
+ context 'the requested IDs have all been loaded', ->
252
+ beforeEach ->
253
+ loader.storageManager.storage('tasks').add([taskOne, taskTwo])
254
+
255
+ it 'calls #_onLoadSuccess with the models from the cache', ->
256
+ loader.setup(opts)
257
+ loader._checkCacheForData()
258
+ expect(loader._onLoadSuccess).toHaveBeenCalledWith([taskOne, taskTwo])
259
+
260
+ context 'the requested IDs have not all been loaded', ->
261
+ beforeEach ->
262
+ loader.storageManager.storage('tasks').add([taskOne])
263
+
264
+ it 'returns false and does not call #_onLoadSuccess', ->
265
+ loader.setup(opts)
266
+ notFound(loader, opts)
267
+
268
+ context 'not an only query', ->
269
+ context 'there exists a cache with this cacheKey', ->
270
+ beforeEach ->
271
+ loader.storageManager.storage('tasks').add taskOne
272
+ loader.storageManager.getCollectionDetails('tasks').cache['updated_at:desc|||||'] = [key: "tasks", id: taskOne.id]
273
+
274
+ context 'all of the cached models have their associations loaded', ->
275
+ beforeEach ->
276
+ taskOne.set('project_id', buildAndCacheProject().id)
277
+
278
+ it 'calls #_onLoadSuccess with the models from the cache', ->
279
+ opts.include = ['project']
280
+ loader.setup(opts)
281
+ loader._checkCacheForData()
282
+ expect(loader._onLoadSuccess).toHaveBeenCalledWith([taskOne])
283
+
284
+ context 'all of the cached models do not have their associations loaded', ->
285
+ it 'returns false and does not call #_onLoadSuccess', ->
286
+ opts.include = ['project']
287
+ loader.setup(opts)
288
+ notFound(loader, opts)
289
+
290
+ context 'there is no cache with this cacheKey', ->
291
+ it 'does not call #_onLoadSuccess and returns false', ->
292
+ loader.setup(opts)
293
+ notFound(loader, opts)
294
+
295
+ describe '#_loadFromServer', ->
296
+ opts = syncOpts = null
297
+
298
+ beforeEach ->
299
+ loader = createLoader()
300
+ opts = defaultLoadOptions()
301
+ syncOpts = data: 'foo'
302
+
303
+ spyOn(Backbone, 'sync').andReturn $.ajax()
304
+ spyOn(loader, '_buildSyncOptions').andReturn(syncOpts)
305
+
306
+ it 'calls Backbone.sync with the read, the, internalObject, and #_buildSyncOptions', ->
307
+ loader.setup(opts)
308
+ loader._loadFromServer()
309
+ expect(Backbone.sync).toHaveBeenCalledWith 'read', loader.internalObject, syncOpts
310
+
311
+ it 'puts the jqXhr on the returnValues if present', ->
312
+ opts.returnValues = returnValues = {}
313
+ loader.setup(opts)
314
+
315
+ loader._loadFromServer()
316
+ expect(returnValues.jqXhr.success).not.toBeUndefined()
317
+
318
+ it 'returns the externalObject', ->
319
+ loader.setup(opts)
320
+ ret = loader._loadFromServer()
321
+ expect(ret).toEqual loader.externalObject
322
+
323
+ describe '#_shouldUseOnly', ->
324
+ it 'returns true if internalObject is an instance of a Backbone.Collection', ->
325
+ loader = createLoader()
326
+ loader.internalObject = new Backbone.Collection()
327
+ expect(loader._shouldUseOnly()).toEqual true
328
+
329
+ it 'returns false if internalObject is not an instance of a Backbone.Collection', ->
330
+ loader = createLoader()
331
+ loader.internalObject = new Backbone.Model()
332
+ expect(loader._shouldUseOnly()).toEqual false
333
+
334
+ describe '#_buildSyncOptions', ->
335
+ syncOptions = opts = null
336
+
337
+ beforeEach ->
338
+ loader = createLoader()
339
+ opts = defaultLoadOptions()
340
+
341
+ getSyncOptions = (loader, opts) ->
342
+ loader.setup(opts)
343
+ loader._buildSyncOptions()
344
+
345
+ it 'sets parse to true', ->
346
+ expect(getSyncOptions(loader, opts).parse).toEqual(true)
347
+
348
+ it 'sets error as loadOptions.error', ->
349
+ opts.error = spy = jasmine.createSpy()
350
+ expect(getSyncOptions(loader, opts).error).toEqual spy
351
+
352
+ it 'sets success as #_onServerLoadSuccess', ->
353
+ expect(getSyncOptions(loader, opts).success).toEqual(loader._onServerLoadSuccess)
354
+
355
+ it 'sets data.include to be the layer of includes that this loader is loading', ->
356
+ opts.include = [
357
+ task: [ workspace: ['participants'] ]
358
+ 'time_entries'
359
+ ]
360
+
361
+ expect(getSyncOptions(loader, opts).data.include).toEqual('task,time_entries')
362
+
363
+ describe 'data.only', ->
364
+ context 'this is an only load', ->
365
+ context '#_shouldUseOnly returns true', ->
366
+ beforeEach ->
367
+ spyOn(loader, '_shouldUseOnly').andReturn(true)
368
+
369
+ it 'sets data.only to be the difference between the only query and the already loaded ids', ->
370
+ loader.alreadyLoadedIds = ['1', '2']
371
+ opts.only = [1, 2, 3, 4]
372
+ expect(getSyncOptions(loader, opts).data.only).toEqual '3,4'
373
+
374
+ context '#_shouldUseOnly returns false', ->
375
+ beforeEach ->
376
+ spyOn(loader, '_shouldUseOnly').andReturn(true)
377
+
378
+ it 'does not set data.only', ->
379
+ expect(getSyncOptions(loader, opts).data.only).toBeUndefined()
380
+
381
+ context 'this is not an only load', ->
382
+ it 'does not set data.only', ->
383
+ expect(getSyncOptions(loader, opts).data.only).toBeUndefined()
384
+
385
+ describe 'data.order', ->
386
+ it 'sets order to be loadOptions.order if present', ->
387
+ opts.order = 'foo'
388
+ expect(getSyncOptions(loader, opts).data.order).toEqual 'foo'
389
+
390
+ describe 'extending data with filters', ->
391
+ it 'extends data with anything on filters that does not meet the omit list', ->
392
+ omitList = ['include', 'only', 'order', 'per_page', 'page', 'limit', 'offset', 'search']
393
+ opts.filters = {}
394
+
395
+ for k in omitList
396
+ opts.filters[k] = true
397
+
398
+ opts.filters.foo = 'bar'
399
+
400
+ data = getSyncOptions(loader, opts).data
401
+
402
+ for k in omitList
403
+ expect(data[k]).not.toEqual true
404
+
405
+ expect(data.foo).toEqual 'bar'
406
+
407
+ describe 'pagination', ->
408
+ beforeEach ->
409
+ opts.offset = 0
410
+ opts.limit = 25
411
+ opts.perPage = 25
412
+ opts.page = 1
413
+
414
+ context 'not an only request', ->
415
+ context 'there is a limit and offset', ->
416
+ it 'adds limit and offset', ->
417
+ data = getSyncOptions(loader, opts).data
418
+ expect(data.limit).toEqual 25
419
+ expect(data.offset).toEqual 0
420
+
421
+ it 'does not add per_page and page', ->
422
+ data = getSyncOptions(loader, opts).data
423
+ expect(data.per_page).toBeUndefined()
424
+ expect(data.page).toBeUndefined()
425
+
426
+ context 'there is not a limit and offset', ->
427
+ beforeEach ->
428
+ delete opts.limit
429
+ delete opts.offset
430
+
431
+ it 'adds per_page and page', ->
432
+ data = getSyncOptions(loader, opts).data
433
+ expect(data.per_page).toEqual 25
434
+ expect(data.page).toEqual 1
435
+
436
+ it 'does not add limit and offset', ->
437
+ data = getSyncOptions(loader, opts).data
438
+ expect(data.limit).toBeUndefined()
439
+ expect(data.offset).toBeUndefined()
440
+
441
+ context 'only request', ->
442
+ beforeEach ->
443
+ opts.only = 1
444
+
445
+ it 'does not add limit, offset, per_page, or page', ->
446
+ data = getSyncOptions(loader, opts).data
447
+ expect(data.limit).toBeUndefined()
448
+ expect(data.offset).toBeUndefined()
449
+ expect(data.per_page).toBeUndefined()
450
+ expect(data.page).toBeUndefined()
451
+
452
+ describe 'data.search', ->
453
+ it 'sets data.search to be loadOptions.search if present', ->
454
+ opts.search = 'term'
455
+ expect(getSyncOptions(loader, opts).data.search).toEqual 'term'
456
+
457
+ describe '#_onServerLoadSuccess', ->
458
+ beforeEach ->
459
+ loader = createLoader()
460
+ spyOn(loader, '_onLoadSuccess')
461
+
462
+ it 'calls #_updateStorageManagerFromResponse with the response', ->
463
+ loader._onServerLoadSuccess('response')
464
+ expect(loader._updateStorageManagerFromResponse).toHaveBeenCalledWith 'response'
465
+
466
+ it 'calls #_onServerLoadSuccess with the result from #_updateStorageManagerFromResponse', ->
467
+ loader._updateStorageManagerFromResponse.andReturn 'data'
468
+
469
+ loader._onServerLoadSuccess()
470
+ expect(loader._onLoadSuccess).toHaveBeenCalledWith 'data'
471
+
472
+ describe '#_calculateAdditionalIncludes', ->
473
+ opts = null
474
+
475
+ beforeEach ->
476
+ loader = createLoader()
477
+ opts = defaultLoadOptions()
478
+
479
+ spyOn(loader, '_getIdsForAssociation').andReturn [1, 2]
480
+
481
+ it 'adds each additional (sub) include to the additionalIncludes array', ->
482
+ opts.include = fakeNestedInclude
483
+ loader.setup(opts)
484
+
485
+ loader._calculateAdditionalIncludes()
486
+ expect(loader.additionalIncludes.length).toEqual 2
487
+ expect(loader.additionalIncludes).toEqual [
488
+ { name: 'project', ids: [1, 2], include: [participants: []] }
489
+ { name: 'assignees', ids: [1, 2], include: [something_else: []] }
490
+ ]
491
+
492
+ describe '#_onLoadSuccess', ->
493
+ beforeEach ->
494
+ loader = createLoader()
495
+ loader.additionalIncludes = []
496
+ spyOn(loader, '_onLoadingCompleted')
497
+ spyOn(loader, '_loadAdditionalIncludes')
498
+ spyOn(loader, '_calculateAdditionalIncludes')
499
+
500
+ it 'calls #_updateObjects with the internalObject, the data, and silent set to true', ->
501
+ loader._onLoadSuccess('test data')
502
+ expect(loader._updateObjects).toHaveBeenCalledWith(loader.internalObject, 'test data', true)
503
+
504
+ it 'calls #_calculateAdditionalIncludes', ->
505
+ loader._onLoadSuccess()
506
+ expect(loader._calculateAdditionalIncludes).toHaveBeenCalled()
507
+
508
+ context 'additional includes are needed', ->
509
+ it 'calls #_loadAdditionalIncludes', ->
510
+ loader._calculateAdditionalIncludes.andCallFake -> @additionalIncludes = ['foo']
511
+
512
+ loader._onLoadSuccess()
513
+ expect(loader._loadAdditionalIncludes).toHaveBeenCalled()
514
+ expect(loader._onLoadingCompleted).not.toHaveBeenCalled()
515
+
516
+ context 'additional includes are not needed', ->
517
+ it 'calls #_onLoadingCompleted', ->
518
+ loader._onLoadSuccess()
519
+ expect(loader._onLoadingCompleted).toHaveBeenCalled()
520
+ expect(loader._loadAdditionalIncludes).not.toHaveBeenCalled()
521
+
522
+ describe '#_onLoadingCompleted', ->
523
+ beforeEach ->
524
+ loader = createLoader()
525
+
526
+ it 'calls #_updateObjects with the externalObject and internalObject', ->
527
+ loader._onLoadingCompleted()
528
+ expect(loader._updateObjects).toHaveBeenCalledWith(loader.externalObject, loader.internalObject)
529
+
530
+ it 'resolves the deferred object with the externalObject', ->
531
+ spy = jasmine.createSpy()
532
+ loader.then(spy)
533
+
534
+ loader._onLoadingCompleted()
535
+ expect(spy).toHaveBeenCalledWith(loader.externalObject)
536
+
537
+ describe '#_updateObjects', ->
538
+ fakeObj = null
539
+
540
+ beforeEach ->
541
+ loader = createLoader()
542
+ fakeObj = setLoaded: jasmine.createSpy()
543
+ loader._updateObjects.andCallThrough()
544
+
545
+ it 'sets the object to loaded if silent is false', ->
546
+ loader._updateObjects(fakeObj, {})
547
+ expect(fakeObj.setLoaded).toHaveBeenCalled()
548
+
549
+ it 'does not set the object to loaded if silent is true', ->
550
+ loader._updateObjects(fakeObj, {}, true)
551
+ expect(fakeObj.setLoaded).not.toHaveBeenCalled()
552
+
553
+ describe '#_loadAdditionalIncludes', ->
554
+ opts = null
555
+
556
+ beforeEach ->
557
+ loader = createLoader()
558
+ opts = defaultLoadOptions()
559
+ opts.include = fakeNestedInclude
560
+
561
+ loader.setup(opts)
562
+ loader._calculateAdditionalIncludes()
563
+
564
+ spyOn(loader, '_onLoadingCompleted')
565
+
566
+ it 'creates a request for each additional include and calls #_onLoadingCompleted when they all are done', ->
567
+ promises = []
568
+ spyOn(loader.storageManager, 'loadObject').andCallFake ->
569
+ promise = $.Deferred()
570
+ promises.push(promise)
571
+ promise
572
+
573
+ loader._loadAdditionalIncludes()
574
+ expect(loader.storageManager.loadObject.callCount).toEqual 2
575
+ expect(promises.length).toEqual 2
576
+ expect(loader._onLoadingCompleted).not.toHaveBeenCalled()
577
+
578
+ for promise in promises
579
+ promise.resolve()
580
+
581
+ expect(loader._onLoadingCompleted).toHaveBeenCalled()
582
+
583
+ describe '#_getIdsForAssociation', ->
584
+ it 'returns the flattened, unique, sorted, and non-null IDs from the models that are returned from #_getModelsForAssociation', ->
585
+ loader = createLoader()
586
+ expect(loader._getIdsForAssociation('foo')).toEqual [1, 2, 4, 5, 6]
587
+
588
+ describe '#_modelsOrObj', ->
589
+ beforeEach ->
590
+ loader = createLoader()
591
+
592
+ context 'obj is a Backbone.Collection', ->
593
+ it 'returns the models from the collection', ->
594
+ collection = new Backbone.Collection()
595
+ collection.add([new Backbone.Model(), new Backbone.Model])
596
+ expect(loader._modelsOrObj(collection)).toEqual(collection.models)
597
+
598
+ context 'obj is not a Backbone.Collection', ->
599
+ it 'returns the obj or an empty array', ->
600
+ obj = []
601
+ expect(loader._modelsOrObj(obj)).toEqual(obj)
602
+
603
+ obj = null
604
+ expect(loader._modelsOrObj(obj)).toEqual([])