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
@@ -0,0 +1,3 @@
1
+ describe 'Loaders AbstractLoader', ->
2
+ loaderClass = Brainstem.AbstractLoader
3
+ itShouldBehaveLike "AbstractLoaderSharedBehavior", loaderClass: loaderClass
@@ -0,0 +1,146 @@
1
+ describe 'Loaders CollectionLoader', ->
2
+ loader = opts = null
3
+ fakeNestedInclude = ['parent', { project: ['participants'] }, { assignees: ['something_else'] }]
4
+ loaderClass = Brainstem.CollectionLoader
5
+
6
+ defaultLoadOptions = ->
7
+ name: 'tasks'
8
+
9
+ createLoader = (opts = {}) ->
10
+ storageManager = new Brainstem.StorageManager()
11
+ storageManager.addCollection('tasks', App.Collections.Tasks)
12
+
13
+ defaults =
14
+ storageManager: storageManager
15
+
16
+ loader = new loaderClass(_.extend {}, defaults, opts)
17
+ loader
18
+
19
+ # It should keep the AbstractLoader behavior.
20
+ itShouldBehaveLike "AbstractLoaderSharedBehavior", loaderClass: loaderClass
21
+
22
+ describe 'CollectionLoader behavior', ->
23
+ beforeEach ->
24
+ loader = createLoader()
25
+ opts = defaultLoadOptions()
26
+
27
+ describe '#getCollection', ->
28
+ it 'should return the externalObject', ->
29
+ loader.setup(opts)
30
+ expect(loader.getCollection()).toEqual loader.externalObject
31
+
32
+ describe '#_getCollectionName', ->
33
+ it 'should return the name from loadOptions', ->
34
+ loader.setup(opts)
35
+ expect(loader._getCollectionName()).toEqual 'tasks'
36
+
37
+ describe '#_createObjects', ->
38
+ collection = null
39
+
40
+ beforeEach ->
41
+ collection = new App.Collections.Tasks()
42
+ spyOn(loader.storageManager, 'createNewCollection').andReturn collection
43
+
44
+ it 'creates a new collection from the name in loadOptions', ->
45
+ loader.setup(opts)
46
+ expect(loader.storageManager.createNewCollection.callCount).toEqual 2
47
+ expect(loader.internalObject).toEqual collection
48
+
49
+ context 'collection is passed in to loadOptions', ->
50
+ it 'uses the collection that is passed in', ->
51
+ opts.collection ?= new App.Collections.Tasks()
52
+ loader.setup(opts)
53
+ expect(loader.externalObject).toEqual opts.collection
54
+
55
+ context 'collection is not passed in to loadOptions', ->
56
+ it 'creates a new collection from the name in loadOptions', ->
57
+ loader.setup(opts)
58
+ expect(loader.storageManager.createNewCollection.callCount).toEqual 2
59
+ expect(loader.externalObject).toEqual collection
60
+
61
+ it 'sets the collection to not loaded', ->
62
+ spyOn(collection, 'setLoaded')
63
+ loader.setup(opts)
64
+ expect(collection.setLoaded).toHaveBeenCalledWith false
65
+
66
+ describe 'resetting the collection', ->
67
+ context 'loadOptions.reset is true', ->
68
+ beforeEach ->
69
+ opts.reset = true
70
+
71
+ it 'calls reset on the collection', ->
72
+ spyOn(collection, 'reset')
73
+ loader.setup(opts)
74
+ expect(collection.reset).toHaveBeenCalled()
75
+
76
+ context 'loadOptions.reset is false', ->
77
+ it 'does not reset the collection', ->
78
+ spyOn(collection, 'reset')
79
+ loader.setup(opts)
80
+ expect(collection.reset).not.toHaveBeenCalled()
81
+
82
+ it 'sets lastFetchOptions on the collection', ->
83
+ list = ['filters', 'page', 'perPage', 'limit', 'offset', 'order', 'search']
84
+
85
+ for e in list
86
+ opts[e] = true
87
+
88
+ opts.include = 'parent'
89
+ loader.setup(opts)
90
+
91
+ expect(loader.externalObject.lastFetchOptions.name).toEqual 'tasks'
92
+ expect(loader.externalObject.lastFetchOptions.include).toEqual 'parent'
93
+
94
+ for e in list
95
+ expect(loader.externalObject.lastFetchOptions[e]).toEqual true
96
+
97
+ # describe '#_updateStorageManagerFromResponse', ->
98
+ # TODO: test this, it's tested right now through integration tests.
99
+
100
+ describe '#_updateObject', ->
101
+ it 'triggers loaded on the object after the attributes have been set', ->
102
+ loadedSpy = jasmine.createSpy().andCallFake ->
103
+ expect(this.length).toEqual 1 # make sure that the spy is called after the models have been added (tests the trigger: false)
104
+
105
+ loader.setup(opts)
106
+ loader.internalObject.listenTo loader.internalObject, 'loaded', loadedSpy
107
+
108
+ loader._updateObjects(loader.internalObject, [{foo: 'bar'}])
109
+ expect(loadedSpy).toHaveBeenCalled()
110
+
111
+ it 'works with a Backbone.Collection', ->
112
+ loader.setup(opts)
113
+ loader._updateObjects(loader.internalObject, new Backbone.Collection([new Backbone.Model(name: 'foo')]))
114
+ expect(loader.internalObject.length).toEqual 1
115
+
116
+ it 'works with an array of models', ->
117
+ loader.setup(opts)
118
+ loader._updateObjects(loader.internalObject, [new Backbone.Model(name: 'foo'), new Backbone.Model(name: 'test')])
119
+ expect(loader.internalObject.length).toEqual 2
120
+
121
+ it 'works with a single model', ->
122
+ loader.setup(opts)
123
+ spy = jasmine.createSpy()
124
+ loader.internalObject.listenTo loader.internalObject, 'reset', spy
125
+
126
+ loader._updateObjects(loader.internalObject, new Backbone.Model(name: 'foo'))
127
+ expect(loader.internalObject.length).toEqual 1
128
+ expect(spy).toHaveBeenCalled()
129
+
130
+ describe '#_getModel', ->
131
+ it 'returns the model from the internal collection', ->
132
+ loader.setup(opts)
133
+ expect(loader._getModel()).toEqual App.Models.Task
134
+
135
+ describe '#_getModelsForAssociation', ->
136
+ it 'returns the models for a given association from all of the models in the internal collection', ->
137
+ loader.setup(opts)
138
+ user = buildAndCacheUser()
139
+ user2 = buildAndCacheUser()
140
+
141
+ loader.internalObject.add(new App.Models.Task(assignee_ids: [user.id]))
142
+ loader.internalObject.add(new App.Models.Task(assignee_ids: [user2.id]))
143
+
144
+ expect(loader._getModelsForAssociation('assignees')).toEqual [[user], [user2]] # Association with a model in it
145
+ expect(loader._getModelsForAssociation('parent')).toEqual [[], []] # Association without any models
146
+ expect(loader._getModelsForAssociation('adfasfa')).toEqual [[], []] # Association that does not exist
@@ -0,0 +1,99 @@
1
+ describe 'Loaders ModelLoader', ->
2
+ loader = opts = null
3
+ fakeNestedInclude = ['parent', { project: ['participants'] }, { assignees: ['something_else'] }]
4
+ loaderClass = Brainstem.ModelLoader
5
+
6
+ defaultLoadOptions = ->
7
+ name: 'task'
8
+ only: 1
9
+
10
+ createLoader = (opts = {}) ->
11
+ storageManager = base.data
12
+ storageManager.addCollection('tasks', App.Collections.Tasks)
13
+
14
+ defaults =
15
+ storageManager: storageManager
16
+
17
+ loader = new loaderClass(_.extend {}, defaults, opts)
18
+ loader
19
+
20
+ # It should keep the AbstractLoader behavior.
21
+ itShouldBehaveLike "AbstractLoaderSharedBehavior", loaderClass: loaderClass
22
+
23
+ describe 'ModelLoader behavior', ->
24
+ beforeEach ->
25
+ loader = createLoader()
26
+ opts = defaultLoadOptions()
27
+
28
+ describe '#getModel', ->
29
+ it 'should return the externalObject', ->
30
+ loader.setup(opts)
31
+ expect(loader.getModel()).toEqual loader.externalObject
32
+
33
+ describe '#_getCollectionName', ->
34
+ it 'returns the pluralized name of the model', ->
35
+ loader.setup(opts)
36
+ expect(loader._getCollectionName()).toEqual 'tasks'
37
+
38
+ describe '#_createObjects', ->
39
+ model = null
40
+
41
+ context 'there is a matching model in the storageManager', ->
42
+ it 'sets the internalObject to be the cached model', ->
43
+ model = buildAndCacheTask(id: 1)
44
+ loader.setup(opts)
45
+ expect(loader.internalObject).toEqual model
46
+
47
+ context 'there is not a matching model in the storageManager', ->
48
+ it 'creates a new model and uses that as the internalObject', ->
49
+ model = new App.Models.Task()
50
+ spyOn(loader.storageManager, 'createNewModel').andReturn model
51
+ loader.setup(opts)
52
+ expect(loader.internalObject).toEqual model
53
+
54
+ it 'sets the ID on that model', ->
55
+ loader.setup(opts)
56
+ expect(loader.internalObject.id).toEqual '1'
57
+
58
+ it 'uses the internalObject as the externalObject', ->
59
+ loader.setup(opts)
60
+ expect(loader.internalObject).toEqual loader.externalObject
61
+
62
+ describe '#_updateStorageManagerFromResponse', ->
63
+ it 'calls parse on the internalObject with the response', ->
64
+ loader.setup(opts)
65
+ spyOn(loader.internalObject, 'parse')
66
+
67
+ loader._updateStorageManagerFromResponse('test response')
68
+ expect(loader.internalObject.parse).toHaveBeenCalledWith 'test response'
69
+
70
+ describe '#_updateObject', ->
71
+ it 'works with a Backbone.Model', ->
72
+ loader.setup(opts)
73
+ loader._updateObjects(loader.internalObject, new Backbone.Model(name: 'foo'))
74
+ expect(loader.internalObject.get('name')).toEqual 'foo'
75
+
76
+ it 'works with an array with a Backbone.Model', ->
77
+ loader.setup(opts)
78
+ loader._updateObjects(loader.internalObject, [new Backbone.Model(name: 'foo')])
79
+ expect(loader.internalObject.get('name')).toEqual 'foo'
80
+
81
+ it 'works with an array of data', ->
82
+ loader.setup(opts)
83
+ loader._updateObjects(loader.internalObject, [name: 'foo'])
84
+ expect(loader.internalObject.get('name')).toEqual 'foo'
85
+
86
+ describe '#_getModel', ->
87
+ it 'returns the constructor of the internalObject', ->
88
+ loader.setup(opts)
89
+ expect(loader._getModel()).toEqual App.Models.Task
90
+
91
+ describe '#_getModelsForAssociation', ->
92
+ it 'returns the models from the internalObject for a given association', ->
93
+ loader.setup(opts)
94
+ user = buildAndCacheUser()
95
+ loader.internalObject.set('assignee_ids', [user.id])
96
+
97
+ expect(loader._getModelsForAssociation('assignees')).toEqual [user] # Association with a model in it
98
+ expect(loader._getModelsForAssociation('parent')).toEqual [] # Association without any models
99
+ expect(loader._getModelsForAssociation('adfasfa')).toEqual [] # Association that does not exist
@@ -40,26 +40,107 @@ describe 'Brainstem Storage Manager', ->
40
40
  tasks = [buildTask(id: 2, title: "a task", project_id: 15)]
41
41
  projects = [buildProject(id: 15)]
42
42
  timeEntries = [buildTimeEntry(id: 1, task_id: 2, project_id: 15, title: "a time entry")]
43
- respondWith server, "/api/time_entries?only=1", resultsFrom: "time_entries", data: { time_entries: timeEntries }
44
- respondWith server, "/api/time_entries?include=project%2Ctask&only=1", resultsFrom: "time_entries", data: { time_entries: timeEntries, tasks: tasks, projects: projects }
43
+ respondWith server, "/api/time_entries/1", resultsFrom: "time_entries", data: { time_entries: timeEntries }
44
+ respondWith server, "/api/time_entries/1?include=project%2Ctask", resultsFrom: "time_entries", data: { time_entries: timeEntries, tasks: tasks, projects: projects }
45
+
46
+ it "creates a new model with the supplied id", ->
47
+ loader = base.data.loadModel "time_entry", "333"
48
+ expect(loader.getModel().id).toEqual "333"
49
+
50
+ it "calls Backbone.sync with the model from the loader", ->
51
+ spyOn(Backbone, 'sync')
52
+ loader = base.data.loadModel "time_entry", "333"
53
+ expect(Backbone.sync).toHaveBeenCalledWith 'read', loader.getModel(), loader._buildSyncOptions()
45
54
 
46
55
  it "loads a single model from the server, including associations", ->
47
- model = base.data.loadModel "time_entry", 1, include: ["project", "task"]
48
- expect(model.loaded).toBe false
56
+ loaded = false
57
+ loader = base.data.loadModel "time_entry", 1, include: ["project", "task"]
58
+ loader.done -> loaded = true
59
+ model = loader.getModel()
60
+
61
+ expect(loaded).toBe false
49
62
  server.respond()
50
- expect(model.loaded).toBe true
63
+ expect(loaded).toBe true
51
64
  expect(model.id).toEqual "1"
52
65
  expect(model.get("title")).toEqual "a time entry"
53
66
  expect(model.get('task').get('title')).toEqual "a task"
54
67
  expect(model.get('project').id).toEqual "15"
55
68
 
69
+ it "works with complex associations", ->
70
+ mainProject = buildProject(title: "my project")
71
+ mainTask = buildTask(project_id: mainProject.id, title: "foo")
72
+ timeTask = buildTask(title: 'hello')
73
+ timeEntry = buildTimeEntry(project_id: mainProject.id, task_id: timeTask.id, time: 50)
74
+ mainProject.set('time_entry_ids', [timeEntry.id])
75
+
76
+ subTask = buildTask()
77
+ mainTask.set('sub_task_ids', [subTask.id])
78
+
79
+ mainTaskAssignee = buildUser(name: 'Kimbo')
80
+ mainTask.set('assignee_ids', [mainTaskAssignee.id])
81
+
82
+ subTaskAssignee = buildUser(name: 'Slice')
83
+ subTask.set('assignee_ids', [subTaskAssignee.id])
84
+
85
+ respondWith server, "/api/tasks/#{mainTask.id}?include=assignees%2Csub_tasks%2Cproject", resultsFrom: "tasks", data: { results: resultsArray("tasks", [mainTask]), tasks: resultsObject([mainTask, subTask]), projects: resultsObject([mainProject]), users: resultsObject([mainTaskAssignee]) }
86
+ respondWith server, "/api/tasks?include=assignees&only=#{subTask.id}", resultsFrom: "tasks", data: { results: resultsArray("tasks", [subTask]), tasks: resultsObject([subTask]), users: resultsObject([subTaskAssignee]) }
87
+ respondWith server, "/api/projects?include=time_entries&only=#{mainProject.id}", resultsFrom: "projects", data: { results: resultsArray("projects", [mainProject]), time_entries: resultsObject([timeEntry]), projects: resultsObject([mainProject]) }
88
+ respondWith server, "/api/time_entries?include=task&only=" + timeEntry.id, resultsFrom: "time_entries", data: { results: resultsArray("time_entries", [timeEntry]), time_entries: resultsObject([timeEntry]), tasks: resultsObject([timeTask]) }
89
+
90
+ loader = base.data.loadModel "task", mainTask.id, include: ["assignees", {"sub_tasks": ["assignees"]}, { "project" : [{ "time_entries": ["task"] }] }]
91
+ model = loader.getModel()
92
+
93
+ server.respond()
94
+
95
+ # check main model
96
+ expect(model.attributes).toEqual(mainTask.attributes)
97
+
98
+ # check assignees
99
+ expect(model.get('assignees').length).toEqual(1)
100
+ expect(model.get('assignees').first().get('name')).toEqual('Kimbo')
101
+
102
+ # check sub_tasks
103
+ subTasks = model.get('sub_tasks')
104
+ expect(subTasks.length).toEqual(1)
105
+
106
+ # check sub_tasks -> assignees
107
+ assignees = subTasks.at(0).get('assignees')
108
+ expect(assignees.length).toEqual(1)
109
+ expect(assignees.at(0).get('name')).toEqual('Slice')
110
+
111
+ # check project
112
+ project = model.get('project')
113
+ expect(project.get('title')).toEqual('my project')
114
+
115
+ # check project -> time_entries
116
+ timeEntries = project.get('time_entries')
117
+ expect(timeEntries.length).toEqual(1)
118
+
119
+ timeEntry = timeEntries.at(0)
120
+ expect(timeEntry.get('time')).toEqual(50)
121
+
122
+ # check project -> time_entries -> task
123
+ expect(timeEntry.get('task').get('title')).toEqual('hello')
124
+
125
+ it "uses the cache if it can", ->
126
+ task = buildAndCacheTask(id: 200)
127
+ spy = spyOn(Brainstem.AbstractLoader.prototype, '_loadFromServer')
128
+
129
+ loader = base.data.loadModel "task", task.id
130
+ model = loader.getModel()
131
+ expect(model.attributes).toEqual(task.attributes)
132
+ expect(spy).not.toHaveBeenCalled()
133
+
56
134
  it "works even when the server returned associations of the same type", ->
57
135
  posts = [buildPost(id: 2, reply: true), buildPost(id: 3, reply: true), buildPost(id: 1, reply: false, reply_ids: [2, 3])]
58
- respondWith server, "/api/posts?include=replies&only=1", data: { results: [{ key: "posts", id: 1 }], posts: posts }
59
- model = base.data.loadModel "post", 1, include: ["replies"]
60
- expect(model.loaded).toBe false
136
+ respondWith server, "/api/posts/1?include=replies", data: { results: [{ key: "posts", id: 1 }], posts: posts }
137
+ loaded = false
138
+ loader = base.data.loadModel "post", 1, include: ["replies"]
139
+ loader.done -> loaded = true
140
+ model = loader.getModel()
141
+ expect(loaded).toBe false
61
142
  server.respond()
62
- expect(model.loaded).toBe true
143
+ expect(loaded).toBe true
63
144
  expect(model.id).toEqual "1"
64
145
  expect(model.get("replies").pluck("id")).toEqual ["2", "3"]
65
146
 
@@ -72,29 +153,83 @@ describe 'Brainstem Storage Manager', ->
72
153
  expect(events).toEqual ["tasks", "time_entries"]
73
154
 
74
155
  it "triggers changes", ->
75
- model = base.data.loadModel "time_entry", 1, include: ["project", "task"]
156
+ loaded = false
157
+ loader = base.data.loadModel "time_entry", 1, include: ["project", "task"]
158
+ loader.done -> loaded = true
159
+ model = loader.getModel()
76
160
  spy = jasmine.createSpy().andCallFake ->
77
- expect(model.loaded).toBe true
78
161
  expect(model.get("title")).toEqual "a time entry"
79
162
  expect(model.get('task').get('title')).toEqual "a task"
80
163
  expect(model.get('project').id).toEqual "15"
81
164
  model.bind "change", spy
82
165
  expect(spy).not.toHaveBeenCalled()
166
+ expect(loaded).toBe false
83
167
  server.respond()
84
168
  expect(spy).toHaveBeenCalled()
85
169
  expect(spy.callCount).toEqual 1
170
+ expect(loaded).toBe true
86
171
 
87
172
  it "accepts a success function", ->
88
- spy = jasmine.createSpy().andCallFake (model) ->
89
- expect(model.loaded).toBe true
90
- model = base.data.loadModel "time_entry", 1, success: spy
173
+ spy = jasmine.createSpy()
174
+ base.data.loadModel "time_entry", 1, success: spy
91
175
  server.respond()
92
176
  expect(spy).toHaveBeenCalled()
93
177
 
94
- it "can disbale caching", ->
95
- spy = spyOn(base.data, 'loadCollection')
96
- model = base.data.loadModel "time_entry", 1, cache: false
97
- expect(spy.mostRecentCall.args[1]['cache']).toBe(false)
178
+ it "can disable caching", ->
179
+ spy = spyOn(Brainstem.ModelLoader.prototype, '_checkCacheForData').andCallThrough()
180
+ base.data.loadModel "time_entry", 1, cache: false
181
+ expect(spy).not.toHaveBeenCalled()
182
+
183
+ it "invokes the error callback when the server responds with a 404", ->
184
+ successSpy = jasmine.createSpy('successSpy')
185
+ errorSpy = jasmine.createSpy('errorSpy')
186
+ respondWith server, "/api/time_entries/1337", data: { results: [] }, status: 404
187
+ base.data.loadModel "time_entry", 1337, success: successSpy, error: errorSpy
188
+
189
+ server.respond()
190
+ expect(successSpy).not.toHaveBeenCalled()
191
+ expect(errorSpy).toHaveBeenCalled()
192
+
193
+ it "does not resolve until all of the associations are included", ->
194
+ base.data.enableExpectations()
195
+
196
+ project = buildProject()
197
+ user = buildUser()
198
+
199
+ task = buildTask(title: 'foobar', project_id: project.id)
200
+ task2 = buildTask(project_id: project.id)
201
+ task3 = buildTask(project_id: project.id, assignee_ids: [user.id])
202
+
203
+ project.set('task_ids', [task.id, task2.id, task3.id])
204
+
205
+ taskExpectation = base.data.stubModel "task", task.id, include: ['project': [{ 'tasks': ['assignees'] }]], response: (stub) ->
206
+ stub.result = task
207
+ stub.associated.project = [project]
208
+ stub.recursive = true
209
+
210
+ projectExpectation = base.data.stub "projects", only: project.id, include: ['tasks': ['assignees']], response: (stub) ->
211
+ stub.results = [project]
212
+ stub.associated.tasks = [task, task2, task3]
213
+ stub.recursive = true
214
+
215
+ taskWithAssigneesExpectation = base.data.stub "tasks", only: [task.id, task2.id, task3.id], include: ['assignees'], response: (stub) ->
216
+ stub.results = [task]
217
+ stub.associated.users = [user]
218
+
219
+ resolvedSpy = jasmine.createSpy('resolved')
220
+
221
+ model = buildAndCacheTask(id: task.id)
222
+ loader = base.data.loadModel "task", model.id, include: ['project': [{ 'tasks': ['assignees'] }]]
223
+ loader.done(resolvedSpy)
224
+
225
+ taskExpectation.respond()
226
+ expect(resolvedSpy).not.toHaveBeenCalled()
227
+
228
+ projectExpectation.respond()
229
+ expect(resolvedSpy).not.toHaveBeenCalled()
230
+
231
+ taskWithAssigneesExpectation.respond()
232
+ expect(resolvedSpy).toHaveBeenCalled()
98
233
 
99
234
  describe 'loadCollection', ->
100
235
  it "loads a collection of models", ->
@@ -225,8 +360,8 @@ describe 'Brainstem Storage Manager', ->
225
360
  taskTwoSub = buildTask(project_id: projectTwo.id, parent_id: 11); taskTwoSubWithAssignees = buildTask(id: taskTwoSub.id, assignee_ids: [taskTwoAssignee.id], parent_id: 11)
226
361
  taskOne = buildTask(id: 10, project_id: projectOne.id, assignee_ids: [taskOneAssignee.id], sub_task_ids: [taskOneSub.id])
227
362
  taskTwo = buildTask(id: 11, project_id: projectTwo.id, assignee_ids: [taskTwoAssignee.id], sub_task_ids: [taskTwoSub.id])
228
- respondWith server, "/api/tasks.json?include=assignees%2Cproject%2Csub_tasks&parents_only=true&per_page=20&page=1", data: { results: resultsArray("tasks", [taskOne, taskTwo]), tasks: resultsObject([taskOne, taskTwo, taskOneSub, taskTwoSub]), users: resultsObject([taskOneAssignee, taskTwoAssignee]), projects: resultsObject([projectOne, projectTwo]) }
229
- respondWith server, "/api/tasks.json?include=assignees&only=#{taskOneSub.id}%2C#{taskTwoSub.id}", data: { results: resultsArray("tasks", [taskOneSub, taskTwoSub]), tasks: resultsObject([taskOneSubWithAssignees, taskTwoSubWithAssignees]), users: resultsObject([taskOneSubAssignee, taskTwoAssignee]) }
363
+ respondWith server, "/api/tasks?include=assignees%2Cproject%2Csub_tasks&parents_only=true&per_page=20&page=1", data: { results: resultsArray("tasks", [taskOne, taskTwo]), tasks: resultsObject([taskOne, taskTwo, taskOneSub, taskTwoSub]), users: resultsObject([taskOneAssignee, taskTwoAssignee]), projects: resultsObject([projectOne, projectTwo]) }
364
+ respondWith server, "/api/tasks?include=assignees&only=#{taskOneSub.id}%2C#{taskTwoSub.id}", data: { results: resultsArray("tasks", [taskOneSub, taskTwoSub]), tasks: resultsObject([taskOneSubWithAssignees, taskTwoSubWithAssignees]), users: resultsObject([taskOneSubAssignee, taskTwoAssignee]) }
230
365
  respondWith server, "/api/projects?include=time_entries&only=#{projectOne.id}%2C#{projectTwo.id}", data: { results: resultsArray("projects", [projectOne, projectTwo]), projects: resultsObject([projectOneWithTimeEntries, projectTwoWithTimeEntries]), time_entries: resultsObject([projectOneTimeEntry]) }
231
366
  respondWith server, "/api/time_entries?include=task&only=#{projectOneTimeEntry.id}", data: { results: resultsArray("time_entries", [projectOneTimeEntry]), time_entries: resultsObject([projectOneTimeEntryWithTask]), tasks: resultsObject([projectOneTimeEntryTask]) }
232
367
 
@@ -274,16 +409,33 @@ describe 'Brainstem Storage Manager', ->
274
409
  expect(collection2.get(1).get('project').id).toEqual "15"
275
410
  expect(collection2.get(2).get('project').id).toEqual "10"
276
411
 
277
- it "does go to the server when more records are requested than it has previously requested, and remembers previously requested pages", ->
278
- collection1 = base.data.loadCollection "time_entries", include: ["project", "task"], page: 1, perPage: 2
279
- server.respond()
280
- expect(collection1.loaded).toBe true
281
- collection2 = base.data.loadCollection "time_entries", include: ["project", "task"], page: 2, perPage: 2
282
- expect(collection2.loaded).toBe false
283
- server.respond()
284
- expect(collection2.loaded).toBe true
285
- collection3 = base.data.loadCollection "time_entries", include: ["project"], page: 1, perPage: 2
286
- expect(collection3.loaded).toBe true
412
+ context "using perPage and page", ->
413
+ it "does go to the server when more records are requested than it has previously requested, and remembers previously requested pages", ->
414
+ collection1 = base.data.loadCollection "time_entries", include: ["project", "task"], page: 1, perPage: 2
415
+ server.respond()
416
+ expect(collection1.loaded).toBe true
417
+ collection2 = base.data.loadCollection "time_entries", include: ["project", "task"], page: 2, perPage: 2
418
+ expect(collection2.loaded).toBe false
419
+ server.respond()
420
+ expect(collection2.loaded).toBe true
421
+ collection3 = base.data.loadCollection "time_entries", include: ["project"], page: 1, perPage: 2
422
+ expect(collection3.loaded).toBe true
423
+
424
+ context "using limit and offset", ->
425
+ it "does go to the server when more records are requested than it knows about", ->
426
+ timeEntries = [buildTimeEntry(), buildTimeEntry()]
427
+ respondWith server, "/api/time_entries?limit=2&offset=0", resultsFrom: "time_entries", data: { time_entries: timeEntries }
428
+ respondWith server, "/api/time_entries?limit=2&offset=2", resultsFrom: "time_entries", data: { time_entries: timeEntries }
429
+
430
+ collection1 = base.data.loadCollection "time_entries", limit: 2, offset: 0
431
+ server.respond()
432
+ expect(collection1.loaded).toBe true
433
+ collection2 = base.data.loadCollection "time_entries", limit: 2, offset: 2
434
+ expect(collection2.loaded).toBe false
435
+ server.respond()
436
+ expect(collection2.loaded).toBe true
437
+ collection3 = base.data.loadCollection "time_entries", limit: 2, offset: 0
438
+ expect(collection3.loaded).toBe true
287
439
 
288
440
  it "does go to the server when some associations are missing, when otherwise it would have the data", ->
289
441
  collection1 = base.data.loadCollection "time_entries", include: ["project"], page: 1, perPage: 2
@@ -502,7 +654,7 @@ describe 'Brainstem Storage Manager', ->
502
654
 
503
655
  beforeEach ->
504
656
  item = buildTask()
505
- respondWith server, "/api/tasks.json?per_page=20&page=1", resultsFrom: "tasks", data: { tasks: [item] }
657
+ respondWith server, "/api/tasks?per_page=20&page=1", resultsFrom: "tasks", data: { tasks: [item] }
506
658
 
507
659
  it "goes to server even if we have matching items in cache", ->
508
660
  syncSpy = spyOn(Backbone, 'sync')
@@ -515,15 +667,37 @@ describe 'Brainstem Storage Manager', ->
515
667
  server.respond()
516
668
  expect(spy).toHaveBeenCalled()
517
669
 
670
+ describe "types of pagination", ->
671
+ it "prioritizes limit and offset over per page and page", ->
672
+ respondWith server, "/api/time_entries?limit=1&offset=0", resultsFrom: "time_entries", data: { time_entries: [buildTimeEntry()] }
673
+ base.data.loadCollection "time_entries", limit: 1, offset: 0, perPage: 5, page: 10
674
+ server.respond()
675
+
676
+ it "limits to at least 1 and offset 0", ->
677
+ respondWith server, "/api/time_entries?limit=1&offset=0", resultsFrom: "time_entries", data: { time_entries: [buildTimeEntry()] }
678
+ base.data.loadCollection "time_entries", limit: -5, offset: -5
679
+ server.respond()
680
+
681
+ it "falls back to per page and page if both limit and offset are not complete", ->
682
+ respondWith server, "/api/time_entries?per_page=5&page=10", resultsFrom: "time_entries", data: { time_entries: [buildTimeEntry()] }
683
+ base.data.loadCollection "time_entries", limit: "", offset: "", perPage: 5, page: 10
684
+ server.respond()
685
+
686
+ base.data.loadCollection "time_entries", limit: "", perPage: 5, page: 10
687
+ server.respond()
688
+
689
+ base.data.loadCollection "time_entries", offset: "", perPage: 5, page: 10
690
+ server.respond()
691
+
518
692
  describe "searching", ->
519
693
  it 'turns off caching', ->
520
- spy = spyOn(base.data, '_loadCollectionWithFirstLayer')
694
+ spy = spyOn(Brainstem.AbstractLoader.prototype, '_checkCacheForData').andCallThrough()
521
695
  collection = base.data.loadCollection "tasks", search: "the meaning of life"
522
- expect(spy.mostRecentCall.args[0]['cache']).toBe(false)
696
+ expect(spy).not.toHaveBeenCalled()
523
697
 
524
698
  it "returns the matching items with includes, triggering reset and success", ->
525
699
  task = buildTask()
526
- respondWith server, "/api/tasks.json?per_page=20&page=1&search=go+go+gadget+search",
700
+ respondWith server, "/api/tasks?per_page=20&page=1&search=go+go+gadget+search",
527
701
  data: { results: [{key: "tasks", id: task.id}], tasks: [task] }
528
702
  spy2 = jasmine.createSpy().andCallFake (collection) ->
529
703
  expect(collection.loaded).toBe true
@@ -538,15 +712,31 @@ describe 'Brainstem Storage Manager', ->
538
712
  expect(spy2).toHaveBeenCalled()
539
713
 
540
714
  it 'does not blow up when no results are returned', ->
541
- respondWith server, "/api/tasks.json?per_page=20&page=1&search=go+go+gadget+search", data: { results: [], tasks: [] }
715
+ respondWith server, "/api/tasks?per_page=20&page=1&search=go+go+gadget+search", data: { results: [], tasks: [] }
542
716
  collection = base.data.loadCollection "tasks", search: "go go gadget search"
543
717
  server.respond()
544
718
 
545
719
  it 'acts as if no search options were passed if the search string is blank', ->
546
- respondWith server, "/api/tasks.json?per_page=20&page=1", data: { results: [], tasks: [] }
720
+ respondWith server, "/api/tasks?per_page=20&page=1", data: { results: [], tasks: [] }
547
721
  collection = base.data.loadCollection "tasks", search: ""
548
722
  server.respond()
549
723
 
724
+ describe 'return values', ->
725
+ it 'adds the jQuery XHR object to the return values if returnValues is passed in', ->
726
+ baseXhr = $.ajax()
727
+ returnValues = {}
728
+
729
+ base.data.loadCollection "tasks", search: "the meaning of life", returnValues: returnValues
730
+ expect(returnValues.jqXhr).not.toBeUndefined()
731
+
732
+ # if it has most of the functions of a jQuery XHR object then it's probably a jQuery XHR object
733
+ jqXhrKeys = ['setRequestHeader', 'getAllResponseHeaders', 'getResponseHeader', 'overrideMimeType', 'abort']
734
+
735
+ for functionName in jqXhrKeys
736
+ funct = returnValues.jqXhr[functionName]
737
+ expect(funct).not.toBeUndefined()
738
+ expect(funct.toString()).toEqual(baseXhr[functionName].toString())
739
+
550
740
  describe "createNewCollection", ->
551
741
  it "makes a new collection of the appropriate type", ->
552
742
  expect(base.data.createNewCollection("tasks", [buildTask(), buildTask()]) instanceof App.Collections.Tasks).toBe true
@@ -557,26 +747,6 @@ describe 'Brainstem Storage Manager', ->
557
747
  collection = base.data.createNewCollection("tasks", [buildTask(), buildTask()], loaded: true)
558
748
  expect(collection.loaded).toBe true
559
749
 
560
- describe "_wrapObjects", ->
561
- it "wraps elements in an array with objects unless they are already objects", ->
562
- expect(base.data._wrapObjects([])).toEqual []
563
- expect(base.data._wrapObjects(['a', 'b'])).toEqual [{a: []}, {b: []}]
564
- expect(base.data._wrapObjects(['a', 'b': []])).toEqual [{a: []}, {b: []}]
565
- expect(base.data._wrapObjects(['a', 'b': 'c'])).toEqual [{a: []}, {b: [{c: []}]}]
566
- expect(base.data._wrapObjects([{'a':[], b: 'c', d: 'e' }])).toEqual [{a: []}, {b: [{c: []}]}, {d: [{e: []}]}]
567
- expect(base.data._wrapObjects(['a', { b: 'c', d: 'e' }])).toEqual [{a: []}, {b: [{c: []}]}, {d: [{e: []}]}]
568
- expect(base.data._wrapObjects([{'a': []}, {'b': ['c', d: []]}])).toEqual [{a: []}, {b: [{c: []}, {d: []}]}]
569
-
570
- describe "_countRequiredServerRequests", ->
571
- it "should count the number of loads needed to get the date", ->
572
- expect(base.data._countRequiredServerRequests(['a'])).toEqual 1
573
- expect(base.data._countRequiredServerRequests(['a', 'b', 'c': []])).toEqual 1
574
- expect(base.data._countRequiredServerRequests([{'a': ['d']}, 'b', 'c': ['e']])).toEqual 3
575
- expect(base.data._countRequiredServerRequests([{'a': ['d']}, 'b', 'c': ['e': []]])).toEqual 3
576
- expect(base.data._countRequiredServerRequests([{'a': ['d']}, 'b', 'c': ['e': ['f']]])).toEqual 4
577
- expect(base.data._countRequiredServerRequests([{'a': ['d']}, 'b', 'c': ['e': ['f', 'g': ['h']]]])).toEqual 5
578
- expect(base.data._countRequiredServerRequests([{'a': ['d': ['h']]}, { 'b':['g'] }, 'c': ['e': ['f', 'i']]])).toEqual 6
579
-
580
750
  describe "error handling", ->
581
751
  describe "setting a storage manager default error handler", ->
582
752
  it "allows an error interceptor to be set on construction", ->
@@ -600,6 +770,22 @@ describe 'Brainstem Storage Manager', ->
600
770
  server.respond()
601
771
  expect(customHandler).toHaveBeenCalled()
602
772
 
773
+ it "should also get called any amount of layers deep", ->
774
+ errorHandler = jasmine.createSpy('errorHandler')
775
+ successHandler = jasmine.createSpy('successHandler')
776
+ taskOne = buildTask(id: 10, sub_task_ids: [12])
777
+ taskOneSub = buildTask(id: 12, parent_id: 10, sub_task_ids: [13], project_id: taskOne.get('workspace_id'))
778
+ respondWith server, "/api/tasks?include=sub_tasks&parents_only=true&per_page=20&page=1", data: { results: resultsArray("tasks", [taskOne]), tasks: resultsObject([taskOne, taskOneSub]) }
779
+ server.respondWith "GET", "/api/tasks?include=sub_tasks&only=12", [ 401, {"Content-Type": "application/json"}, JSON.stringify({ errors: ["Invalid OAuth 2 Request"]}) ]
780
+ base.data.loadCollection("tasks", filters: { parents_only: "true" }, include: [ "sub_tasks": ["sub_tasks"] ], success: successHandler, error: errorHandler)
781
+
782
+ expect(successHandler).not.toHaveBeenCalled()
783
+ expect(errorHandler).not.toHaveBeenCalled()
784
+ server.respond()
785
+ expect(successHandler).not.toHaveBeenCalled()
786
+ expect(errorHandler).toHaveBeenCalled()
787
+ expect(errorHandler.callCount).toEqual(1)
788
+
603
789
  describe "when no storage manager error interceptor is given", ->
604
790
  it "has a default error interceptor", ->
605
791
  manager = new Brainstem.StorageManager()