brainstem-js 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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,283 @@
1
+ describe 'Brainstem.Model', ->
2
+ model = null
3
+
4
+ beforeEach ->
5
+ model = new App.Models.Task()
6
+
7
+ describe 'parse', ->
8
+ response = null
9
+
10
+ beforeEach ->
11
+ response = count: 1, results: [id: 1, key: 'tasks'], tasks: { 1: { id: 1, title: 'Do Work' } }
12
+
13
+ it "extracts object data from JSON with root keys", ->
14
+ parsed = model.parse(response)
15
+ expect(parsed.id).toEqual(1)
16
+
17
+ it "passes through object data from flat JSON", ->
18
+ parsed = model.parse({id: 1})
19
+ expect(parsed.id).toEqual(1)
20
+
21
+ it 'should update the storage manager with the new model and its associations', ->
22
+ response.tasks[1].assignee_ids = [5, 6]
23
+ response.users = { 5: {id: 5, name: 'Jon'}, 6: {id: 6, name: 'Betty'} }
24
+
25
+ model.parse(response)
26
+
27
+ expect(base.data.storage('tasks').get(1).attributes).toEqual(response.tasks[1])
28
+ expect(base.data.storage('users').get(5).attributes).toEqual(response.users[5])
29
+ expect(base.data.storage('users').get(6).attributes).toEqual(response.users[6])
30
+
31
+ it 'should work with an empty response', ->
32
+ expect( -> model.parse(tasks: {}, results: [], count: 0)).not.toThrow()
33
+
34
+ describe 'updateStorageManager', ->
35
+ it 'should update the associations before the new model', ->
36
+ response.tasks[1].assignee_ids = [5]
37
+ response.users = { 5: {id: 5, name: 'Jon'} }
38
+
39
+ spy = spyOn(base.data, 'storage').andCallThrough()
40
+ model.updateStorageManager(response)
41
+ expect(spy.calls[0].args[0]).toEqual('users')
42
+ expect(spy.calls[1].args[0]).toEqual('tasks')
43
+
44
+ it 'should work with an empty response', ->
45
+ expect( -> model.updateStorageManager(count: 0, results: [])).not.toThrow()
46
+
47
+ it 'should return the first object from the result set', ->
48
+ response.tasks[2] = (id: 2, title: 'foo')
49
+ response.results.unshift(id: 2, key: 'tasks')
50
+ parsed = model.parse(response)
51
+ expect(parsed.id).toEqual 2
52
+ expect(parsed.title).toEqual 'foo'
53
+
54
+ it 'should not blow up on server side validation error', ->
55
+ response = errors: ["Invalid task state. Valid states are:'notstarted','started',and'completed'."]
56
+ expect(-> model.parse(response)).not.toThrow()
57
+
58
+ describe 'date handling', ->
59
+ it "parses ISO 8601 dates into date objects / milliseconds", ->
60
+ parsed = model.parse({created_at: "2013-01-25T11:25:57-08:00"})
61
+ expect(parsed.created_at).toEqual(1359141957000)
62
+
63
+ it "passes through dates in milliseconds already", ->
64
+ parsed = model.parse({created_at: 1359142047000})
65
+ expect(parsed.created_at).toEqual(1359142047000)
66
+
67
+ it 'parses dates on associated models', ->
68
+ response.tasks[1].created_at = "2013-01-25T11:25:57-08:00"
69
+ response.tasks[1].assignee_ids = [5, 6]
70
+ response.users = { 5: {id: 5, name: 'John', created_at: "2013-02-25T11:25:57-08:00"}, 6: {id: 6, name: 'Betty', created_at: "2013-01-30T11:25:57-08:00"} }
71
+
72
+ parsed = model.parse(response)
73
+ expect(parsed.created_at).toEqual(1359141957000)
74
+ expect(base.data.storage('users').get(5).get('created_at')).toEqual(1361820357000)
75
+ expect(base.data.storage('users').get(6).get('created_at')).toEqual(1359573957000)
76
+
77
+ describe 'setLoaded', ->
78
+ it "should set the values of @loaded", ->
79
+ model.setLoaded true
80
+ expect(model.loaded).toEqual(true)
81
+ model.setLoaded false
82
+ expect(model.loaded).toEqual(false)
83
+
84
+ it "triggers 'loaded' when becoming true", ->
85
+ spy = jasmine.createSpy()
86
+ model.bind "loaded", spy
87
+ model.setLoaded false
88
+ expect(spy).not.toHaveBeenCalled()
89
+ model.setLoaded true
90
+ expect(spy).toHaveBeenCalled()
91
+
92
+ it "doesn't trigger loaded if trigger: false is provided", ->
93
+ spy = jasmine.createSpy()
94
+ model.bind "loaded", spy
95
+ model.setLoaded true, trigger: false
96
+ expect(spy).not.toHaveBeenCalled()
97
+
98
+ it "returns self", ->
99
+ spy = jasmine.createSpy()
100
+ model.bind "loaded", spy
101
+ model.setLoaded true
102
+ expect(spy).toHaveBeenCalledWith(model)
103
+
104
+ describe 'associations', ->
105
+ describe 'associationDetails', ->
106
+
107
+ class TestClass extends Brainstem.Model
108
+ @associations:
109
+ my_users: ["storage_system_collection_name"]
110
+ my_user: "users"
111
+ user: "users"
112
+ users: ["users"]
113
+
114
+ it "returns a hash containing the key, type and plural of the association", ->
115
+ testClass = new TestClass()
116
+ expect(TestClass.associationDetails('my_users')).toEqual key: "my_user_ids", type: "HasMany", collectionName: "storage_system_collection_name"
117
+ expect(TestClass.associationDetails('my_user')).toEqual key: "my_user_id", type: "BelongsTo", collectionName: "users"
118
+ expect(TestClass.associationDetails('user')).toEqual key: "user_id", type: "BelongsTo", collectionName: "users"
119
+ expect(TestClass.associationDetails('users')).toEqual key: "user_ids", type: "HasMany", collectionName: "users"
120
+
121
+ expect(testClass.constructor.associationDetails('users')).toEqual key: "user_ids", type: "HasMany", collectionName: "users"
122
+
123
+ it "is cached on the class for speed", ->
124
+ original = TestClass.associationDetails('my_users')
125
+ TestClass.associations.my_users = "something_else"
126
+ expect(TestClass.associationDetails('my_users')).toEqual original
127
+
128
+ it "returns falsy if the association cannot be found", ->
129
+ expect(TestClass.associationDetails("I'mNotAThing")).toBeFalsy()
130
+
131
+ describe 'associationsAreLoaded', ->
132
+ describe "with BelongsTo associations", ->
133
+ it "should return true when all provided associations are loaded for the model", ->
134
+ timeEntry = new App.Models.TimeEntry(id: 5, project_id: 10, task_id: 2)
135
+ expect(timeEntry.associationsAreLoaded(["project", "task"])).toBeFalsy()
136
+ buildAndCacheProject( id: 10, title: "a project!")
137
+ expect(timeEntry.associationsAreLoaded(["project", "task"])).toBeFalsy()
138
+ expect(timeEntry.associationsAreLoaded(["project"])).toBeTruthy()
139
+ buildAndCacheTask(id: 2, title: "a task!")
140
+ expect(timeEntry.associationsAreLoaded(["project", "task"])).toBeTruthy()
141
+ expect(timeEntry.associationsAreLoaded(["project"])).toBeTruthy()
142
+ expect(timeEntry.associationsAreLoaded(["task"])).toBeTruthy()
143
+
144
+ it "should default to all of the associations defined on the model", ->
145
+ timeEntry = new App.Models.TimeEntry(id: 5, project_id: 10, task_id: 2, user_id: 666)
146
+ expect(timeEntry.associationsAreLoaded()).toBeFalsy()
147
+ buildAndCacheProject(id: 10, title: "a project!")
148
+ expect(timeEntry.associationsAreLoaded()).toBeFalsy()
149
+ buildAndCacheTask(id: 2, title: "a task!")
150
+ expect(timeEntry.associationsAreLoaded()).toBeFalsy()
151
+ buildAndCacheUser(id:666)
152
+ expect(timeEntry.associationsAreLoaded()).toBeTruthy()
153
+
154
+ it "should appear loaded when an association is null, but not loaded when the key is missing", ->
155
+ timeEntry = buildAndCacheTimeEntry()
156
+ delete timeEntry.attributes.project_id
157
+ expect(timeEntry.associationsAreLoaded(["project"])).toBeFalsy()
158
+ timeEntry = new App.Models.TimeEntry(id: 5, project_id: null)
159
+ expect(timeEntry.associationsAreLoaded(["project"])).toBeTruthy()
160
+ timeEntry = new App.Models.TimeEntry(id: 5, project_id: 2)
161
+ expect(timeEntry.associationsAreLoaded(["project"])).toBeFalsy()
162
+
163
+ describe "with HasMany associations", ->
164
+ it "should return true when all provided associations are loaded", ->
165
+ project = new App.Models.Project(id: 5, time_entry_ids: [10, 11], task_ids: [2, 3])
166
+ expect(project.associationsAreLoaded(["time_entries", "tasks"])).toBeFalsy()
167
+ buildAndCacheTimeEntry(id: 10)
168
+ expect(project.associationsAreLoaded(["time_entries"])).toBeFalsy()
169
+ buildAndCacheTimeEntry(id: 11)
170
+ expect(project.associationsAreLoaded(["time_entries"])).toBeTruthy()
171
+ expect(project.associationsAreLoaded(["time_entries", "tasks"])).toBeFalsy()
172
+ expect(project.associationsAreLoaded(["tasks"])).toBeFalsy()
173
+ buildAndCacheTask(id: 2)
174
+ expect(project.associationsAreLoaded(["tasks"])).toBeFalsy()
175
+ buildAndCacheTask(id: 3)
176
+ expect(project.associationsAreLoaded(["tasks"])).toBeTruthy()
177
+ expect(project.associationsAreLoaded(["tasks", "time_entries"])).toBeTruthy()
178
+
179
+ it "should appear loaded when an association is an empty array, but not loaded when the key is missing", ->
180
+ project = new App.Models.Project(id: 5, time_entry_ids: [])
181
+ expect(project.associationsAreLoaded(["time_entries"])).toBeTruthy()
182
+ expect(project.associationsAreLoaded(["tasks"])).toBeFalsy()
183
+
184
+ describe "get", ->
185
+ it "should delegate to Backbone.Model#get for anything that is not an association", ->
186
+ timeEntry = new App.Models.TimeEntry(id: 5, project_id: 10, task_id: 2, title: "foo")
187
+ expect(timeEntry.get("title")).toEqual "foo"
188
+ expect(timeEntry.get("missing")).toBeUndefined()
189
+
190
+ describe "BelongsTo associations", ->
191
+ it "should return associations", ->
192
+ timeEntry = new App.Models.TimeEntry(id: 5, project_id: 10, task_id: 2)
193
+ expect(-> timeEntry.get("project")).toThrow()
194
+ base.data.storage("projects").add { id: 10, title: "a project!" }
195
+ expect(timeEntry.get("project").get("title")).toEqual "a project!"
196
+ expect(timeEntry.get("project")).toEqual base.data.storage("projects").get(10)
197
+
198
+ it "should return null when we don't have an association id", ->
199
+ timeEntry = new App.Models.TimeEntry(id: 5, task_id: 2)
200
+ expect(timeEntry.get("project")).toBeFalsy()
201
+
202
+ it "should throw when we have an association id but it cannot be found", ->
203
+ timeEntry = new App.Models.TimeEntry(id: 5, task_id: 2)
204
+ expect(-> timeEntry.get("task")).toThrow()
205
+
206
+ describe "HasMany associations", ->
207
+ it "should return HasMany associations", ->
208
+ project = new App.Models.Project(id: 5, time_entry_ids: [2, 5])
209
+ expect(-> project.get("time_entries")).toThrow()
210
+ base.data.storage("time_entries").add buildTimeEntry(id: 2, project_id: 5, title: "first time entry")
211
+ base.data.storage("time_entries").add buildTimeEntry(id: 5, project_id: 5, title: "second time entry")
212
+ expect(project.get("time_entries").get(2).get("title")).toEqual "first time entry"
213
+ expect(project.get("time_entries").get(5).get("title")).toEqual "second time entry"
214
+
215
+ it "should return null when we don't have any association ids", ->
216
+ project = new App.Models.Project(id: 5)
217
+ expect(project.get("time_entries").models).toEqual []
218
+
219
+ it "should throw when we have an association id but it cannot be found", ->
220
+ project = new App.Models.Project(id: 5, time_entry_ids: [2, 5])
221
+ expect(-> project.get("time_entries")).toThrow()
222
+
223
+ it "should apply a sort order to has many associations if it is provided at time of get", ->
224
+ task = buildAndCacheTask(id: 5, sub_task_ids: [103, 77, 99])
225
+ buildAndCacheTask(id:103 , position: 3, updated_at: 845785)
226
+ buildAndCacheTask(id:77 , position: 2, updated_at: 995785)
227
+ buildAndCacheTask(id:99 , position: 1, updated_at: 635785)
228
+
229
+ subTasks = task.get("sub_tasks")
230
+ expect(subTasks.at(0).get('position')).toEqual(3)
231
+ expect(subTasks.at(1).get('position')).toEqual(2)
232
+ expect(subTasks.at(2).get('position')).toEqual(1)
233
+
234
+ subTasks = task.get("sub_tasks", order: "position:asc")
235
+ expect(subTasks.at(0).get('position')).toEqual(1)
236
+ expect(subTasks.at(1).get('position')).toEqual(2)
237
+ expect(subTasks.at(2).get('position')).toEqual(3)
238
+
239
+ subTasks = task.get("sub_tasks", order: "updated_at:desc")
240
+ expect(subTasks.at(0).get('id')).toEqual("77")
241
+ expect(subTasks.at(1).get('id')).toEqual("103")
242
+ expect(subTasks.at(2).get('id')).toEqual("99")
243
+
244
+ describe "toServerJSON", ->
245
+ it "calls toJSON", ->
246
+ spy = spyOn(model, "toJSON").andCallThrough()
247
+ model.toServerJSON()
248
+ expect(spy).toHaveBeenCalled()
249
+
250
+ it "always removes default blacklisted keys", ->
251
+ defaultBlacklistKeys = model.defaultJSONBlacklist()
252
+ expect(defaultBlacklistKeys.length).toEqual(3)
253
+
254
+ model.set('safe', true)
255
+ for key in defaultBlacklistKeys
256
+ model.set(key, true)
257
+
258
+ json = model.toServerJSON("create")
259
+ expect(json['safe']).toEqual(true)
260
+ for key in defaultBlacklistKeys
261
+ expect(json[key]).toBeUndefined()
262
+
263
+ it "removes blacklisted keys for create actions", ->
264
+ createBlacklist = ['flies', 'ants', 'fire ants']
265
+ spyOn(model, 'createJSONBlacklist').andReturn(createBlacklist)
266
+
267
+ for key in createBlacklist
268
+ model.set(key, true)
269
+
270
+ json = model.toServerJSON("create")
271
+ for key in createBlacklist
272
+ expect(json[key]).toBeUndefined()
273
+
274
+ it "removes blacklisted keys for update actions", ->
275
+ updateBlacklist = ['possums', 'racoons', 'potatoes']
276
+ spyOn(model, 'updateJSONBlacklist').andReturn(updateBlacklist)
277
+
278
+ for key in updateBlacklist
279
+ model.set(key, true)
280
+
281
+ json = model.toServerJSON("update")
282
+ for key in updateBlacklist
283
+ expect(json[key]).toBeUndefined()
@@ -0,0 +1,22 @@
1
+ describe "Brainstem.Sync", ->
2
+ describe "updating models", ->
3
+ ajaxSpy = null
4
+
5
+ beforeEach ->
6
+ ajaxSpy = spyOn($, 'ajax')
7
+
8
+ it "should use toServerJSON instead of toJSON", ->
9
+ modelSpy = spyOn(Brainstem.Model.prototype, 'toServerJSON')
10
+ model = buildTimeEntry()
11
+ model.save()
12
+ expect(modelSpy).toHaveBeenCalled()
13
+
14
+ it "should pass options.includes through the JSON", ->
15
+ model = buildTimeEntry()
16
+ model.save({}, include: 'creator')
17
+ expect(ajaxSpy.mostRecentCall.args[0].data).toMatch(/"include":"creator"/)
18
+
19
+ it "should setup param roots when models have a paramRoot set", ->
20
+ model = buildTimeEntry()
21
+ model.save({})
22
+ expect(ajaxSpy.mostRecentCall.args[0].data).toMatch(/"time_entry":{/)
@@ -0,0 +1,12 @@
1
+ describe 'Brainstem Utils', ->
2
+ describe "matches", ->
3
+ it "should recursively compare objects and arrays", ->
4
+ expect(Brainstem.Utils.matches(2, 2)).toBe true
5
+ expect(Brainstem.Utils.matches([2], [2])).toBe true, '[2], [2]'
6
+ expect(Brainstem.Utils.matches([2, 3], [2])).toBe false
7
+ expect(Brainstem.Utils.matches([2, 3], [2, 3])).toBe true, '[2, 3], [2, 3]'
8
+ expect(Brainstem.Utils.matches({ hi: "there" }, { hi: "there" })).toBe true, '{ hi: "there" }, { hi: "there" }'
9
+ expect(Brainstem.Utils.matches([2, { hi: "there" }], [2, { hi: 2 }])).toBe false
10
+ expect(Brainstem.Utils.matches([2, { hi: "there" }], [2, { hi: "there" }])).toBe true, '[2, { hi: "there" }], [2, { hi: "there" }]'
11
+ expect(Brainstem.Utils.matches([2, { hi: ["there", 3] }], [2, { hi: ["there", 2] }])).toBe false
12
+ expect(Brainstem.Utils.matches([2, { hi: ["there", 2] }], [2, { hi: ["there", 2] }])).toBe true, '[2, { hi: ["there", 2] }], [2, { hi: ["there", 2] }]'
@@ -0,0 +1,209 @@
1
+ describe 'Brainstem Expectations', ->
2
+ manager = project1 = project2 = task1 = null
3
+
4
+ beforeEach ->
5
+ manager = base.data
6
+ manager.enableExpectations()
7
+
8
+ project1 = buildProject(id: 1, task_ids: [1])
9
+ project2 = buildProject(id: 2)
10
+ task1 = buildTask(id: 1, project_id: project1.id)
11
+
12
+ describe "stubbing responses", ->
13
+ it "should update returned collections", ->
14
+ expectation = manager.stub "projects", response: (stub) ->
15
+ stub.results = [project1, project2]
16
+ collection = manager.loadCollection "projects"
17
+ expect(collection.length).toEqual 0
18
+ expectation.respond()
19
+ expect(collection.length).toEqual 2
20
+ expect(collection.get(1)).toEqual project1
21
+ expect(collection.get(2)).toEqual project2
22
+
23
+ it "should call callbacks", ->
24
+ expectation = manager.stub "projects", response: (stub) ->
25
+ stub.results = [project1, project2]
26
+ collection = null
27
+ manager.loadCollection "projects", success: (c) -> collection = c
28
+ expect(collection).toBeNull()
29
+ expectation.respond()
30
+ expect(collection.length).toEqual 2
31
+ expect(collection.get(1)).toEqual project1
32
+ expect(collection.get(2)).toEqual project2
33
+
34
+ it "should add to passed-in collections", ->
35
+ expectation = manager.stub "projects", response: (stub) ->
36
+ stub.results = [project1, project2]
37
+ collection = new Brainstem.Collection()
38
+ manager.loadCollection "projects", collection: collection
39
+ expect(collection.length).toEqual 0
40
+ expectation.respond()
41
+ expect(collection.length).toEqual 2
42
+ expect(collection.get(1)).toEqual project1
43
+ expect(collection.get(2)).toEqual project2
44
+
45
+ it "should work with results hashes", ->
46
+ expectation = manager.stub "projects", response: (stub) ->
47
+ stub.results = [{ key: "projects", id: 2 }, { key: "projects", id: 1 }]
48
+ stub.associated.projects = [project1, project2]
49
+ collection = manager.loadCollection "projects"
50
+ expectation.respond()
51
+ expect(collection.length).toEqual 2
52
+ expect(collection.models[0]).toEqual project2
53
+ expect(collection.models[1]).toEqual project1
54
+
55
+ it "can populate associated objects", ->
56
+ expectation = manager.stub "projects", include: ["tasks"], response: (stub) ->
57
+ stub.results = [project1, project2]
58
+ stub.associated.projects = [project1, project2]
59
+ stub.associated.tasks = [task1]
60
+ collection = new Brainstem.Collection()
61
+ manager.loadCollection "projects", collection: collection, include: ["tasks"]
62
+ expectation.respond()
63
+ expect(collection.get(1).get("tasks").models).toEqual [task1]
64
+ expect(collection.get(2).get("tasks").models).toEqual []
65
+
66
+ describe "triggering errors", ->
67
+ it "triggers errors when asked to do so", ->
68
+ errorSpy = jasmine.createSpy()
69
+
70
+ collection = new Brainstem.Collection()
71
+
72
+ resp =
73
+ readyState: 4
74
+ status: 401
75
+ responseText: ""
76
+
77
+ expectation = manager.stub "projects", collection: collection, triggerError: resp
78
+
79
+ manager.loadCollection "projects", error: errorSpy
80
+
81
+ expectation.respond()
82
+ expect(errorSpy).toHaveBeenCalled()
83
+ expect(errorSpy.mostRecentCall.args[0] instanceof Brainstem.Collection).toBe true
84
+ expect(errorSpy.mostRecentCall.args[1]).toEqual resp
85
+
86
+ it "does not trigger errors when asked not to", ->
87
+ errorSpy = jasmine.createSpy()
88
+ expectation = manager.stub "projects", response: (exp) -> exp.results = [project1, project2]
89
+
90
+ manager.loadCollection "projects", error: errorSpy
91
+
92
+ expectation.respond()
93
+ expect(errorSpy).not.toHaveBeenCalled()
94
+
95
+ describe "responding immediately", ->
96
+ it "uses stubImmediate", ->
97
+ expectation = manager.stubImmediate "projects", include: ["tasks"], response: (stub) ->
98
+ stub.results = [project1, project2]
99
+ stub.associated.tasks = [task1]
100
+ collection = manager.loadCollection "projects", include: ["tasks"]
101
+ expect(collection.get(1).get("tasks").models).toEqual [task1]
102
+
103
+ describe "multiple stubs", ->
104
+ it "should match the first valid expectation", ->
105
+ manager.stubImmediate "projects", only: [1], response: (stub) ->
106
+ stub.results = [project1]
107
+ manager.stubImmediate "projects", response: (stub) ->
108
+ stub.results = [project1, project2]
109
+ manager.stubImmediate "projects", only: [2], response: (stub) ->
110
+ stub.results = [project2]
111
+ expect(manager.loadCollection("projects", only: 1).models).toEqual [project1]
112
+ expect(manager.loadCollection("projects").models).toEqual [project1, project2]
113
+ expect(manager.loadCollection("projects", only: 2).models).toEqual [project2]
114
+
115
+ it "should fail if it cannot find a specific match", ->
116
+ manager.stubImmediate "projects", response: (stub) ->
117
+ stub.results = [project1]
118
+ manager.stubImmediate "projects", include: ["tasks"], filters: { something: "else" }, response: (stub) ->
119
+ stub.results = [project1, project2]
120
+ stub.associated.tasks = [task1]
121
+ expect(manager.loadCollection("projects", include: ["tasks"], filters: { something: "else" }).models).toEqual [project1, project2]
122
+ expect(-> manager.loadCollection("projects", include: ["tasks"], filters: { something: "wrong" })).toThrow()
123
+ expect(-> manager.loadCollection("projects", include: ["users"], filters: { something: "else" })).toThrow()
124
+ expect(-> manager.loadCollection("projects", filters: { something: "else" })).toThrow()
125
+ expect(-> manager.loadCollection("projects", include: ["users"])).toThrow()
126
+ expect(manager.loadCollection("projects").models).toEqual [project1]
127
+
128
+ it "should ignore empty arrays", ->
129
+ manager.stubImmediate "projects", response: (stub) ->
130
+ stub.results = [project1, project2]
131
+ expect(manager.loadCollection("projects", include: []).models).toEqual [project1, project2]
132
+
133
+ it "should allow wildcard params", ->
134
+ manager.stubImmediate "projects", include: '*', response: (stub) ->
135
+ stub.results = [project1, project2]
136
+ expect(manager.loadCollection("projects", include: ["tasks"]).models).toEqual [project1, project2]
137
+ expect(manager.loadCollection("projects", include: ["users"]).models).toEqual [project1, project2]
138
+ expect(manager.loadCollection("projects").models).toEqual [project1, project2]
139
+
140
+ describe "recording", ->
141
+ it "should record options", ->
142
+ expectation = manager.stubImmediate "projects", filters: { something: "else" }, response: (stub) ->
143
+ stub.results = [project1, project2]
144
+ manager.loadCollection("projects", filters: { something: "else" })
145
+ expect(expectation.matches[0].filters).toEqual { something: "else" }
146
+
147
+ describe "clearing expectations", ->
148
+ it "expectations can be removed", ->
149
+ expectation = manager.stub "projects", include: ["tasks"], response: (stub) ->
150
+ stub.results = [project1, project2]
151
+ stub.associated.tasks = [task1]
152
+
153
+ collection = manager.loadCollection "projects", include: ["tasks"]
154
+ expectation.respond()
155
+ expect(collection.get(1).get("tasks").models).toEqual [task1]
156
+
157
+ collection2 = manager.loadCollection "projects", include: ["tasks"]
158
+ expect(collection2.get(1)).toBeFalsy()
159
+ expectation.respond()
160
+ expect(collection2.get(1).get("tasks").models).toEqual [task1]
161
+
162
+ expectation.remove()
163
+ expect(-> manager.loadCollection "projects").toThrow()
164
+
165
+ describe "lastMatch", ->
166
+ it "retrives the last match object", ->
167
+ expectation = manager.stubImmediate "projects", include: "*", response: (stub) ->
168
+ stub.results = []
169
+
170
+ manager.loadCollection("projects", include: ["tasks"])
171
+ manager.loadCollection("projects", include: ["users"])
172
+
173
+ expect(expectation.matches.length).toEqual(2)
174
+ expect(expectation.lastMatch().include).toEqual(["users"])
175
+
176
+ it "returns undefined if no matches exist", ->
177
+ expectation = manager.stub "projects", response: (stub) ->
178
+ stub.results = []
179
+ expect(expectation.lastMatch()).toBeUndefined()
180
+
181
+ describe "optionsMatch", ->
182
+ it "should ignore wrapping arrays", ->
183
+ expectation = new Brainstem.Expectation("projects", { include: "workspaces" }, manager)
184
+ expect(expectation.optionsMatch("projects", { include: "workspaces" })).toBe true
185
+ expect(expectation.optionsMatch("projects", { include: ["workspaces"] })).toBe true
186
+
187
+ it "should treat * as an any match", ->
188
+ expectation = new Brainstem.Expectation("projects", { include: "*" }, manager)
189
+ expect(expectation.optionsMatch("projects", { include: "workspaces" })).toBe true
190
+ expect(expectation.optionsMatch("projects", { include: ["anything"] })).toBe true
191
+ expect(expectation.optionsMatch("projects", {})).toBe true
192
+
193
+ it "should treat strings and numbers the same when appropriate", ->
194
+ expectation = new Brainstem.Expectation("projects", { only: "1" }, manager)
195
+ expect(expectation.optionsMatch("projects", {only: 1})).toBe true
196
+ expect(expectation.optionsMatch("projects", {only: "1"})).toBe true
197
+
198
+ it "should treat null, empty array, and empty object the same", ->
199
+ expectation = new Brainstem.Expectation("projects", { filters: {} }, manager)
200
+ expect(expectation.optionsMatch("projects", { filters: null })).toBe true
201
+ expect(expectation.optionsMatch("projects", { filters: {} })).toBe true
202
+ expect(expectation.optionsMatch("projects", { })).toBe true
203
+ expect(expectation.optionsMatch("projects", { filters: { foo: "bar" } })).toBe false
204
+
205
+ expectation = new Brainstem.Expectation("projects", {}, manager)
206
+ expect(expectation.optionsMatch("projects", { filters: null })).toBe true
207
+ expect(expectation.optionsMatch("projects", { filters: {} })).toBe true
208
+ expect(expectation.optionsMatch("projects", { })).toBe true
209
+ expect(expectation.optionsMatch("projects", { filters: { foo: "bar" } })).toBe false
@@ -0,0 +1,80 @@
1
+ window.spec ?= {}
2
+
3
+ spec.defineBuilders = ->
4
+ window.defineBuilder = (name, klass, defaultOptions) ->
5
+ class_defaults = {}
6
+
7
+ for key, value of defaultOptions
8
+ if typeof(value) == "function"
9
+ do ->
10
+ seq_name = name + "_" + key
11
+ BackboneFactory.define_sequence(seq_name, value)
12
+ class_defaults[key] = ->
13
+ next = BackboneFactory.next(seq_name)
14
+ if isIdAttr(seq_name) then arrayPreservedToString(next) else next
15
+ else
16
+ class_defaults[key] = if isIdAttr(key) then arrayPreservedToString(value) else value
17
+
18
+ factory = BackboneFactory.define(name, klass, -> return class_defaults)
19
+ builder = (opts) ->
20
+ BackboneFactory.create(name, $.extend({}, class_defaults, idsToStrings(opts)))
21
+
22
+ creator = (opts) ->
23
+ obj = builder(idsToStrings(opts))
24
+ storageName = name.underscore().pluralize()
25
+ window.base.data.storage(storageName).add obj if window.base.data.collectionExists(storageName)
26
+ obj
27
+
28
+ eval("window.#{"build_#{name.underscore()}".camelize(true)} = builder")
29
+ eval("window.#{"build_and_cache_#{name.underscore()}".camelize(true)} = creator")
30
+
31
+ isIdAttr = (attrName) ->
32
+ attrName == 'id' || attrName.match(/_id$/) || (attrName.match(/_ids$/))
33
+
34
+ arrayPreservedToString = (value) ->
35
+ if _.isArray(value)
36
+ _.map(value, (v) -> arrayPreservedToString(v))
37
+ else if value? && !$.isPlainObject(value)
38
+ String(value)
39
+ else
40
+ value
41
+
42
+ idsToStrings = (builderOpts) ->
43
+ for key, value of builderOpts
44
+ if isIdAttr(key)
45
+ builderOpts[key] = arrayPreservedToString(value)
46
+
47
+ builderOpts
48
+
49
+ window.defineBuilder "user", App.Models.User, {
50
+ id: (n) -> return n
51
+ }
52
+
53
+ window.defineBuilder "project", App.Models.Project, {
54
+ id: (n) -> return n
55
+ title: "new project"
56
+ }
57
+
58
+ getTimeEntryDefaults = ->
59
+ project = buildProject()
60
+
61
+ return {
62
+ id: (n)-> return n
63
+ project_id: project.get("id")
64
+ }
65
+ window.defineBuilder "timeEntry", App.Models.TimeEntry, getTimeEntryDefaults()
66
+
67
+ getTaskDefaults = ->
68
+ project = buildProject()
69
+
70
+ return {
71
+ id: (n) -> n
72
+ project_id: project.get("id")
73
+ description: "a very interesting task"
74
+ title: (n) -> "new Task#{n}"
75
+ archived: false
76
+ parent_id: null
77
+ }
78
+ window.defineBuilder "task", App.Models.Task, getTaskDefaults()
79
+
80
+ window.defineBuilder "post", App.Models.Post, {}