brainstem-js 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/.gitignore +8 -0
  2. data/.pairs +21 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.tm_properties +1 -0
  6. data/.travis.yml +12 -0
  7. data/Assetfile +79 -0
  8. data/Gemfile +6 -0
  9. data/Gemfile.lock +50 -0
  10. data/LICENSE.txt +22 -0
  11. data/README.md +143 -0
  12. data/Rakefile +25 -0
  13. data/brainstemjs.gemspec +24 -0
  14. data/lib/brainstem/js/engine.rb +5 -0
  15. data/lib/brainstem/js/version.rb +5 -0
  16. data/lib/brainstem/js.rb +10 -0
  17. data/spec/brainstem-collection-spec.coffee +141 -0
  18. data/spec/brainstem-model-spec.coffee +283 -0
  19. data/spec/brainstem-sync-spec.coffee +22 -0
  20. data/spec/brainstem-utils-spec.coffee +12 -0
  21. data/spec/brianstem-expectation-spec.coffee +209 -0
  22. data/spec/helpers/builders.coffee +80 -0
  23. data/spec/helpers/jquery-matchers.js +137 -0
  24. data/spec/helpers/models/post.coffee +14 -0
  25. data/spec/helpers/models/project.coffee +13 -0
  26. data/spec/helpers/models/task.coffee +14 -0
  27. data/spec/helpers/models/time-entry.coffee +13 -0
  28. data/spec/helpers/models/user.coffee +8 -0
  29. data/spec/helpers/spec-helper.coffee +79 -0
  30. data/spec/storage-manager-spec.coffee +613 -0
  31. data/spec/support/.DS_Store +0 -0
  32. data/spec/support/console-runner.js +103 -0
  33. data/spec/support/headless.coffee +47 -0
  34. data/spec/support/headless.html +60 -0
  35. data/spec/support/runner.html +85 -0
  36. data/spec/vendor/backbone-factory.js +62 -0
  37. data/spec/vendor/backbone.js +1571 -0
  38. data/spec/vendor/inflection.js +448 -0
  39. data/spec/vendor/jquery-1.7.js +9300 -0
  40. data/spec/vendor/jquery.cookie.js +47 -0
  41. data/spec/vendor/minispade.js +67 -0
  42. data/spec/vendor/sinon-1.3.4.js +3561 -0
  43. data/spec/vendor/underscore.js +1221 -0
  44. data/vendor/assets/.DS_Store +0 -0
  45. data/vendor/assets/javascripts/.DS_Store +0 -0
  46. data/vendor/assets/javascripts/brainstem/brainstem-collection.coffee +53 -0
  47. data/vendor/assets/javascripts/brainstem/brainstem-expectation.coffee +65 -0
  48. data/vendor/assets/javascripts/brainstem/brainstem-inflections.js +449 -0
  49. data/vendor/assets/javascripts/brainstem/brainstem-model.coffee +141 -0
  50. data/vendor/assets/javascripts/brainstem/brainstem-sync.coffee +76 -0
  51. data/vendor/assets/javascripts/brainstem/index.js +1 -0
  52. data/vendor/assets/javascripts/brainstem/iso8601.js +41 -0
  53. data/vendor/assets/javascripts/brainstem/loading-mixin.coffee +13 -0
  54. data/vendor/assets/javascripts/brainstem/storage-manager.coffee +275 -0
  55. data/vendor/assets/javascripts/brainstem/utils.coffee +35 -0
  56. metadata +198 -0
@@ -0,0 +1,613 @@
1
+ describe 'Brainstem Storage Manager', ->
2
+ manager = null
3
+
4
+ beforeEach ->
5
+ manager = new Brainstem.StorageManager()
6
+
7
+ describe 'addCollection and getCollectionDetails', ->
8
+ it "tracks a named collection", ->
9
+ manager.addCollection 'time_entries', App.Collections.TimeEntries
10
+ expect(manager.getCollectionDetails("time_entries").klass).toBe App.Collections.TimeEntries
11
+
12
+ it "raises an error if the named collection doesn't exist", ->
13
+ expect(-> manager.getCollectionDetails('foo')).toThrow()
14
+
15
+ describe "storage", ->
16
+ beforeEach ->
17
+ manager.addCollection 'time_entries', App.Collections.TimeEntries
18
+
19
+ it "accesses a cached collection of the appropriate type", ->
20
+ expect(manager.storage('time_entries') instanceof App.Collections.TimeEntries).toBeTruthy()
21
+ expect(manager.storage('time_entries').length).toBe 0
22
+
23
+ it "raises an error if the named collection doesn't exist", ->
24
+ expect(-> manager.storage('foo')).toThrow()
25
+
26
+ describe "reset", ->
27
+ it "should clear all storage and sort lengths", ->
28
+ buildAndCacheTask()
29
+ buildAndCacheProject()
30
+ expect(base.data.storage("projects").length).toEqual 1
31
+ expect(base.data.storage("tasks").length).toEqual 1
32
+ base.data.collections["projects"].cache = { "foo": "bar" }
33
+ base.data.reset()
34
+ expect(base.data.collections["projects"].cache).toEqual {}
35
+ expect(base.data.storage("projects").length).toEqual 0
36
+ expect(base.data.storage("tasks").length).toEqual 0
37
+
38
+ describe "loadModel", ->
39
+ beforeEach ->
40
+ tasks = [buildTask(id: 2, title: "a task", project_id: 15)]
41
+ projects = [buildProject(id: 15)]
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 }
45
+
46
+ 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
49
+ server.respond()
50
+ expect(model.loaded).toBe true
51
+ expect(model.id).toEqual "1"
52
+ expect(model.get("title")).toEqual "a time entry"
53
+ expect(model.get('task').get('title')).toEqual "a task"
54
+ expect(model.get('project').id).toEqual "15"
55
+
56
+ it "works even when the server returned associations of the same type", ->
57
+ 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
61
+ server.respond()
62
+ expect(model.loaded).toBe true
63
+ expect(model.id).toEqual "1"
64
+ expect(model.get("replies").pluck("id")).toEqual ["2", "3"]
65
+
66
+ it "updates associations before the primary model", ->
67
+ events = []
68
+ base.data.storage('time_entries').on "add", -> events.push "time_entries"
69
+ base.data.storage('tasks').on "add", -> events.push "tasks"
70
+ base.data.loadModel "time_entry", 1, include: ["project", "task"]
71
+ server.respond()
72
+ expect(events).toEqual ["tasks", "time_entries"]
73
+
74
+ it "triggers changes", ->
75
+ model = base.data.loadModel "time_entry", 1, include: ["project", "task"]
76
+ spy = jasmine.createSpy().andCallFake ->
77
+ expect(model.loaded).toBe true
78
+ expect(model.get("title")).toEqual "a time entry"
79
+ expect(model.get('task').get('title')).toEqual "a task"
80
+ expect(model.get('project').id).toEqual "15"
81
+ model.bind "change", spy
82
+ expect(spy).not.toHaveBeenCalled()
83
+ server.respond()
84
+ expect(spy).toHaveBeenCalled()
85
+ expect(spy.callCount).toEqual 1
86
+
87
+ 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
91
+ server.respond()
92
+ expect(spy).toHaveBeenCalled()
93
+
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)
98
+
99
+ describe 'loadCollection', ->
100
+ it "loads a collection of models", ->
101
+ timeEntries = [buildTimeEntry(), buildTimeEntry()]
102
+ respondWith server, "/api/time_entries?per_page=20&page=1", resultsFrom: "time_entries", data: { time_entries: timeEntries }
103
+ collection = base.data.loadCollection "time_entries"
104
+ expect(collection.length).toBe 0
105
+ server.respond()
106
+ expect(collection.length).toBe 2
107
+
108
+ it "accepts a success function", ->
109
+ timeEntries = [buildTimeEntry(), buildTimeEntry()]
110
+ respondWith server, "/api/time_entries?per_page=20&page=1", resultsFrom: "time_entries", data: { time_entries: timeEntries }
111
+ spy = jasmine.createSpy().andCallFake (collection) ->
112
+ expect(collection.loaded).toBe true
113
+ collection = base.data.loadCollection "time_entries", success: spy
114
+ server.respond()
115
+ expect(spy).toHaveBeenCalledWith(collection)
116
+
117
+ it "saves it's options onto the returned collection", ->
118
+ collection = base.data.loadCollection "time_entries", order: "baz:desc", filters: { bar: 2 }
119
+ expect(collection.lastFetchOptions.order).toEqual "baz:desc"
120
+ expect(collection.lastFetchOptions.filters).toEqual { bar: 2 }
121
+ expect(collection.lastFetchOptions.collection).toBeFalsy()
122
+
123
+ describe "passing an optional collection", ->
124
+ it "accepts an optional collection instead of making a new one", ->
125
+ timeEntry = buildTimeEntry()
126
+ respondWith server, "/api/time_entries?per_page=20&page=1", data: { results: [{ key: "time_entries", id: timeEntry.id }], time_entries: [timeEntry] }
127
+ collection = new App.Collections.TimeEntries([buildTimeEntry(), buildTimeEntry()])
128
+ collection.setLoaded true
129
+ base.data.loadCollection "time_entries", collection: collection
130
+ expect(collection.lastFetchOptions.collection).toBeFalsy()
131
+ expect(collection.loaded).toBe false
132
+ expect(collection.length).toEqual 2
133
+ server.respond()
134
+ expect(collection.loaded).toBe true
135
+ expect(collection.length).toEqual 3
136
+
137
+ it "can take an optional reset command to reset the collection before using it", ->
138
+ timeEntry = buildTimeEntry()
139
+ respondWith server, "/api/time_entries?per_page=20&page=1", data: { results: [{ key: "time_entries", id: timeEntry.id }], time_entries: [timeEntry] }
140
+ collection = new App.Collections.TimeEntries([buildTimeEntry(), buildTimeEntry()])
141
+ collection.setLoaded true
142
+ spyOn(collection, 'reset').andCallThrough()
143
+ base.data.loadCollection "time_entries", collection: collection, reset: true
144
+ expect(collection.reset).toHaveBeenCalled()
145
+ expect(collection.lastFetchOptions.collection).toBeFalsy()
146
+ expect(collection.loaded).toBe false
147
+ expect(collection.length).toEqual 0
148
+ server.respond()
149
+ expect(collection.loaded).toBe true
150
+ expect(collection.length).toEqual 1
151
+
152
+ it "accepts filters", ->
153
+ posts = [buildPost(project_id: 15, id: 1), buildPost(project_id: 15, id: 2)]
154
+ respondWith server, "/api/posts?filter1=true&filter2=false&filter3=true&filter4=false&filter5=2&filter6=baz&per_page=20&page=1", data: { results: [{ key: "posts", id: 1}], posts: posts }
155
+ collection = base.data.loadCollection "posts", filters: { filter1: true, filter2: false, filter3: "true", filter4: "false", filter5: 2, filter6: "baz" }
156
+ server.respond()
157
+
158
+ it "triggers reset", ->
159
+ timeEntry = buildTimeEntry()
160
+ respondWith server, "/api/time_entries?per_page=20&page=1", data: { results: [{ key: "time_entries", id: timeEntry.id}], time_entries: [timeEntry] }
161
+ collection = base.data.loadCollection "time_entries"
162
+ expect(collection.loaded).toBe false
163
+ spy = jasmine.createSpy().andCallFake ->
164
+ expect(collection.loaded).toBe true
165
+ collection.bind "reset", spy
166
+ server.respond()
167
+ expect(spy).toHaveBeenCalled()
168
+
169
+ it "ignores count and honors results", ->
170
+ server.respondWith "GET", "/api/time_entries?per_page=20&page=1", [ 200, {"Content-Type": "application/json"}, JSON.stringify(count: 2, results: [{ key: "time_entries", id: 2 }], time_entries: [buildTimeEntry(), buildTimeEntry()]) ]
171
+ collection = base.data.loadCollection "time_entries"
172
+ server.respond()
173
+ expect(collection.length).toEqual(1)
174
+
175
+ it "works with an empty response", ->
176
+ exceptionSpy = spyOn(sinon, 'logError').andCallThrough()
177
+ respondWith server, "/api/time_entries?per_page=20&page=1", resultsFrom: "time_entries", data: { time_entries: [] }
178
+ base.data.loadCollection "time_entries"
179
+ server.respond()
180
+ expect(exceptionSpy).not.toHaveBeenCalled()
181
+
182
+ describe "fetching of associations", ->
183
+ json = null
184
+
185
+ beforeEach ->
186
+ tasks = [buildTask(id: 2, title: "a task")]
187
+ projects = [buildProject(id: 15), buildProject(id: 10)]
188
+ timeEntries = [buildTimeEntry(task_id: 2, project_id: 15, id: 1), buildTimeEntry(task_id: null, project_id: 10, id: 2)]
189
+
190
+ respondWith server, /\/api\/time_entries\?include=project%2Ctask&per_page=\d+&page=\d+/, resultsFrom: "time_entries", data: { time_entries: timeEntries, tasks: tasks, projects: projects }
191
+ respondWith server, /\/api\/time_entries\?include=project&per_page=\d+&page=\d+/, resultsFrom: "time_entries", data: { time_entries: timeEntries, projects: projects }
192
+
193
+ it "loads collections that should be included", ->
194
+ collection = base.data.loadCollection "time_entries", include: ["project", "task"]
195
+ spy = jasmine.createSpy().andCallFake ->
196
+ expect(collection.loaded).toBe true
197
+ expect(collection.get(1).get('task').get('title')).toEqual "a task"
198
+ expect(collection.get(2).get('task')).toBeFalsy()
199
+ expect(collection.get(1).get('project').id).toEqual "15"
200
+ expect(collection.get(2).get('project').id).toEqual "10"
201
+ collection.bind "reset", spy
202
+ expect(collection.loaded).toBe false
203
+ server.respond()
204
+ expect(collection.loaded).toBe true
205
+ expect(spy).toHaveBeenCalled()
206
+
207
+ it "applies uses the results array from the server (so that associations of the same type as the primary can be handled- posts with replies; tasks with subtasks, etc.)", ->
208
+ posts = [buildPost(project_id: 15, id: 1, reply_ids: [2]), buildPost(project_id: 15, id: 2, subject_id: 1, reply: true)]
209
+ respondWith server, "/api/posts?include=replies&parents_only=true&per_page=20&page=1", data: { results: [{ key: "posts", id: 1}], posts: posts }
210
+ collection = base.data.loadCollection "posts", include: ["replies"], filters: { parents_only: "true" }
211
+ server.respond()
212
+ expect(collection.pluck("id")).toEqual ["1"]
213
+ expect(collection.get(1).get('replies').pluck("id")).toEqual ["2"]
214
+
215
+ describe "fetching multiple levels of associations", ->
216
+ it "seperately requests each layer of associations", ->
217
+ projectOneTimeEntryTask = buildTask()
218
+ projectOneTimeEntry = buildTimeEntry(title: "without task"); projectOneTimeEntryWithTask = buildTimeEntry(id: projectOneTimeEntry.id, task_id: projectOneTimeEntryTask.id, title: "with task")
219
+ projectOne = buildProject(); projectOneWithTimeEntries = buildProject(id: projectOne.id, time_entry_ids: [projectOneTimeEntry.id])
220
+ projectTwo = buildProject(); projectTwoWithTimeEntries = buildProject(id: projectTwo.id, time_entry_ids: [])
221
+ taskOneAssignee = buildUser()
222
+ taskTwoAssignee = buildUser()
223
+ taskOneSubAssignee = buildUser()
224
+ taskOneSub = buildTask(project_id: projectOne.id, parent_id: 10); taskOneSubWithAssignees = buildTask(id: taskOneSub.id, assignee_ids: [taskOneSubAssignee.id], parent_id: 10)
225
+ taskTwoSub = buildTask(project_id: projectTwo.id, parent_id: 11); taskTwoSubWithAssignees = buildTask(id: taskTwoSub.id, assignee_ids: [taskTwoAssignee.id], parent_id: 11)
226
+ taskOne = buildTask(id: 10, project_id: projectOne.id, assignee_ids: [taskOneAssignee.id], sub_task_ids: [taskOneSub.id])
227
+ 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]) }
230
+ 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
+ respondWith server, "/api/time_entries?include=task&only=#{projectOneTimeEntry.id}", data: { results: resultsArray("time_entries", [projectOneTimeEntry]), time_entries: resultsObject([projectOneTimeEntryWithTask]), tasks: resultsObject([projectOneTimeEntryTask]) }
232
+
233
+ callCount = 0
234
+ checkStructure = (collection) ->
235
+ expect(collection.pluck("id").sort()).toEqual [taskOne.id, taskTwo.id]
236
+ expect(collection.get(taskOne.id).get("project").id).toEqual projectOne.id
237
+ expect(collection.get(taskOne.id).get("assignees").pluck("id")).toEqual [taskOneAssignee.id]
238
+ expect(collection.get(taskTwo.id).get("assignees").pluck("id")).toEqual [taskTwoAssignee.id]
239
+ expect(collection.get(taskOne.id).get("sub_tasks").pluck("id")).toEqual [taskOneSub.id]
240
+ expect(collection.get(taskTwo.id).get("sub_tasks").pluck("id")).toEqual [taskTwoSub.id]
241
+ expect(collection.get(taskOne.id).get("sub_tasks").get(taskOneSub.id).get("assignees").pluck("id")).toEqual [taskOneSubAssignee.id]
242
+ expect(collection.get(taskTwo.id).get("sub_tasks").get(taskTwoSub.id).get("assignees").pluck("id")).toEqual [taskTwoAssignee.id]
243
+ expect(collection.get(taskOne.id).get("project").get("time_entries").pluck("id")).toEqual [projectOneTimeEntry.id]
244
+ expect(collection.get(taskOne.id).get("project").get("time_entries").models[0].get("task").id).toEqual projectOneTimeEntryTask.id
245
+ callCount += 1
246
+
247
+ success = jasmine.createSpy().andCallFake checkStructure
248
+ collection = base.data.loadCollection "tasks", filters: { parents_only: "true" }, success: success, include: [
249
+ "assignees",
250
+ "project": ["time_entries": "task"],
251
+ "sub_tasks": ["assignees"]
252
+ ]
253
+ collection.bind "loaded", checkStructure
254
+ collection.bind "reset", checkStructure
255
+ expect(success).not.toHaveBeenCalled()
256
+ server.respond()
257
+ expect(success).toHaveBeenCalledWith(collection)
258
+ expect(callCount).toEqual 3
259
+
260
+ describe "caching", ->
261
+ describe "without ordering", ->
262
+ it "doesn't go to the server when it already has the data", ->
263
+ collection1 = base.data.loadCollection "time_entries", include: ["project", "task"], page: 1, perPage: 2
264
+ server.respond()
265
+ expect(collection1.loaded).toBe true
266
+ expect(collection1.get(1).get('project').id).toEqual "15"
267
+ expect(collection1.get(2).get('project').id).toEqual "10"
268
+ spy = jasmine.createSpy()
269
+ collection2 = base.data.loadCollection "time_entries", include: ["project", "task"], page: 1, perPage: 2, success: spy
270
+ expect(spy).toHaveBeenCalled()
271
+ expect(collection2.loaded).toBe true
272
+ expect(collection2.get(1).get('task').get('title')).toEqual "a task"
273
+ expect(collection2.get(2).get('task')).toBeFalsy()
274
+ expect(collection2.get(1).get('project').id).toEqual "15"
275
+ expect(collection2.get(2).get('project').id).toEqual "10"
276
+
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
287
+
288
+ it "does go to the server when some associations are missing, when otherwise it would have the data", ->
289
+ collection1 = base.data.loadCollection "time_entries", include: ["project"], page: 1, perPage: 2
290
+ server.respond()
291
+ expect(collection1.loaded).toBe true
292
+ collection2 = base.data.loadCollection "time_entries", include: ["project", "task"], page: 1, perPage: 2
293
+ expect(collection2.loaded).toBe false
294
+
295
+ describe "with ordering and filtering", ->
296
+ now = ws10 = ws11 = te1Ws10 = te2Ws10 = te1Ws11 = te2Ws11 = null
297
+
298
+ beforeEach ->
299
+ now = (new Date()).getTime()
300
+ ws10 = buildProject(id: 10)
301
+ ws11 = buildProject(id: 11)
302
+ te1Ws10 = buildTimeEntry(task_id: null, project_id: 10, id: 1, created_at: now - 20 * 1000, updated_at: now - 10 * 1000)
303
+ te2Ws10 = buildTimeEntry(task_id: null, project_id: 10, id: 2, created_at: now - 10 * 1000, updated_at: now - 5 * 1000)
304
+ te1Ws11 = buildTimeEntry(task_id: null, project_id: 11, id: 3, created_at: now - 100 * 1000, updated_at: now - 4 * 1000)
305
+ te2Ws11 = buildTimeEntry(task_id: null, project_id: 11, id: 4, created_at: now - 200 * 1000, updated_at: now - 12 * 1000)
306
+
307
+ it "goes to the server for pages of data and updates the collection", ->
308
+ respondWith server, "/api/time_entries?order=created_at%3Aasc&per_page=2&page=1", data: { results: resultsArray("time_entries", [te2Ws11, te1Ws11]), time_entries: [te2Ws11, te1Ws11] }
309
+ respondWith server, "/api/time_entries?order=created_at%3Aasc&per_page=2&page=2", data: { results: resultsArray("time_entries", [te1Ws10, te2Ws10]), time_entries: [te1Ws10, te2Ws10] }
310
+ collection = base.data.loadCollection "time_entries", order: "created_at:asc", page: 1, perPage: 2
311
+ server.respond()
312
+ expect(collection.pluck("id")).toEqual [te2Ws11.id, te1Ws11.id]
313
+ base.data.loadCollection "time_entries", collection: collection, order: "created_at:asc", page: 2, perPage: 2
314
+ server.respond()
315
+ expect(collection.pluck("id")).toEqual [te2Ws11.id, te1Ws11.id, te1Ws10.id, te2Ws10.id]
316
+
317
+ it "does not re-sort the results", ->
318
+ respondWith server, "/api/time_entries?order=created_at%3Adesc&per_page=2&page=1", data: { results: resultsArray("time_entries", [te2Ws11, te1Ws11]), time_entries: [te1Ws11, te2Ws11] }
319
+ # it's really created_at:asc
320
+ collection = base.data.loadCollection "time_entries", order: "created_at:desc", page: 1, perPage: 2
321
+ server.respond()
322
+ expect(collection.pluck("id")).toEqual [te2Ws11.id, te1Ws11.id]
323
+
324
+ it "seperately caches data requested by different sort orders and filters", ->
325
+ server.responses = []
326
+ respondWith server, "/api/time_entries?include=project%2Ctask&order=updated_at%3Adesc&project_id=10&per_page=2&page=1",
327
+ data: { results: resultsArray("time_entries", [te2Ws10, te1Ws10]), time_entries: [te2Ws10, te1Ws10], tasks: [], projects: [ws10] }
328
+ respondWith server, "/api/time_entries?include=project%2Ctask&order=updated_at%3Adesc&project_id=11&per_page=2&page=1",
329
+ data: { results: resultsArray("time_entries", [te1Ws11, te2Ws11]), time_entries: [te1Ws11, te2Ws11], tasks: [], projects: [ws11] }
330
+ respondWith server, "/api/time_entries?include=project%2Ctask&order=created_at%3Aasc&project_id=11&per_page=2&page=1",
331
+ data: { results: resultsArray("time_entries", [te2Ws11, te1Ws11]), time_entries: [te2Ws11, te1Ws11], tasks: [], projects: [ws11] }
332
+ respondWith server, "/api/time_entries?include=project%2Ctask&order=created_at%3Aasc&per_page=4&page=1",
333
+ data: { results: resultsArray("time_entries", [te2Ws11, te1Ws11, te1Ws10, te2Ws10]), time_entries: [te2Ws11, te1Ws11, te1Ws10, te2Ws10], tasks: [], projects: [ws10, ws11] }
334
+ respondWith server, "/api/time_entries?include=project%2Ctask&per_page=4&page=1",
335
+ data: { results: resultsArray("time_entries", [te1Ws11, te2Ws10, te1Ws10, te2Ws11]), time_entries: [te1Ws11, te2Ws10, te1Ws10, te2Ws11], tasks: [], projects: [ws10, ws11] }
336
+ # Make a server request
337
+ collection1 = base.data.loadCollection "time_entries", include: ["project", "task"], order: "updated_at:desc", filters: { project_id: 10 }, page: 1, perPage: 2
338
+ expect(collection1.loaded).toBe false
339
+ server.respond()
340
+ expect(collection1.loaded).toBe true
341
+ expect(collection1.pluck("id")).toEqual [te2Ws10.id, te1Ws10.id] # Show that it came back in the explicit order setup above
342
+ # Make another request, this time handled by the cache.
343
+ collection2 = base.data.loadCollection "time_entries", include: ["project", "task"], order: "updated_at:desc", filters: { project_id: 10 }, page: 1, perPage: 2
344
+ expect(collection2.loaded).toBe true
345
+
346
+ # Do it again, this time with a different filter.
347
+ collection3 = base.data.loadCollection "time_entries", include: ["project", "task"], order: "updated_at:desc", filters: { project_id: 11 }, page: 1, perPage: 2
348
+ expect(collection3.loaded).toBe false
349
+ server.respond()
350
+ expect(collection3.loaded).toBe true
351
+ expect(collection3.pluck("id")).toEqual [te1Ws11.id, te2Ws11.id]
352
+ collection4 = base.data.loadCollection "time_entries", include: ["project"], order: "updated_at:desc", filters: { project_id: 11 }, page: 1, perPage: 2
353
+ expect(collection4.loaded).toBe true
354
+ expect(collection4.pluck("id")).toEqual [te1Ws11.id, te2Ws11.id]
355
+
356
+ # Do it again, this time with a different order.
357
+ collection5 = base.data.loadCollection "time_entries", include: ["project", "task"], order: "created_at:asc", filters: { project_id: 11 } , page: 1, perPage: 2
358
+ expect(collection5.loaded).toBe false
359
+ server.respond()
360
+ expect(collection5.loaded).toBe true
361
+ expect(collection5.pluck("id")).toEqual [te2Ws11.id, te1Ws11.id]
362
+ collection6 = base.data.loadCollection "time_entries", include: ["task"], order: "created_at:asc", filters: { project_id: 11 }, page: 1, perPage: 2
363
+ expect(collection6.loaded).toBe true
364
+ expect(collection6.pluck("id")).toEqual [te2Ws11.id, te1Ws11.id]
365
+
366
+ # Do it again, this time without a filter.
367
+ collection7 = base.data.loadCollection "time_entries", include: ["project", "task"], order: "created_at:asc", page: 1, perPage: 4
368
+ expect(collection7.loaded).toBe false
369
+ server.respond()
370
+ expect(collection7.loaded).toBe true
371
+ expect(collection7.pluck("id")).toEqual [te2Ws11.id, te1Ws11.id, te1Ws10.id, te2Ws10.id]
372
+
373
+ # Do it again, this time without an order, so it should use the default (updated_at:desc).
374
+ collection9 = base.data.loadCollection "time_entries", include: ["project", "task"], page: 1, perPage: 4
375
+ expect(collection9.loaded).toBe false
376
+ server.respond()
377
+ expect(collection9.loaded).toBe true
378
+ expect(collection9.pluck("id")).toEqual [te1Ws11.id, te2Ws10.id, te1Ws10.id, te2Ws11.id]
379
+
380
+ describe "handling of only", ->
381
+ describe "when getting data from the server", ->
382
+ it "returns the requested ids with includes, triggering reset and success", ->
383
+ respondWith server, "/api/time_entries?include=project%2Ctask&only=2",
384
+ resultsFrom: "time_entries", data: { time_entries: [buildTimeEntry(task_id: null, project_id: 10, id: 2)], tasks: [], projects: [buildProject(id: 10)] }
385
+ spy2 = jasmine.createSpy().andCallFake (collection) ->
386
+ expect(collection.loaded).toBe true
387
+ collection = base.data.loadCollection "time_entries", include: ["project", "task"], only: 2, success: spy2
388
+ spy = jasmine.createSpy().andCallFake ->
389
+ expect(collection.loaded).toBe true
390
+ expect(collection.get(2).get('task')).toBeFalsy()
391
+ expect(collection.get(2).get('project').id).toEqual "10"
392
+ expect(collection.length).toEqual 1
393
+ collection.bind "reset", spy
394
+ expect(collection.loaded).toBe false
395
+ server.respond()
396
+ expect(collection.loaded).toBe true
397
+ expect(spy).toHaveBeenCalled()
398
+ expect(spy2).toHaveBeenCalled()
399
+
400
+ it "only requests ids that we don't already have", ->
401
+ respondWith server, "/api/time_entries?include=project%2Ctask&only=2",
402
+ resultsFrom: "time_entries", data: { time_entries: [buildTimeEntry(task_id: null, project_id: 10, id: 2)], tasks: [], projects: [buildProject(id: 10)] }
403
+ respondWith server, "/api/time_entries?include=project%2Ctask&only=3",
404
+ resultsFrom: "time_entries", data: { time_entries: [buildTimeEntry(task_id: null, project_id: 11, id: 3)], tasks: [], projects: [buildProject(id: 11)] }
405
+
406
+ collection = base.data.loadCollection "time_entries", include: ["project", "task"], only: 2
407
+ expect(collection.loaded).toBe false
408
+ server.respond()
409
+ expect(collection.loaded).toBe true
410
+ expect(collection.get(2).get('project').id).toEqual "10"
411
+ collection2 = base.data.loadCollection "time_entries", include: ["project", "task"], only: ["2", "3"]
412
+ expect(collection2.loaded).toBe false
413
+ server.respond()
414
+ expect(collection2.loaded).toBe true
415
+ expect(collection2.length).toEqual 2
416
+ expect(collection2.get(2).get('project').id).toEqual "10"
417
+ expect(collection2.get(3).get('project').id).toEqual "11"
418
+
419
+ it "does request ids from the server again when they don't have all associations loaded yet", ->
420
+ respondWith server, "/api/time_entries?include=project&only=2",
421
+ resultsFrom: "time_entries", data: { time_entries: [buildTimeEntry(project_id: 10, id: 2, task_id: 5)], projects: [buildProject(id: 10)] }
422
+ respondWith server, "/api/time_entries?include=project%2Ctask&only=2",
423
+ resultsFrom: "time_entries", data: { time_entries: [buildTimeEntry(project_id: 10, id: 2, task_id: 5)], tasks: [buildTask(id: 5)], projects: [buildProject(id: 10)] }
424
+ respondWith server, "/api/time_entries?include=project%2Ctask&only=3",
425
+ resultsFrom: "time_entries", data: { time_entries: [buildTimeEntry(project_id: 11, id: 3, task_id: null)], tasks: [], projects: [buildProject(id: 11)] }
426
+
427
+ base.data.loadCollection "time_entries", include: ["project"], only: 2
428
+ server.respond()
429
+ base.data.loadCollection "time_entries", include: ["project", "task"], only: 3
430
+ server.respond()
431
+ collection2 = base.data.loadCollection "time_entries", include: ["project", "task"], only: [2, 3]
432
+ expect(collection2.loaded).toBe false
433
+ server.respond()
434
+ expect(collection2.loaded).toBe true
435
+ expect(collection2.get(2).get('task').id).toEqual "5"
436
+ expect(collection2.length).toEqual 2
437
+
438
+ it "doesn't go to the server if it doesn't need to", ->
439
+ respondWith server, "/api/time_entries?include=project%2Ctask&only=2%2C3",
440
+ resultsFrom: "time_entries", data: { time_entries: [buildTimeEntry(project_id: 10, id: 2, task_id: null), buildTimeEntry(project_id: 11, id: 3)], tasks: [], projects: [buildProject(id: 10), buildProject(id: 11)] }
441
+ collection = base.data.loadCollection "time_entries", include: ["project", "task"], only: [2, 3]
442
+ expect(collection.loaded).toBe false
443
+ server.respond()
444
+ expect(collection.loaded).toBe true
445
+ expect(collection.get(2).get('project').id).toEqual "10"
446
+ expect(collection.get(3).get('project').id).toEqual "11"
447
+ expect(collection.length).toEqual 2
448
+ spy = jasmine.createSpy()
449
+ collection2 = base.data.loadCollection "time_entries", include: ["project"], only: [2, 3], success: spy
450
+ expect(spy).toHaveBeenCalled()
451
+ expect(collection2.loaded).toBe true
452
+ expect(collection2.get(2).get('project').id).toEqual "10"
453
+ expect(collection2.get(3).get('project').id).toEqual "11"
454
+ expect(collection2.length).toEqual 2
455
+
456
+ it "returns an empty collection when passed in an empty array", ->
457
+ timeEntries = [buildTimeEntry(task_id: 2, project_id: 15, id: 1), buildTimeEntry(project_id: 10, id: 2)]
458
+ respondWith server, "/api/time_entries?per_page=20&page=1", resultsFrom: "time_entries", data: { time_entries: timeEntries }
459
+ collection = base.data.loadCollection "time_entries", only: []
460
+ expect(collection.loaded).toBe true
461
+ expect(collection.length).toEqual 0
462
+
463
+ collection = base.data.loadCollection "time_entries", only: null
464
+ server.respond()
465
+ expect(collection.loaded).toBe true
466
+ expect(collection.length).toEqual 2
467
+
468
+ it "accepts a success function that gets triggered on cache hit", ->
469
+ respondWith server, "/api/time_entries?include=project%2Ctask&only=2%2C3",
470
+ resultsFrom: "time_entries", data: { time_entries: [buildTimeEntry(project_id: 10, id: 2, task_id: null), buildTimeEntry(project_id: 11, id: 3, task_id: null)], tasks: [], projects: [buildProject(id: 10), buildProject(id: 11)] }
471
+ base.data.loadCollection "time_entries", include: ["project", "task"], only: [2, 3]
472
+ server.respond()
473
+ spy = jasmine.createSpy().andCallFake (collection) ->
474
+ expect(collection.loaded).toBe true
475
+ expect(collection.get(2).get('project').id).toEqual "10"
476
+ expect(collection.get(3).get('project').id).toEqual "11"
477
+ collection2 = base.data.loadCollection "time_entries", include: ["project"], only: [2, 3], success: spy
478
+ expect(spy).toHaveBeenCalled()
479
+
480
+ it "does not cache only queries", ->
481
+ respondWith server, "/api/time_entries?include=project%2Ctask&only=2%2C3",
482
+ resultsFrom: "time_entries", data: { time_entries: [buildTimeEntry(project_id: 10, id: 2, task_id: null), buildTimeEntry(project_id: 11, id: 3, task_id: null)], tasks: [], projects: [buildProject(id: 10), buildProject(id: 11)] }
483
+ collection = base.data.loadCollection "time_entries", include: ["project", "task"], only: [2, 3]
484
+ expect(Object.keys base.data.getCollectionDetails("time_entries")["cache"]).toEqual []
485
+ server.respond()
486
+ expect(Object.keys base.data.getCollectionDetails("time_entries")["cache"]).toEqual []
487
+
488
+ it "does go to the server on a repeat request if an association is missing", ->
489
+ respondWith server, "/api/time_entries?include=project&only=2",
490
+ resultsFrom: "time_entries", data: { time_entries: [buildTimeEntry(project_id: 10, id: 2, task_id: 6)], projects: [buildProject(id: 10)] }
491
+ respondWith server, "/api/time_entries?include=project%2Ctask&only=2",
492
+ resultsFrom: "time_entries", data: { time_entries: [buildTimeEntry(project_id: 10, id: 2, task_id: 6)], tasks: [buildTask(id: 6)], projects: [buildProject(id: 10)] }
493
+ collection = base.data.loadCollection "time_entries", include: ["project"], only: 2
494
+ expect(collection.loaded).toBe false
495
+ server.respond()
496
+ expect(collection.loaded).toBe true
497
+ collection2 = base.data.loadCollection "time_entries", include: ["project", "task"], only: 2
498
+ expect(collection2.loaded).toBe false
499
+
500
+ describe "disabling caching", ->
501
+ item = null
502
+
503
+ beforeEach ->
504
+ item = buildTask()
505
+ respondWith server, "/api/tasks.json?per_page=20&page=1", resultsFrom: "tasks", data: { tasks: [item] }
506
+
507
+ it "goes to server even if we have matching items in cache", ->
508
+ syncSpy = spyOn(Backbone, 'sync')
509
+ collection = base.data.loadCollection "tasks", cache: false, only: item.id
510
+ expect(syncSpy).toHaveBeenCalled()
511
+
512
+ it "still adds results to the cache", ->
513
+ spy = spyOn(base.data.storage('tasks'), 'update')
514
+ collection = base.data.loadCollection "tasks", cache: false
515
+ server.respond()
516
+ expect(spy).toHaveBeenCalled()
517
+
518
+ describe "searching", ->
519
+ it 'turns off caching', ->
520
+ spy = spyOn(base.data, '_loadCollectionWithFirstLayer')
521
+ collection = base.data.loadCollection "tasks", search: "the meaning of life"
522
+ expect(spy.mostRecentCall.args[0]['cache']).toBe(false)
523
+
524
+ it "returns the matching items with includes, triggering reset and success", ->
525
+ task = buildTask()
526
+ respondWith server, "/api/tasks.json?per_page=20&page=1&search=go+go+gadget+search",
527
+ data: { results: [{key: "tasks", id: task.id}], tasks: [task] }
528
+ spy2 = jasmine.createSpy().andCallFake (collection) ->
529
+ expect(collection.loaded).toBe true
530
+ collection = base.data.loadCollection "tasks", search: "go go gadget search", success: spy2
531
+ spy = jasmine.createSpy().andCallFake ->
532
+ expect(collection.loaded).toBe true
533
+ collection.bind "reset", spy
534
+ expect(collection.loaded).toBe false
535
+ server.respond()
536
+ expect(collection.loaded).toBe true
537
+ expect(spy).toHaveBeenCalled()
538
+ expect(spy2).toHaveBeenCalled()
539
+
540
+ 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: [] }
542
+ collection = base.data.loadCollection "tasks", search: "go go gadget search"
543
+ server.respond()
544
+
545
+ 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: [] }
547
+ collection = base.data.loadCollection "tasks", search: ""
548
+ server.respond()
549
+
550
+ describe "createNewCollection", ->
551
+ it "makes a new collection of the appropriate type", ->
552
+ expect(base.data.createNewCollection("tasks", [buildTask(), buildTask()]) instanceof App.Collections.Tasks).toBe true
553
+
554
+ it "can accept a 'loaded' flag", ->
555
+ collection = base.data.createNewCollection("tasks", [buildTask(), buildTask()])
556
+ expect(collection.loaded).toBe false
557
+ collection = base.data.createNewCollection("tasks", [buildTask(), buildTask()], loaded: true)
558
+ expect(collection.loaded).toBe true
559
+
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
+ describe "error handling", ->
581
+ describe "setting a storage manager default error handler", ->
582
+ it "allows an error interceptor to be set on construction", ->
583
+ interceptor = (handler, modelOrCollection, options, jqXHR, requestParams) -> 5
584
+ manager = new Brainstem.StorageManager(errorInterceptor: interceptor)
585
+ expect(manager.errorInterceptor).toEqual interceptor
586
+
587
+ it "allows an error interceptor to be set later", ->
588
+ spy = jasmine.createSpy()
589
+ base.data.setErrorInterceptor (handler, modelOrCollection, options, jqXHR) -> spy(modelOrCollection, jqXHR)
590
+ server.respondWith "GET", "/api/time_entries?per_page=20&page=1", [ 401, {"Content-Type": "application/json"}, JSON.stringify({ errors: ["Invalid OAuth 2 Request"]}) ]
591
+ base.data.loadCollection 'time_entries'
592
+ server.respond()
593
+ expect(spy).toHaveBeenCalled()
594
+
595
+ describe "passing in a custom error handler when loading a collection", ->
596
+ it "gets called when there is an error", ->
597
+ customHandler = jasmine.createSpy('customHandler')
598
+ server.respondWith "GET", "/api/time_entries?per_page=20&page=1", [ 401, {"Content-Type": "application/json"}, JSON.stringify({ errors: ["Invalid OAuth 2 Request"]}) ]
599
+ base.data.loadCollection('time_entries', error: customHandler)
600
+ server.respond()
601
+ expect(customHandler).toHaveBeenCalled()
602
+
603
+ describe "when no storage manager error interceptor is given", ->
604
+ it "has a default error interceptor", ->
605
+ manager = new Brainstem.StorageManager()
606
+ expect(manager.errorInterceptor).not.toBeUndefined()
607
+
608
+ it "does nothing on unhandled errors", ->
609
+ spyOn(sinon, 'logError').andCallThrough()
610
+ server.respondWith "GET", "/api/time_entries?per_page=20&page=1", [ 401, {"Content-Type": "application/json"}, JSON.stringify({ errors: ["Invalid OAuth 2 Request"]}) ]
611
+ base.data.loadCollection 'time_entries'
612
+ server.respond()
613
+ expect(sinon.logError).not.toHaveBeenCalled()
Binary file