statesmin 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,120 @@
1
+ require "spec_helper"
2
+
3
+ describe Statesmin::Callback do
4
+ let(:cb_lambda) { -> {} }
5
+ let(:callback) do
6
+ Statesmin::Callback.new(from: nil, to: nil, callback: cb_lambda)
7
+ end
8
+
9
+ describe "#initialize" do
10
+ context "with no callback" do
11
+ let(:cb_lambda) { nil }
12
+
13
+ it "raises an error" do
14
+ expect { callback }.to raise_error(Statesmin::InvalidCallbackError)
15
+ end
16
+ end
17
+ end
18
+
19
+ describe "#call" do
20
+ let(:spy) { double.as_null_object }
21
+ let(:cb_lambda) { -> { spy.call } }
22
+
23
+ it "delegates to callback" do
24
+ callback.call
25
+ expect(spy).to have_received(:call)
26
+ end
27
+ end
28
+
29
+ describe "#applies_to" do
30
+ let(:callback) do
31
+ Statesmin::Callback.new(from: :x, to: :y, callback: cb_lambda)
32
+ end
33
+ subject { callback.applies_to?(from: from, to: to) }
34
+
35
+ context "with any from value" do
36
+ let(:from) { nil }
37
+
38
+ context "and an allowed to value" do
39
+ let(:to) { :y }
40
+ it { is_expected.to be_truthy }
41
+ end
42
+
43
+ context "and a disallowed to value" do
44
+ let(:to) { :a }
45
+ it { is_expected.to be_falsey }
46
+ end
47
+ end
48
+
49
+ context "with any to value" do
50
+ let(:to) { nil }
51
+
52
+ context "and an allowed 'from' value" do
53
+ let(:from) { :x }
54
+ it { is_expected.to be_truthy }
55
+ end
56
+
57
+ context "and a disallowed 'from' value" do
58
+ let(:from) { :a }
59
+ it { is_expected.to be_falsey }
60
+ end
61
+ end
62
+
63
+ context "with any to and any from value on the callback" do
64
+ let(:callback) { Statesmin::Callback.new(callback: cb_lambda) }
65
+ let(:from) { :x }
66
+ let(:to) { :y }
67
+
68
+ it { is_expected.to be_truthy }
69
+ end
70
+
71
+ context "with any from value on the callback" do
72
+ let(:callback) do
73
+ Statesmin::Callback.new(to: [:y, :z], callback: cb_lambda)
74
+ end
75
+ let(:from) { :x }
76
+
77
+ context "and an allowed to value" do
78
+ let(:to) { :y }
79
+ it { is_expected.to be_truthy }
80
+ end
81
+
82
+ context "and another allowed to value" do
83
+ let(:to) { :z }
84
+ it { is_expected.to be_truthy }
85
+ end
86
+
87
+ context "and a disallowed to value" do
88
+ let(:to) { :a }
89
+ it { is_expected.to be_falsey }
90
+ end
91
+ end
92
+
93
+ context "with any to value on the callback" do
94
+ let(:callback) { Statesmin::Callback.new(from: :x, callback: cb_lambda) }
95
+ let(:to) { :y }
96
+
97
+ context "and an allowed to value" do
98
+ let(:from) { :x }
99
+ it { is_expected.to be_truthy }
100
+ end
101
+
102
+ context "and a disallowed to value" do
103
+ let(:from) { :a }
104
+ it { is_expected.to be_falsey }
105
+ end
106
+ end
107
+
108
+ context "with allowed 'from' and 'to' values" do
109
+ let(:from) { :x }
110
+ let(:to) { :y }
111
+ it { is_expected.to be_truthy }
112
+ end
113
+
114
+ context "with disallowed 'from' and 'to' values" do
115
+ let(:from) { :a }
116
+ let(:to) { :b }
117
+ it { is_expected.to be_falsey }
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,22 @@
1
+ require "spec_helper"
2
+
3
+ describe Statesmin::Guard do
4
+ let(:callback) { -> {} }
5
+ let(:guard) { Statesmin::Guard.new(from: nil, to: nil, callback: callback) }
6
+
7
+ specify { expect(guard).to be_a(Statesmin::Callback) }
8
+
9
+ describe "#call" do
10
+ subject(:call) { guard.call }
11
+
12
+ context "success" do
13
+ let(:callback) { -> { true } }
14
+ specify { expect { call }.to_not raise_error }
15
+ end
16
+
17
+ context "error" do
18
+ let(:callback) { -> { false } }
19
+ specify { expect { call }.to raise_error(Statesmin::GuardFailedError) }
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,704 @@
1
+ require "spec_helper"
2
+
3
+ describe Statesmin::Machine do
4
+ let(:machine) { Class.new { include Statesmin::Machine } }
5
+ let(:my_model) { Class.new { attr_accessor :current_state }.new }
6
+
7
+ describe ".state" do
8
+ before { machine.state(:x) }
9
+ before { machine.state(:y) }
10
+ specify { expect(machine.states).to eq(%w(x y)) }
11
+
12
+ context "initial" do
13
+ before { machine.state(:x, initial: true) }
14
+ specify { expect(machine.initial_state).to eq("x") }
15
+
16
+ context "when an initial state is already defined" do
17
+ it "raises an error" do
18
+ expect { machine.state(:y, initial: true) }.
19
+ to raise_error(Statesmin::InvalidStateError)
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ describe ".retry_conflicts" do
26
+ before do
27
+ machine.class_eval do
28
+ state :x, initial: true
29
+ state :y
30
+ state :z
31
+ transition from: :x, to: :y
32
+ transition from: :y, to: :z
33
+ end
34
+ end
35
+ let(:instance) { machine.new(my_model) }
36
+ let(:retry_attempts) { 2 }
37
+
38
+ subject(:transition_state) do
39
+ Statesmin::Machine.retry_conflicts(retry_attempts) do
40
+ instance.transition_to(:y)
41
+ end
42
+ end
43
+
44
+ context "when no exception occurs" do
45
+ it "runs the transition once" do
46
+ expect(instance).to receive(:transition_to).once
47
+ transition_state
48
+ end
49
+ end
50
+
51
+ context "when an irrelevant exception occurs" do
52
+ it "runs the transition once" do
53
+ expect(instance).
54
+ to receive(:transition_to).once.
55
+ and_raise(StandardError)
56
+ transition_state rescue nil # rubocop:disable RescueModifier
57
+ end
58
+
59
+ it "re-raises the exception" do
60
+ allow(instance).to receive(:transition_to).once.
61
+ and_raise(StandardError)
62
+ expect { transition_state }.to raise_error(StandardError)
63
+ end
64
+ end
65
+
66
+ context "when a TransitionConflictError occurs" do
67
+ context "and is resolved on the second attempt" do
68
+ it "runs the transition twice" do
69
+ expect(instance).
70
+ to receive(:transition_to).once.
71
+ and_raise(Statesmin::TransitionConflictError).
72
+ ordered
73
+ expect(instance).
74
+ to receive(:transition_to).once.ordered.and_call_original
75
+ transition_state
76
+ end
77
+ end
78
+
79
+ context "and keeps occurring" do
80
+ it "runs the transition `retry_attempts + 1` times" do
81
+ expect(instance).
82
+ to receive(:transition_to).
83
+ exactly(retry_attempts + 1).times.
84
+ and_raise(Statesmin::TransitionConflictError)
85
+ transition_state rescue nil # rubocop:disable RescueModifier
86
+ end
87
+
88
+ it "re-raises the conflict" do
89
+ allow(instance).
90
+ to receive(:transition_to).
91
+ and_raise(Statesmin::TransitionConflictError)
92
+ expect { transition_state }.
93
+ to raise_error(Statesmin::TransitionConflictError)
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ describe ".transition" do
100
+ before do
101
+ machine.class_eval do
102
+ state :x
103
+ state :y
104
+ state :z
105
+ end
106
+ end
107
+
108
+ context "given neither a 'from' nor a 'to' state" do
109
+ it "raises an error" do
110
+ expect { machine.transition }.
111
+ to raise_error(Statesmin::InvalidStateError)
112
+ end
113
+ end
114
+
115
+ context "given no 'from' state and a valid 'to' state" do
116
+ it "raises an error" do
117
+ expect { machine.transition from: nil, to: :x }.
118
+ to raise_error(Statesmin::InvalidStateError)
119
+ end
120
+ end
121
+
122
+ context "given a valid 'from' state and a no 'to' state" do
123
+ it "raises an error" do
124
+ expect { machine.transition from: :x, to: nil }.
125
+ to raise_error(Statesmin::InvalidStateError)
126
+ end
127
+ end
128
+
129
+ context "given a valid 'from' state and an empty 'to' state array" do
130
+ it "raises an error" do
131
+ expect { machine.transition from: :x, to: [] }.
132
+ to raise_error(Statesmin::InvalidStateError)
133
+ end
134
+ end
135
+
136
+ context "given an invalid 'from' state" do
137
+ it "raises an error" do
138
+ expect { machine.transition(from: :a, to: :x) }.
139
+ to raise_error(Statesmin::InvalidStateError)
140
+ end
141
+ end
142
+
143
+ context "given an invalid 'to' state" do
144
+ it "raises an error" do
145
+ expect { machine.transition(from: :x, to: :a) }.
146
+ to raise_error(Statesmin::InvalidStateError)
147
+ end
148
+ end
149
+
150
+ context "valid 'from' and 'to' states" do
151
+ it "records the transition" do
152
+ machine.transition(from: :x, to: :y)
153
+ machine.transition(from: :x, to: :z)
154
+ expect(machine.successors).to eq("x" => %w(y z))
155
+ end
156
+ end
157
+ end
158
+
159
+ describe ".validate_callback_condition" do
160
+ before do
161
+ machine.class_eval do
162
+ state :x
163
+ state :y
164
+ state :z
165
+ transition from: :x, to: :y
166
+ transition from: :y, to: :z
167
+ end
168
+ end
169
+
170
+ context "with a terminal 'from' state" do
171
+ it "raises an exception" do
172
+ expect { machine.validate_callback_condition(from: :z, to: :y) }.
173
+ to raise_error(Statesmin::InvalidTransitionError)
174
+ end
175
+ end
176
+
177
+ context "with an initial 'to' state" do
178
+ it "raises an exception" do
179
+ expect { machine.validate_callback_condition(from: :y, to: :x) }.
180
+ to raise_error(Statesmin::InvalidTransitionError)
181
+ end
182
+ end
183
+
184
+ context "with an invalid transition" do
185
+ it "raises an exception" do
186
+ expect { machine.validate_callback_condition(from: :x, to: :z) }.
187
+ to raise_error(Statesmin::InvalidTransitionError)
188
+ end
189
+ end
190
+
191
+ context "with any states" do
192
+ it "does not raise an exception" do
193
+ expect { machine.validate_callback_condition }.to_not raise_error
194
+ end
195
+ end
196
+
197
+ context "with a valid transition" do
198
+ it "does not raise an exception" do
199
+ expect { machine.validate_callback_condition(from: :x, to: :y) }.
200
+ to_not raise_error
201
+ end
202
+ end
203
+ end
204
+
205
+ shared_examples "a callback store" do |assignment_method, callback_store|
206
+ before do
207
+ machine.class_eval do
208
+ state :x, initial: true
209
+ state :y
210
+ state :z
211
+ transition from: :x, to: [:y, :z]
212
+ end
213
+ end
214
+
215
+ let(:options) { { from: nil, to: [] } }
216
+ let(:set_callback) { machine.send(assignment_method, options) {} }
217
+
218
+ shared_examples "fails" do |error_type|
219
+ specify { expect { set_callback }.to raise_error(error_type) }
220
+
221
+ it "does not add a callback" do
222
+ expect do
223
+ begin
224
+ set_callback
225
+ rescue error_type
226
+ nil
227
+ end
228
+ end.to_not change(machine.callbacks[callback_store], :count)
229
+ end
230
+ end
231
+
232
+ shared_examples "adds callback" do
233
+ specify { expect { set_callback }.to_not raise_error }
234
+
235
+ it "stores callbacks" do
236
+ expect { set_callback }.
237
+ to change(machine.callbacks[callback_store], :count).by(1)
238
+ end
239
+
240
+ it "stores callback instances" do
241
+ set_callback
242
+ machine.callbacks[callback_store].each do |callback|
243
+ expect(callback).to be_a(Statesmin::Callback)
244
+ end
245
+ end
246
+ end
247
+
248
+ context "with invalid states" do
249
+ context "when both are invalid" do
250
+ let(:options) { { from: :foo, to: :bar } }
251
+ it_behaves_like "fails", Statesmin::InvalidStateError
252
+ end
253
+
254
+ context "from a terminal state to anything" do
255
+ let(:options) { { from: :y, to: [] } }
256
+ it_behaves_like "fails", Statesmin::InvalidTransitionError
257
+ end
258
+
259
+ context "to an initial state and from anything" do
260
+ let(:options) { { from: nil, to: :x } }
261
+ it_behaves_like "fails", Statesmin::InvalidTransitionError
262
+ end
263
+
264
+ context "from a terminal state and to multiple states" do
265
+ let(:options) { { from: :y, to: [:x, :z] } }
266
+ it_behaves_like "fails", Statesmin::InvalidTransitionError
267
+ end
268
+
269
+ context "to an initial state and other states" do
270
+ let(:options) { { from: nil, to: [:y, :x, :z] } }
271
+ it_behaves_like "fails", Statesmin::InvalidTransitionError
272
+ end
273
+ end
274
+
275
+ context "with validate_states" do
276
+ context "from anything" do
277
+ let(:options) { { from: nil, to: :y } }
278
+ it_behaves_like "adds callback"
279
+ end
280
+
281
+ context "to anything" do
282
+ let(:options) { { from: :x, to: [] } }
283
+ it_behaves_like "adds callback"
284
+ end
285
+
286
+ context "to several" do
287
+ let(:options) { { from: :x, to: [:y, :z] } }
288
+ it_behaves_like "adds callback"
289
+ end
290
+
291
+ context "from any to several" do
292
+ let(:options) { { from: nil, to: [:y, :z] } }
293
+ it_behaves_like "adds callback"
294
+ end
295
+ end
296
+ end
297
+
298
+ describe ".before_transition" do
299
+ it_behaves_like "a callback store", :before_transition, :before
300
+ end
301
+
302
+ describe ".after_transition" do
303
+ it_behaves_like "a callback store", :after_transition, :after
304
+ end
305
+
306
+ describe ".guard_transition" do
307
+ it_behaves_like "a callback store", :guard_transition, :guards
308
+ end
309
+
310
+ describe "#initialize" do
311
+ before do
312
+ machine.class_eval do
313
+ state :x, initial: true
314
+ state :y
315
+ end
316
+ end
317
+
318
+ it "accepts an object to manipulate" do
319
+ machine_instance = machine.new(my_model)
320
+ expect(machine_instance.object).to be(my_model)
321
+ end
322
+
323
+ context "with a state option given" do
324
+ context "and the option is a valid state" do
325
+ it "sets the current_state to the supplied state option" do
326
+ machine_instance = machine.new(my_model, state: :y)
327
+ expect(machine_instance.current_state).to eq("y")
328
+ end
329
+ end
330
+
331
+ context "and the option is not a valid state" do
332
+ it "raises an InvalidStateError" do
333
+ expect { machine.new(my_model, state: :xyz) }.
334
+ to raise_error(Statesmin::InvalidStateError)
335
+ end
336
+ end
337
+ end
338
+
339
+ context "without a state option given" do
340
+ it "sets the current_state to the class defined initial state" do
341
+ machine_instance = machine.new(my_model)
342
+ expect(machine_instance.current_state).to eq("x")
343
+ end
344
+ end
345
+ end
346
+
347
+ describe "#after_initialize" do
348
+ it "is called after initialize" do
349
+ machine.class_eval do
350
+ def after_initialize; end
351
+ end
352
+ expect_any_instance_of(machine).to receive :after_initialize
353
+ machine.new(my_model)
354
+ end
355
+ end
356
+
357
+ describe "#current_state" do
358
+ before do
359
+ machine.class_eval do
360
+ state :x, initial: true
361
+ state :y
362
+ state :z
363
+ transition from: :x, to: :y
364
+ transition from: :y, to: :z
365
+ end
366
+ end
367
+
368
+ let(:instance) { machine.new(my_model) }
369
+ subject { instance.current_state }
370
+
371
+ context "with no transitions" do
372
+ it { is_expected.to eq(machine.initial_state) }
373
+ end
374
+
375
+ context "with multiple transitions" do
376
+ before { instance.transition_to!(:y) }
377
+ before { instance.transition_to!(:z) }
378
+
379
+ it { is_expected.to eq("z") }
380
+ end
381
+ end
382
+
383
+ describe "#in_state?" do
384
+ before do
385
+ machine.class_eval do
386
+ state :x, initial: true
387
+ state :y
388
+ transition from: :x, to: :y
389
+ end
390
+ end
391
+
392
+ let(:instance) { machine.new(my_model) }
393
+ subject { instance.in_state?(state) }
394
+ before { instance.transition_to!(:y) }
395
+
396
+ context "when machine is in given state" do
397
+ let(:state) { "y" }
398
+ it { is_expected.to eq(true) }
399
+ end
400
+
401
+ context "when machine is not in given state" do
402
+ let(:state) { "x" }
403
+ it { is_expected.to eq(false) }
404
+ end
405
+
406
+ context "when given a symbol" do
407
+ let(:state) { :y }
408
+ it { is_expected.to eq(true) }
409
+ end
410
+
411
+ context "when given multiple states" do
412
+ context "when given multiple arguments" do
413
+ context "when one of the states is the current state" do
414
+ subject { instance.in_state?(:x, :y) }
415
+ it { is_expected.to eq(true) }
416
+ end
417
+
418
+ context "when none of the states are the current state" do
419
+ subject { instance.in_state?(:x, :z) }
420
+ it { is_expected.to eq(false) }
421
+ end
422
+ end
423
+
424
+ context "when given an array" do
425
+ context "when one of the states is the current state" do
426
+ subject { instance.in_state?([:x, :y]) }
427
+ it { is_expected.to eq(true) }
428
+ end
429
+
430
+ context "when none of the states are the current state" do
431
+ subject { instance.in_state?([:x, :z]) }
432
+ it { is_expected.to eq(false) }
433
+ end
434
+ end
435
+ end
436
+ end
437
+
438
+ describe "#allowed_transitions" do
439
+ before do
440
+ machine.class_eval do
441
+ state :x, initial: true
442
+ state :y
443
+ state :z
444
+ transition from: :x, to: [:y, :z]
445
+ transition from: :y, to: :z
446
+ end
447
+ end
448
+
449
+ let(:instance) { machine.new(my_model) }
450
+ subject { instance.allowed_transitions }
451
+
452
+ context "with multiple possible states" do
453
+ it { is_expected.to eq(%w(y z)) }
454
+ end
455
+
456
+ context "with one possible state" do
457
+ before { instance.transition_to!(:y) }
458
+ it { is_expected.to eq(['z']) }
459
+ end
460
+
461
+ context "with no possible transitions" do
462
+ before { instance.transition_to!(:z) }
463
+ it { is_expected.to eq([]) }
464
+ end
465
+ end
466
+
467
+ describe "#can_transition_to?" do
468
+ before do
469
+ machine.class_eval do
470
+ state :x, initial: true
471
+ state :y
472
+ state :z
473
+ transition from: :x, to: :y
474
+ transition from: :y, to: :z
475
+ end
476
+ end
477
+
478
+ let(:instance) { machine.new(my_model) }
479
+ subject { instance.can_transition_to?(new_state) }
480
+
481
+ context "when the transition is invalid" do
482
+ context "with an initial to state" do
483
+ let(:new_state) { :x }
484
+ it { is_expected.to be_falsey }
485
+ end
486
+
487
+ context "with a terminal from state" do
488
+ before { instance.transition_to!(:y) }
489
+ let(:new_state) { :y }
490
+ it { is_expected.to be_falsey }
491
+ end
492
+
493
+ context "and is guarded" do
494
+ let(:guard_cb) { -> { false } }
495
+ let(:new_state) { :z }
496
+ before { machine.guard_transition(to: new_state, &guard_cb) }
497
+
498
+ it "does not fire guard" do
499
+ expect(guard_cb).not_to receive(:call)
500
+ is_expected.to be_falsey
501
+ end
502
+ end
503
+ end
504
+
505
+ context "when the transition valid" do
506
+ let(:new_state) { :y }
507
+ it { is_expected.to be_truthy }
508
+
509
+ context "but it has a failing guard" do
510
+ before { machine.guard_transition(to: :y) { false } }
511
+ it { is_expected.to be_falsey }
512
+ end
513
+ end
514
+ end
515
+
516
+ describe "#transition_to!" do
517
+ before do
518
+ machine.class_eval do
519
+ state :x, initial: true
520
+ state :y
521
+ state :z
522
+ transition from: :x, to: :y
523
+ transition from: :y, to: :z
524
+ end
525
+ end
526
+
527
+ let(:instance) { machine.new(my_model) }
528
+
529
+ context "when it is called with a block" do
530
+ let(:block_spy) { double(called: 'called') }
531
+ let(:block) { proc { block_spy.called } }
532
+
533
+ context "and the state cannot be transitioned to" do
534
+ it "does not call the block" do
535
+ expect(block_spy).to_not receive(:called)
536
+ expect { instance.transition_to!(:z, &block) }.to raise_error
537
+ end
538
+ end
539
+
540
+ context "and the state can be transitioned to" do
541
+ it "calls the block" do
542
+ expect(block_spy).to receive(:called)
543
+ instance.transition_to(:y, &block)
544
+ end
545
+
546
+ context 'and the block errors' do
547
+ let(:error_block) { proc { raise } }
548
+
549
+ it "raises the error" do
550
+ expect { instance.transition_to(:y, &error_block) }.
551
+ to raise_error(RuntimeError)
552
+ end
553
+
554
+ it "does not change the current_state" do
555
+ expect { instance.transition_to(:y, &error_block) }.to raise_error
556
+ expect(instance.current_state).to eq('x')
557
+ end
558
+ end
559
+
560
+ context 'and the block does not error' do
561
+ it "returns the value of the block" do
562
+ expect(instance.transition_to(:y, &block)).to eq('called')
563
+ end
564
+
565
+ it "updates the current_state" do
566
+ instance.transition_to(:y, &block)
567
+ expect(instance.current_state).to eq('y')
568
+ end
569
+ end
570
+ end
571
+ end
572
+
573
+ context "when the state cannot be transitioned to" do
574
+ it "raises an error" do
575
+ expect { instance.transition_to!(:z) }.
576
+ to raise_error(Statesmin::TransitionFailedError)
577
+ end
578
+ end
579
+
580
+ context "when the state can be transitioned to" do
581
+ it "changes state" do
582
+ instance.transition_to!(:y)
583
+ expect(instance.current_state).to eq("y")
584
+ end
585
+
586
+ specify { expect(instance.transition_to!(:y)).to eq(true) }
587
+
588
+ context "with a guard" do
589
+ let(:result) { true }
590
+ let(:guard_cb) { ->(*_args) { result } }
591
+ before { machine.guard_transition(from: :x, to: :y, &guard_cb) }
592
+
593
+ context "and an object to act on" do
594
+ let(:instance) { machine.new(my_model) }
595
+
596
+ it "passes the object to the guard" do
597
+ expect(guard_cb).to receive(:call).once.
598
+ with(my_model, {}).and_return(true)
599
+ instance.transition_to!(:y)
600
+ end
601
+ end
602
+
603
+ context "which passes" do
604
+ it "changes state" do
605
+ instance.transition_to!(:y)
606
+ expect(instance.current_state).to eq("y")
607
+ end
608
+ end
609
+
610
+ context "which fails" do
611
+ let(:result) { false }
612
+
613
+ it "raises an exception" do
614
+ expect { instance.transition_to!(:y) }.
615
+ to raise_error(Statesmin::GuardFailedError)
616
+ end
617
+ end
618
+ end
619
+ end
620
+ end
621
+
622
+ describe "#transition_to" do
623
+ let(:instance) { machine.new(my_model) }
624
+ let(:metadata) { { some: :metadata } }
625
+ subject { instance.transition_to(:some_state, metadata, &proc {}) }
626
+
627
+ context "when it is succesful" do
628
+ before do
629
+ expect(instance).to receive(:transition_to!).once.
630
+ with(:some_state, metadata).and_return(:some_state)
631
+ end
632
+ it { is_expected.to be(:some_state) }
633
+ end
634
+
635
+ context "when it is unsuccesful" do
636
+ before do
637
+ allow(instance).to receive(:transition_to!).
638
+ and_raise(Statesmin::GuardFailedError)
639
+ end
640
+ it { is_expected.to be_falsey }
641
+ end
642
+
643
+ context "when a non statesmin exception is raised" do
644
+ before do
645
+ allow(instance).to receive(:transition_to!).
646
+ and_raise(RuntimeError, 'user defined exception')
647
+ end
648
+
649
+ it "should not rescue the exception" do
650
+ expect { instance.transition_to(:some_state, metadata) }.
651
+ to raise_error(RuntimeError, 'user defined exception')
652
+ end
653
+ end
654
+ end
655
+
656
+ shared_examples "a callback filter" do |definer, phase|
657
+ before do
658
+ machine.class_eval do
659
+ state :x
660
+ state :y
661
+ state :z
662
+ transition from: :x, to: :y
663
+ transition from: :y, to: :z
664
+ end
665
+ end
666
+
667
+ let(:instance) { machine.new(my_model) }
668
+ let(:callbacks) { instance.send(:callbacks_for, phase, from: :x, to: :y) }
669
+
670
+ context "with no defined callbacks" do
671
+ specify { expect(callbacks).to eq([]) }
672
+ end
673
+
674
+ context "with defined callbacks" do
675
+ let(:callback_1) { -> { "Hi" } }
676
+ let(:callback_2) { -> { "Bye" } }
677
+
678
+ before do
679
+ machine.send(definer, from: :x, to: :y, &callback_1)
680
+ machine.send(definer, from: :y, to: :z, &callback_2)
681
+ end
682
+
683
+ it "contains the relevant callback" do
684
+ expect(callbacks.map(&:callback)).to include(callback_1)
685
+ end
686
+
687
+ it "does not contain the irrelevant callback" do
688
+ expect(callbacks.map(&:callback)).to_not include(callback_2)
689
+ end
690
+ end
691
+ end
692
+
693
+ describe "#guards_for" do
694
+ it_behaves_like "a callback filter", :guard_transition, :guards
695
+ end
696
+
697
+ describe "#before_callbacks_for" do
698
+ it_behaves_like "a callback filter", :before_transition, :before
699
+ end
700
+
701
+ describe "#after_callbacks_for" do
702
+ it_behaves_like "a callback filter", :after_transition, :after
703
+ end
704
+ end