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.
Files changed (52) hide show
  1. data/.gitignore +21 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +12 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +187 -0
  6. data/Rakefile +15 -0
  7. data/app/assets/javascripts/backbone-nested-attributes/all.js +2 -0
  8. data/app/assets/javascripts/backbone-nested-attributes/model.js +175 -0
  9. data/app/assets/javascripts/backbone-nested-attributes/undoable.js +60 -0
  10. data/backbone-nested-attributes.gemspec +20 -0
  11. data/lib/backbone-nested-attributes.rb +6 -0
  12. data/lib/backbone-nested-attributes/engine.rb +7 -0
  13. data/lib/backbone-nested-attributes/version.rb +5 -0
  14. data/spec/dummy/README.rdoc +261 -0
  15. data/spec/dummy/Rakefile +7 -0
  16. data/spec/dummy/app/assets/javascripts/application.js +18 -0
  17. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  18. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  19. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  20. data/spec/dummy/app/mailers/.gitkeep +0 -0
  21. data/spec/dummy/app/models/.gitkeep +0 -0
  22. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  23. data/spec/dummy/config.ru +4 -0
  24. data/spec/dummy/config/application.rb +79 -0
  25. data/spec/dummy/config/boot.rb +10 -0
  26. data/spec/dummy/config/database.yml +25 -0
  27. data/spec/dummy/config/environment.rb +5 -0
  28. data/spec/dummy/config/environments/development.rb +37 -0
  29. data/spec/dummy/config/environments/production.rb +67 -0
  30. data/spec/dummy/config/environments/test.rb +37 -0
  31. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  32. data/spec/dummy/config/initializers/inflections.rb +15 -0
  33. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  34. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  35. data/spec/dummy/config/initializers/session_store.rb +8 -0
  36. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  37. data/spec/dummy/config/locales/en.yml +5 -0
  38. data/spec/dummy/config/routes.rb +58 -0
  39. data/spec/dummy/db/.gitkeep +0 -0
  40. data/spec/dummy/lib/assets/.gitkeep +0 -0
  41. data/spec/dummy/log/.gitkeep +0 -0
  42. data/spec/dummy/public/404.html +26 -0
  43. data/spec/dummy/public/422.html +26 -0
  44. data/spec/dummy/public/500.html +25 -0
  45. data/spec/dummy/public/favicon.ico +0 -0
  46. data/spec/dummy/script/rails +6 -0
  47. data/spec/javascripts/backbone-nested-attributes/ModelSpec.js +626 -0
  48. data/spec/javascripts/backbone-nested-attributes/UndoableSpec.js +489 -0
  49. data/spec/javascripts/helpers/SpecHelper.js +7 -0
  50. data/spec/javascripts/helpers/mock-ajax.js +207 -0
  51. data/spec/javascripts/support/jasmine.yml +76 -0
  52. 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
+ })