backbone-nested-attributes 0.2.2
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/.gitignore +21 -0
- data/.rvmrc +1 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +22 -0
- data/README.md +187 -0
- data/Rakefile +15 -0
- data/app/assets/javascripts/backbone-nested-attributes/all.js +2 -0
- data/app/assets/javascripts/backbone-nested-attributes/model.js +175 -0
- data/app/assets/javascripts/backbone-nested-attributes/undoable.js +60 -0
- data/backbone-nested-attributes.gemspec +20 -0
- data/lib/backbone-nested-attributes.rb +6 -0
- data/lib/backbone-nested-attributes/engine.rb +7 -0
- data/lib/backbone-nested-attributes/version.rb +5 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +18 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.gitkeep +0 -0
- data/spec/dummy/app/models/.gitkeep +0 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +79 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +58 -0
- data/spec/dummy/db/.gitkeep +0 -0
- data/spec/dummy/lib/assets/.gitkeep +0 -0
- data/spec/dummy/log/.gitkeep +0 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/javascripts/backbone-nested-attributes/ModelSpec.js +626 -0
- data/spec/javascripts/backbone-nested-attributes/UndoableSpec.js +489 -0
- data/spec/javascripts/helpers/SpecHelper.js +7 -0
- data/spec/javascripts/helpers/mock-ajax.js +207 -0
- data/spec/javascripts/support/jasmine.yml +76 -0
- metadata +156 -0
@@ -0,0 +1,489 @@
|
|
1
|
+
describe("Backbone.UndoableModel", function() {
|
2
|
+
var model,
|
3
|
+
originalAttributes,
|
4
|
+
afterSyncAttributes,
|
5
|
+
called,
|
6
|
+
post,
|
7
|
+
Post,
|
8
|
+
comments,
|
9
|
+
comment,
|
10
|
+
Comment,
|
11
|
+
author,
|
12
|
+
Person
|
13
|
+
|
14
|
+
beforeEach(function() {
|
15
|
+
jasmine.Ajax.useMock()
|
16
|
+
called = false
|
17
|
+
|
18
|
+
originalAttributes = { title: 'some title', body: 'some body' }
|
19
|
+
model = new Backbone.UndoableModel(originalAttributes)
|
20
|
+
model.url = 'http://someapi.com'
|
21
|
+
})
|
22
|
+
|
23
|
+
function performSync(model) {
|
24
|
+
var request = mostRecentAjaxRequest();
|
25
|
+
request.response({status: 200, responseText: model.toJSON()})
|
26
|
+
}
|
27
|
+
|
28
|
+
it("should be a Backbone.NestedAttributesModel", function() {
|
29
|
+
expect(model).toBeAnInstanceOf(Backbone.NestedAttributesModel)
|
30
|
+
})
|
31
|
+
|
32
|
+
describe("when the initialize method is overriden", function() {
|
33
|
+
var Model
|
34
|
+
|
35
|
+
describe("and the undoable method is not called", function() {
|
36
|
+
beforeEach(function() {
|
37
|
+
Model = Backbone.UndoableModel.extend({
|
38
|
+
initialize: function () {}
|
39
|
+
})
|
40
|
+
|
41
|
+
model = new Model(originalAttributes)
|
42
|
+
})
|
43
|
+
|
44
|
+
it("does not reverts the model own attributes to its original attributes", function() {
|
45
|
+
model.set({ title: 'new title', body: 'new body' })
|
46
|
+
model.undo()
|
47
|
+
|
48
|
+
expect(model.toJSON()).not.toEqual(originalAttributes)
|
49
|
+
})
|
50
|
+
})
|
51
|
+
|
52
|
+
describe("and the undoable method is called", function() {
|
53
|
+
describe("in the initialize method", function() {
|
54
|
+
beforeEach(function() {
|
55
|
+
Model = Backbone.UndoableModel.extend({
|
56
|
+
initialize: function () {
|
57
|
+
this.undoable()
|
58
|
+
}
|
59
|
+
})
|
60
|
+
|
61
|
+
model = new Model(originalAttributes)
|
62
|
+
})
|
63
|
+
|
64
|
+
it("reverts the model own attributes to its original attributes", function() {
|
65
|
+
model.set({ title: 'new title', body: 'new body' })
|
66
|
+
model.undo()
|
67
|
+
|
68
|
+
expect(model.toJSON()).toEqual(originalAttributes)
|
69
|
+
})
|
70
|
+
})
|
71
|
+
|
72
|
+
describe("by calling the super method", function() {
|
73
|
+
beforeEach(function() {
|
74
|
+
Model = Backbone.UndoableModel.extend({
|
75
|
+
initialize: function () {
|
76
|
+
Backbone.UndoableModel.prototype.initialize.apply(this, arguments)
|
77
|
+
}
|
78
|
+
})
|
79
|
+
|
80
|
+
model = new Model(originalAttributes)
|
81
|
+
})
|
82
|
+
|
83
|
+
it("reverts the model own attributes to its original attributes", function() {
|
84
|
+
model.set({ title: 'new title', body: 'new body' })
|
85
|
+
model.undo()
|
86
|
+
|
87
|
+
expect(model.toJSON()).toEqual(originalAttributes)
|
88
|
+
})
|
89
|
+
})
|
90
|
+
|
91
|
+
describe("after it is initialized", function() {
|
92
|
+
beforeEach(function() {
|
93
|
+
Model = Backbone.UndoableModel.extend({})
|
94
|
+
|
95
|
+
model = new Model(originalAttributes)
|
96
|
+
})
|
97
|
+
|
98
|
+
it("reverts the model own attributes to the attributes that the model had before calling undoable", function() {
|
99
|
+
var beforeUndoableAttributes = { title: 'before undoable title', body: 'before undoable body' }
|
100
|
+
|
101
|
+
model.set(beforeUndoableAttributes)
|
102
|
+
model.undoable()
|
103
|
+
model.set({ title: 'new title', body: 'new body' })
|
104
|
+
model.undo()
|
105
|
+
|
106
|
+
expect(model.toJSON()).toEqual(beforeUndoableAttributes)
|
107
|
+
})
|
108
|
+
})
|
109
|
+
})
|
110
|
+
})
|
111
|
+
|
112
|
+
describe("when it is a new model", function() {
|
113
|
+
it("should not have changes since sync", function() {
|
114
|
+
expect(model.hasChangedSinceSync()).toBeFalsy()
|
115
|
+
})
|
116
|
+
|
117
|
+
describe("and it is changed", function() {
|
118
|
+
it("should have changes since sync", function() {
|
119
|
+
model.set({ title: 'new title', body: 'new body' })
|
120
|
+
expect(model.hasChangedSinceSync()).toBeTruthy()
|
121
|
+
})
|
122
|
+
|
123
|
+
describe("and then undo", function() {
|
124
|
+
it("should not have changes since sync", function() {
|
125
|
+
model.set({ title: 'new title', body: 'new body' })
|
126
|
+
model.undo()
|
127
|
+
expect(model.hasChangedSinceSync()).toBeFalsy()
|
128
|
+
})
|
129
|
+
|
130
|
+
it("reverts the model own attributes to its original attributes", function() {
|
131
|
+
model.set({ title: 'new title', body: 'new body' })
|
132
|
+
model.undo()
|
133
|
+
|
134
|
+
expect(model.toJSON()).toEqual(originalAttributes)
|
135
|
+
})
|
136
|
+
})
|
137
|
+
|
138
|
+
describe("and then sync", function() {
|
139
|
+
it("should not have changes since sync", function() {
|
140
|
+
model.set({ title: 'new title', body: 'new body' })
|
141
|
+
model.save()
|
142
|
+
performSync(model)
|
143
|
+
expect(model.hasChangedSinceSync()).toBeFalsy()
|
144
|
+
})
|
145
|
+
|
146
|
+
describe("then undo", function() {
|
147
|
+
it("reverts the model own attributes to their value right after sync", function() {
|
148
|
+
model.set({ title: 'sync title', body: 'sync body' })
|
149
|
+
model.save()
|
150
|
+
performSync(model)
|
151
|
+
|
152
|
+
afterSyncAttributes = model.toJSON()
|
153
|
+
|
154
|
+
model.set({ title: 'new title', body: 'new body' })
|
155
|
+
model.undo()
|
156
|
+
|
157
|
+
expect(model.toJSON()).toEqual(afterSyncAttributes)
|
158
|
+
})
|
159
|
+
})
|
160
|
+
})
|
161
|
+
})
|
162
|
+
})
|
163
|
+
|
164
|
+
describe("when it is an existing model", function() {
|
165
|
+
beforeEach(function() {
|
166
|
+
|
167
|
+
originalAttributes = { id: 123, title: 'some title', body: 'some body' }
|
168
|
+
model = new Backbone.UndoableModel(originalAttributes)
|
169
|
+
model.url = 'http://someapi.com'
|
170
|
+
})
|
171
|
+
|
172
|
+
describe("and it is changed", function() {
|
173
|
+
it("should have changes since sync", function() {
|
174
|
+
model.set({ title: 'new title', body: 'new body' })
|
175
|
+
expect(model.hasChangedSinceSync()).toBeTruthy()
|
176
|
+
})
|
177
|
+
|
178
|
+
describe("and then undo", function() {
|
179
|
+
it("should not have changes since sync", function() {
|
180
|
+
model.set({ title: 'new title', body: 'new body' })
|
181
|
+
model.undo()
|
182
|
+
expect(model.hasChangedSinceSync()).toBeFalsy()
|
183
|
+
})
|
184
|
+
|
185
|
+
it("reverts the model own attributes to its original attributes", function() {
|
186
|
+
model.set({ title: 'new title', body: 'new body' })
|
187
|
+
model.undo()
|
188
|
+
|
189
|
+
expect(model.toJSON()).toEqual(originalAttributes)
|
190
|
+
})
|
191
|
+
})
|
192
|
+
|
193
|
+
describe("and then sync", function() {
|
194
|
+
it("should not have changes since sync", function() {
|
195
|
+
model.set({ title: 'new title', body: 'new body' })
|
196
|
+
model.save()
|
197
|
+
performSync(model)
|
198
|
+
expect(model.hasChangedSinceSync()).toBeFalsy()
|
199
|
+
})
|
200
|
+
|
201
|
+
describe("then undo", function() {
|
202
|
+
it("reverts the model own attributes to their value right after sync", function() {
|
203
|
+
model.set({ title: 'sync title', body: 'sync body' })
|
204
|
+
model.save()
|
205
|
+
performSync(model)
|
206
|
+
|
207
|
+
afterSyncAttributes = model.toJSON()
|
208
|
+
|
209
|
+
model.set({ title: 'new title', body: 'new body' })
|
210
|
+
model.undo()
|
211
|
+
|
212
|
+
expect(model.toJSON()).toEqual(afterSyncAttributes)
|
213
|
+
})
|
214
|
+
})
|
215
|
+
})
|
216
|
+
})
|
217
|
+
})
|
218
|
+
|
219
|
+
describe("state:restore event", function() {
|
220
|
+
var called
|
221
|
+
|
222
|
+
beforeEach(function() {
|
223
|
+
called = false
|
224
|
+
})
|
225
|
+
|
226
|
+
it("is triggered when the model is undo", function() {
|
227
|
+
model.on('state:restore', function () {
|
228
|
+
called = true
|
229
|
+
})
|
230
|
+
|
231
|
+
model.undo()
|
232
|
+
expect(called).toBeTruthy()
|
233
|
+
})
|
234
|
+
})
|
235
|
+
|
236
|
+
describe("state:store event", function() {
|
237
|
+
it("is triggered when the model is saved", function() {
|
238
|
+
model.on('state:store', function () {
|
239
|
+
called = true
|
240
|
+
})
|
241
|
+
|
242
|
+
model.save()
|
243
|
+
performSync(model)
|
244
|
+
expect(called).toBeTruthy()
|
245
|
+
})
|
246
|
+
|
247
|
+
describe("when it is a new model", function() {
|
248
|
+
it("is triggered when the model is saved", function() {
|
249
|
+
model.on('state:store', function () {
|
250
|
+
called = true
|
251
|
+
})
|
252
|
+
|
253
|
+
model.save()
|
254
|
+
performSync(model)
|
255
|
+
expect(called).toBeTruthy()
|
256
|
+
})
|
257
|
+
|
258
|
+
it("is not triggered when the model is destroyed", function() {
|
259
|
+
model.on('state:store', function () {
|
260
|
+
called = true
|
261
|
+
})
|
262
|
+
|
263
|
+
model.destroy()
|
264
|
+
performSync(model)
|
265
|
+
expect(called).toBeFalsy()
|
266
|
+
})
|
267
|
+
})
|
268
|
+
|
269
|
+
describe("when it is not a new model", function() {
|
270
|
+
beforeEach(function() {
|
271
|
+
model.set({ id: 123 })
|
272
|
+
});
|
273
|
+
|
274
|
+
it("is triggered when the model is saved", function() {
|
275
|
+
model.on('state:store', function () {
|
276
|
+
called = true
|
277
|
+
})
|
278
|
+
|
279
|
+
model.save()
|
280
|
+
performSync(model)
|
281
|
+
expect(called).toBeTruthy()
|
282
|
+
})
|
283
|
+
|
284
|
+
it("is triggered when the model is destroyed", function() {
|
285
|
+
model.on('state:store', function () {
|
286
|
+
called = true
|
287
|
+
})
|
288
|
+
|
289
|
+
model.destroy()
|
290
|
+
performSync(model)
|
291
|
+
expect(called).toBeTruthy()
|
292
|
+
})
|
293
|
+
})
|
294
|
+
})
|
295
|
+
|
296
|
+
describe("when it has a has one relationship", function() {
|
297
|
+
beforeEach(function() {
|
298
|
+
Post = Backbone.UndoableModel.extend({
|
299
|
+
relations: [
|
300
|
+
{
|
301
|
+
type: 'one',
|
302
|
+
key: 'author',
|
303
|
+
relatedModel: function () { return Person }
|
304
|
+
}
|
305
|
+
]
|
306
|
+
})
|
307
|
+
|
308
|
+
Person = Backbone.UndoableModel.extend({})
|
309
|
+
|
310
|
+
originalAttributes = { title: 'some title', author: { name: 'Jon Snow' } }
|
311
|
+
post = new Post(_(originalAttributes).clone())
|
312
|
+
author = post.get('author')
|
313
|
+
})
|
314
|
+
|
315
|
+
describe("and a nested model changes", function() {
|
316
|
+
it("should have changes since sync", function() {
|
317
|
+
author.set({ name: 'Robb Stark' })
|
318
|
+
expect(author.hasChangedSinceSync()).toBeTruthy()
|
319
|
+
})
|
320
|
+
|
321
|
+
it("its parent should have changes since sync", function() {
|
322
|
+
author.set({ name: 'Robb Stark' })
|
323
|
+
expect(post.hasChangedSinceSync()).toBeTruthy()
|
324
|
+
})
|
325
|
+
|
326
|
+
describe("and the nested model state is saved", function() {
|
327
|
+
it("should not have changes since sync", function() {
|
328
|
+
author.set({ name: 'Robb Stark' })
|
329
|
+
author.saveState()
|
330
|
+
expect(author.hasChangedSinceSync()).toBeFalsy()
|
331
|
+
})
|
332
|
+
|
333
|
+
it("its parent should have changes since sync", function() {
|
334
|
+
author.set({ name: 'Robb Stark' })
|
335
|
+
author.saveState()
|
336
|
+
expect(post.hasChangedSinceSync()).toBeTruthy()
|
337
|
+
})
|
338
|
+
|
339
|
+
describe("when undoing changes", function() {
|
340
|
+
it("revert to the last saved state", function() {
|
341
|
+
author.set({ name: 'Robb Stark' })
|
342
|
+
author.saveState()
|
343
|
+
author.set({ name: 'Tyrion Lanninster' })
|
344
|
+
author.undo()
|
345
|
+
expect(author.get('name')).toEqual('Robb Stark')
|
346
|
+
})
|
347
|
+
})
|
348
|
+
})
|
349
|
+
})
|
350
|
+
|
351
|
+
describe("undoing a change", function() {
|
352
|
+
describe("in its own attributes", function() {
|
353
|
+
it("reverts the model own attributes to its original attributes", function() {
|
354
|
+
post.set({ title: 'new title' })
|
355
|
+
post.undo()
|
356
|
+
|
357
|
+
expect(post.toJSON()).toEqual(originalAttributes)
|
358
|
+
})
|
359
|
+
})
|
360
|
+
|
361
|
+
describe("in a nested model", function() {
|
362
|
+
it("reverts the model own attributes as well as the nested ones to its original attributes, creating a new model reference", function() {
|
363
|
+
author.set({ name: 'Robb Stark' })
|
364
|
+
post.undo()
|
365
|
+
|
366
|
+
expect(post.toJSON()).toEqual(originalAttributes)
|
367
|
+
expect(post.get('author')).not.toEqual(author)
|
368
|
+
})
|
369
|
+
})
|
370
|
+
})
|
371
|
+
|
372
|
+
describe("undoing a nested model", function() {
|
373
|
+
it("does not trigger a state:restore event on the parent model", function() {
|
374
|
+
post.on('state:restore', function () {
|
375
|
+
called = true
|
376
|
+
})
|
377
|
+
|
378
|
+
author.undo()
|
379
|
+
expect(called).toBeFalsy()
|
380
|
+
})
|
381
|
+
|
382
|
+
it("trigger a state:restore event on the nested model", function() {
|
383
|
+
author.on('state:restore', function () {
|
384
|
+
called = true
|
385
|
+
})
|
386
|
+
|
387
|
+
author.undo()
|
388
|
+
expect(called).toBeTruthy()
|
389
|
+
})
|
390
|
+
})
|
391
|
+
})
|
392
|
+
|
393
|
+
describe("when it has a has many relationship", function() {
|
394
|
+
beforeEach(function() {
|
395
|
+
Post = Backbone.UndoableModel.extend({
|
396
|
+
relations: [
|
397
|
+
{
|
398
|
+
key: 'comments',
|
399
|
+
relatedModel: function () { return Comment }
|
400
|
+
}
|
401
|
+
]
|
402
|
+
})
|
403
|
+
|
404
|
+
Comment = Backbone.UndoableModel.extend({})
|
405
|
+
|
406
|
+
originalAttributes = { title: 'some title', comments: [ { body: 'some body' } ] }
|
407
|
+
post = new Post(_(originalAttributes).clone())
|
408
|
+
comments = post.get('comments')
|
409
|
+
comment = comments.at(0)
|
410
|
+
})
|
411
|
+
|
412
|
+
describe("and a nested model changes", function() {
|
413
|
+
it("should have changes since sync", function() {
|
414
|
+
comment.set({ body: 'new body' })
|
415
|
+
expect(comment.hasChangedSinceSync()).toBeTruthy()
|
416
|
+
})
|
417
|
+
|
418
|
+
it("its parent should have changes since sync", function() {
|
419
|
+
comment.set({ body: 'new body' })
|
420
|
+
expect(post.hasChangedSinceSync()).toBeTruthy()
|
421
|
+
})
|
422
|
+
|
423
|
+
describe("and the nested model state is saved", function() {
|
424
|
+
it("should not have changes since sync", function() {
|
425
|
+
comment.set({ body: 'new body' })
|
426
|
+
comment.saveState()
|
427
|
+
expect(comment.hasChangedSinceSync()).toBeFalsy()
|
428
|
+
})
|
429
|
+
|
430
|
+
it("its parent should have changes since sync", function() {
|
431
|
+
comment.set({ body: 'new body' })
|
432
|
+
comment.saveState()
|
433
|
+
expect(post.hasChangedSinceSync()).toBeTruthy()
|
434
|
+
})
|
435
|
+
|
436
|
+
describe("when undoing changes", function() {
|
437
|
+
it("revert to the last saved state", function() {
|
438
|
+
comment.set({ body: 'new body' })
|
439
|
+
comment.saveState()
|
440
|
+
comment.set({ body: 'other body' })
|
441
|
+
comment.undo()
|
442
|
+
expect(comment.get('body')).toEqual('new body')
|
443
|
+
})
|
444
|
+
})
|
445
|
+
})
|
446
|
+
})
|
447
|
+
|
448
|
+
describe("undoing a change", function() {
|
449
|
+
describe("in its own attributes", function() {
|
450
|
+
it("reverts the model own attributes to its original attributes", function() {
|
451
|
+
post.set({ title: 'new title' })
|
452
|
+
post.undo()
|
453
|
+
|
454
|
+
expect(post.toJSON()).toEqual(originalAttributes)
|
455
|
+
})
|
456
|
+
})
|
457
|
+
|
458
|
+
describe("in a nested model", function() {
|
459
|
+
it("reverts the model own attributes as well as the nested ones to its original attributes, keeping the existing collection untouched", function() {
|
460
|
+
comment.set({ body: 'new body' })
|
461
|
+
post.undo()
|
462
|
+
|
463
|
+
expect(post.toJSON()).toEqual(originalAttributes)
|
464
|
+
expect(post.get('comments')).toEqual(comments)
|
465
|
+
})
|
466
|
+
})
|
467
|
+
})
|
468
|
+
|
469
|
+
describe("undoing a nested model", function() {
|
470
|
+
it("does not trigger a state:restore event on the parent model", function() {
|
471
|
+
post.on('state:restore', function () {
|
472
|
+
called = true
|
473
|
+
})
|
474
|
+
|
475
|
+
comment.undo()
|
476
|
+
expect(called).toBeFalsy()
|
477
|
+
})
|
478
|
+
|
479
|
+
it("trigger a state:restore event on the nested model", function() {
|
480
|
+
comment.on('state:restore', function () {
|
481
|
+
called = true
|
482
|
+
})
|
483
|
+
|
484
|
+
comment.undo()
|
485
|
+
expect(called).toBeTruthy()
|
486
|
+
})
|
487
|
+
})
|
488
|
+
})
|
489
|
+
})
|