brainstem-js 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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([])