statesmin 1.0.0

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.
@@ -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