state_machines-activemodel 0.8.0 → 0.101.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +8 -27
  4. data/lib/state_machines/integrations/active_model/locale.rb +7 -3
  5. data/lib/state_machines/integrations/active_model/version.rb +3 -1
  6. data/lib/state_machines/integrations/active_model.rb +74 -99
  7. data/lib/state_machines-activemodel.rb +2 -0
  8. data/test/event_human_name_test.rb +176 -0
  9. data/test/human_name_preservation_test.rb +75 -0
  10. data/test/integration_test.rb +4 -2
  11. data/test/machine_by_default_test.rb +3 -1
  12. data/test/machine_errors_test.rb +3 -1
  13. data/test/machine_initialization_compatibility_test.rb +57 -0
  14. data/test/machine_multiple_test.rb +4 -2
  15. data/test/machine_with_callbacks_test.rb +3 -1
  16. data/test/machine_with_dirty_attribute_and_custom_attributes_during_loopback_test.rb +8 -5
  17. data/test/machine_with_dirty_attribute_and_state_events_test.rb +7 -5
  18. data/test/machine_with_dirty_attributes_and_custom_attribute_test.rb +11 -8
  19. data/test/machine_with_dirty_attributes_during_loopback_test.rb +7 -5
  20. data/test/machine_with_dirty_attributes_test.rb +10 -8
  21. data/test/machine_with_dynamic_initial_state_test.rb +3 -1
  22. data/test/machine_with_events_test.rb +3 -1
  23. data/test/machine_with_failed_after_callbacks_test.rb +3 -1
  24. data/test/machine_with_failed_before_callbacks_test.rb +3 -1
  25. data/test/machine_with_initialized_aliased_attribute_test.rb +44 -0
  26. data/test/machine_with_initialized_state_test.rb +3 -1
  27. data/test/machine_with_internationalization_test.rb +12 -10
  28. data/test/machine_with_model_state_attribute_test.rb +3 -1
  29. data/test/machine_with_non_model_state_attribute_undefined_test.rb +5 -4
  30. data/test/machine_with_state_driven_validations_test.rb +4 -2
  31. data/test/machine_with_states_test.rb +3 -1
  32. data/test/machine_with_static_initial_state_test.rb +3 -1
  33. data/test/machine_with_validations_and_custom_attribute_test.rb +3 -1
  34. data/test/machine_with_validations_test.rb +3 -1
  35. data/test/state_human_name_test.rb +152 -0
  36. data/test/test_helper.rb +26 -32
  37. metadata +18 -24
  38. data/.gitignore +0 -22
  39. data/.travis.yml +0 -21
  40. data/Appraisals +0 -20
  41. data/Gemfile +0 -8
  42. data/Rakefile +0 -9
  43. data/gemfiles/active_model_5.1.gemfile +0 -11
  44. data/gemfiles/active_model_5.2.gemfile +0 -11
  45. data/gemfiles/active_model_6.0.gemfile +0 -11
  46. data/gemfiles/active_model_6.1.gemfile +0 -11
  47. data/gemfiles/active_model_edge.gemfile +0 -11
  48. data/state_machines-activemodel.gemspec +0 -28
  49. data/test/files/en.yml +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4451c4d642636e3d81d9bb608b8dd78cfbd01bf7340989959e12791ec2fd76e0
4
- data.tar.gz: a7c86d2041b2e8deefdc809831802b413f1e0a8f7fec7aa867fcd54685262f9d
3
+ metadata.gz: ca8150306fc5ce879e2c381e4689b049b5208189f4e15efb4f6a0d378647cbb5
4
+ data.tar.gz: 1bad79de7e3337208c134dcea4fbaa90eee5677182dca38f6bfd4b9b943546b9
5
5
  SHA512:
6
- metadata.gz: 2fff7546bfd98859e0a8c5e5eb6e97909a4b5d42a3ade4440c8c21bb7728d4257093532a7b63e140c8418682d0da410f657d8a78db48c1bd13cd5f90ef59ff90
7
- data.tar.gz: 12bdad22386d96dc415f2eea856c5ca1329cb1fd7f213613102b9cfc0cfcb7da550d1548f633771491cd92b8a8491b390952ec44fb62db7e4cfa1f7e395366cc
6
+ metadata.gz: 1d8cca8f00006c4a43b9c82fade3babc662027b7d5971c5d009c9b6da087cc3d93a4ed60a2f8656cb59346c1dfab115405f23db2237b25e185ac718ca09f8e1f
7
+ data.tar.gz: 8a39da61112b9c6e7869e1d4779381062857ea36ffd1e026031c04852f30561d3cacc60e8a0a98daa112576924009d109f02fb1f738672996a7ec5a89b2ef6bc
data/LICENSE.txt CHANGED
@@ -1,5 +1,5 @@
1
1
  Copyright (c) 2006-2012 Aaron Pfeifer
2
- Copyright (c) 2014-2021 Abdelkader Boudih
2
+ Copyright (c) 2014-2023 Abdelkader Boudih
3
3
 
4
4
  MIT License
5
5
 
data/README.md CHANGED
@@ -1,5 +1,4 @@
1
- [![Build Status](https://travis-ci.com/state-machines/state_machines-activemodel.svg?branch=master)](https://travis-ci.org/state-machines/state_machines-activemodel)
2
- [![Code Climate](https://codeclimate.com/github/state-machines/state_machines-activemodel.svg)](https://codeclimate.com/github/state-machines/state_machines-activemodel)
1
+ ![Build Status](https://github.com/state-machines/state_machines-activemodel/actions/workflows/ruby.yml/badge.svg)
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.8.0'
6
+ VERSION = '0.101.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,62 +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. Because of the way
188
- # ActiveModel observers are designed, there is less flexibility around the
189
- # specific transitions that can be hooked in. However, a large number of
190
- # hooks *are* supported. For example, if a transition for a object's
191
- # +state+ attribute changes the state from +parked+ to +idling+ via the
192
- # +ignite+ event, the following observer methods are supported:
193
- # * before/after/after_failure_to-_ignite_from_parked_to_idling
194
- # * before/after/after_failure_to-_ignite_from_parked
195
- # * before/after/after_failure_to-_ignite_to_idling
196
- # * before/after/after_failure_to-_ignite
197
- # * before/after/after_failure_to-_transition_state_from_parked_to_idling
198
- # * before/after/after_failure_to-_transition_state_from_parked
199
- # * before/after/after_failure_to-_transition_state_to_idling
200
- # * before/after/after_failure_to-_transition_state
201
- # * before/after/after_failure_to-_transition
202
- #
203
- # The following class shows an example of some of these hooks:
204
- #
205
- # class VehicleObserver < ActiveModel::Observer
206
- # # Callback for :ignite event *before* the transition is performed
207
- # def before_ignite(vehicle, transition)
208
- # # log message
209
- # end
210
- #
211
- # # Callback for :ignite event *after* the transition has been performed
212
- # def after_ignite(vehicle, transition)
213
- # # put on seatbelt
214
- # end
215
- #
216
- # # Generic transition callback *before* the transition is performed
217
- # def after_transition(vehicle, transition)
218
- # Audit.log(vehicle, transition)
219
- # end
220
- #
221
- # def after_failure_to_transition(vehicle, transition)
222
- # Audit.error(vehicle, transition)
223
- # end
224
- # end
225
- #
226
- # More flexible transition callbacks can be defined directly within the
227
- # model as described in StateMachine::Machine#before_transition
228
- # and StateMachine::Machine#after_transition.
229
- #
230
- # To define a single observer for multiple state machines:
231
- #
232
- # class StateMachineObserver < ActiveModel::Observer
233
- # observe Vehicle, Switch, Project
234
- #
235
- # def after_transition(object, transition)
236
- # Audit.log(object, transition)
237
- # end
238
- # end
239
- #
240
192
  # == Internationalization
241
193
  #
242
194
  # Any error message that is generated from performing invalid transitions
@@ -306,9 +258,9 @@ module StateMachines
306
258
  # include ActiveModel::Dirty
307
259
  # attr_accessor :state
308
260
  #
309
- # state_machine :initial => :parked do
261
+ # state_machine initial: :parked do
310
262
  # event :park do
311
- # transition :parked => :parked, ...
263
+ # transition parked: :parked, ...
312
264
  # end
313
265
  # end
314
266
  # end
@@ -320,7 +272,7 @@ module StateMachines
320
272
  #
321
273
  # class Vehicle
322
274
  # ...
323
- # state_machine :initial => :parked do
275
+ # state_machine initial: :parked do
324
276
  # before_transition all => same do |vehicle|
325
277
  # vehicle.state_will_change!
326
278
  #
@@ -342,7 +294,7 @@ module StateMachines
342
294
  # module StateMachine::Integrations::MyORM
343
295
  # include ActiveModel
344
296
  #
345
- # mattr_accessor(:defaults) { :action => :persist }
297
+ # mattr_accessor(:defaults) { { action: :persist } }
346
298
  #
347
299
  # def self.matches?(klass)
348
300
  # defined?(::MyORM::Base) && klass <= ::MyORM::Base
@@ -372,22 +324,19 @@ module StateMachines
372
324
 
373
325
  # Adds a validation error to the given object
374
326
  def invalidate(object, attribute, message, values = [])
375
- if supports_validations?
376
- attribute = self.attribute(attribute)
377
- options = values.reduce({}) do |h, (key, value)|
378
- h[key] = value
379
- h
380
- end
327
+ return unless supports_validations?
381
328
 
382
- default_options = default_error_message_options(object, attribute, message)
383
- object.errors.add(attribute, message, **options, **default_options)
384
- 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)
385
334
  end
386
335
 
387
336
  # Describes the current validation errors on the given object. If none
388
337
  # are specific, then the default error is interpeted as a "halt".
389
338
  def errors_for(object)
390
- object.errors.empty? ? 'Transition halted' : object.errors.full_messages * ', '
339
+ object.errors.empty? ? 'Transition halted' : object.errors.full_messages.join(', ')
391
340
  end
392
341
 
393
342
  # Resets any errors previously added when invalidating the given object
@@ -396,18 +345,32 @@ module StateMachines
396
345
  end
397
346
 
398
347
  # Runs state events around the object's validation process
399
- def around_validation(object)
400
- 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(&)
401
350
  end
402
351
 
403
352
  protected
404
353
 
405
354
  def define_state_initializer
406
- define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
407
- def initialize(params = {})
408
- self.class.state_machines.initialize_states(self, {}, params) { super }
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|
361
+ self.class.attribute_aliases[key.to_s] || key.to_s
362
+ end if self.class.respond_to?(:attribute_aliases)
363
+
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
409
372
  end
410
- end_eval
373
+ END_EVAL
411
374
  end
412
375
 
413
376
  # Whether validations are supported in the integration. Only true if
@@ -425,7 +388,7 @@ module StateMachines
425
388
 
426
389
  # Gets the terminator to use for callbacks
427
390
  def callback_terminator
428
- @terminator ||= ->(result) { result == false }
391
+ @callback_terminator ||= ->(result) { result == false }
429
392
  end
430
393
 
431
394
  # Determines the base scope to use when looking up translations
@@ -455,7 +418,7 @@ module StateMachines
455
418
  # Generate all possible translation keys
456
419
  translations = ancestors.map { |ancestor| :"#{ancestor.model_name.to_s.underscore}.#{name}.#{group}.#{value}" }
457
420
  translations.concat(ancestors.map { |ancestor| :"#{ancestor.model_name.to_s.underscore}.#{group}.#{value}" })
458
- translations.concat([:"#{name}.#{group}.#{value}", :"#{group}.#{value}", value.humanize.downcase])
421
+ translations.push(:"#{name}.#{group}.#{value}", :"#{group}.#{value}", value.humanize.downcase)
459
422
  I18n.translate(translations.shift, default: translations, scope: [i18n_scope(klass), :state_machines])
460
423
  end
461
424
 
@@ -469,10 +432,12 @@ module StateMachines
469
432
  def define_state_accessor
470
433
  name = self.name
471
434
 
435
+ return unless supports_validations?
436
+
472
437
  owner_class.validates_each(attribute) do |object|
473
438
  machine = object.class.state_machine(name)
474
439
  machine.invalidate(object, :state, :invalid) unless machine.states.match(object)
475
- end if supports_validations?
440
+ end
476
441
  end
477
442
 
478
443
  # Adds hooks into validation for automatically firing events
@@ -490,7 +455,7 @@ module StateMachines
490
455
  # Creates a new callback in the callback chain, always inserting it
491
456
  # before the default Observer callbacks that were created after
492
457
  # initialization.
493
- def add_callback(type, options, &block)
458
+ def add_callback(type, options, &)
494
459
  options[:terminator] = callback_terminator
495
460
  super
496
461
  end
@@ -498,14 +463,24 @@ module StateMachines
498
463
  # Configures new states with the built-in humanize scheme
499
464
  def add_states(*)
500
465
  super.each do |new_state|
501
- 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
502
472
  end
503
473
  end
504
474
 
505
475
  # Configures new event with the built-in humanize scheme
506
476
  def add_events(*)
507
477
  super.each do |new_event|
508
- 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
509
484
  end
510
485
  end
511
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