brainstem-js 0.2.1 → 0.3.0
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.
- 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()
|