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.
- checksums.yaml +4 -4
- data/README.md +7 -26
- data/lib/state_machines/integrations/active_model/locale.rb +7 -3
- data/lib/state_machines/integrations/active_model/version.rb +3 -1
- data/lib/state_machines/integrations/active_model.rb +71 -102
- data/lib/state_machines-activemodel.rb +2 -0
- data/test/event_human_name_test.rb +176 -0
- data/test/human_name_preservation_test.rb +75 -0
- data/test/integration_test.rb +3 -1
- data/test/machine_by_default_test.rb +3 -1
- data/test/machine_errors_test.rb +3 -1
- data/test/machine_initialization_compatibility_test.rb +60 -0
- data/test/machine_multiple_test.rb +3 -1
- data/test/machine_with_callbacks_test.rb +3 -1
- data/test/machine_with_dirty_attribute_and_custom_attributes_during_loopback_test.rb +3 -1
- data/test/machine_with_dirty_attribute_and_state_events_test.rb +3 -1
- data/test/machine_with_dirty_attributes_and_custom_attribute_test.rb +3 -1
- data/test/machine_with_dirty_attributes_during_loopback_test.rb +3 -1
- data/test/machine_with_dirty_attributes_test.rb +3 -1
- data/test/machine_with_dynamic_initial_state_test.rb +3 -1
- data/test/machine_with_events_test.rb +3 -1
- data/test/machine_with_failed_after_callbacks_test.rb +3 -1
- data/test/machine_with_failed_before_callbacks_test.rb +3 -1
- data/test/machine_with_initialized_aliased_attribute_test.rb +3 -1
- data/test/machine_with_initialized_state_test.rb +3 -1
- data/test/machine_with_internationalization_test.rb +12 -10
- data/test/machine_with_model_state_attribute_test.rb +3 -1
- data/test/machine_with_non_model_state_attribute_undefined_test.rb +3 -1
- data/test/machine_with_state_driven_validations_test.rb +4 -2
- data/test/machine_with_states_test.rb +3 -1
- data/test/machine_with_static_initial_state_test.rb +3 -1
- data/test/machine_with_validations_and_custom_attribute_test.rb +3 -1
- data/test/machine_with_validations_test.rb +3 -1
- data/test/state_human_name_test.rb +152 -0
- data/test/test_helper.rb +55 -0
- metadata +18 -11
@@ -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
|
data/test/integration_test.rb
CHANGED
data/test/machine_errors_test.rb
CHANGED
@@ -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
|
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
2
4
|
require 'i18n'
|
3
5
|
|
4
6
|
class MachineWithInternationalizationTest < BaseTestCase
|
@@ -54,7 +56,7 @@ class MachineWithInternationalizationTest < BaseTestCase
|
|
54
56
|
|
55
57
|
machine = StateMachines::Machine.new(@model)
|
56
58
|
machine.state :parked
|
57
|
-
assert_equal 'shutdown', machine.state(:parked).human_name
|
59
|
+
assert_equal 'shutdown', machine.state(:parked).human_name(@model)
|
58
60
|
end
|
59
61
|
|
60
62
|
def test_should_allow_customized_state_key_scoped_to_class
|
@@ -65,7 +67,7 @@ class MachineWithInternationalizationTest < BaseTestCase
|
|
65
67
|
machine = StateMachines::Machine.new(@model)
|
66
68
|
machine.state :parked
|
67
69
|
|
68
|
-
assert_equal 'shutdown', machine.state(:parked).human_name
|
70
|
+
assert_equal 'shutdown', machine.state(:parked).human_name(@model)
|
69
71
|
end
|
70
72
|
|
71
73
|
def test_should_allow_customized_state_key_scoped_to_machine
|
@@ -76,7 +78,7 @@ class MachineWithInternationalizationTest < BaseTestCase
|
|
76
78
|
machine = StateMachines::Machine.new(@model)
|
77
79
|
machine.state :parked
|
78
80
|
|
79
|
-
assert_equal 'shutdown', machine.state(:parked).human_name
|
81
|
+
assert_equal 'shutdown', machine.state(:parked).human_name(@model)
|
80
82
|
end
|
81
83
|
|
82
84
|
def test_should_allow_customized_state_key_unscoped
|
@@ -87,7 +89,7 @@ class MachineWithInternationalizationTest < BaseTestCase
|
|
87
89
|
machine = StateMachines::Machine.new(@model)
|
88
90
|
machine.state :parked
|
89
91
|
|
90
|
-
assert_equal 'shutdown', machine.state(:parked).human_name
|
92
|
+
assert_equal 'shutdown', machine.state(:parked).human_name(@model)
|
91
93
|
end
|
92
94
|
|
93
95
|
def test_should_support_nil_state_key
|
@@ -97,7 +99,7 @@ class MachineWithInternationalizationTest < BaseTestCase
|
|
97
99
|
|
98
100
|
machine = StateMachines::Machine.new(@model)
|
99
101
|
|
100
|
-
assert_equal 'empty', machine.state(nil).human_name
|
102
|
+
assert_equal 'empty', machine.state(nil).human_name(@model)
|
101
103
|
end
|
102
104
|
|
103
105
|
def test_should_allow_customized_event_key_scoped_to_class_and_machine
|
@@ -108,7 +110,7 @@ class MachineWithInternationalizationTest < BaseTestCase
|
|
108
110
|
machine = StateMachines::Machine.new(@model)
|
109
111
|
machine.event :park
|
110
112
|
|
111
|
-
assert_equal 'stop', machine.event(:park).human_name
|
113
|
+
assert_equal 'stop', machine.event(:park).human_name(@model)
|
112
114
|
end
|
113
115
|
|
114
116
|
def test_should_allow_customized_event_key_scoped_to_class
|
@@ -119,7 +121,7 @@ class MachineWithInternationalizationTest < BaseTestCase
|
|
119
121
|
machine = StateMachines::Machine.new(@model)
|
120
122
|
machine.event :park
|
121
123
|
|
122
|
-
assert_equal 'stop', machine.event(:park).human_name
|
124
|
+
assert_equal 'stop', machine.event(:park).human_name(@model)
|
123
125
|
end
|
124
126
|
|
125
127
|
def test_should_allow_customized_event_key_scoped_to_machine
|
@@ -130,7 +132,7 @@ class MachineWithInternationalizationTest < BaseTestCase
|
|
130
132
|
machine = StateMachines::Machine.new(@model)
|
131
133
|
machine.event :park
|
132
134
|
|
133
|
-
assert_equal 'stop', machine.event(:park).human_name
|
135
|
+
assert_equal 'stop', machine.event(:park).human_name(@model)
|
134
136
|
end
|
135
137
|
|
136
138
|
def test_should_allow_customized_event_key_unscoped
|
@@ -141,7 +143,7 @@ class MachineWithInternationalizationTest < BaseTestCase
|
|
141
143
|
machine = StateMachines::Machine.new(@model)
|
142
144
|
machine.event :park
|
143
145
|
|
144
|
-
assert_equal 'stop', machine.event(:park).human_name
|
146
|
+
assert_equal 'stop', machine.event(:park).human_name(@model)
|
145
147
|
end
|
146
148
|
|
147
149
|
def test_should_have_locale_once_in_load_path
|
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
2
4
|
|
3
5
|
class MachineWithStateDrivenValidationsTest < BaseTestCase
|
4
6
|
def setup
|
@@ -9,7 +11,7 @@ class MachineWithStateDrivenValidationsTest < BaseTestCase
|
|
9
11
|
|
10
12
|
@machine = StateMachines::Machine.new(@model)
|
11
13
|
@machine.state :first_gear, :second_gear do
|
12
|
-
|
14
|
+
validates :seatbelt, presence: true
|
13
15
|
end
|
14
16
|
@machine.other_states :parked
|
15
17
|
end
|
@@ -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
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'debug'
|
4
|
+
|
5
|
+
require 'state_machines-activemodel'
|
6
|
+
require 'minitest/autorun'
|
7
|
+
require 'minitest/reporters'
|
8
|
+
require 'active_support/all'
|
9
|
+
Minitest::Reporters.use! [Minitest::Reporters::ProgressReporter.new]
|
10
|
+
I18n.enforce_available_locales = true
|
11
|
+
|
12
|
+
class BaseTestCase < ActiveSupport::TestCase
|
13
|
+
protected
|
14
|
+
# Creates a new ActiveModel model (and the associated table)
|
15
|
+
def new_model(&block)
|
16
|
+
# Simple ActiveModel superclass
|
17
|
+
parent = Class.new do
|
18
|
+
def self.model_attribute(name)
|
19
|
+
define_method(name) { instance_variable_defined?(:"@#{name}") ? instance_variable_get(:"@#{name}") : nil }
|
20
|
+
define_method("#{name}=") do |value|
|
21
|
+
send(:"#{name}_will_change!") if self.class <= ActiveModel::Dirty && value != send(name)
|
22
|
+
instance_variable_set("@#{name}", value)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.create
|
27
|
+
object = new
|
28
|
+
object.save
|
29
|
+
object
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(attrs = {})
|
33
|
+
attrs.each { |attr, value| send("#{attr}=", value) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def attributes
|
37
|
+
@attributes ||= {}
|
38
|
+
end
|
39
|
+
|
40
|
+
def save
|
41
|
+
true
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
model = Class.new(parent) do
|
46
|
+
def self.name
|
47
|
+
'Foo'
|
48
|
+
end
|
49
|
+
|
50
|
+
model_attribute :state
|
51
|
+
end
|
52
|
+
model.class_eval(&block) if block_given?
|
53
|
+
model
|
54
|
+
end
|
55
|
+
end
|