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,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, {}