verborghs-state_machine 0.9.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. data/CHANGELOG.rdoc +360 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +635 -0
  4. data/Rakefile +77 -0
  5. data/examples/AutoShop_state.png +0 -0
  6. data/examples/Car_state.png +0 -0
  7. data/examples/TrafficLight_state.png +0 -0
  8. data/examples/Vehicle_state.png +0 -0
  9. data/examples/auto_shop.rb +11 -0
  10. data/examples/car.rb +19 -0
  11. data/examples/merb-rest/controller.rb +51 -0
  12. data/examples/merb-rest/model.rb +28 -0
  13. data/examples/merb-rest/view_edit.html.erb +24 -0
  14. data/examples/merb-rest/view_index.html.erb +23 -0
  15. data/examples/merb-rest/view_new.html.erb +13 -0
  16. data/examples/merb-rest/view_show.html.erb +17 -0
  17. data/examples/rails-rest/controller.rb +43 -0
  18. data/examples/rails-rest/migration.rb +11 -0
  19. data/examples/rails-rest/model.rb +23 -0
  20. data/examples/rails-rest/view_edit.html.erb +25 -0
  21. data/examples/rails-rest/view_index.html.erb +23 -0
  22. data/examples/rails-rest/view_new.html.erb +14 -0
  23. data/examples/rails-rest/view_show.html.erb +17 -0
  24. data/examples/traffic_light.rb +7 -0
  25. data/examples/vehicle.rb +31 -0
  26. data/init.rb +1 -0
  27. data/lib/state_machine/assertions.rb +36 -0
  28. data/lib/state_machine/callback.rb +241 -0
  29. data/lib/state_machine/condition_proxy.rb +106 -0
  30. data/lib/state_machine/eval_helpers.rb +83 -0
  31. data/lib/state_machine/event.rb +267 -0
  32. data/lib/state_machine/event_collection.rb +122 -0
  33. data/lib/state_machine/extensions.rb +149 -0
  34. data/lib/state_machine/guard.rb +230 -0
  35. data/lib/state_machine/initializers/merb.rb +1 -0
  36. data/lib/state_machine/initializers/rails.rb +5 -0
  37. data/lib/state_machine/initializers.rb +4 -0
  38. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  39. data/lib/state_machine/integrations/active_model/observer.rb +45 -0
  40. data/lib/state_machine/integrations/active_model.rb +445 -0
  41. data/lib/state_machine/integrations/active_record/locale.rb +20 -0
  42. data/lib/state_machine/integrations/active_record.rb +522 -0
  43. data/lib/state_machine/integrations/data_mapper/observer.rb +175 -0
  44. data/lib/state_machine/integrations/data_mapper.rb +379 -0
  45. data/lib/state_machine/integrations/mongo_mapper.rb +309 -0
  46. data/lib/state_machine/integrations/sequel.rb +356 -0
  47. data/lib/state_machine/integrations.rb +83 -0
  48. data/lib/state_machine/machine.rb +1645 -0
  49. data/lib/state_machine/machine_collection.rb +64 -0
  50. data/lib/state_machine/matcher.rb +123 -0
  51. data/lib/state_machine/matcher_helpers.rb +54 -0
  52. data/lib/state_machine/node_collection.rb +152 -0
  53. data/lib/state_machine/state.rb +260 -0
  54. data/lib/state_machine/state_collection.rb +112 -0
  55. data/lib/state_machine/transition.rb +399 -0
  56. data/lib/state_machine/transition_collection.rb +244 -0
  57. data/lib/state_machine.rb +421 -0
  58. data/lib/tasks/state_machine.rake +1 -0
  59. data/lib/tasks/state_machine.rb +27 -0
  60. data/test/files/en.yml +9 -0
  61. data/test/files/switch.rb +11 -0
  62. data/test/functional/state_machine_test.rb +980 -0
  63. data/test/test_helper.rb +4 -0
  64. data/test/unit/assertions_test.rb +40 -0
  65. data/test/unit/callback_test.rb +728 -0
  66. data/test/unit/condition_proxy_test.rb +328 -0
  67. data/test/unit/eval_helpers_test.rb +222 -0
  68. data/test/unit/event_collection_test.rb +324 -0
  69. data/test/unit/event_test.rb +795 -0
  70. data/test/unit/guard_test.rb +909 -0
  71. data/test/unit/integrations/active_model_test.rb +956 -0
  72. data/test/unit/integrations/active_record_test.rb +1918 -0
  73. data/test/unit/integrations/data_mapper_test.rb +1814 -0
  74. data/test/unit/integrations/mongo_mapper_test.rb +1382 -0
  75. data/test/unit/integrations/sequel_test.rb +1492 -0
  76. data/test/unit/integrations_test.rb +50 -0
  77. data/test/unit/invalid_event_test.rb +7 -0
  78. data/test/unit/invalid_transition_test.rb +7 -0
  79. data/test/unit/machine_collection_test.rb +565 -0
  80. data/test/unit/machine_test.rb +2349 -0
  81. data/test/unit/matcher_helpers_test.rb +37 -0
  82. data/test/unit/matcher_test.rb +155 -0
  83. data/test/unit/node_collection_test.rb +207 -0
  84. data/test/unit/state_collection_test.rb +280 -0
  85. data/test/unit/state_machine_test.rb +31 -0
  86. data/test/unit/state_test.rb +848 -0
  87. data/test/unit/transition_collection_test.rb +2098 -0
  88. data/test/unit/transition_test.rb +1384 -0
  89. metadata +176 -0
@@ -0,0 +1,956 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../test_helper')
2
+
3
+ # Load library
4
+ require 'rubygems'
5
+
6
+ gem 'activemodel', ENV['VERSION'] ? "=#{ENV['VERSION']}" : '>=3.0.0.beta'
7
+ require 'active_model'
8
+ require 'active_model/observing'
9
+ require 'active_support/all'
10
+
11
+ module ActiveModelTest
12
+ class BaseTestCase < Test::Unit::TestCase
13
+ def default_test
14
+ end
15
+
16
+ protected
17
+ # Creates a new ActiveRecord model (and the associated table)
18
+ def new_model(&block)
19
+ # Simple ActiveModel superclass
20
+ parent = Class.new do
21
+ def self.model_attribute(name)
22
+ define_method(name) { instance_variable_get("@#{name}") }
23
+ define_method("#{name}=") do |value|
24
+ send("#{name}_will_change!") if self.class <= ActiveModel::Dirty && !send("#{name}_changed?")
25
+ instance_variable_set("@#{name}", value)
26
+ end
27
+ end
28
+
29
+ def self.create
30
+ object = new
31
+ object.save
32
+ object
33
+ end
34
+
35
+ def initialize(attrs = {})
36
+ attrs.each {|attr, value| send("#{attr}=", value)}
37
+ @changed_attributes = {}
38
+ end
39
+
40
+ def attributes
41
+ @attributes ||= {}
42
+ end
43
+
44
+ def save
45
+ @changed_attributes = {}
46
+ true
47
+ end
48
+ end
49
+
50
+ model = Class.new(parent) do
51
+ def self.name
52
+ 'ActiveModelTest::Foo'
53
+ end
54
+
55
+ model_attribute :state
56
+ end
57
+ model.class_eval(&block) if block_given?
58
+ model
59
+ end
60
+
61
+ # Creates a new ActiveRecord observer
62
+ def new_observer(model, &block)
63
+ observer = Class.new(ActiveModel::Observer) do
64
+ attr_accessor :notifications
65
+
66
+ def initialize
67
+ super
68
+ @notifications = []
69
+ end
70
+ end
71
+ observer.observe(model)
72
+ observer.class_eval(&block) if block_given?
73
+ observer
74
+ end
75
+ end
76
+
77
+ class IntegrationTest < BaseTestCase
78
+ def test_should_match_if_class_includes_dirty_feature
79
+ assert StateMachine::Integrations::ActiveModel.matches?(new_model { include ActiveModel::Dirty })
80
+ end
81
+
82
+ def test_should_match_if_class_includes_observing_feature
83
+ assert StateMachine::Integrations::ActiveModel.matches?(new_model { include ActiveModel::Observing })
84
+ end
85
+
86
+ def test_should_match_if_class_includes_validations_feature
87
+ assert StateMachine::Integrations::ActiveModel.matches?(new_model { include ActiveModel::Validations })
88
+ end
89
+
90
+ def test_should_not_match_if_class_does_not_include_active_model_features
91
+ assert !StateMachine::Integrations::ActiveModel.matches?(new_model)
92
+ end
93
+
94
+ def test_should_have_no_defaults
95
+ assert_equal e = {}, StateMachine::Integrations::ActiveModel.defaults
96
+ end
97
+ end
98
+
99
+ class MachineByDefaultTest < BaseTestCase
100
+ def setup
101
+ @model = new_model
102
+ @machine = StateMachine::Machine.new(@model, :integration => :active_model)
103
+ end
104
+
105
+ def test_should_not_have_action
106
+ assert_nil @machine.action
107
+ end
108
+
109
+ def test_should_use_transactions
110
+ assert_equal true, @machine.use_transactions
111
+ end
112
+
113
+ def test_should_not_have_any_before_callbacks
114
+ assert_equal 0, @machine.callbacks[:before].size
115
+ end
116
+
117
+ def test_should_not_have_any_after_callbacks
118
+ assert_equal 0, @machine.callbacks[:after].size
119
+ end
120
+ end
121
+
122
+ class MachineWithStatesTest < BaseTestCase
123
+ def setup
124
+ @model = new_model
125
+ @machine = StateMachine::Machine.new(@model)
126
+ @machine.state :first_gear
127
+ end
128
+
129
+ def test_should_humanize_name
130
+ assert_equal 'first gear', @machine.state(:first_gear).human_name
131
+ end
132
+ end
133
+
134
+ class MachineWithStaticInitialStateTest < BaseTestCase
135
+ def setup
136
+ @model = new_model
137
+ @machine = StateMachine::Machine.new(@model, :initial => :parked, :integration => :active_model)
138
+ end
139
+
140
+ def test_should_set_initial_state_on_created_object
141
+ record = @model.new
142
+ assert_equal 'parked', record.state
143
+ end
144
+ end
145
+
146
+ class MachineWithDynamicInitialStateTest < BaseTestCase
147
+ def setup
148
+ @model = new_model
149
+ @machine = StateMachine::Machine.new(@model, :initial => lambda {|object| :parked}, :integration => :active_model)
150
+ @machine.state :parked
151
+ end
152
+
153
+ def test_should_set_initial_state_on_created_object
154
+ record = @model.new
155
+ assert_equal 'parked', record.state
156
+ end
157
+ end
158
+
159
+ class MachineWithEventsTest < BaseTestCase
160
+ def setup
161
+ @model = new_model
162
+ @machine = StateMachine::Machine.new(@model)
163
+ @machine.event :shift_up
164
+ end
165
+
166
+ def test_should_humanize_name
167
+ assert_equal 'shift up', @machine.event(:shift_up).human_name
168
+ end
169
+ end
170
+
171
+ class MachineWithModelStateAttributeTest < BaseTestCase
172
+ def setup
173
+ @model = new_model
174
+ @machine = StateMachine::Machine.new(@model, :initial => :parked, :integration => :active_model)
175
+ @machine.other_states(:idling)
176
+
177
+ @record = @model.new
178
+ end
179
+
180
+ def test_should_have_an_attribute_predicate
181
+ assert @record.respond_to?(:state?)
182
+ end
183
+
184
+ def test_should_raise_exception_for_predicate_without_parameters
185
+ assert_raise(IndexError) { @record.state? }
186
+ end
187
+
188
+ def test_should_return_false_for_predicate_if_does_not_match_current_value
189
+ assert !@record.state?(:idling)
190
+ end
191
+
192
+ def test_should_return_true_for_predicate_if_matches_current_value
193
+ assert @record.state?(:parked)
194
+ end
195
+
196
+ def test_should_raise_exception_for_predicate_if_invalid_state_specified
197
+ assert_raise(IndexError) { @record.state?(:invalid) }
198
+ end
199
+ end
200
+
201
+ class MachineWithNonModelStateAttributeUndefinedTest < BaseTestCase
202
+ def setup
203
+ @model = new_model do
204
+ def initialize
205
+ end
206
+ end
207
+
208
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked, :integration => :active_model)
209
+ @machine.other_states(:idling)
210
+ @record = @model.new
211
+ end
212
+
213
+ def test_should_not_define_a_reader_attribute_for_the_attribute
214
+ assert !@record.respond_to?(:status)
215
+ end
216
+
217
+ def test_should_not_define_a_writer_attribute_for_the_attribute
218
+ assert !@record.respond_to?(:status=)
219
+ end
220
+
221
+ def test_should_define_an_attribute_predicate
222
+ assert @record.respond_to?(:status?)
223
+ end
224
+ end
225
+
226
+ class MachineWithInitializedStateTest < BaseTestCase
227
+ def setup
228
+ @model = new_model
229
+ @machine = StateMachine::Machine.new(@model, :initial => :parked, :integration => :active_model)
230
+ @machine.state nil, :idling
231
+ end
232
+
233
+ def test_should_should_use_initialized_state_when_static
234
+ record = @model.new(:state => nil)
235
+ assert_nil record.state
236
+ end
237
+
238
+ def test_should_should_not_use_initialized_state_when_dynamic
239
+ @machine.initial_state = lambda {:parked}
240
+ record = @model.new(:state => nil)
241
+ assert_equal 'parked', record.state
242
+ end
243
+ end
244
+
245
+ class MachineWithDirtyAttributesTest < BaseTestCase
246
+ def setup
247
+ @model = new_model do
248
+ include ActiveModel::Dirty
249
+ define_attribute_methods [:state]
250
+ end
251
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
252
+ @machine.event :ignite
253
+ @machine.state :idling
254
+
255
+ @record = @model.create
256
+
257
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
258
+ @transition.perform
259
+ end
260
+
261
+ def test_should_include_state_in_changed_attributes
262
+ assert_equal %w(state), @record.changed
263
+ end
264
+
265
+ def test_should_track_attribute_change
266
+ assert_equal %w(parked idling), @record.changes['state']
267
+ end
268
+
269
+ def test_should_not_reset_changes_on_multiple_transitions
270
+ transition = StateMachine::Transition.new(@record, @machine, :ignite, :idling, :idling)
271
+ transition.perform
272
+
273
+ assert_equal %w(parked idling), @record.changes['state']
274
+ end
275
+ end
276
+
277
+ class MachineWithDirtyAttributesDuringLoopbackTest < BaseTestCase
278
+ def setup
279
+ @model = new_model do
280
+ include ActiveModel::Dirty
281
+ define_attribute_methods [:state]
282
+ end
283
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
284
+ @machine.event :park
285
+
286
+ @record = @model.create
287
+
288
+ @transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
289
+ @transition.perform
290
+ end
291
+
292
+ def test_should_include_state_in_changed_attributes
293
+ assert_equal %w(state), @record.changed
294
+ end
295
+
296
+ def test_should_track_attribute_changes
297
+ assert_equal %w(parked parked), @record.changes['state']
298
+ end
299
+ end
300
+
301
+ class MachineWithDirtyAttributesAndCustomAttributeTest < BaseTestCase
302
+ def setup
303
+ @model = new_model do
304
+ include ActiveModel::Dirty
305
+ model_attribute :status
306
+ define_attribute_methods [:status]
307
+ end
308
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
309
+ @machine.event :ignite
310
+ @machine.state :idling
311
+
312
+ @record = @model.create
313
+
314
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
315
+ @transition.perform
316
+ end
317
+
318
+ def test_should_include_state_in_changed_attributes
319
+ assert_equal %w(status), @record.changed
320
+ end
321
+
322
+ def test_should_track_attribute_change
323
+ assert_equal %w(parked idling), @record.changes['status']
324
+ end
325
+
326
+ def test_should_not_reset_changes_on_multiple_transitions
327
+ transition = StateMachine::Transition.new(@record, @machine, :ignite, :idling, :idling)
328
+ transition.perform
329
+
330
+ assert_equal %w(parked idling), @record.changes['status']
331
+ end
332
+ end
333
+
334
+ class MachineWithDirtyAttributeAndCustomAttributesDuringLoopbackTest < BaseTestCase
335
+ def setup
336
+ @model = new_model do
337
+ include ActiveModel::Dirty
338
+ model_attribute :status
339
+ define_attribute_methods [:status]
340
+ end
341
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
342
+ @machine.event :park
343
+
344
+ @record = @model.create
345
+
346
+ @transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
347
+ @transition.perform
348
+ end
349
+
350
+ def test_should_include_state_in_changed_attributes
351
+ assert_equal %w(status), @record.changed
352
+ end
353
+
354
+ def test_should_track_attribute_changes
355
+ assert_equal %w(parked parked), @record.changes['status']
356
+ end
357
+ end
358
+
359
+ class MachineWithCallbacksTest < BaseTestCase
360
+ def setup
361
+ @model = new_model
362
+ @machine = StateMachine::Machine.new(@model, :initial => :parked, :integration => :active_model)
363
+ @machine.other_states :idling
364
+ @machine.event :ignite
365
+
366
+ @record = @model.new(:state => 'parked')
367
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
368
+ end
369
+
370
+ def test_should_run_before_callbacks
371
+ called = false
372
+ @machine.before_transition {called = true}
373
+
374
+ @transition.perform
375
+ assert called
376
+ end
377
+
378
+ def test_should_pass_record_to_before_callbacks_with_one_argument
379
+ record = nil
380
+ @machine.before_transition {|arg| record = arg}
381
+
382
+ @transition.perform
383
+ assert_equal @record, record
384
+ end
385
+
386
+ def test_should_pass_record_and_transition_to_before_callbacks_with_multiple_arguments
387
+ callback_args = nil
388
+ @machine.before_transition {|*args| callback_args = args}
389
+
390
+ @transition.perform
391
+ assert_equal [@record, @transition], callback_args
392
+ end
393
+
394
+ def test_should_run_before_callbacks_outside_the_context_of_the_record
395
+ context = nil
396
+ @machine.before_transition {context = self}
397
+
398
+ @transition.perform
399
+ assert_equal self, context
400
+ end
401
+
402
+ def test_should_run_after_callbacks
403
+ called = false
404
+ @machine.after_transition {called = true}
405
+
406
+ @transition.perform
407
+ assert called
408
+ end
409
+
410
+ def test_should_pass_record_to_after_callbacks_with_one_argument
411
+ record = nil
412
+ @machine.after_transition {|arg| record = arg}
413
+
414
+ @transition.perform
415
+ assert_equal @record, record
416
+ end
417
+
418
+ def test_should_pass_record_and_transition_to_after_callbacks_with_multiple_arguments
419
+ callback_args = nil
420
+ @machine.after_transition {|*args| callback_args = args}
421
+
422
+ @transition.perform
423
+ assert_equal [@record, @transition], callback_args
424
+ end
425
+
426
+ def test_should_run_after_callbacks_outside_the_context_of_the_record
427
+ context = nil
428
+ @machine.after_transition {context = self}
429
+
430
+ @transition.perform
431
+ assert_equal self, context
432
+ end
433
+
434
+ def test_should_run_around_callbacks
435
+ before_called = false
436
+ after_called = false
437
+ @machine.around_transition {|block| before_called = true; block.call; after_called = true}
438
+
439
+ @transition.perform
440
+ assert before_called
441
+ assert after_called
442
+ end
443
+
444
+ def test_should_include_transition_states_in_known_states
445
+ @machine.before_transition :to => :first_gear, :do => lambda {}
446
+
447
+ assert_equal [:parked, :idling, :first_gear], @machine.states.map {|state| state.name}
448
+ end
449
+
450
+ def test_should_allow_symbolic_callbacks
451
+ callback_args = nil
452
+
453
+ klass = class << @record; self; end
454
+ klass.send(:define_method, :after_ignite) do |*args|
455
+ callback_args = args
456
+ end
457
+
458
+ @machine.before_transition(:after_ignite)
459
+
460
+ @transition.perform
461
+ assert_equal [@transition], callback_args
462
+ end
463
+
464
+ def test_should_allow_string_callbacks
465
+ class << @record
466
+ attr_reader :callback_result
467
+ end
468
+
469
+ @machine.before_transition('@callback_result = [1, 2, 3]')
470
+ @transition.perform
471
+
472
+ assert_equal [1, 2, 3], @record.callback_result
473
+ end
474
+ end
475
+
476
+ class MachineWithFailedBeforeCallbacksTest < BaseTestCase
477
+ def setup
478
+ @callbacks = []
479
+
480
+ @model = new_model
481
+ @machine = StateMachine::Machine.new(@model, :integration => :active_model)
482
+ @machine.state :parked, :idling
483
+ @machine.event :ignite
484
+ @machine.before_transition {@callbacks << :before_1; false}
485
+ @machine.before_transition {@callbacks << :before_2}
486
+ @machine.after_transition {@callbacks << :after}
487
+ @machine.around_transition {|block| @callbacks << :around_before; block.call; @callbacks << :around_after}
488
+
489
+ @record = @model.new(:state => 'parked')
490
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
491
+ @result = @transition.perform
492
+ end
493
+
494
+ def test_should_not_be_successful
495
+ assert !@result
496
+ end
497
+
498
+ def test_should_not_change_current_state
499
+ assert_equal 'parked', @record.state
500
+ end
501
+
502
+ def test_should_not_run_further_callbacks
503
+ assert_equal [:before_1], @callbacks
504
+ end
505
+ end
506
+
507
+ class MachineWithFailedAfterCallbacksTest < BaseTestCase
508
+ def setup
509
+ @callbacks = []
510
+
511
+ @model = new_model
512
+ @machine = StateMachine::Machine.new(@model, :integration => :active_model)
513
+ @machine.state :parked, :idling
514
+ @machine.event :ignite
515
+ @machine.after_transition {@callbacks << :after_1; false}
516
+ @machine.after_transition {@callbacks << :after_2}
517
+ @machine.around_transition {|block| @callbacks << :around_before; block.call; @callbacks << :around_after}
518
+
519
+ @record = @model.new(:state => 'parked')
520
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
521
+ @result = @transition.perform
522
+ end
523
+
524
+ def test_should_be_successful
525
+ assert @result
526
+ end
527
+
528
+ def test_should_change_current_state
529
+ assert_equal 'idling', @record.state
530
+ end
531
+
532
+ def test_should_not_run_further_after_callbacks
533
+ assert_equal [:around_before, :around_after, :after_1], @callbacks
534
+ end
535
+ end
536
+
537
+ class MachineWithValidationsTest < BaseTestCase
538
+ def setup
539
+ @model = new_model { include ActiveModel::Validations }
540
+ @machine = StateMachine::Machine.new(@model, :action => :save)
541
+ @machine.state :parked
542
+
543
+ @record = @model.new
544
+ end
545
+
546
+ def test_should_invalidate_using_errors
547
+ I18n.backend = I18n::Backend::Simple.new if Object.const_defined?(:I18n)
548
+ @record.state = 'parked'
549
+
550
+ @machine.invalidate(@record, :state, :invalid_transition, [[:event, 'park']])
551
+ assert_equal ['State cannot transition via "park"'], @record.errors.full_messages
552
+ end
553
+
554
+ def test_should_auto_prefix_custom_attributes_on_invalidation
555
+ @machine.invalidate(@record, :event, :invalid)
556
+
557
+ assert_equal ['State event is invalid'], @record.errors.full_messages
558
+ end
559
+
560
+ def test_should_clear_errors_on_reset
561
+ @record.state = 'parked'
562
+ @record.errors.add(:state, 'is invalid')
563
+
564
+ @machine.reset(@record)
565
+ assert_equal [], @record.errors.full_messages
566
+ end
567
+
568
+ def test_should_be_valid_if_state_is_known
569
+ @record.state = 'parked'
570
+
571
+ assert @record.valid?
572
+ end
573
+
574
+ def test_should_not_be_valid_if_state_is_unknown
575
+ @record.state = 'invalid'
576
+
577
+ assert !@record.valid?
578
+ assert_equal ['State is invalid'], @record.errors.full_messages
579
+ end
580
+ end
581
+
582
+ class MachineWithValidationsAndCustomAttributeTest < BaseTestCase
583
+ def setup
584
+ @model = new_model { include ActiveModel::Validations }
585
+
586
+ @machine = StateMachine::Machine.new(@model, :status, :attribute => :state)
587
+ @machine.state :parked
588
+
589
+ @record = @model.new
590
+ end
591
+
592
+ def test_should_add_validation_errors_to_custom_attribute
593
+ @record.state = 'invalid'
594
+
595
+ assert !@record.valid?
596
+ assert_equal ['State is invalid'], @record.errors.full_messages
597
+
598
+ @record.state = 'parked'
599
+ assert @record.valid?
600
+ end
601
+ end
602
+
603
+ class MachineWithStateDrivenValidationsTest < BaseTestCase
604
+ def setup
605
+ @model = new_model do
606
+ include ActiveModel::Validations
607
+ attr_accessor :seatbelt
608
+ end
609
+
610
+ @machine = StateMachine::Machine.new(@model)
611
+ @machine.state :first_gear, :second_gear do
612
+ validates_presence_of :seatbelt
613
+ end
614
+ @machine.other_states :parked
615
+ end
616
+
617
+ def test_should_be_valid_if_validation_fails_outside_state_scope
618
+ record = @model.new(:state => 'parked', :seatbelt => nil)
619
+ assert record.valid?
620
+ end
621
+
622
+ def test_should_be_invalid_if_validation_fails_within_state_scope
623
+ record = @model.new(:state => 'first_gear', :seatbelt => nil)
624
+ assert !record.valid?
625
+ end
626
+
627
+ def test_should_be_valid_if_validation_succeeds_within_state_scope
628
+ record = @model.new(:state => 'second_gear', :seatbelt => true)
629
+ assert record.valid?
630
+ end
631
+ end
632
+
633
+ class MachineWithObserversTest < BaseTestCase
634
+ def setup
635
+ @model = new_model { include ActiveModel::Observing }
636
+ @machine = StateMachine::Machine.new(@model)
637
+ @machine.state :parked, :idling
638
+ @machine.event :ignite
639
+ @record = @model.new(:state => 'parked')
640
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
641
+ end
642
+
643
+ def test_should_call_all_transition_callback_permutations
644
+ callbacks = [
645
+ :before_ignite_from_parked_to_idling,
646
+ :before_ignite_from_parked,
647
+ :before_ignite_to_idling,
648
+ :before_ignite,
649
+ :before_transition_state_from_parked_to_idling,
650
+ :before_transition_state_from_parked,
651
+ :before_transition_state_to_idling,
652
+ :before_transition_state,
653
+ :before_transition
654
+ ]
655
+
656
+ notified = false
657
+ observer = new_observer(@model) do
658
+ callbacks.each do |callback|
659
+ define_method(callback) do |*args|
660
+ notifications << callback
661
+ end
662
+ end
663
+ end
664
+
665
+ instance = observer.instance
666
+
667
+ @transition.perform
668
+ assert_equal callbacks, instance.notifications
669
+ end
670
+
671
+ def test_should_pass_record_and_transition_to_before_callbacks
672
+ observer = new_observer(@model) do
673
+ def before_transition(*args)
674
+ notifications << args
675
+ end
676
+ end
677
+ instance = observer.instance
678
+
679
+ @transition.perform
680
+ assert_equal [[@record, @transition]], instance.notifications
681
+ end
682
+
683
+ def test_should_pass_record_and_transition_to_after_callbacks
684
+ observer = new_observer(@model) do
685
+ def after_transition(*args)
686
+ notifications << args
687
+ end
688
+ end
689
+ instance = observer.instance
690
+
691
+ @transition.perform
692
+ assert_equal [[@record, @transition]], instance.notifications
693
+ end
694
+
695
+ def test_should_call_methods_outside_the_context_of_the_record
696
+ observer = new_observer(@model) do
697
+ def before_ignite(*args)
698
+ notifications << self
699
+ end
700
+ end
701
+ instance = observer.instance
702
+
703
+ @transition.perform
704
+ assert_equal [instance], instance.notifications
705
+ end
706
+ end
707
+
708
+ class MachineWithNamespacedObserversTest < BaseTestCase
709
+ def setup
710
+ @model = new_model { include ActiveModel::Observing }
711
+ @machine = StateMachine::Machine.new(@model, :state, :namespace => 'alarm')
712
+ @machine.state :active, :off
713
+ @machine.event :enable
714
+ @record = @model.new(:state => 'off')
715
+ @transition = StateMachine::Transition.new(@record, @machine, :enable, :off, :active)
716
+ end
717
+
718
+ def test_should_call_namespaced_before_event_method
719
+ observer = new_observer(@model) do
720
+ def before_enable_alarm(*args)
721
+ notifications << args
722
+ end
723
+ end
724
+ instance = observer.instance
725
+
726
+ @transition.perform
727
+ assert_equal [[@record, @transition]], instance.notifications
728
+ end
729
+
730
+ def test_should_call_namespaced_after_event_method
731
+ observer = new_observer(@model) do
732
+ def after_enable_alarm(*args)
733
+ notifications << args
734
+ end
735
+ end
736
+ instance = observer.instance
737
+
738
+ @transition.perform
739
+ assert_equal [[@record, @transition]], instance.notifications
740
+ end
741
+ end
742
+
743
+ class MachineWithMixedCallbacksTest < BaseTestCase
744
+ def setup
745
+ @model = new_model { include ActiveModel::Observing }
746
+ @machine = StateMachine::Machine.new(@model)
747
+ @machine.state :parked, :idling
748
+ @machine.event :ignite
749
+ @record = @model.new(:state => 'parked')
750
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
751
+
752
+ @notifications = []
753
+
754
+ # Create callbacks
755
+ @machine.before_transition {@notifications << :callback_before_transition}
756
+ @machine.after_transition {@notifications << :callback_after_transition}
757
+ @machine.around_transition {|block| @notifications << :callback_around_before_transition; block.call; @notifications << :callback_around_after_transition}
758
+
759
+ # Create observer callbacks
760
+ observer = new_observer(@model) do
761
+ def before_ignite(*args)
762
+ notifications << :observer_before_ignite
763
+ end
764
+
765
+ def before_transition(*args)
766
+ notifications << :observer_before_transition
767
+ end
768
+
769
+ def after_ignite(*args)
770
+ notifications << :observer_after_ignite
771
+ end
772
+
773
+ def after_transition(*args)
774
+ notifications << :observer_after_transition
775
+ end
776
+ end
777
+ instance = observer.instance
778
+ instance.notifications = @notifications
779
+
780
+ @transition.perform
781
+ end
782
+
783
+ def test_should_invoke_callbacks_in_specific_order
784
+ expected = [
785
+ :callback_before_transition,
786
+ :callback_around_before_transition,
787
+ :observer_before_ignite,
788
+ :observer_before_transition,
789
+ :callback_around_after_transition,
790
+ :callback_after_transition,
791
+ :observer_after_ignite,
792
+ :observer_after_transition
793
+ ]
794
+
795
+ assert_equal expected, @notifications
796
+ end
797
+ end
798
+
799
+ class MachineWithInternationalizationTest < BaseTestCase
800
+ def setup
801
+ I18n.backend = I18n::Backend::Simple.new
802
+
803
+ # Initialize the backend
804
+ I18n.backend.translate(:en, 'activemodel.errors.messages.invalid_transition', :event => 'ignite', :value => 'idling')
805
+
806
+ @model = new_model { include ActiveModel::Validations }
807
+ end
808
+
809
+ def test_should_use_defaults
810
+ I18n.backend.store_translations(:en, {
811
+ :activemodel => {:errors => {:messages => {:invalid_transition => 'cannot %{event}'}}}
812
+ })
813
+
814
+ machine = StateMachine::Machine.new(@model, :action => :save)
815
+ machine.state :parked, :idling
816
+ machine.event :ignite
817
+
818
+ record = @model.new(:state => 'idling')
819
+
820
+ machine.invalidate(record, :state, :invalid_transition, [[:event, 'ignite']])
821
+ assert_equal ['State cannot ignite'], record.errors.full_messages
822
+ end
823
+
824
+ def test_should_allow_customized_error_key
825
+ I18n.backend.store_translations(:en, {
826
+ :activemodel => {:errors => {:messages => {:bad_transition => 'cannot %{event}'}}}
827
+ })
828
+
829
+ machine = StateMachine::Machine.new(@model, :action => :save, :messages => {:invalid_transition => :bad_transition})
830
+ machine.state :parked, :idling
831
+
832
+ record = @model.new
833
+ record.state = 'idling'
834
+
835
+ machine.invalidate(record, :state, :invalid_transition, [[:event, 'ignite']])
836
+ assert_equal ['State cannot ignite'], record.errors.full_messages
837
+ end
838
+
839
+ def test_should_allow_customized_error_string
840
+ machine = StateMachine::Machine.new(@model, :action => :save, :messages => {:invalid_transition => 'cannot %{event}'})
841
+ machine.state :parked, :idling
842
+
843
+ record = @model.new(:state => 'idling')
844
+
845
+ machine.invalidate(record, :state, :invalid_transition, [[:event, 'ignite']])
846
+ assert_equal ['State cannot ignite'], record.errors.full_messages
847
+ end
848
+
849
+ def test_should_allow_customized_state_key_scoped_to_class_and_machine
850
+ I18n.backend.store_translations(:en, {
851
+ :activemodel => {:state_machines => {:'active_model_test/foo' => {:state => {:states => {:parked => 'shutdown'}}}}}
852
+ })
853
+
854
+ machine = StateMachine::Machine.new(@model)
855
+ machine.state :parked
856
+
857
+ assert_equal 'shutdown', machine.state(:parked).human_name
858
+ end
859
+
860
+ def test_should_allow_customized_state_key_scoped_to_machine
861
+ I18n.backend.store_translations(:en, {
862
+ :activemodel => {:state_machines => {:state => {:states => {:parked => 'shutdown'}}}}
863
+ })
864
+
865
+ machine = StateMachine::Machine.new(@model)
866
+ machine.state :parked
867
+
868
+ assert_equal 'shutdown', machine.state(:parked).human_name
869
+ end
870
+
871
+ def test_should_allow_customized_state_key_unscoped
872
+ I18n.backend.store_translations(:en, {
873
+ :activemodel => {:state_machines => {:states => {:parked => 'shutdown'}}}
874
+ })
875
+
876
+ machine = StateMachine::Machine.new(@model)
877
+ machine.state :parked
878
+
879
+ assert_equal 'shutdown', machine.state(:parked).human_name
880
+ end
881
+
882
+ def test_should_allow_customized_event_key_scoped_to_class_and_machine
883
+ I18n.backend.store_translations(:en, {
884
+ :activemodel => {:state_machines => {:'active_model_test/foo' => {:state => {:events => {:park => 'stop'}}}}}
885
+ })
886
+
887
+ machine = StateMachine::Machine.new(@model)
888
+ machine.event :park
889
+
890
+ assert_equal 'stop', machine.event(:park).human_name
891
+ end
892
+
893
+ def test_should_allow_customized_event_key_scoped_to_machine
894
+ I18n.backend.store_translations(:en, {
895
+ :activemodel => {:state_machines => {:state => {:events => {:park => 'stop'}}}}
896
+ })
897
+
898
+ machine = StateMachine::Machine.new(@model)
899
+ machine.event :park
900
+
901
+ assert_equal 'stop', machine.event(:park).human_name
902
+ end
903
+
904
+ def test_should_allow_customized_event_key_unscoped
905
+ I18n.backend.store_translations(:en, {
906
+ :activemodel => {:state_machines => {:events => {:park => 'stop'}}}
907
+ })
908
+
909
+ machine = StateMachine::Machine.new(@model)
910
+ machine.event :park
911
+
912
+ assert_equal 'stop', machine.event(:park).human_name
913
+ end
914
+
915
+ def test_should_only_add_locale_once_in_load_path
916
+ assert_equal 1, I18n.load_path.select {|path| path =~ %r{active_model/locale\.rb$}}.length
917
+
918
+ # Create another ActiveRecord model that will triger the i18n feature
919
+ new_model
920
+
921
+ assert_equal 1, I18n.load_path.select {|path| path =~ %r{active_model/locale\.rb$}}.length
922
+ end
923
+
924
+ def test_should_add_locale_to_beginning_of_load_path
925
+ @original_load_path = I18n.load_path
926
+ I18n.backend = I18n::Backend::Simple.new
927
+
928
+ app_locale = File.dirname(__FILE__) + '/../../files/en.yml'
929
+ default_locale = File.dirname(__FILE__) + '/../../../lib/state_machine/integrations/active_model/locale.rb'
930
+ I18n.load_path = [app_locale]
931
+
932
+ StateMachine::Machine.new(@model)
933
+
934
+ assert_equal [default_locale, app_locale].map {|path| File.expand_path(path)}, I18n.load_path.map {|path| File.expand_path(path)}
935
+ ensure
936
+ I18n.load_path = @original_load_path
937
+ end
938
+
939
+ def test_should_prefer_other_locales_first
940
+ @original_load_path = I18n.load_path
941
+ I18n.backend = I18n::Backend::Simple.new
942
+ I18n.load_path = [File.dirname(__FILE__) + '/../../files/en.yml']
943
+
944
+ machine = StateMachine::Machine.new(@model)
945
+ machine.state :parked, :idling
946
+ machine.event :ignite
947
+
948
+ record = @model.new(:state => 'idling')
949
+
950
+ machine.invalidate(record, :state, :invalid_transition, [[:event, 'ignite']])
951
+ assert_equal ['State cannot ignite'], record.errors.full_messages
952
+ ensure
953
+ I18n.load_path = @original_load_path
954
+ end
955
+ end
956
+ end