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.
- data/.travis.yml +3 -6
- data/Gemfile.lock +1 -1
- data/README.md +3 -1
- data/Rakefile +2 -0
- data/lib/brainstem/js/version.rb +1 -1
- data/spec/brainstem-collection-spec.coffee +25 -0
- data/spec/{brianstem-expectation-spec.coffee → brainstem-expectation-spec.coffee} +132 -17
- data/spec/brainstem-model-spec.coffee +67 -27
- data/spec/brainstem-sync-spec.coffee +29 -6
- data/spec/brainstem-utils-spec.coffee +11 -1
- data/spec/helpers/builders.coffee +2 -2
- data/spec/helpers/models/post.coffee +1 -1
- data/spec/helpers/models/project.coffee +1 -1
- data/spec/helpers/models/task.coffee +2 -2
- data/spec/helpers/models/time-entry.coffee +1 -1
- data/spec/helpers/models/user.coffee +1 -1
- data/spec/helpers/spec-helper.coffee +21 -0
- data/spec/loaders/abstract-loader-shared-behavior.coffee +604 -0
- data/spec/loaders/abstract-loader-spec.coffee +3 -0
- data/spec/loaders/collection-loader-spec.coffee +146 -0
- data/spec/loaders/model-loader-spec.coffee +99 -0
- data/spec/storage-manager-spec.coffee +242 -56
- data/vendor/assets/javascripts/brainstem/brainstem-collection.coffee +16 -6
- data/vendor/assets/javascripts/brainstem/brainstem-expectation.coffee +70 -20
- data/vendor/assets/javascripts/brainstem/brainstem-model.coffee +13 -13
- data/vendor/assets/javascripts/brainstem/brainstem-sync.coffee +8 -3
- data/vendor/assets/javascripts/brainstem/loaders/abstract-loader.coffee +289 -0
- data/vendor/assets/javascripts/brainstem/loaders/collection-loader.coffee +68 -0
- data/vendor/assets/javascripts/brainstem/loaders/model-loader.coffee +35 -0
- data/vendor/assets/javascripts/brainstem/loading-mixin.coffee +1 -7
- data/vendor/assets/javascripts/brainstem/storage-manager.coffee +79 -196
- data/vendor/assets/javascripts/brainstem/utils.coffee +18 -4
- 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
|
-
|
29
|
-
|
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.Task extends Brainstem.Model
|
2
2
|
brainstemKey: "tasks"
|
3
3
|
paramRoot: 'task'
|
4
|
-
|
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
|
14
|
+
url: '/api/tasks'
|
@@ -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([])
|