verborghs-state_machine 0.9.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 (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