require 'spec_helper' Multiflow.persistence = :none class Robot include Multiflow stateflow do initial :green state :green, :yellow, :red event :change_color do transitions :from => :green, :to => :yellow transitions :from => :yellow, :to => :red transitions :from => :red, :to => :green end end end class Car include Multiflow stateflow do initial :parked state :parked do enter do "Entering parked" end exit do "Exiting parked" end end state :driving do enter do "Entering parked" end end event :drive do transitions :from => :parked, :to => :driving end event :park do transitions :from => :driving, :to => :parked end end end class Bob include Multiflow stateflow do state :yellow, :red, :purple event :change_hair_color do transitions :from => :purple, :to => :yellow transitions :from => :yellow, :to => :red transitions :from => :red, :to => :purple end end end class Dater include Multiflow stateflow do state :single, :dating, :married event :take_out do transitions :from => :single, :to => :dating end event :gift do transitions :from => :dating, :to => [:single, :married], :decide => :girls_mood? end event :blank_decision do transitions :from => :single, :to => [:single, :married], :decide => :girls_mood? end event :fail do transitions :from => :dating, :to => [:single, :married] end end def girls_mood? end end class Priority include Multiflow stateflow do initial :medium state :low, :medium, :high event :low do transitions :from => any, :to => :low end event :medium do transitions :from => any, :to => :medium end event :high do transitions :from => any, :to => :high end end end class Stater include Multiflow stateflow do initial :bill state :bob, :bill event :lolcats do transitions :from => :bill, :to => :bob end end end describe Multiflow do describe "class methods" do it "should respond to stateflow block to setup the intial stateflow" do Robot.should respond_to(:stateflow) end it "should respond to the machine attr accessor" do Robot.should respond_to(:machine_state) end it "should return all active persistence layers" do Set[*Multiflow::Persistence.active.should] == Set[:active_record, :mongo_mapper, :mongoid, :none] end end describe "instance methods" do before(:each) do @r = Robot.new end it "should respond to current state" do @r.should respond_to(:current_state) end it "should respond to the current state setter" do @r.should respond_to(:set_current_state) end it "should respond to the current machine" do @r.should respond_to(:machine_state) end it "should respond to load from persistence" do @r.should respond_to(:load_from_persistence) end it "should respond to save to persistence" do @r.should respond_to(:save_to_persistence) end end describe "initial state" do it "should set the initial state" do robot = Robot.new robot.current_state.name.should == :green end it "should return true for green?" do robot = Robot.new robot.green?.should be_true end it "should return false for yellow?" do robot = Robot.new robot.yellow?.should be_false end it "should return false for red?" do robot = Robot.new robot.red?.should be_false end it "should set the initial state to the first state set" do bob = Bob.new bob.current_state.name.should == :yellow bob.yellow?.should be_true end end it "robot class should contain red, yellow and green states" do robot = Robot.new robot.machine_state.states.keys.should include(:red, :yellow, :green) end describe "firing events" do let(:robot) { Robot.new } let(:event) { :change_color } subject { robot } it "should raise an exception if the event does not exist" do lambda { robot.send(:fire_event, robot.machine_state, :fake) }.should raise_error(Multiflow::NoEventFound) end shared_examples_for "an entity supporting state changes" do context "when firing" do after(:each) { subject.send(event_method) } it "should call the fire method on event" do subject.machine_state.events[event].should_receive(:fire) end it "should call the fire_event method" do subject.should_receive(:fire_event).with(subject.machine_state, event, {:save=>persisted}) end end it "should update the current state" do subject.current_state.name.should == :green subject.send(event_method) subject.current_state.name.should == :yellow end end describe "bang event" do let(:event_method) { :change_color! } let(:persisted) { true } it_should_behave_like "an entity supporting state changes" end describe "non-bang event" do let(:event_method) { :change_color } let(:persisted) { false } it_should_behave_like "an entity supporting state changes" end describe "before filters" do before(:each) do @car = Car.new end it "should call the exit state before filter on the exiting old state" do @car.machine_state.states[:parked].should_receive(:execute_action).with(:exit, @car) @car.drive! end it "should call the enter state before filter on the entering new state" do @car.machine_state.states[:driving].should_receive(:execute_action).with(:enter, @car) @car.drive! end end end describe "persistence" do it "should attempt to persist the new state and the name should be a string" do robot = Robot.new robot.should_receive(:save_to_persistence).with(robot.machine_state, "yellow", {:save=>true}) robot.change_color! end it "should attempt to read the initial state from the persistence" do robot = Robot.new def robot.load_from_persistence(machine) :red end robot.current_state.name.should == :red end end describe "dynamic to transitions" do it "should raise an error without any decide argument" do date = Dater.new def date.load_from_persistence(machine) :dating end lambda { date.fail! }.should raise_error(Multiflow::IncorrectTransition) end it "should raise an error if the decision method does not return a valid state" do date = Dater.new def date.girls_mood? :lol end lambda { date.blank_decision! }.should raise_error(Multiflow::NoStateFound, "Decision did not return a state that was set in the 'to' argument") end it "should raise an error if the decision method returns blank/nil" do date = Dater.new def date.girls_mood? end lambda { date.blank_decision! }.should raise_error(Multiflow::NoStateFound, "Decision did not return a state that was set in the 'to' argument") end it "should calculate the decide block or method and transition to the correct state" do date = Dater.new def date.load_from_persistence(machine) :dating end date.current_state.name.should == :dating def date.girls_mood? :single end date.gift! date.current_state.name.should == :single end end describe "transitions from any" do it "should properly change state" do priority = Priority.new priority.low! priority.should be_low priority.medium! priority.should be_medium priority.high! priority.should be_high end end describe 'sub-classes of Multiflow\'d classes' do subject { Class.new(Stater) } it 'shouldn\'t raise an exception' do expect { subject.new.lolcats! }.not_to raise_exception end end end