backbone-nested-attributes 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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
+ })