iknow_view_models 2.8.4

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 (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