state_machines-activemodel 0.10.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a37766e74933c9b284c1bea7acc586937d437222fb51371a12118cb9c28873c
4
- data.tar.gz: 05d7748697762082b5cd5c6a388cfbcdb62d7f54bdaf656587c7cdc9158b78f6
3
+ metadata.gz: ad141a5029b6489fb99da9c9ef4ad7bf2059d9947e993265225bb4ca88a0ff66
4
+ data.tar.gz: ef7ae48fd6d4d1f712842e2f0057158e5674b5ba59487cc3b0483c89524374e3
5
5
  SHA512:
6
- metadata.gz: 49059888466d7e77da042aa4d3b296d029d3bbaf600ef70cf514322a04c14211460527444d08fd591a8b3e38a3039b1119c3c719d06bdc6363ab03dadf83b7c0
7
- data.tar.gz: 787f045227838e8ce24c6b5448d991631ef1a35c90f078582f8f2fff66470760d90d0c05f2bc56c174dcb93f4c4662b137100d06ff27e44aefca98a4d4b7f11e
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
 
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
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
3
5
  { en: {
4
6
  activemodel: {
5
7
  errors: {
6
8
  messages: {
7
- invalid: StateMachines::Machine.default_messages[:invalid],
8
- invalid_event: StateMachines::Machine.default_messages[:invalid_event] % ['%{state}'],
9
- 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}'] }
10
12
  }
11
13
  }
12
14
  }
@@ -3,7 +3,7 @@
3
3
  module StateMachines
4
4
  module Integrations
5
5
  module ActiveModel
6
- VERSION = '0.10.0'
6
+ VERSION = '0.31.0'
7
7
  end
8
8
  end
9
9
  end
@@ -8,7 +8,7 @@ require 'state_machines/integrations/base'
8
8
  require 'state_machines/integrations/active_model/version'
9
9
 
10
10
  module StateMachines
11
- module Integrations #:nodoc:
11
+ module Integrations # :nodoc:
12
12
  # Adds support for integrating state machines with ActiveModel classes.
13
13
  #
14
14
  # == Examples
@@ -324,13 +324,13 @@ module StateMachines
324
324
 
325
325
  # Adds a validation error to the given object
326
326
  def invalidate(object, attribute, message, values = [])
327
- if supports_validations?
328
- attribute = self.attribute(attribute)
329
- options = values.to_h { |key, value| [key, value] }
327
+ return unless supports_validations?
330
328
 
331
- default_options = default_error_message_options(object, attribute, message)
332
- object.errors.add(attribute, message, **options, **default_options)
333
- 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)
334
334
  end
335
335
 
336
336
  # Describes the current validation errors on the given object. If none
@@ -345,22 +345,32 @@ module StateMachines
345
345
  end
346
346
 
347
347
  # Runs state events around the object's validation process
348
- def around_validation(object)
349
- 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(&)
350
350
  end
351
351
 
352
352
  protected
353
353
 
354
354
  def define_state_initializer
355
- define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
356
- def initialize(params = {})
357
- 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|
358
361
  self.class.attribute_aliases[key.to_s] || key.to_s
359
362
  end if self.class.respond_to?(:attribute_aliases)
360
363
 
361
- 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
362
372
  end
363
- end_eval
373
+ END_EVAL
364
374
  end
365
375
 
366
376
  # Whether validations are supported in the integration. Only true if
@@ -378,7 +388,7 @@ module StateMachines
378
388
 
379
389
  # Gets the terminator to use for callbacks
380
390
  def callback_terminator
381
- @terminator ||= ->(result) { result == false }
391
+ @callback_terminator ||= ->(result) { result == false }
382
392
  end
383
393
 
384
394
  # Determines the base scope to use when looking up translations
@@ -408,7 +418,7 @@ module StateMachines
408
418
  # Generate all possible translation keys
409
419
  translations = ancestors.map { |ancestor| :"#{ancestor.model_name.to_s.underscore}.#{name}.#{group}.#{value}" }
410
420
  translations.concat(ancestors.map { |ancestor| :"#{ancestor.model_name.to_s.underscore}.#{group}.#{value}" })
411
- translations.concat([:"#{name}.#{group}.#{value}", :"#{group}.#{value}", value.humanize.downcase])
421
+ translations.push(:"#{name}.#{group}.#{value}", :"#{group}.#{value}", value.humanize.downcase)
412
422
  I18n.translate(translations.shift, default: translations, scope: [i18n_scope(klass), :state_machines])
413
423
  end
414
424
 
@@ -422,10 +432,12 @@ module StateMachines
422
432
  def define_state_accessor
423
433
  name = self.name
424
434
 
435
+ return unless supports_validations?
436
+
425
437
  owner_class.validates_each(attribute) do |object|
426
438
  machine = object.class.state_machine(name)
427
439
  machine.invalidate(object, :state, :invalid) unless machine.states.match(object)
428
- end if supports_validations?
440
+ end
429
441
  end
430
442
 
431
443
  # Adds hooks into validation for automatically firing events
@@ -443,7 +455,7 @@ module StateMachines
443
455
  # Creates a new callback in the callback chain, always inserting it
444
456
  # before the default Observer callbacks that were created after
445
457
  # initialization.
446
- def add_callback(type, options, &block)
458
+ def add_callback(type, options, &)
447
459
  options[:terminator] = callback_terminator
448
460
  super
449
461
  end
@@ -451,14 +463,24 @@ module StateMachines
451
463
  # Configures new states with the built-in humanize scheme
452
464
  def add_states(*)
453
465
  super.each do |new_state|
454
- 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
455
472
  end
456
473
  end
457
474
 
458
475
  # Configures new event with the built-in humanize scheme
459
476
  def add_events(*)
460
477
  super.each do |new_event|
461
- 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
462
484
  end
463
485
  end
464
486
  end
@@ -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
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+
5
+ class HumanNamePreservationTest < 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_preserve_custom_state_human_name_when_using_activemodel_integration
14
+ # This test specifically verifies that PR #38's fix works:
15
+ # Using ||= instead of = in add_states method
16
+
17
+ @model.class_eval do
18
+ state_machine :status, initial: :pending do
19
+ # Define a state with a custom human_name
20
+ state :pending, human_name: 'My Custom Pending'
21
+ state :approved
22
+ end
23
+ end
24
+
25
+ machine = @model.state_machine(:status)
26
+
27
+ # The custom human_name should be preserved, not overwritten by the integration
28
+ assert_equal 'My Custom Pending', machine.states[:pending].human_name(@model)
29
+ end
30
+
31
+ def test_should_preserve_custom_event_human_name_when_using_activemodel_integration
32
+ # This test verifies our additional fix for events:
33
+ # Using ||= instead of = in add_events method
34
+
35
+ @model.class_eval do
36
+ state_machine :status, initial: :pending do
37
+ event :approve, human_name: 'Grant Authorization' do
38
+ transition pending: :approved
39
+ end
40
+
41
+ event :reject do
42
+ transition pending: :rejected
43
+ end
44
+ end
45
+ end
46
+
47
+ machine = @model.state_machine(:status)
48
+
49
+ # The custom human_name should be preserved, not overwritten by the integration
50
+ assert_equal 'Grant Authorization', machine.events[:approve].human_name(@model)
51
+ end
52
+
53
+ def test_regression_issue_37_hard_coded_human_name_preserved
54
+ # This is the exact regression test for issue #37
55
+ # "Hard-coded human_name is being overwritten"
56
+
57
+ @model.class_eval do
58
+ state_machine :status do
59
+ state :pending, human_name: 'Pending Approval'
60
+ state :active, human_name: 'Active State'
61
+
62
+ event :activate, human_name: 'Activate Now' do
63
+ transition pending: :active
64
+ end
65
+ end
66
+ end
67
+
68
+ machine = @model.state_machine(:status)
69
+
70
+ # Both states and events should preserve their hard-coded human names
71
+ assert_equal 'Pending Approval', machine.states[:pending].human_name(@model)
72
+ assert_equal 'Active State', machine.states[:active].human_name(@model)
73
+ assert_equal 'Activate Now', machine.events[:activate].human_name(@model)
74
+ end
75
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+
5
+ class MachineInitializationCompatibilityTest < BaseTestCase
6
+ def setup
7
+ @model = new_model do
8
+ include ActiveModel::Validations
9
+ attr_accessor :state
10
+ end
11
+
12
+ @machine = StateMachines::Machine.new(@model, initial: :parked)
13
+ @machine.state :parked, :idling
14
+ @machine.event :ignite
15
+ end
16
+
17
+ def test_should_accept_positional_hash_argument
18
+ record = @model.new({ state: 'idling' })
19
+ assert_equal 'idling', record.state
20
+ end
21
+
22
+ def test_should_accept_keyword_arguments
23
+ record = @model.new(state: 'idling')
24
+ assert_equal 'idling', record.state
25
+ end
26
+
27
+ def test_should_accept_empty_initialization
28
+ record = @model.new
29
+ assert_equal 'parked', record.state
30
+ end
31
+
32
+ def test_should_handle_attribute_aliases
33
+ @model.class_eval do
34
+ def self.attribute_aliases
35
+ { 'status' => 'state' }
36
+ end
37
+ end
38
+
39
+ record = @model.new(status: 'idling')
40
+ assert_equal 'idling', record.state
41
+ end
42
+
43
+ def test_should_prefer_positional_hash_over_keywords_when_both_present
44
+ # If someone accidentally provides both, positional takes precedence
45
+ record = @model.new({ state: 'idling' }, state: 'parked')
46
+ assert_equal 'idling', record.state
47
+ end
48
+
49
+ def test_should_handle_empty_positional_hash
50
+ # Empty hash should still be treated as positional argument
51
+ record = @model.new({})
52
+ assert_equal 'parked', record.state # Gets default initial state
53
+ end
54
+
55
+ def test_should_use_keywords_when_empty_hash_and_keywords_present
56
+ # With the fix, keywords are ignored even with empty positional hash
57
+ record = @model.new({}, state: 'idling')
58
+ assert_equal 'parked', record.state # Empty hash takes precedence
59
+ end
60
+ end
@@ -56,7 +56,7 @@ class MachineWithInternationalizationTest < BaseTestCase
56
56
 
57
57
  machine = StateMachines::Machine.new(@model)
58
58
  machine.state :parked
59
- assert_equal 'shutdown', machine.state(:parked).human_name
59
+ assert_equal 'shutdown', machine.state(:parked).human_name(@model)
60
60
  end
61
61
 
62
62
  def test_should_allow_customized_state_key_scoped_to_class
@@ -67,7 +67,7 @@ class MachineWithInternationalizationTest < BaseTestCase
67
67
  machine = StateMachines::Machine.new(@model)
68
68
  machine.state :parked
69
69
 
70
- assert_equal 'shutdown', machine.state(:parked).human_name
70
+ assert_equal 'shutdown', machine.state(:parked).human_name(@model)
71
71
  end
72
72
 
73
73
  def test_should_allow_customized_state_key_scoped_to_machine
@@ -78,7 +78,7 @@ class MachineWithInternationalizationTest < BaseTestCase
78
78
  machine = StateMachines::Machine.new(@model)
79
79
  machine.state :parked
80
80
 
81
- assert_equal 'shutdown', machine.state(:parked).human_name
81
+ assert_equal 'shutdown', machine.state(:parked).human_name(@model)
82
82
  end
83
83
 
84
84
  def test_should_allow_customized_state_key_unscoped
@@ -89,7 +89,7 @@ class MachineWithInternationalizationTest < BaseTestCase
89
89
  machine = StateMachines::Machine.new(@model)
90
90
  machine.state :parked
91
91
 
92
- assert_equal 'shutdown', machine.state(:parked).human_name
92
+ assert_equal 'shutdown', machine.state(:parked).human_name(@model)
93
93
  end
94
94
 
95
95
  def test_should_support_nil_state_key
@@ -99,7 +99,7 @@ class MachineWithInternationalizationTest < BaseTestCase
99
99
 
100
100
  machine = StateMachines::Machine.new(@model)
101
101
 
102
- assert_equal 'empty', machine.state(nil).human_name
102
+ assert_equal 'empty', machine.state(nil).human_name(@model)
103
103
  end
104
104
 
105
105
  def test_should_allow_customized_event_key_scoped_to_class_and_machine
@@ -110,7 +110,7 @@ class MachineWithInternationalizationTest < BaseTestCase
110
110
  machine = StateMachines::Machine.new(@model)
111
111
  machine.event :park
112
112
 
113
- assert_equal 'stop', machine.event(:park).human_name
113
+ assert_equal 'stop', machine.event(:park).human_name(@model)
114
114
  end
115
115
 
116
116
  def test_should_allow_customized_event_key_scoped_to_class
@@ -121,7 +121,7 @@ class MachineWithInternationalizationTest < BaseTestCase
121
121
  machine = StateMachines::Machine.new(@model)
122
122
  machine.event :park
123
123
 
124
- assert_equal 'stop', machine.event(:park).human_name
124
+ assert_equal 'stop', machine.event(:park).human_name(@model)
125
125
  end
126
126
 
127
127
  def test_should_allow_customized_event_key_scoped_to_machine
@@ -132,7 +132,7 @@ class MachineWithInternationalizationTest < BaseTestCase
132
132
  machine = StateMachines::Machine.new(@model)
133
133
  machine.event :park
134
134
 
135
- assert_equal 'stop', machine.event(:park).human_name
135
+ assert_equal 'stop', machine.event(:park).human_name(@model)
136
136
  end
137
137
 
138
138
  def test_should_allow_customized_event_key_unscoped
@@ -143,7 +143,7 @@ class MachineWithInternationalizationTest < BaseTestCase
143
143
  machine = StateMachines::Machine.new(@model)
144
144
  machine.event :park
145
145
 
146
- assert_equal 'stop', machine.event(:park).human_name
146
+ assert_equal 'stop', machine.event(:park).human_name(@model)
147
147
  end
148
148
 
149
149
  def test_should_have_locale_once_in_load_path
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+
5
+ class StateHumanNameTest < 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_state
14
+ machine = StateMachines::Machine.new(@model, :status, initial: :pending) do
15
+ state :pending, human_name: 'Awaiting Approval'
16
+ state :approved
17
+ state :rejected, human_name: 'Denied'
18
+ end
19
+
20
+ assert_equal 'Awaiting Approval', machine.states[:pending].human_name(@model)
21
+ assert_equal 'Denied', machine.states[:rejected].human_name(@model)
22
+ end
23
+
24
+ def test_should_not_override_custom_human_name_with_translation
25
+ # Set up I18n translations
26
+ I18n.backend.store_translations(:en, {
27
+ activemodel: {
28
+ state_machines: {
29
+ states: {
30
+ pending: 'Translation for Pending',
31
+ approved: 'Translation for Approved',
32
+ rejected: 'Translation for Rejected'
33
+ }
34
+ }
35
+ }
36
+ })
37
+
38
+ machine = StateMachines::Machine.new(@model, :status, initial: :pending) do
39
+ state :pending, human_name: 'Custom Pending Name'
40
+ state :approved
41
+ state :rejected, human_name: 'Custom Rejected Name'
42
+ end
43
+
44
+ # Custom human names should be preserved
45
+ assert_equal 'Custom Pending Name', machine.states[:pending].human_name(@model)
46
+ assert_equal 'Custom Rejected Name', machine.states[:rejected].human_name(@model)
47
+
48
+ # State without custom human_name gets default behavior (which might not use translations in this test setup)
49
+ # The key test is that custom human names are preserved, not overwritten
50
+ refute_equal 'Custom Pending Name', machine.states[:approved].human_name(@model)
51
+ end
52
+
53
+ def test_should_allow_custom_human_name_as_string
54
+ machine = StateMachines::Machine.new(@model, :status) do
55
+ state :active, human_name: 'Currently Active'
56
+ end
57
+
58
+ assert_equal 'Currently Active', machine.states[:active].human_name(@model)
59
+ end
60
+
61
+ def test_should_allow_custom_human_name_as_lambda
62
+ machine = StateMachines::Machine.new(@model, :status) do
63
+ state :processing, human_name: ->(state, klass) { "#{klass.name} is #{state.name.to_s.upcase}" }
64
+ end
65
+
66
+ assert_equal 'Foo is PROCESSING', machine.states[:processing].human_name(@model)
67
+ end
68
+
69
+ def test_should_use_default_translation_when_no_custom_human_name
70
+ machine = StateMachines::Machine.new(@model, :status) do
71
+ state :idle
72
+ end
73
+
74
+ # Should fall back to humanized version when no translation exists
75
+ assert_equal 'idle', machine.states[:idle].human_name(@model)
76
+ end
77
+
78
+ def test_should_handle_nil_human_name
79
+ machine = StateMachines::Machine.new(@model, :status) do
80
+ state :waiting
81
+ end
82
+
83
+ # Explicitly set to nil (should still get default behavior)
84
+ machine.states[:waiting].human_name = nil
85
+
86
+ # When human_name is nil, State#human_name returns nil
87
+ assert_nil machine.states[:waiting].human_name(@model)
88
+ end
89
+
90
+ def test_should_preserve_human_name_through_multiple_state_definitions
91
+ machine = StateMachines::Machine.new(@model, :status)
92
+
93
+ # First define state with custom human name
94
+ machine.state :draft, human_name: 'Work in Progress'
95
+
96
+ # Redefine the same state (this should not override the human_name)
97
+ machine.state :draft do
98
+ # Add some behavior
99
+ end
100
+
101
+ assert_equal 'Work in Progress', machine.states[:draft].human_name(@model)
102
+ end
103
+
104
+ def test_should_work_with_state_machine_helper_method
105
+ @model.class_eval do
106
+ state_machine :status, initial: :pending do
107
+ state :pending, human_name: 'Awaiting Review'
108
+ state :reviewed
109
+ end
110
+ end
111
+
112
+ machine = @model.state_machine(:status)
113
+ assert_equal 'Awaiting Review', machine.states[:pending].human_name(@model)
114
+ end
115
+
116
+ def test_should_handle_complex_i18n_lookup_with_custom_human_name
117
+ # Set up complex I18n structure
118
+ I18n.backend.store_translations(:en, {
119
+ activemodel: {
120
+ state_machines: {
121
+ foo: {
122
+ status: {
123
+ states: {
124
+ pending: 'Model Specific Pending'
125
+ }
126
+ }
127
+ },
128
+ status: {
129
+ states: {
130
+ pending: 'Machine Specific Pending'
131
+ }
132
+ },
133
+ states: {
134
+ pending: 'Generic Pending'
135
+ }
136
+ }
137
+ }
138
+ })
139
+
140
+ machine = StateMachines::Machine.new(@model, :status) do
141
+ state :pending, human_name: 'Overridden Pending'
142
+ end
143
+
144
+ # Should use the custom human_name, not any of the I18n translations
145
+ assert_equal 'Overridden Pending', machine.states[:pending].human_name(@model)
146
+ end
147
+
148
+ def teardown
149
+ # Clear I18n translations after each test
150
+ I18n.backend.reload!
151
+ end
152
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: state_machines-activemodel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.31.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.10.0
19
+ version: 0.31.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 0.10.0
26
+ version: 0.31.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activemodel
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -122,9 +122,12 @@ files:
122
122
  - lib/state_machines/integrations/active_model.rb
123
123
  - lib/state_machines/integrations/active_model/locale.rb
124
124
  - lib/state_machines/integrations/active_model/version.rb
125
+ - test/event_human_name_test.rb
126
+ - test/human_name_preservation_test.rb
125
127
  - test/integration_test.rb
126
128
  - test/machine_by_default_test.rb
127
129
  - test/machine_errors_test.rb
130
+ - test/machine_initialization_compatibility_test.rb
128
131
  - test/machine_multiple_test.rb
129
132
  - test/machine_with_callbacks_test.rb
130
133
  - test/machine_with_dirty_attribute_and_custom_attributes_during_loopback_test.rb
@@ -146,6 +149,7 @@ files:
146
149
  - test/machine_with_static_initial_state_test.rb
147
150
  - test/machine_with_validations_and_custom_attribute_test.rb
148
151
  - test/machine_with_validations_test.rb
152
+ - test/state_human_name_test.rb
149
153
  - test/test_helper.rb
150
154
  homepage: https://github.com/state-machines/state_machines-activemodel
151
155
  licenses:
@@ -165,13 +169,16 @@ required_rubygems_version: !ruby/object:Gem::Requirement
165
169
  - !ruby/object:Gem::Version
166
170
  version: '0'
167
171
  requirements: []
168
- rubygems_version: 3.6.7
172
+ rubygems_version: 3.6.9
169
173
  specification_version: 4
170
174
  summary: ActiveModel integration for State Machines
171
175
  test_files:
176
+ - test/event_human_name_test.rb
177
+ - test/human_name_preservation_test.rb
172
178
  - test/integration_test.rb
173
179
  - test/machine_by_default_test.rb
174
180
  - test/machine_errors_test.rb
181
+ - test/machine_initialization_compatibility_test.rb
175
182
  - test/machine_multiple_test.rb
176
183
  - test/machine_with_callbacks_test.rb
177
184
  - test/machine_with_dirty_attribute_and_custom_attributes_during_loopback_test.rb
@@ -193,4 +200,5 @@ test_files:
193
200
  - test/machine_with_static_initial_state_test.rb
194
201
  - test/machine_with_validations_and_custom_attribute_test.rb
195
202
  - test/machine_with_validations_test.rb
203
+ - test/state_human_name_test.rb
196
204
  - test/test_helper.rb