# frozen_string_literal: true RSpec.describe FiniteMachine, '#choice' do before(:each) { stub_const("User", Class.new do def promo?(token = false) token == :yes end end) } it "allows for static choice based on conditional branching" do called = [] fsm = FiniteMachine.new do initial :company_form event :next, from: :company_form do choice :agreement_form, if: -> { false } choice :promo_form, if: -> { false } choice :official_form, if: -> { true } end on_exit do |event| called << "on_exit_#{event.from}" end on_enter do |event| called << "on_enter_#{event.to}" end end expect(fsm.current).to eq(:company_form) fsm.next expect(fsm.current).to eq(:official_form) expect(called).to eq([ 'on_exit_company_form', 'on_enter_official_form' ]) end it "allows for dynamic choice based on conditional branching" do fsm = FiniteMachine.new do initial :company_form event :next, from: :company_form do choice :agreement_form, if: proc { |_, a| a < 1 } choice :promo_form, if: proc { |_, a| a == 1 } choice :official_form, if: proc { |_, a| a > 1 } end end expect(fsm.current).to eq(:company_form) fsm.next(0) expect(fsm.current).to eq(:agreement_form) fsm.restore!(:company_form) fsm.next(1) expect(fsm.current).to eq(:promo_form) fsm.restore!(:company_form) fsm.next(2) expect(fsm.current).to eq(:official_form) end it "allows for dynamic choice based on conditional branching and target" do user = User.new fsm = FiniteMachine.new(user) do initial :company_form event :next, from: :company_form do choice :agreement_form, if: proc { |_user, token| _user.promo?(token) } choice :promo_form, unless: proc { |_user, token| _user.promo?(token) } end end expect(fsm.current).to eq(:company_form) fsm.next(:no) expect(fsm.current).to eq(:promo_form) fsm.restore!(:company_form) fsm.next(:yes) expect(fsm.current).to eq(:agreement_form) end it "chooses state when skipped if/unless" do fsm = FiniteMachine.new do initial :company_form event :next, from: :company_form do choice :agreement_form, if: -> { false } choice :promo_form choice :official_form, if: -> { true } end end expect(fsm.current).to eq(:company_form) fsm.next expect(fsm.current).to eq(:promo_form) end it "chooses default state when branching conditions don't match" do fsm = FiniteMachine.new do initial :company_form event :next, from: :company_form do choice :agreement_form, if: -> { false } choice :promo_form, if: -> { false } default :official_form end end expect(fsm.current).to eq(:company_form) fsm.next expect(fsm.current).to eq(:official_form) end it "fails to transition when no condition matches without default state" do fsm = FiniteMachine.new do initial :company_form event :next, from: :company_form do choice :agreement_form, if: -> { false } choice :promo_form, if: -> { false } end end expect(fsm.current).to eq(:company_form) fsm.next expect(fsm.current).to eq(:company_form) end it "allows to transition from multiple states to choice pseudostate" do fsm = FiniteMachine.new do initial :red event :go, from: [:yellow, :red] do choice :pink, if: -> { false } choice :green end end expect(fsm.current).to eq(:red) fsm.go expect(fsm.current).to eq(:green) fsm.restore!(:yellow) expect(fsm.current).to eq(:yellow) fsm.go expect(fsm.current).to eq(:green) end it "allows to transition from any state to choice pseudo state" do fsm = FiniteMachine.new do initial :red event :go, from: any_state do choice :pink, if: -> { false } choice :green end end expect(fsm.current).to eq(:red) fsm.go expect(fsm.current).to eq(:green) end it "groups correctly events under the same name" do fsm = FiniteMachine.new do initial :red event :next, from: :yellow, to: :green event :next, from: :red do choice :pink, if: -> { false } choice :yellow end end expect(fsm.current).to eq(:red) fsm.next expect(fsm.current).to eq(:yellow) fsm.next expect(fsm.current).to eq(:green) end it "performs matching transitions for multiple event definitions with the same name" do ticket = double(:ticket, :pending? => true, :finished? => true) fsm = FiniteMachine.new(ticket) do initial :inactive event :advance, from: [:inactive, :paused, :fulfilled] do choice :active, if: proc { |_ticket| !_ticket.pending? } end event :advance, from: [:inactive, :active, :fulfilled] do choice :paused, if: proc { |_ticket| _ticket.pending? } end event :advance, from: [:inactive, :active, :paused] do choice :fulfilled, if: proc { |_ticket| _ticket.finished? } end end expect(fsm.current).to eq(:inactive) fsm.advance expect(fsm.current).to eq(:paused) fsm.advance expect(fsm.current).to eq(:fulfilled) end it "does not transition when no matching choice for multiple event definitions" do ticket = double(:ticket, :pending? => true, :finished? => false) called = [] fsm = FiniteMachine.new(ticket) do initial :inactive event :advance, from: [:inactive, :paused, :fulfilled] do choice :active, if: proc { |_ticket| !_ticket.pending? } end event :advance, from: [:inactive, :active, :fulfilled] do choice :paused, if: proc { |_ticket| _ticket.pending? } end event :advance, from: [:inactive, :active, :paused] do choice :fulfilled, if: proc { |_ticket| _ticket.finished? } end on_before(:advance) { called << 'on_before_advance' } on_after(:advance) { called << 'on_after_advance' } end expect(fsm.current).to eq(:inactive) fsm.advance expect(fsm.current).to eq(:paused) fsm.advance expect(fsm.current).to eq(:paused) expect(called).to eq([ 'on_before_advance', 'on_after_advance', 'on_before_advance', 'on_after_advance' ]) end it "sets callback properties correctly" do expected = {name: :init, from: :none, to: :red, a: nil, b: nil, c: nil } callback = Proc.new { |event, a, b, c| target.expect(event.from).to target.eql(expected[:from]) target.expect(event.to).to target.eql(expected[:to]) target.expect(event.name).to target.eql(expected[:name]) target.expect(a).to target.eql(expected[:a]) target.expect(b).to target.eql(expected[:b]) target.expect(c).to target.eql(expected[:c]) } fsm = FiniteMachine.new(self) do initial :red event :next, from: :red do choice :green, if: -> { false } choice :yellow end event :next, from: :yellow do choice :green, if: -> { true } choice :yellow end event :finish, from: any_state do choice :green, if: -> { false } choice :red end # generic state callbacks on_enter(&callback) on_transition(&callback) on_exit(&callback) # generic event callbacks on_before(&callback) on_after(&callback) # state callbacks on_enter :green, &callback on_enter :yellow, &callback on_enter :red, &callback on_transition :green, &callback on_transition :yellow, &callback on_transition :red, &callback on_exit :green, &callback on_exit :yellow, &callback on_exit :red, &callback # event callbacks on_before :next, &callback on_after :next, &callback end expect(fsm.current).to eq(:red) expected = {name: :next, from: :red, to: :yellow, a: 1, b: 2, c: 3} fsm.next(1, 2, 3) expected = {name: :next, from: :yellow, to: :green, a: 4, b: 5, c: 6} fsm.next(4, 5, 6) expected = {name: :finish, from: :green, to: :red, a: 7, b: 8, c: 9} fsm.finish(7, 8, 9) end end