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