workflow-rails4 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.travis.yml +21 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +663 -0
- data/Rakefile +31 -0
- data/gemfiles/Gemfile.rails-2.3.x +11 -0
- data/gemfiles/Gemfile.rails-3.x +11 -0
- data/gemfiles/Gemfile.rails-edge +12 -0
- data/lib/workflow.rb +277 -0
- data/lib/workflow/adapters/active_record.rb +66 -0
- data/lib/workflow/adapters/remodel.rb +15 -0
- data/lib/workflow/draw.rb +79 -0
- data/lib/workflow/errors.rb +18 -0
- data/lib/workflow/event.rb +18 -0
- data/lib/workflow/specification.rb +64 -0
- data/lib/workflow/state.rb +44 -0
- data/lib/workflow/version.rb +3 -0
- data/orders_workflow.png +0 -0
- data/test/active_record_scopes_test.rb +49 -0
- data/test/advanced_examples_test.rb +82 -0
- data/test/advanced_hooks_and_validation_test.rb +119 -0
- data/test/attr_protected_test.rb +107 -0
- data/test/before_transition_test.rb +36 -0
- data/test/couchtiny_example.rb +46 -0
- data/test/inheritance_test.rb +60 -0
- data/test/main_test.rb +544 -0
- data/test/multiple_workflows_test.rb +84 -0
- data/test/new_versions/compare_states_test.rb +32 -0
- data/test/new_versions/persistence_test.rb +62 -0
- data/test/on_error_test.rb +52 -0
- data/test/readme_example.rb +37 -0
- data/test/test_helper.rb +39 -0
- data/test/without_active_record_test.rb +54 -0
- data/workflow.gemspec +32 -0
- metadata +202 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
module Workflow
|
2
|
+
class TransitionHalted < Exception
|
3
|
+
|
4
|
+
attr_reader :halted_because
|
5
|
+
|
6
|
+
def initialize(msg = nil)
|
7
|
+
@halted_because = msg
|
8
|
+
super msg
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
class NoTransitionAllowed < Exception; end
|
14
|
+
|
15
|
+
class WorkflowError < Exception; end
|
16
|
+
|
17
|
+
class WorkflowDefinitionError < Exception; end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Workflow
|
2
|
+
class Event
|
3
|
+
|
4
|
+
attr_accessor :name, :transitions_to, :meta, :action
|
5
|
+
|
6
|
+
def initialize(name, transitions_to, meta = {}, &action)
|
7
|
+
@name, @transitions_to, @meta, @action = name, transitions_to.to_sym, meta, action
|
8
|
+
end
|
9
|
+
|
10
|
+
def draw(graph, from_state)
|
11
|
+
graph.add_edges(from_state.name.to_s, transitions_to.to_s, meta.merge(:label => to_s))
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
@name.to_s
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'workflow/state'
|
2
|
+
require 'workflow/event'
|
3
|
+
require 'workflow/errors'
|
4
|
+
|
5
|
+
module Workflow
|
6
|
+
class Specification
|
7
|
+
attr_accessor :states, :initial_state, :meta,
|
8
|
+
:on_transition_proc, :before_transition_proc, :after_transition_proc, :on_error_proc
|
9
|
+
|
10
|
+
def initialize(meta = {}, &specification)
|
11
|
+
@states = Hash.new
|
12
|
+
@meta = meta
|
13
|
+
instance_eval(&specification)
|
14
|
+
end
|
15
|
+
|
16
|
+
def state_names
|
17
|
+
states.keys
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def state(name, meta = {:meta => {}}, &events_and_etc)
|
23
|
+
# meta[:meta] to keep the API consistent..., gah
|
24
|
+
new_state = Workflow::State.new(name, self, meta[:meta])
|
25
|
+
@initial_state = new_state if @states.empty?
|
26
|
+
@states[name.to_sym] = new_state
|
27
|
+
@scoped_state = new_state
|
28
|
+
instance_eval(&events_and_etc) if events_and_etc
|
29
|
+
end
|
30
|
+
|
31
|
+
def event(name, args = {}, &action)
|
32
|
+
target = args[:transitions_to] || args[:transition_to]
|
33
|
+
raise WorkflowDefinitionError.new(
|
34
|
+
"missing ':transitions_to' in workflow event definition for '#{name}'") \
|
35
|
+
if target.nil?
|
36
|
+
@scoped_state.events[name.to_sym] =
|
37
|
+
Workflow::Event.new(name, target, (args[:meta] or {}), &action)
|
38
|
+
end
|
39
|
+
|
40
|
+
def on_entry(&proc)
|
41
|
+
@scoped_state.on_entry = proc
|
42
|
+
end
|
43
|
+
|
44
|
+
def on_exit(&proc)
|
45
|
+
@scoped_state.on_exit = proc
|
46
|
+
end
|
47
|
+
|
48
|
+
def after_transition(&proc)
|
49
|
+
@after_transition_proc = proc
|
50
|
+
end
|
51
|
+
|
52
|
+
def before_transition(&proc)
|
53
|
+
@before_transition_proc = proc
|
54
|
+
end
|
55
|
+
|
56
|
+
def on_transition(&proc)
|
57
|
+
@on_transition_proc = proc
|
58
|
+
end
|
59
|
+
|
60
|
+
def on_error(&proc)
|
61
|
+
@on_error_proc = proc
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Workflow
|
2
|
+
class State
|
3
|
+
attr_accessor :name, :events, :meta, :on_entry, :on_exit
|
4
|
+
attr_reader :spec
|
5
|
+
|
6
|
+
def initialize(name, spec, meta = {})
|
7
|
+
@name, @spec, @events, @meta = name, spec, Hash.new, meta
|
8
|
+
end
|
9
|
+
|
10
|
+
def draw(graph)
|
11
|
+
defaults = {
|
12
|
+
:label => to_s,
|
13
|
+
:width => '1',
|
14
|
+
:height => '1',
|
15
|
+
:shape => 'ellipse'
|
16
|
+
}
|
17
|
+
|
18
|
+
node = graph.add_nodes(to_s, defaults.merge(meta))
|
19
|
+
|
20
|
+
# Add open arrow for initial state
|
21
|
+
# graph.add_edge(graph.add_node('starting_state', :shape => 'point'), node) if initial?
|
22
|
+
|
23
|
+
node
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
if RUBY_VERSION >= '1.9'
|
28
|
+
include Comparable
|
29
|
+
def <=>(other_state)
|
30
|
+
states = spec.states.keys
|
31
|
+
raise ArgumentError, "state `#{other_state}' does not exist" unless states.include?(other_state.to_sym)
|
32
|
+
states.index(self.to_sym) <=> states.index(other_state.to_sym)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
"#{name}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_sym
|
41
|
+
name.to_sym
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/orders_workflow.png
ADDED
Binary file
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
|
3
|
+
$VERBOSE = false
|
4
|
+
require 'active_record'
|
5
|
+
require 'sqlite3'
|
6
|
+
require 'workflow'
|
7
|
+
|
8
|
+
ActiveRecord::Migration.verbose = false
|
9
|
+
|
10
|
+
class Article < ActiveRecord::Base
|
11
|
+
include Workflow
|
12
|
+
|
13
|
+
workflow do
|
14
|
+
state :new
|
15
|
+
state :accepted
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class ActiveRecordScopesTest < ActiveRecordTestCase
|
20
|
+
|
21
|
+
def setup
|
22
|
+
super
|
23
|
+
|
24
|
+
ActiveRecord::Schema.define do
|
25
|
+
create_table :articles do |t|
|
26
|
+
t.string :title
|
27
|
+
t.string :body
|
28
|
+
t.string :blame_reason
|
29
|
+
t.string :reject_reason
|
30
|
+
t.string :workflow_state
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def assert_state(title, expected_state, klass = Order)
|
36
|
+
o = klass.find_by_title(title)
|
37
|
+
assert_equal expected_state, o.read_attribute(klass.workflow_column)
|
38
|
+
o
|
39
|
+
end
|
40
|
+
|
41
|
+
test 'have "with_new_state" scope' do
|
42
|
+
assert_respond_to Article, :with_new_state
|
43
|
+
end
|
44
|
+
|
45
|
+
test 'have "with_accepted_state" scope' do
|
46
|
+
assert_respond_to Article, :with_accepted_state
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
require 'workflow'
|
3
|
+
class AdvanceExamplesTest < ActiveRecordTestCase
|
4
|
+
|
5
|
+
class Article
|
6
|
+
include Workflow
|
7
|
+
workflow do
|
8
|
+
state :new do
|
9
|
+
event :submit, :transitions_to => :awaiting_review
|
10
|
+
end
|
11
|
+
state :awaiting_review do
|
12
|
+
event :review, :transitions_to => :being_reviewed
|
13
|
+
end
|
14
|
+
state :being_reviewed do
|
15
|
+
event :accept, :transitions_to => :accepted
|
16
|
+
event :reject, :transitions_to => :rejected
|
17
|
+
end
|
18
|
+
state :accepted do
|
19
|
+
end
|
20
|
+
state :rejected do
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
test '#63 undoing event - automatically add revert events for every defined event' do
|
26
|
+
# also see https://github.com/geekq/workflow/issues/63
|
27
|
+
spec = Article.workflow_spec
|
28
|
+
spec.state_names.each do |state_name|
|
29
|
+
state = spec.states[state_name]
|
30
|
+
|
31
|
+
(state.events.values.reject {|e| e.name.to_s =~ /^revert_/ }).each do |event|
|
32
|
+
event_name = event.name
|
33
|
+
revert_event_name = "revert_" + event_name.to_s
|
34
|
+
|
35
|
+
# Add revert events
|
36
|
+
spec.states[event.transitions_to.to_sym].events[revert_event_name.to_sym] =
|
37
|
+
Workflow::Event.new(revert_event_name, state, {})
|
38
|
+
|
39
|
+
# Add methods for revert events
|
40
|
+
Article.module_eval do
|
41
|
+
define_method "#{revert_event_name}!".to_sym do |*args|
|
42
|
+
process_event!(revert_event_name, *args)
|
43
|
+
end
|
44
|
+
define_method "can_#{revert_event_name}?" do
|
45
|
+
return self.current_state.events.include?(revert_event_name)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
a = Article.new
|
53
|
+
assert(a.new?, "should start with the 'new' state")
|
54
|
+
a.submit!
|
55
|
+
assert(a.awaiting_review?, "should now be in 'awaiting_review' state")
|
56
|
+
assert_equal(['revert_submit', 'review'], a.current_state.events.keys.map(&:to_s).sort)
|
57
|
+
a.revert_submit! # this method is added by our meta programming magic above
|
58
|
+
assert(a.new?, "should now be back in the 'new' state")
|
59
|
+
end
|
60
|
+
|
61
|
+
test '#92 Load workflow specification' do
|
62
|
+
c = Class.new
|
63
|
+
c.class_eval do
|
64
|
+
include Workflow
|
65
|
+
end
|
66
|
+
|
67
|
+
# build a Specification (you can load it from yaml file too)
|
68
|
+
myspec = Workflow::Specification.new do
|
69
|
+
state :one do
|
70
|
+
event :dynamic_transition, :transitions_to => :one_a
|
71
|
+
end
|
72
|
+
state :one_a
|
73
|
+
end
|
74
|
+
|
75
|
+
c.send :assign_workflow, myspec
|
76
|
+
|
77
|
+
a = c.new
|
78
|
+
a.dynamic_transition!(1)
|
79
|
+
assert a.one_a?, 'Expected successful transition to a new state'
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
|
3
|
+
$VERBOSE = false
|
4
|
+
require 'active_record'
|
5
|
+
require 'sqlite3'
|
6
|
+
require 'workflow'
|
7
|
+
|
8
|
+
ActiveRecord::Migration.verbose = false
|
9
|
+
|
10
|
+
# Transition based validation
|
11
|
+
# ---------------------------
|
12
|
+
# If you are using ActiveRecord you might want to define different validations
|
13
|
+
# for different transitions. There is a `validates_presence_of` hook that let's
|
14
|
+
# you specify the attributes that need to be present for an successful transition.
|
15
|
+
# If the object is not valid at the end of the transition event the transition
|
16
|
+
# is halted and a TransitionHalted exception is thrown.
|
17
|
+
#
|
18
|
+
# Here is a sample that illustrates how to use the presence validation:
|
19
|
+
# (use case suggested by http://github.com/southdesign)
|
20
|
+
class Article < ActiveRecord::Base
|
21
|
+
include Workflow
|
22
|
+
workflow do
|
23
|
+
state :new do
|
24
|
+
event :accept, :transitions_to => :accepted, :meta => {:validates_presence_of => [:title, :body]}
|
25
|
+
event :reject, :transitions_to => :rejected
|
26
|
+
end
|
27
|
+
state :accepted do
|
28
|
+
event :blame, :transitions_to => :blamed, :meta => {:validates_presence_of => [:title, :body, :blame_reason]}
|
29
|
+
event :delete, :transitions_to => :deleted
|
30
|
+
end
|
31
|
+
state :rejected do
|
32
|
+
event :delete, :transitions_to => :deleted
|
33
|
+
end
|
34
|
+
state :blamed do
|
35
|
+
event :delete, :transitions_to => :deleted
|
36
|
+
end
|
37
|
+
state :deleted do
|
38
|
+
event :accept, :transitions_to => :accepted
|
39
|
+
end
|
40
|
+
|
41
|
+
on_transition do |from, to, triggering_event, *event_args|
|
42
|
+
if self.class.superclass.to_s.split("::").first == "ActiveRecord"
|
43
|
+
singleton = class << self; self end
|
44
|
+
validations = Proc.new {}
|
45
|
+
|
46
|
+
meta = Article.workflow_spec.states[from].events[triggering_event].meta
|
47
|
+
fields_to_validate = meta[:validates_presence_of]
|
48
|
+
if fields_to_validate
|
49
|
+
validations = Proc.new {
|
50
|
+
errors.add_on_blank(fields_to_validate) if fields_to_validate
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
singleton.send :define_method, :validate_for_transition, &validations
|
55
|
+
validate_for_transition
|
56
|
+
halt! "Event[#{triggering_event}]'s transitions_to[#{to}] is not valid." unless self.errors.empty?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class AdvancedHooksAndValidationTest < ActiveRecordTestCase
|
63
|
+
|
64
|
+
def setup
|
65
|
+
super
|
66
|
+
|
67
|
+
ActiveRecord::Schema.define do
|
68
|
+
create_table :articles do |t|
|
69
|
+
t.string :title
|
70
|
+
t.string :body
|
71
|
+
t.string :blame_reason
|
72
|
+
t.string :reject_reason
|
73
|
+
t.string :workflow_state
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
exec "INSERT INTO articles(title, body, blame_reason, reject_reason, workflow_state) VALUES('new1', NULL, NULL, NULL, 'new')"
|
78
|
+
exec "INSERT INTO articles(title, body, blame_reason, reject_reason, workflow_state) VALUES('new2', 'some content', NULL, NULL, 'new')"
|
79
|
+
exec "INSERT INTO articles(title, body, blame_reason, reject_reason, workflow_state) VALUES('accepted1', 'some content', NULL, NULL, 'accepted')"
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
def assert_state(title, expected_state, klass = Order)
|
84
|
+
o = klass.find_by_title(title)
|
85
|
+
assert_equal expected_state, o.read_attribute(klass.workflow_column)
|
86
|
+
o
|
87
|
+
end
|
88
|
+
|
89
|
+
test 'deny transition from new to accepted because of the missing presence of the body' do
|
90
|
+
a = Article.find_by_title('new1');
|
91
|
+
assert_raise Workflow::TransitionHalted do
|
92
|
+
a.accept!
|
93
|
+
end
|
94
|
+
assert_state 'new1', 'new', Article
|
95
|
+
end
|
96
|
+
|
97
|
+
test 'allow transition from new to accepted because body is present this time' do
|
98
|
+
a = Article.find_by_title('new2');
|
99
|
+
assert a.accept!
|
100
|
+
assert_state 'new2', 'accepted', Article
|
101
|
+
end
|
102
|
+
|
103
|
+
test 'allow transition from accepted to blamed because of a blame_reason' do
|
104
|
+
a = Article.find_by_title('accepted1');
|
105
|
+
a.blame_reason = "Provocant thesis"
|
106
|
+
assert a.blame!
|
107
|
+
assert_state 'accepted1', 'blamed', Article
|
108
|
+
end
|
109
|
+
|
110
|
+
test 'deny transition from accepted to blamed because of no blame_reason' do
|
111
|
+
a = Article.find_by_title('accepted1');
|
112
|
+
assert_raise Workflow::TransitionHalted do
|
113
|
+
assert a.blame!
|
114
|
+
end
|
115
|
+
assert_state 'accepted1', 'accepted', Article
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
$VERBOSE = false
|
4
|
+
require 'active_record'
|
5
|
+
require 'logger'
|
6
|
+
require 'sqlite3'
|
7
|
+
require 'workflow'
|
8
|
+
require 'mocha/setup'
|
9
|
+
require 'stringio'
|
10
|
+
require 'protected_attributes' if ActiveRecord::VERSION::MAJOR >= 4
|
11
|
+
|
12
|
+
ActiveRecord::Migration.verbose = false
|
13
|
+
|
14
|
+
class AttrProtectedTestOrder < ActiveRecord::Base
|
15
|
+
include Workflow
|
16
|
+
|
17
|
+
workflow do
|
18
|
+
state :submitted do
|
19
|
+
event :accept, :transitions_to => :accepted, :meta => {:doc_weight => 8} do |reviewer, args|
|
20
|
+
end
|
21
|
+
end
|
22
|
+
state :accepted do
|
23
|
+
event :ship, :transitions_to => :shipped
|
24
|
+
end
|
25
|
+
state :shipped
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_accessible :title # protecting all the other attributes
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
AttrProtectedTestOrder.logger = Logger.new(STDOUT) # active_record 2.3 expects a logger instance
|
33
|
+
AttrProtectedTestOrder.logger.level = Logger::WARN # switch to Logger::DEBUG to see the SQL statements
|
34
|
+
|
35
|
+
class AttrProtectedTest < ActiveRecordTestCase
|
36
|
+
|
37
|
+
def setup
|
38
|
+
super
|
39
|
+
|
40
|
+
ActiveRecord::Schema.define do
|
41
|
+
create_table :attr_protected_test_orders do |t|
|
42
|
+
t.string :title, :null => false
|
43
|
+
t.string :workflow_state
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
exec "INSERT INTO attr_protected_test_orders(title, workflow_state) VALUES('order1', 'submitted')"
|
48
|
+
exec "INSERT INTO attr_protected_test_orders(title, workflow_state) VALUES('order2', 'accepted')"
|
49
|
+
exec "INSERT INTO attr_protected_test_orders(title, workflow_state) VALUES('order3', 'accepted')"
|
50
|
+
exec "INSERT INTO attr_protected_test_orders(title, workflow_state) VALUES('order4', 'accepted')"
|
51
|
+
exec "INSERT INTO attr_protected_test_orders(title, workflow_state) VALUES('order5', 'accepted')"
|
52
|
+
exec "INSERT INTO attr_protected_test_orders(title, workflow_state) VALUES('protected order', 'submitted')"
|
53
|
+
end
|
54
|
+
|
55
|
+
def assert_state(title, expected_state, klass = AttrProtectedTestOrder)
|
56
|
+
o = klass.find_by_title(title)
|
57
|
+
assert_equal expected_state, o.read_attribute(klass.workflow_column)
|
58
|
+
o
|
59
|
+
end
|
60
|
+
|
61
|
+
test 'cannot mass-assign workflow_state if attr_protected' do
|
62
|
+
o = AttrProtectedTestOrder.find_by_title('order1')
|
63
|
+
assert_equal 'submitted', o.read_attribute(:workflow_state)
|
64
|
+
AttrProtectedTestOrder.logger.level = Logger::ERROR # ignore warnings
|
65
|
+
o.update_attributes :workflow_state => 'some_bad_value'
|
66
|
+
AttrProtectedTestOrder.logger.level = Logger::WARN
|
67
|
+
assert_equal 'submitted', o.read_attribute(:workflow_state)
|
68
|
+
o.update_attribute :workflow_state, 'some_overridden_value'
|
69
|
+
assert_equal 'some_overridden_value', o.read_attribute(:workflow_state)
|
70
|
+
end
|
71
|
+
|
72
|
+
test 'immediately save the new workflow_state on state machine transition' do
|
73
|
+
o = assert_state 'order2', 'accepted'
|
74
|
+
assert o.ship!
|
75
|
+
assert_state 'order2', 'shipped'
|
76
|
+
end
|
77
|
+
|
78
|
+
test 'persist workflow_state in the db and reload' do
|
79
|
+
o = assert_state 'order3', 'accepted'
|
80
|
+
assert_equal :accepted, o.current_state.name
|
81
|
+
o.ship! # should save in the database, no `o.save!` needed
|
82
|
+
|
83
|
+
assert_state 'order3', 'shipped'
|
84
|
+
|
85
|
+
o.reload
|
86
|
+
assert_equal 'shipped', o.read_attribute(:workflow_state)
|
87
|
+
end
|
88
|
+
|
89
|
+
test 'default workflow column should be workflow_state' do
|
90
|
+
o = assert_state 'order4', 'accepted'
|
91
|
+
assert_equal :workflow_state, o.class.workflow_column
|
92
|
+
end
|
93
|
+
|
94
|
+
test 'access workflow specification' do
|
95
|
+
assert_equal 3, AttrProtectedTestOrder.workflow_spec.states.length
|
96
|
+
assert_equal ['submitted', 'accepted', 'shipped'].sort,
|
97
|
+
AttrProtectedTestOrder.workflow_spec.state_names.map{|n| n.to_s}.sort
|
98
|
+
end
|
99
|
+
|
100
|
+
test 'current state object' do
|
101
|
+
o = assert_state 'order5', 'accepted'
|
102
|
+
assert_equal 'accepted', o.current_state.to_s
|
103
|
+
assert_equal 1, o.current_state.events.length
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|