state_machines-activemodel 0.9.0 → 0.31.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -26
  3. data/lib/state_machines/integrations/active_model/locale.rb +7 -3
  4. data/lib/state_machines/integrations/active_model/version.rb +3 -1
  5. data/lib/state_machines/integrations/active_model.rb +71 -102
  6. data/lib/state_machines-activemodel.rb +2 -0
  7. data/test/event_human_name_test.rb +176 -0
  8. data/test/human_name_preservation_test.rb +75 -0
  9. data/test/integration_test.rb +3 -1
  10. data/test/machine_by_default_test.rb +3 -1
  11. data/test/machine_errors_test.rb +3 -1
  12. data/test/machine_initialization_compatibility_test.rb +60 -0
  13. data/test/machine_multiple_test.rb +3 -1
  14. data/test/machine_with_callbacks_test.rb +3 -1
  15. data/test/machine_with_dirty_attribute_and_custom_attributes_during_loopback_test.rb +3 -1
  16. data/test/machine_with_dirty_attribute_and_state_events_test.rb +3 -1
  17. data/test/machine_with_dirty_attributes_and_custom_attribute_test.rb +3 -1
  18. data/test/machine_with_dirty_attributes_during_loopback_test.rb +3 -1
  19. data/test/machine_with_dirty_attributes_test.rb +3 -1
  20. data/test/machine_with_dynamic_initial_state_test.rb +3 -1
  21. data/test/machine_with_events_test.rb +3 -1
  22. data/test/machine_with_failed_after_callbacks_test.rb +3 -1
  23. data/test/machine_with_failed_before_callbacks_test.rb +3 -1
  24. data/test/machine_with_initialized_aliased_attribute_test.rb +3 -1
  25. data/test/machine_with_initialized_state_test.rb +3 -1
  26. data/test/machine_with_internationalization_test.rb +12 -10
  27. data/test/machine_with_model_state_attribute_test.rb +3 -1
  28. data/test/machine_with_non_model_state_attribute_undefined_test.rb +3 -1
  29. data/test/machine_with_state_driven_validations_test.rb +4 -2
  30. data/test/machine_with_states_test.rb +3 -1
  31. data/test/machine_with_static_initial_state_test.rb +3 -1
  32. data/test/machine_with_validations_and_custom_attribute_test.rb +3 -1
  33. data/test/machine_with_validations_test.rb +3 -1
  34. data/test/state_human_name_test.rb +152 -0
  35. data/test/test_helper.rb +55 -0
  36. metadata +18 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8bb31a6d52d73caf948365731b8670f86a57814d839c4606cd12c88b178f48f
4
- data.tar.gz: c3f2f12fc7077f4fb230b08908314684fbbe7c59153cd12cc35dbacd767dd386
3
+ metadata.gz: ad141a5029b6489fb99da9c9ef4ad7bf2059d9947e993265225bb4ca88a0ff66
4
+ data.tar.gz: ef7ae48fd6d4d1f712842e2f0057158e5674b5ba59487cc3b0483c89524374e3
5
5
  SHA512:
6
- metadata.gz: 8dd82a7786ea47d62fb36e04fd85be1451d1d37666702545bf9db2c0b13601d61ca65c1ab0b28844f64e7cdab82e62a1ea8221706b5d8342b003caa1fb8d9f08
7
- data.tar.gz: 944d5c9ce3320e1f46e638c120ee2fcf4bd97f4c98ab3fad8e1598bc2fe2cd0a80ed2db61dc86535b551366c38108c4a007729eaeef4703e5a9e51c874921a2a
6
+ metadata.gz: 82b54e46bc9713fdda9c22701d4bec699c8065344bb32e10217dc730a0743b0296bb97245d38ffc8d3b18bd65c770058cb1485a723f2620d66051ce93ce4b106
7
+ data.tar.gz: 3ae6a52dd12a7c7ff56666fc9d691218f043ba2a0915d80262800a873589c0275a538d5b08ab956d046e23a3ba73e9c6d12ceb732ed0c75ee63d89cb77414b09
data/README.md CHANGED
@@ -1,5 +1,4 @@
1
1
  ![Build Status](https://github.com/state-machines/state_machines-activemodel/actions/workflows/ruby.yml/badge.svg)
2
- [![Code Climate](https://codeclimate.com/github/state-machines/state_machines-activemodel.svg)](https://codeclimate.com/github/state-machines/state_machines-activemodel)
3
2
 
4
3
  # StateMachines ActiveModel Integration
5
4
 
@@ -23,7 +22,7 @@ Or install it yourself as:
23
22
 
24
23
  ## Dependencies
25
24
 
26
- Active Model 5.1+
25
+ Active Model 7.1+
27
26
 
28
27
  ## Usage
29
28
 
@@ -36,19 +35,19 @@ class Vehicle
36
35
  attr_accessor :state
37
36
  define_attribute_methods [:state]
38
37
 
39
- state_machine :initial => :parked do
40
- before_transition :parked => any - :parked, :do => :put_on_seatbelt
41
- after_transition any => :parked do |vehicle, transition|
38
+ state_machine initial: :parked do
39
+ before_transition parked: any - :parked, do: :put_on_seatbelt
40
+ after_transition any: :parked do |vehicle, transition|
42
41
  vehicle.seatbelt = 'off'
43
42
  end
44
43
  around_transition :benchmark
45
44
 
46
- event :ignite do
47
- transition :parked => :idling
45
+ event ignite: do
46
+ transition parked: :idling
48
47
  end
49
48
 
50
49
  state :first_gear, :second_gear do
51
- validates_presence_of :seatbelt_on
50
+ validates :seatbelt_on, presence: true
52
51
  end
53
52
  end
54
53
 
@@ -62,24 +61,6 @@ class Vehicle
62
61
  ...
63
62
  end
64
63
  end
65
-
66
- class VehicleObserver < ActiveModel::Observer
67
- # Callback for :ignite event *before* the transition is performed
68
- def before_ignite(vehicle, transition)
69
- # log message
70
- end
71
-
72
- # Generic transition callback *after* the transition is performed
73
- def after_transition(vehicle, transition)
74
- Audit.log(vehicle, transition)
75
- end
76
-
77
- # Generic callback after the transition fails to perform
78
- def after_failure_to_transition(vehicle, transition)
79
- Audit.error(vehicle, transition)
80
- end
81
- end
82
-
83
64
  ```
84
65
 
85
66
  ## Contributing
@@ -1,10 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Use lazy evaluation to avoid circular dependencies with frozen default_messages
4
+ # This ensures messages can be updated after gem loading while maintaining thread safety
1
5
  { en: {
2
6
  activemodel: {
3
7
  errors: {
4
8
  messages: {
5
- invalid: StateMachines::Machine.default_messages[:invalid],
6
- invalid_event: StateMachines::Machine.default_messages[:invalid_event] % ['%{state}'],
7
- invalid_transition: StateMachines::Machine.default_messages[:invalid_transition] % ['%{event}']
9
+ invalid: lambda { |*| StateMachines::Machine.default_messages[:invalid] },
10
+ invalid_event: lambda { |*| StateMachines::Machine.default_messages[:invalid_event] % ['%{state}'] },
11
+ invalid_transition: lambda { |*| StateMachines::Machine.default_messages[:invalid_transition] % ['%{event}'] }
8
12
  }
9
13
  }
10
14
  }
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  module Integrations
3
5
  module ActiveModel
4
- VERSION = '0.9.0'
6
+ VERSION = '0.31.0'
5
7
  end
6
8
  end
7
9
  end
@@ -1,12 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_model'
2
4
  require 'active_support/core_ext/hash/keys'
3
- require 'active_support/core_ext/module/attribute_accessors.rb'
5
+ require 'active_support/core_ext/module/attribute_accessors'
4
6
  require 'state_machines'
5
7
  require 'state_machines/integrations/base'
6
8
  require 'state_machines/integrations/active_model/version'
7
9
 
8
10
  module StateMachines
9
- module Integrations #:nodoc:
11
+ module Integrations # :nodoc:
10
12
  # Adds support for integrating state machines with ActiveModel classes.
11
13
  #
12
14
  # == Examples
@@ -25,9 +27,9 @@ module StateMachines
25
27
  # attr_accessor :state
26
28
  # define_attribute_methods [:state]
27
29
  #
28
- # state_machine :initial => :parked do
30
+ # state_machine initial: :parked do
29
31
  # event :ignite do
30
- # transition :parked => :idling
32
+ # transition parked: :idling
31
33
  # end
32
34
  # end
33
35
  # end
@@ -45,7 +47,7 @@ module StateMachines
45
47
  # include ActiveModel::Validations
46
48
  # attr_accessor :state
47
49
  #
48
- # state_machine :action => :save do
50
+ # state_machine action: :save do
49
51
  # ...
50
52
  # end
51
53
  #
@@ -83,7 +85,7 @@ module StateMachines
83
85
  # state_machine do
84
86
  # ...
85
87
  # state :first_gear, :second_gear do
86
- # validate {|vehicle| vehicle.speed_is_legal}
88
+ # validate { |vehicle| vehicle.speed_is_legal }
87
89
  # end
88
90
  # end
89
91
  # end
@@ -115,38 +117,44 @@ module StateMachines
115
117
  # Beware that public event attributes mean that events can be fired
116
118
  # whenever mass-assignment is being used. If you want to prevent malicious
117
119
  # users from tampering with events through URLs / forms, the attribute
118
- # should be protected like so:
120
+ # should be protected using Strong Parameters in your controllers:
119
121
  #
120
122
  # class Vehicle
121
- # include ActiveModel::MassAssignmentSecurity
122
123
  # attr_accessor :state
123
124
  #
124
- # attr_protected :state_event
125
- # # attr_accessible ... # Alternative technique
126
- #
127
125
  # state_machine do
128
126
  # ...
129
127
  # end
130
128
  # end
131
129
  #
130
+ # # In your controller
131
+ # def vehicle_params
132
+ # params.require(:vehicle).permit(:attribute1, :attribute2) # Exclude :state_event
133
+ # end
134
+ #
132
135
  # If you want to only have *some* events be able to fire via mass-assignment,
133
- # you can build two state machines (one public and one protected) like so:
136
+ # you can build two state machines (one private and one public) like so:
134
137
  #
135
138
  # class Vehicle
136
139
  # attr_accessor :state
137
140
  #
138
- # attr_protected :state_event # Prevent access to events in the first machine
139
- #
140
141
  # state_machine do
141
142
  # # Define private events here
142
143
  # end
143
144
  #
144
145
  # # Public machine targets the same state as the private machine
145
- # state_machine :public_state, :attribute => :state do
146
+ # state_machine :public_state, attribute: :state do
146
147
  # # Define public events here
147
148
  # end
148
149
  # end
149
150
  #
151
+ # # In your controller
152
+ # def vehicle_params
153
+ # # Only permit events from the public state machine
154
+ # params.require(:vehicle).permit(:attribute1, :attribute2, :public_state_event)
155
+ # # The private state_event is not permitted
156
+ # end
157
+ #
150
158
  # == Callbacks
151
159
  #
152
160
  # All before/after transition callbacks defined for ActiveModel models
@@ -159,7 +167,7 @@ module StateMachines
159
167
  # include ActiveModel::Validations
160
168
  # attr_accessor :state
161
169
  #
162
- # state_machine :initial => :parked do
170
+ # state_machine initial: :parked do
163
171
  # before_transition any => :idling do |vehicle|
164
172
  # vehicle.put_on_seatbelt
165
173
  # end
@@ -169,7 +177,7 @@ module StateMachines
169
177
  # end
170
178
  #
171
179
  # event :ignite do
172
- # transition :parked => :idling
180
+ # transition parked: :idling
173
181
  # end
174
182
  # end
175
183
  #
@@ -181,64 +189,6 @@ module StateMachines
181
189
  # Note, also, that the transition can be accessed by simply defining
182
190
  # additional arguments in the callback block.
183
191
  #
184
- # == Observers
185
- #
186
- # In order to hook in observer support for your application, the
187
- # ActiveModel::Observing feature must be included. This can be added by including the
188
- # https://github.com/state-machines/state_machines-activemodel-observers gem in your
189
- # Gemfile. Because of the way
190
- # ActiveModel observers are designed, there is less flexibility around the
191
- # specific transitions that can be hooked in. However, a large number of
192
- # hooks *are* supported. For example, if a transition for a object's
193
- # +state+ attribute changes the state from +parked+ to +idling+ via the
194
- # +ignite+ event, the following observer methods are supported:
195
- # * before/after/after_failure_to-_ignite_from_parked_to_idling
196
- # * before/after/after_failure_to-_ignite_from_parked
197
- # * before/after/after_failure_to-_ignite_to_idling
198
- # * before/after/after_failure_to-_ignite
199
- # * before/after/after_failure_to-_transition_state_from_parked_to_idling
200
- # * before/after/after_failure_to-_transition_state_from_parked
201
- # * before/after/after_failure_to-_transition_state_to_idling
202
- # * before/after/after_failure_to-_transition_state
203
- # * before/after/after_failure_to-_transition
204
- #
205
- # The following class shows an example of some of these hooks:
206
- #
207
- # class VehicleObserver < ActiveModel::Observer
208
- # # Callback for :ignite event *before* the transition is performed
209
- # def before_ignite(vehicle, transition)
210
- # # log message
211
- # end
212
- #
213
- # # Callback for :ignite event *after* the transition has been performed
214
- # def after_ignite(vehicle, transition)
215
- # # put on seatbelt
216
- # end
217
- #
218
- # # Generic transition callback *before* the transition is performed
219
- # def after_transition(vehicle, transition)
220
- # Audit.log(vehicle, transition)
221
- # end
222
- #
223
- # def after_failure_to_transition(vehicle, transition)
224
- # Audit.error(vehicle, transition)
225
- # end
226
- # end
227
- #
228
- # More flexible transition callbacks can be defined directly within the
229
- # model as described in StateMachine::Machine#before_transition
230
- # and StateMachine::Machine#after_transition.
231
- #
232
- # To define a single observer for multiple state machines:
233
- #
234
- # class StateMachineObserver < ActiveModel::Observer
235
- # observe Vehicle, Switch, Project
236
- #
237
- # def after_transition(object, transition)
238
- # Audit.log(object, transition)
239
- # end
240
- # end
241
- #
242
192
  # == Internationalization
243
193
  #
244
194
  # Any error message that is generated from performing invalid transitions
@@ -308,9 +258,9 @@ module StateMachines
308
258
  # include ActiveModel::Dirty
309
259
  # attr_accessor :state
310
260
  #
311
- # state_machine :initial => :parked do
261
+ # state_machine initial: :parked do
312
262
  # event :park do
313
- # transition :parked => :parked, ...
263
+ # transition parked: :parked, ...
314
264
  # end
315
265
  # end
316
266
  # end
@@ -322,7 +272,7 @@ module StateMachines
322
272
  #
323
273
  # class Vehicle
324
274
  # ...
325
- # state_machine :initial => :parked do
275
+ # state_machine initial: :parked do
326
276
  # before_transition all => same do |vehicle|
327
277
  # vehicle.state_will_change!
328
278
  #
@@ -344,7 +294,7 @@ module StateMachines
344
294
  # module StateMachine::Integrations::MyORM
345
295
  # include ActiveModel
346
296
  #
347
- # mattr_accessor(:defaults) { :action => :persist }
297
+ # mattr_accessor(:defaults) { { action: :persist } }
348
298
  #
349
299
  # def self.matches?(klass)
350
300
  # defined?(::MyORM::Base) && klass <= ::MyORM::Base
@@ -374,22 +324,19 @@ module StateMachines
374
324
 
375
325
  # Adds a validation error to the given object
376
326
  def invalidate(object, attribute, message, values = [])
377
- if supports_validations?
378
- attribute = self.attribute(attribute)
379
- options = values.reduce({}) do |h, (key, value)|
380
- h[key] = value
381
- h
382
- end
327
+ return unless supports_validations?
383
328
 
384
- default_options = default_error_message_options(object, attribute, message)
385
- object.errors.add(attribute, message, **options, **default_options)
386
- end
329
+ attribute = self.attribute(attribute)
330
+ options = values.to_h
331
+
332
+ default_options = default_error_message_options(object, attribute, message)
333
+ object.errors.add(attribute, message, **options, **default_options)
387
334
  end
388
335
 
389
336
  # Describes the current validation errors on the given object. If none
390
337
  # are specific, then the default error is interpeted as a "halt".
391
338
  def errors_for(object)
392
- object.errors.empty? ? 'Transition halted' : object.errors.full_messages * ', '
339
+ object.errors.empty? ? 'Transition halted' : object.errors.full_messages.join(', ')
393
340
  end
394
341
 
395
342
  # Resets any errors previously added when invalidating the given object
@@ -398,22 +345,32 @@ module StateMachines
398
345
  end
399
346
 
400
347
  # Runs state events around the object's validation process
401
- def around_validation(object)
402
- object.class.state_machines.transitions(object, action, after: false).perform { yield }
348
+ def around_validation(object, &)
349
+ object.class.state_machines.transitions(object, action, after: false).perform(&)
403
350
  end
404
351
 
405
352
  protected
406
353
 
407
354
  def define_state_initializer
408
- define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
409
- def initialize(params = {})
410
- params.transform_keys! do |key|
355
+ define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1
356
+ def initialize(params = nil, **kwargs)
357
+ # Support both positional hash and keyword arguments
358
+ attrs = params.nil? ? kwargs : params
359
+ #{' '}
360
+ attrs.transform_keys! do |key|
411
361
  self.class.attribute_aliases[key.to_s] || key.to_s
412
362
  end if self.class.respond_to?(:attribute_aliases)
413
363
 
414
- self.class.state_machines.initialize_states(self, {}, params) { super }
364
+ # Call super with the appropriate arguments based on what we received
365
+ self.class.state_machines.initialize_states(self, {}, attrs) do
366
+ if params
367
+ super(params)
368
+ else
369
+ super(**kwargs)
370
+ end
371
+ end
415
372
  end
416
- end_eval
373
+ END_EVAL
417
374
  end
418
375
 
419
376
  # Whether validations are supported in the integration. Only true if
@@ -431,7 +388,7 @@ module StateMachines
431
388
 
432
389
  # Gets the terminator to use for callbacks
433
390
  def callback_terminator
434
- @terminator ||= ->(result) { result == false }
391
+ @callback_terminator ||= ->(result) { result == false }
435
392
  end
436
393
 
437
394
  # Determines the base scope to use when looking up translations
@@ -461,7 +418,7 @@ module StateMachines
461
418
  # Generate all possible translation keys
462
419
  translations = ancestors.map { |ancestor| :"#{ancestor.model_name.to_s.underscore}.#{name}.#{group}.#{value}" }
463
420
  translations.concat(ancestors.map { |ancestor| :"#{ancestor.model_name.to_s.underscore}.#{group}.#{value}" })
464
- translations.concat([:"#{name}.#{group}.#{value}", :"#{group}.#{value}", value.humanize.downcase])
421
+ translations.push(:"#{name}.#{group}.#{value}", :"#{group}.#{value}", value.humanize.downcase)
465
422
  I18n.translate(translations.shift, default: translations, scope: [i18n_scope(klass), :state_machines])
466
423
  end
467
424
 
@@ -475,10 +432,12 @@ module StateMachines
475
432
  def define_state_accessor
476
433
  name = self.name
477
434
 
435
+ return unless supports_validations?
436
+
478
437
  owner_class.validates_each(attribute) do |object|
479
438
  machine = object.class.state_machine(name)
480
439
  machine.invalidate(object, :state, :invalid) unless machine.states.match(object)
481
- end if supports_validations?
440
+ end
482
441
  end
483
442
 
484
443
  # Adds hooks into validation for automatically firing events
@@ -496,7 +455,7 @@ module StateMachines
496
455
  # Creates a new callback in the callback chain, always inserting it
497
456
  # before the default Observer callbacks that were created after
498
457
  # initialization.
499
- def add_callback(type, options, &block)
458
+ def add_callback(type, options, &)
500
459
  options[:terminator] = callback_terminator
501
460
  super
502
461
  end
@@ -504,14 +463,24 @@ module StateMachines
504
463
  # Configures new states with the built-in humanize scheme
505
464
  def add_states(*)
506
465
  super.each do |new_state|
507
- new_state.human_name = ->(state, klass) { translate(klass, :state, state.name) }
466
+ # Only set the translation lambda if human_name is the default auto-generated value
467
+ # This preserves user-specified human names while still applying translations for defaults
468
+ default_human_name = new_state.name ? new_state.name.to_s.tr('_', ' ') : 'nil'
469
+ if new_state.human_name == default_human_name
470
+ new_state.human_name = ->(state, klass) { translate(klass, :state, state.name) }
471
+ end
508
472
  end
509
473
  end
510
474
 
511
475
  # Configures new event with the built-in humanize scheme
512
476
  def add_events(*)
513
477
  super.each do |new_event|
514
- new_event.human_name = ->(event, klass) { translate(klass, :event, event.name) }
478
+ # Only set the translation lambda if human_name is the default auto-generated value
479
+ # This preserves user-specified human names while still applying translations for defaults
480
+ default_human_name = new_event.name ? new_event.name.to_s.tr('_', ' ') : 'nil'
481
+ if new_event.human_name == default_human_name
482
+ new_event.human_name = ->(event, klass) { translate(klass, :event, event.name) }
483
+ end
515
484
  end
516
485
  end
517
486
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support'
2
4
  require 'state_machines/integrations/active_model'
3
5
 
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+
5
+ class EventHumanNameTest < BaseTestCase
6
+ def setup
7
+ @model = new_model do
8
+ include ActiveModel::Validations
9
+ attr_accessor :status
10
+ end
11
+ end
12
+
13
+ def test_should_allow_custom_human_name_on_event
14
+ machine = StateMachines::Machine.new(@model, :status, initial: :parked) do
15
+ event :start, human_name: 'Start Engine' do
16
+ transition parked: :running
17
+ end
18
+
19
+ event :stop do
20
+ transition running: :parked
21
+ end
22
+
23
+ event :pause, human_name: 'Temporarily Pause' do
24
+ transition running: :paused
25
+ end
26
+ end
27
+
28
+ assert_equal 'Start Engine', machine.events[:start].human_name(@model)
29
+ assert_equal 'Temporarily Pause', machine.events[:pause].human_name(@model)
30
+ end
31
+
32
+ def test_should_not_override_custom_event_human_name_with_translation
33
+ # Set up I18n translations
34
+ I18n.backend.store_translations(:en, {
35
+ activemodel: {
36
+ state_machines: {
37
+ events: {
38
+ ignite: 'Translation for Ignite',
39
+ park: 'Translation for Park',
40
+ repair: 'Translation for Repair'
41
+ }
42
+ }
43
+ }
44
+ })
45
+
46
+ machine = StateMachines::Machine.new(@model, :status, initial: :parked) do
47
+ event :ignite, human_name: 'Custom Ignition' do
48
+ transition parked: :idling
49
+ end
50
+
51
+ event :park do
52
+ transition idling: :parked
53
+ end
54
+
55
+ event :repair, human_name: 'Custom Repair Process' do
56
+ transition any => :parked
57
+ end
58
+ end
59
+
60
+ # Custom human names should be preserved
61
+ assert_equal 'Custom Ignition', machine.events[:ignite].human_name(@model)
62
+ assert_equal 'Custom Repair Process', machine.events[:repair].human_name(@model)
63
+
64
+ # Event without custom human_name should use translation
65
+ assert_equal 'Translation for Park', machine.events[:park].human_name(@model)
66
+ end
67
+
68
+ def test_should_allow_custom_event_human_name_as_string
69
+ machine = StateMachines::Machine.new(@model, :status) do
70
+ event :activate, human_name: 'Turn On'
71
+ end
72
+
73
+ assert_equal 'Turn On', machine.events[:activate].human_name(@model)
74
+ end
75
+
76
+ def test_should_allow_custom_event_human_name_as_lambda
77
+ machine = StateMachines::Machine.new(@model, :status) do
78
+ event :process, human_name: ->(event, klass) { "#{klass.name}: #{event.name.to_s.capitalize} Action" }
79
+ end
80
+
81
+ assert_equal 'Foo: Process Action', machine.events[:process].human_name(@model)
82
+ end
83
+
84
+ def test_should_use_default_translation_when_no_custom_event_human_name
85
+ machine = StateMachines::Machine.new(@model, :status) do
86
+ event :idle
87
+ end
88
+
89
+ # Should fall back to humanized version when no translation exists
90
+ assert_equal 'idle', machine.events[:idle].human_name(@model)
91
+ end
92
+
93
+ def test_should_handle_nil_event_human_name
94
+ machine = StateMachines::Machine.new(@model, :status) do
95
+ event :wait
96
+ end
97
+
98
+ # Explicitly set to nil
99
+ machine.events[:wait].human_name = nil
100
+
101
+ # When human_name is nil, Event#human_name returns nil
102
+ assert_nil machine.events[:wait].human_name(@model)
103
+ end
104
+
105
+ def test_should_preserve_event_human_name_through_multiple_definitions
106
+ machine = StateMachines::Machine.new(@model, :status, initial: :draft)
107
+
108
+ # First define event with custom human name
109
+ machine.event :publish, human_name: 'Make Public' do
110
+ transition draft: :published
111
+ end
112
+
113
+ # Redefine the same event (this should not override the human_name)
114
+ machine.event :publish do
115
+ transition pending: :published
116
+ end
117
+
118
+ assert_equal 'Make Public', machine.events[:publish].human_name(@model)
119
+ end
120
+
121
+ def test_should_work_with_state_machine_helper_method
122
+ @model.class_eval do
123
+ state_machine :status, initial: :pending do
124
+ event :approve, human_name: 'Grant Approval' do
125
+ transition pending: :approved
126
+ end
127
+
128
+ event :reject do
129
+ transition pending: :rejected
130
+ end
131
+ end
132
+ end
133
+
134
+ machine = @model.state_machine(:status)
135
+ assert_equal 'Grant Approval', machine.events[:approve].human_name(@model)
136
+ end
137
+
138
+ def test_should_handle_complex_i18n_lookup_with_custom_event_human_name
139
+ # Set up complex I18n structure
140
+ I18n.backend.store_translations(:en, {
141
+ activemodel: {
142
+ state_machines: {
143
+ foo: {
144
+ status: {
145
+ events: {
146
+ submit: 'Model Specific Submit'
147
+ }
148
+ }
149
+ },
150
+ status: {
151
+ events: {
152
+ submit: 'Machine Specific Submit'
153
+ }
154
+ },
155
+ events: {
156
+ submit: 'Generic Submit'
157
+ }
158
+ }
159
+ }
160
+ })
161
+
162
+ machine = StateMachines::Machine.new(@model, :status) do
163
+ event :submit, human_name: 'Send for Review' do
164
+ transition draft: :pending
165
+ end
166
+ end
167
+
168
+ # Should use the custom human_name, not any of the I18n translations
169
+ assert_equal 'Send for Review', machine.events[:submit].human_name(@model)
170
+ end
171
+
172
+ def teardown
173
+ # Clear I18n translations after each test
174
+ I18n.backend.reload!
175
+ end
176
+ end