iknow_view_models 2.8.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +115 -0
  3. data/.gitignore +36 -0
  4. data/.travis.yml +31 -0
  5. data/Appraisals +9 -0
  6. data/Gemfile +19 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +19 -0
  9. data/Rakefile +21 -0
  10. data/appveyor.yml +22 -0
  11. data/gemfiles/rails_5_2.gemfile +15 -0
  12. data/gemfiles/rails_6_0_beta.gemfile +15 -0
  13. data/iknow_view_models.gemspec +49 -0
  14. data/lib/iknow_view_models.rb +12 -0
  15. data/lib/iknow_view_models/railtie.rb +8 -0
  16. data/lib/iknow_view_models/version.rb +5 -0
  17. data/lib/view_model.rb +333 -0
  18. data/lib/view_model/access_control.rb +154 -0
  19. data/lib/view_model/access_control/composed.rb +216 -0
  20. data/lib/view_model/access_control/open.rb +13 -0
  21. data/lib/view_model/access_control/read_only.rb +13 -0
  22. data/lib/view_model/access_control/tree.rb +264 -0
  23. data/lib/view_model/access_control_error.rb +10 -0
  24. data/lib/view_model/active_record.rb +383 -0
  25. data/lib/view_model/active_record/association_data.rb +178 -0
  26. data/lib/view_model/active_record/association_manipulation.rb +389 -0
  27. data/lib/view_model/active_record/cache.rb +265 -0
  28. data/lib/view_model/active_record/cache/cacheable_view.rb +51 -0
  29. data/lib/view_model/active_record/cloner.rb +113 -0
  30. data/lib/view_model/active_record/collection_nested_controller.rb +100 -0
  31. data/lib/view_model/active_record/controller.rb +77 -0
  32. data/lib/view_model/active_record/controller_base.rb +185 -0
  33. data/lib/view_model/active_record/nested_controller_base.rb +93 -0
  34. data/lib/view_model/active_record/singular_nested_controller.rb +34 -0
  35. data/lib/view_model/active_record/update_context.rb +252 -0
  36. data/lib/view_model/active_record/update_data.rb +749 -0
  37. data/lib/view_model/active_record/update_operation.rb +810 -0
  38. data/lib/view_model/active_record/visitor.rb +77 -0
  39. data/lib/view_model/after_transaction_runner.rb +29 -0
  40. data/lib/view_model/callbacks.rb +219 -0
  41. data/lib/view_model/changes.rb +62 -0
  42. data/lib/view_model/config.rb +29 -0
  43. data/lib/view_model/controller.rb +142 -0
  44. data/lib/view_model/deserialization_error.rb +437 -0
  45. data/lib/view_model/deserialize_context.rb +16 -0
  46. data/lib/view_model/error.rb +191 -0
  47. data/lib/view_model/error_view.rb +35 -0
  48. data/lib/view_model/record.rb +367 -0
  49. data/lib/view_model/record/attribute_data.rb +48 -0
  50. data/lib/view_model/reference.rb +31 -0
  51. data/lib/view_model/references.rb +48 -0
  52. data/lib/view_model/registry.rb +73 -0
  53. data/lib/view_model/schemas.rb +45 -0
  54. data/lib/view_model/serialization_error.rb +10 -0
  55. data/lib/view_model/serialize_context.rb +118 -0
  56. data/lib/view_model/test_helpers.rb +103 -0
  57. data/lib/view_model/test_helpers/arvm_builder.rb +111 -0
  58. data/lib/view_model/traversal_context.rb +126 -0
  59. data/lib/view_model/utils.rb +24 -0
  60. data/lib/view_model/utils/collections.rb +49 -0
  61. data/test/helpers/arvm_test_models.rb +59 -0
  62. data/test/helpers/arvm_test_utilities.rb +187 -0
  63. data/test/helpers/callback_tracer.rb +27 -0
  64. data/test/helpers/controller_test_helpers.rb +270 -0
  65. data/test/helpers/match_enumerator.rb +58 -0
  66. data/test/helpers/query_logging.rb +71 -0
  67. data/test/helpers/test_access_control.rb +56 -0
  68. data/test/helpers/viewmodel_spec_helpers.rb +326 -0
  69. data/test/unit/view_model/access_control_test.rb +769 -0
  70. data/test/unit/view_model/active_record/alias_test.rb +35 -0
  71. data/test/unit/view_model/active_record/belongs_to_test.rb +376 -0
  72. data/test/unit/view_model/active_record/cache_test.rb +351 -0
  73. data/test/unit/view_model/active_record/cloner_test.rb +313 -0
  74. data/test/unit/view_model/active_record/controller_test.rb +561 -0
  75. data/test/unit/view_model/active_record/counter_test.rb +80 -0
  76. data/test/unit/view_model/active_record/customization_test.rb +388 -0
  77. data/test/unit/view_model/active_record/has_many_test.rb +957 -0
  78. data/test/unit/view_model/active_record/has_many_through_poly_test.rb +269 -0
  79. data/test/unit/view_model/active_record/has_many_through_test.rb +736 -0
  80. data/test/unit/view_model/active_record/has_one_test.rb +334 -0
  81. data/test/unit/view_model/active_record/namespacing_test.rb +75 -0
  82. data/test/unit/view_model/active_record/optional_attribute_view_test.rb +58 -0
  83. data/test/unit/view_model/active_record/poly_test.rb +320 -0
  84. data/test/unit/view_model/active_record/shared_test.rb +285 -0
  85. data/test/unit/view_model/active_record/version_test.rb +121 -0
  86. data/test/unit/view_model/active_record_test.rb +542 -0
  87. data/test/unit/view_model/callbacks_test.rb +582 -0
  88. data/test/unit/view_model/deserialization_error/unique_violation_test.rb +73 -0
  89. data/test/unit/view_model/record_test.rb +524 -0
  90. data/test/unit/view_model/traversal_context_test.rb +371 -0
  91. data/test/unit/view_model_test.rb +62 -0
  92. metadata +490 -0
@@ -0,0 +1,582 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../helpers/arvm_test_utilities.rb'
4
+ require_relative '../../helpers/arvm_test_models.rb'
5
+ require_relative '../../helpers/callback_tracer.rb'
6
+ require_relative '../../helpers/viewmodel_spec_helpers.rb'
7
+
8
+ require 'minitest/autorun'
9
+
10
+ require 'view_model/active_record'
11
+
12
+ class ViewModel::CallbacksTest < ActiveSupport::TestCase
13
+ include ARVMTestUtilities
14
+ extend Minitest::Spec::DSL
15
+
16
+ def vm_serialize_context(viewmodel_class, **args)
17
+ viewmodel_class.new_serialize_context(callbacks: callbacks, **args)
18
+ end
19
+
20
+ def vm_deserialize_context(viewmodel_class, **args)
21
+ viewmodel_class.new_deserialize_context(callbacks: callbacks, **args)
22
+ end
23
+
24
+ # Override TestHelpers to use the callback contexts
25
+ def serialize(view, serialize_context: vm_serialize_context(view.class))
26
+ super(view, serialize_context: serialize_context)
27
+ end
28
+
29
+ def serialize_with_references(view, serialize_context: vm_serialize_context(view.class))
30
+ super(view, serialize_context: serialize_context)
31
+ end
32
+
33
+ # use `alter_by_view` to test deserialization: only override the deserialize_context
34
+ def alter_by_view!(vm_class, model,
35
+ deserialize_context: vm_deserialize_context(vm_class),
36
+ **args,
37
+ &block)
38
+ super(vm_class, model, deserialize_context: deserialize_context, **args, &block)
39
+ end
40
+
41
+ let(:callbacks) { [callback] }
42
+
43
+ let(:vm) { create_viewmodel! }
44
+
45
+ describe 'tracing each callback' do
46
+ def visit(hook, view)
47
+ CallbackTracer::Visit.new(hook, view)
48
+ end
49
+
50
+ let(:callback) { CallbackTracer.new }
51
+
52
+ describe 'with parent and child test models' do
53
+ include ViewModelSpecHelpers::ParentAndBelongsToChild
54
+
55
+ def new_model
56
+ model_class.new(name: 'a', child: child_model_class.new(name: 'b'))
57
+ end
58
+
59
+ it 'visits in correct order when serializing' do
60
+ serialize(vm)
61
+ value(callback.hook_trace).must_equal(
62
+ [visit(ViewModel::Callbacks::Hook::BeforeVisit, vm),
63
+ visit(ViewModel::Callbacks::Hook::BeforeVisit, vm.child),
64
+ visit(ViewModel::Callbacks::Hook::AfterVisit, vm.child),
65
+ visit(ViewModel::Callbacks::Hook::AfterVisit, vm)])
66
+ end
67
+
68
+ it 'visits in correct order when deserializing' do
69
+ alter_by_view!(viewmodel_class, vm.model) {}
70
+ value(callback.hook_trace).must_equal(
71
+ [visit(ViewModel::Callbacks::Hook::BeforeVisit, vm),
72
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, vm),
73
+
74
+ visit(ViewModel::Callbacks::Hook::BeforeVisit, vm.child),
75
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, vm.child),
76
+ visit(ViewModel::Callbacks::Hook::BeforeValidate, vm.child),
77
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, vm.child),
78
+ visit(ViewModel::Callbacks::Hook::AfterVisit, vm.child),
79
+
80
+ visit(ViewModel::Callbacks::Hook::BeforeValidate, vm),
81
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, vm),
82
+ visit(ViewModel::Callbacks::Hook::AfterVisit, vm)])
83
+ end
84
+
85
+ it 'calls edit hook when updating' do
86
+ alter_by_view!(viewmodel_class, vm.model) do |view, _refs|
87
+ view['name'] = 'q'
88
+ end
89
+ value(callback.hook_trace).must_equal(
90
+ [visit(ViewModel::Callbacks::Hook::BeforeVisit, vm),
91
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, vm),
92
+
93
+ visit(ViewModel::Callbacks::Hook::BeforeVisit, vm.child),
94
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, vm.child),
95
+ visit(ViewModel::Callbacks::Hook::BeforeValidate, vm.child),
96
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, vm.child),
97
+ visit(ViewModel::Callbacks::Hook::AfterVisit, vm.child),
98
+
99
+ visit(ViewModel::Callbacks::Hook::BeforeValidate, vm),
100
+ visit(ViewModel::Callbacks::Hook::OnChange, vm),
101
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, vm),
102
+ visit(ViewModel::Callbacks::Hook::AfterVisit, vm)])
103
+ end
104
+
105
+ it 'calls edit hook when deleting' do
106
+ vm_child = vm.child
107
+ alter_by_view!(viewmodel_class, vm.model) do |view, _refs|
108
+ view['child'] = nil
109
+ end
110
+
111
+ value(callback.hook_trace).must_equal(
112
+ [visit(ViewModel::Callbacks::Hook::BeforeVisit, vm),
113
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, vm),
114
+ visit(ViewModel::Callbacks::Hook::BeforeValidate, vm),
115
+
116
+ visit(ViewModel::Callbacks::Hook::BeforeVisit, vm_child),
117
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, vm_child),
118
+ visit(ViewModel::Callbacks::Hook::OnChange, vm_child),
119
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, vm_child),
120
+ visit(ViewModel::Callbacks::Hook::AfterVisit, vm_child),
121
+
122
+ visit(ViewModel::Callbacks::Hook::OnChange, vm),
123
+
124
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, vm),
125
+ visit(ViewModel::Callbacks::Hook::AfterVisit, vm)])
126
+ end
127
+
128
+ it 'calls hooks on old and new when replacing' do
129
+ old_child = vm.child
130
+ alter_by_view!(viewmodel_class, vm.model) do |view, _refs|
131
+ view['child'] = { '_type' => 'Child', 'name' => 'q' }
132
+ end
133
+
134
+ value(callback.hook_trace).must_equal(
135
+ [visit(ViewModel::Callbacks::Hook::BeforeVisit, vm),
136
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, vm),
137
+
138
+ visit(ViewModel::Callbacks::Hook::BeforeVisit, vm.child),
139
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, vm.child),
140
+ visit(ViewModel::Callbacks::Hook::BeforeValidate, vm.child),
141
+ visit(ViewModel::Callbacks::Hook::OnChange, vm.child),
142
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, vm.child),
143
+ visit(ViewModel::Callbacks::Hook::AfterVisit, vm.child),
144
+
145
+ visit(ViewModel::Callbacks::Hook::BeforeValidate, vm),
146
+
147
+ visit(ViewModel::Callbacks::Hook::BeforeVisit, old_child),
148
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, old_child),
149
+ visit(ViewModel::Callbacks::Hook::OnChange, old_child),
150
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, old_child),
151
+ visit(ViewModel::Callbacks::Hook::AfterVisit, old_child),
152
+
153
+ visit(ViewModel::Callbacks::Hook::OnChange, vm),
154
+
155
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, vm),
156
+ visit(ViewModel::Callbacks::Hook::AfterVisit, vm)])
157
+ end
158
+
159
+ it 'calls hooks on old and new when moving' do
160
+ child = vm.child
161
+ vm2 = viewmodel_class.new(model_class.create!(name: 'z'))
162
+ alter_by_view!(viewmodel_class, [vm.model, vm2.model]) do |views, _refs|
163
+ views[1]['child'] = views[0]['child']
164
+ views[0]['child'] = nil
165
+ end
166
+
167
+ value(callback.hook_trace).must_equal(
168
+ [visit(ViewModel::Callbacks::Hook::BeforeVisit, vm),
169
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, vm),
170
+ visit(ViewModel::Callbacks::Hook::BeforeValidate, vm),
171
+ visit(ViewModel::Callbacks::Hook::OnChange, vm),
172
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, vm),
173
+ visit(ViewModel::Callbacks::Hook::AfterVisit, vm),
174
+
175
+ visit(ViewModel::Callbacks::Hook::BeforeVisit, vm2),
176
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, vm2),
177
+
178
+ visit(ViewModel::Callbacks::Hook::BeforeVisit, child),
179
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, child),
180
+ visit(ViewModel::Callbacks::Hook::BeforeValidate, child),
181
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, child),
182
+ visit(ViewModel::Callbacks::Hook::AfterVisit, child),
183
+
184
+ visit(ViewModel::Callbacks::Hook::BeforeValidate, vm2),
185
+ visit(ViewModel::Callbacks::Hook::OnChange, vm2),
186
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, vm2),
187
+ visit(ViewModel::Callbacks::Hook::AfterVisit, vm2)])
188
+ end
189
+
190
+ it 'calls hooks on delete' do
191
+ ctx = vm_deserialize_context(viewmodel_class)
192
+ vm.destroy!(deserialize_context: ctx)
193
+ value(callback.hook_trace).must_equal(
194
+ [visit(ViewModel::Callbacks::Hook::BeforeVisit, vm),
195
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, vm),
196
+ visit(ViewModel::Callbacks::Hook::OnChange, vm),
197
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, vm),
198
+ visit(ViewModel::Callbacks::Hook::AfterVisit, vm)])
199
+ ## At present, children aren't visited on delete.
200
+ # visit(ViewModel::Callbacks::Hook::BeforeVisit, old_child),
201
+ # visit(ViewModel::Callbacks::Hook::BeforeDeserialize, old_child),
202
+ # visit(ViewModel::Callbacks::Hook::OnChange, old_child),
203
+ # visit(ViewModel::Callbacks::Hook::AfterDeserialize, old_child),
204
+ # visit(ViewModel::Callbacks::Hook::AfterVisit, old_child))
205
+ end
206
+
207
+ it 'calls hooks on replace associated' do
208
+ old_child = vm.child
209
+ ctx = vm_deserialize_context(viewmodel_class)
210
+ new_child_hash = { '_type' => 'Child', 'name' => 'q' }
211
+ vm.replace_associated(:child, new_child_hash, deserialize_context: ctx)
212
+ vm.model.reload
213
+
214
+ value(callback.hook_trace).must_equal(
215
+ [
216
+ visit(ViewModel::Callbacks::Hook::BeforeVisit, vm),
217
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, vm),
218
+
219
+ visit(ViewModel::Callbacks::Hook::BeforeVisit, vm.child),
220
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, vm.child),
221
+ visit(ViewModel::Callbacks::Hook::BeforeValidate, vm.child),
222
+ visit(ViewModel::Callbacks::Hook::OnChange, vm.child),
223
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, vm.child),
224
+ visit(ViewModel::Callbacks::Hook::AfterVisit, vm.child),
225
+
226
+ visit(ViewModel::Callbacks::Hook::BeforeValidate, vm),
227
+
228
+ visit(ViewModel::Callbacks::Hook::BeforeVisit, old_child),
229
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, old_child),
230
+ visit(ViewModel::Callbacks::Hook::OnChange, old_child),
231
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, old_child),
232
+ visit(ViewModel::Callbacks::Hook::AfterVisit, old_child),
233
+
234
+ visit(ViewModel::Callbacks::Hook::OnChange, vm),
235
+
236
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, vm),
237
+ visit(ViewModel::Callbacks::Hook::AfterVisit, vm),
238
+ ])
239
+ end
240
+ end
241
+
242
+ describe 'with parent and children test models' do
243
+ include ViewModelSpecHelpers::ParentAndHasManyChildren
244
+
245
+ def new_model
246
+ model_class.new(name: 'a', children: [child_model_class.new(name: 'b'), child_model_class.new(name: 'c')])
247
+ end
248
+
249
+ let(:new_child_hash) { { '_type' => 'Child', 'name' => 'q' } }
250
+ let(:new_child) { vm.children.detect { |c| c.name == 'q' } }
251
+
252
+ it 'calls hooks on replace associated' do
253
+ old_child_1, old_child_2 = vm.children.sort_by(&:name)
254
+
255
+ ctx = vm_deserialize_context(viewmodel_class)
256
+
257
+ vm.replace_associated(:children, [new_child_hash], deserialize_context: ctx)
258
+ vm.model.reload
259
+
260
+ value(callback.hook_trace).must_equal(
261
+ [visit(ViewModel::Callbacks::Hook::BeforeVisit, vm),
262
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, vm),
263
+ visit(ViewModel::Callbacks::Hook::BeforeValidate, vm),
264
+
265
+ visit(ViewModel::Callbacks::Hook::BeforeVisit, new_child),
266
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, new_child),
267
+ visit(ViewModel::Callbacks::Hook::BeforeValidate, new_child),
268
+ visit(ViewModel::Callbacks::Hook::OnChange, new_child),
269
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, new_child),
270
+ visit(ViewModel::Callbacks::Hook::AfterVisit, new_child),
271
+
272
+ visit(ViewModel::Callbacks::Hook::BeforeVisit, old_child_1),
273
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, old_child_1),
274
+ visit(ViewModel::Callbacks::Hook::OnChange, old_child_1),
275
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, old_child_1),
276
+ visit(ViewModel::Callbacks::Hook::AfterVisit, old_child_1),
277
+
278
+ visit(ViewModel::Callbacks::Hook::BeforeVisit, old_child_2),
279
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, old_child_2),
280
+ visit(ViewModel::Callbacks::Hook::OnChange, old_child_2),
281
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, old_child_2),
282
+ visit(ViewModel::Callbacks::Hook::AfterVisit, old_child_2),
283
+
284
+ visit(ViewModel::Callbacks::Hook::OnChange, vm),
285
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, vm),
286
+ visit(ViewModel::Callbacks::Hook::AfterVisit, vm)])
287
+ end
288
+
289
+ it 'calls hooks on append_associated' do
290
+ ctx = vm_deserialize_context(viewmodel_class)
291
+
292
+ vm.append_associated(:children, [new_child_hash], deserialize_context: ctx)
293
+ vm.model.reload
294
+
295
+ value(callback.hook_trace).must_equal(
296
+ [visit(ViewModel::Callbacks::Hook::BeforeVisit, vm),
297
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, vm),
298
+ visit(ViewModel::Callbacks::Hook::BeforeValidate, vm),
299
+
300
+ visit(ViewModel::Callbacks::Hook::BeforeVisit, new_child),
301
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, new_child),
302
+ visit(ViewModel::Callbacks::Hook::BeforeValidate, new_child),
303
+ visit(ViewModel::Callbacks::Hook::OnChange, new_child),
304
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, new_child),
305
+ visit(ViewModel::Callbacks::Hook::AfterVisit, new_child),
306
+
307
+ visit(ViewModel::Callbacks::Hook::OnChange, vm),
308
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, vm),
309
+ visit(ViewModel::Callbacks::Hook::AfterVisit, vm)])
310
+ end
311
+
312
+ it 'calls hooks on delete_associated' do
313
+ old_child_1, = vm.children.sort_by(&:name)
314
+
315
+ ctx = vm_deserialize_context(viewmodel_class)
316
+
317
+ vm.delete_associated(:children, old_child_1.id, deserialize_context: ctx)
318
+ vm.model.reload
319
+
320
+ value(callback.hook_trace).must_equal(
321
+ [visit(ViewModel::Callbacks::Hook::BeforeVisit, vm),
322
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, vm),
323
+ visit(ViewModel::Callbacks::Hook::BeforeValidate, vm),
324
+
325
+ visit(ViewModel::Callbacks::Hook::BeforeVisit, old_child_1),
326
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, old_child_1),
327
+ visit(ViewModel::Callbacks::Hook::OnChange, old_child_1),
328
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, old_child_1),
329
+ visit(ViewModel::Callbacks::Hook::AfterVisit, old_child_1),
330
+
331
+ visit(ViewModel::Callbacks::Hook::OnChange, vm),
332
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, vm),
333
+ visit(ViewModel::Callbacks::Hook::AfterVisit, vm)])
334
+ end
335
+ end
336
+
337
+ describe 'with list test model' do
338
+ include ViewModelSpecHelpers::List
339
+
340
+ def new_model
341
+ model_class.new(name: 'a', next: model_class.new(name: 'b', next: model_class.new(name: 'c')))
342
+ end
343
+
344
+ it 'calls hooks deeply on delete' do
345
+ child = vm.next
346
+ # grandchild = child.next
347
+
348
+ alter_by_view!(viewmodel_class, vm.model) do |view, _refs|
349
+ view['next'] = nil
350
+ end
351
+ value(callback.hook_trace).must_equal(
352
+ [
353
+ visit(ViewModel::Callbacks::Hook::BeforeVisit, vm),
354
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, vm),
355
+ visit(ViewModel::Callbacks::Hook::BeforeValidate, vm),
356
+
357
+ visit(ViewModel::Callbacks::Hook::BeforeVisit, child),
358
+ visit(ViewModel::Callbacks::Hook::BeforeDeserialize, child),
359
+ visit(ViewModel::Callbacks::Hook::OnChange, child),
360
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, child),
361
+ visit(ViewModel::Callbacks::Hook::AfterVisit, child),
362
+
363
+ visit(ViewModel::Callbacks::Hook::OnChange, vm),
364
+ visit(ViewModel::Callbacks::Hook::AfterDeserialize, vm),
365
+ visit(ViewModel::Callbacks::Hook::AfterVisit, vm),
366
+ ])
367
+ ## At present, children aren't deeplyvisited on delete.
368
+ # visit(ViewModel::Callbacks::Hook::BeforeVisit, grandchild),
369
+ # visit(ViewModel::Callbacks::Hook::BeforeDeserialize, grandchild),
370
+ # visit(ViewModel::Callbacks::Hook::OnChange, grandchild),
371
+ # visit(ViewModel::Callbacks::Hook::AfterDeserialize, grandchild),
372
+ # visit(ViewModel::Callbacks::Hook::AfterVisit, grandchild)
373
+ end
374
+ end
375
+ end
376
+
377
+ describe 'with parent and child test models' do
378
+ include ViewModelSpecHelpers::ParentAndHasOneChild
379
+
380
+ def new_model
381
+ model_class.new(name: 'a', child: child_model_class.new(name: 'b'))
382
+ end
383
+
384
+ describe 'view specific callbacks' do
385
+ class ViewSpecificCallback
386
+ include ViewModel::Callbacks
387
+ attr_reader :models, :children
388
+ def initialize
389
+ @models = []
390
+ @children = []
391
+ end
392
+
393
+ before_visit('Model') do
394
+ models << view
395
+ end
396
+
397
+ before_visit('Child') do
398
+ children << view
399
+ end
400
+ end
401
+
402
+ let(:callback) { ViewSpecificCallback.new }
403
+
404
+ it 'calls view specific callbacks' do
405
+ serialize(vm)
406
+ value(callback.models).must_equal([vm])
407
+ value(callback.children).must_equal([vm.child])
408
+ end
409
+ end
410
+ end
411
+
412
+ describe 'with single test model' do
413
+ include ViewModelSpecHelpers::Single
414
+
415
+ def new_model
416
+ model_class.new(name: 'a')
417
+ end
418
+
419
+ describe 'multiple callbacks on the same hook' do
420
+ class TwoCallbacks
421
+ include ViewModel::Callbacks
422
+
423
+ attr_reader :events
424
+ def initialize
425
+ @events = []
426
+ end
427
+
428
+ before_visit { events << :a }
429
+ before_visit { events << :b }
430
+ end
431
+
432
+ let(:callback) { TwoCallbacks.new }
433
+
434
+ it 'calls view specific callbacks' do
435
+ serialize(vm)
436
+ value(callback.events).must_equal([:a, :b])
437
+ end
438
+ end
439
+
440
+ describe 'callback inheritance' do
441
+ class ParentCallback
442
+ include ViewModel::Callbacks
443
+
444
+ attr_reader :a
445
+ def initialize
446
+ @a = 0
447
+ end
448
+
449
+ def a!
450
+ @a += 1
451
+ end
452
+
453
+ before_visit { a! }
454
+ end
455
+
456
+ class ChildCallback < ParentCallback
457
+ attr_reader :b
458
+ def initialize
459
+ super
460
+ @b = 0
461
+ end
462
+
463
+ def b!
464
+ @b += 1
465
+ end
466
+
467
+ before_visit { b! }
468
+ end
469
+
470
+ let(:callback) { ChildCallback.new }
471
+
472
+ it 'calls view specific callbacks' do
473
+ serialize(vm)
474
+ value(callback.a).must_equal(1)
475
+ value(callback.b).must_equal(1)
476
+ end
477
+ end
478
+
479
+ describe 'callback that raises' do
480
+ class Crash < RuntimeError; end
481
+ class CallbackCrasher
482
+ include ViewModel::Callbacks
483
+
484
+ before_visit do
485
+ raise Crash.new
486
+ end
487
+ end
488
+
489
+ let(:callback) { CallbackCrasher.new }
490
+
491
+ it 'raises the callback error' do
492
+ proc { serialize(vm) }.must_raise(Crash)
493
+ end
494
+
495
+ describe 'with an access control that rejects' do
496
+ def vm_serialize_context(viewmodel_class, **args)
497
+ super(viewmodel_class, access_control: ViewModel::AccessControl.new, **args)
498
+ end
499
+
500
+ it 'fails access control first' do
501
+ proc { serialize(vm) }.must_raise(ViewModel::AccessControlError)
502
+ end
503
+
504
+ describe 'and a view-mutating callback that crashes' do
505
+ class MutatingCrasher < CallbackCrasher
506
+ updates_view!
507
+ end
508
+
509
+ let(:callback) { MutatingCrasher.new }
510
+
511
+ it 'raises the callback error first' do
512
+ proc { serialize(vm) }.must_raise(Crash)
513
+ end
514
+ end
515
+ end
516
+ end
517
+
518
+ describe 'multiple callbacks' do
519
+ class RecordingCallback
520
+ include ViewModel::Callbacks
521
+ def initialize(events, name)
522
+ @events = events
523
+ @name = name
524
+ end
525
+
526
+ def record!
527
+ @events << @name
528
+ end
529
+
530
+ before_visit { record! }
531
+ end
532
+
533
+ class UpdatingCallback < RecordingCallback
534
+ updates_view!
535
+ end
536
+
537
+ let(:events) { [] }
538
+
539
+ let(:callbacks) do
540
+ [RecordingCallback.new(events, :a),
541
+ UpdatingCallback.new(events, :b),
542
+ RecordingCallback.new(events, :c),
543
+ UpdatingCallback.new(events, :d)]
544
+ end
545
+
546
+ it 'calls callbacks in order specified partitioned by update' do
547
+ serialize(vm)
548
+ value(events).must_equal([:b, :d, :a, :c])
549
+ end
550
+ end
551
+
552
+ describe 'provides details to the execution environment' do
553
+ class EnvCallback
554
+ include ViewModel::Callbacks
555
+ attr_accessor :env_contents
556
+
557
+ on_change do
558
+ self.env_contents = {
559
+ view: view,
560
+ model: model,
561
+ context: context,
562
+ changes: changes,
563
+ }
564
+ end
565
+ end
566
+
567
+ let(:callback) { EnvCallback.new }
568
+
569
+ it 'records the environment as expected' do
570
+ ctx = vm_deserialize_context(viewmodel_class)
571
+
572
+ alter_by_view!(viewmodel_class, vm.model, deserialize_context: ctx) do |view, _refs|
573
+ view['name'] = 'q'
574
+ end
575
+ value(callback.env_contents[:view]).must_equal(vm)
576
+ value(callback.env_contents[:model]).must_equal(vm.model)
577
+ value(callback.env_contents[:context]).must_equal(ctx)
578
+ value(callback.env_contents[:changes]).must_equal(ViewModel::Changes.new(changed_attributes: ['name']))
579
+ end
580
+ end
581
+ end
582
+ end