├── Cakefile ├── package.json ├── src └── coffee_machine.coffee ├── lib ├── state_machine.js └── coffee_machine.js ├── README.md └── test └── test_coffee_machine.coffee /Cakefile: -------------------------------------------------------------------------------- 1 | {exec} = require 'child_process' 2 | 3 | task 'build', 'Build project from src/*.coffee to lib/*.js', -> 4 | exec 'coffee --compile --output lib/ src/', (error, stdout, stderr) -> 5 | if error 6 | console.log "build failed: #{error}" 7 | throw error 8 | console.log "build complete. #{stdout} #{stderr}" 9 | 10 | task 'test', 'Runs vowsjs test suite', -> 11 | exec './node_modules/vows/bin/vows test/test_coffee_machine.coffee --spec', (error, stdout, stderr) -> 12 | console.log stdout -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Stephen Blankenship", 3 | "name": "coffee-machine", 4 | "description": "A simple state machine written in CoffeeScript.", 5 | "version": "0.0.3", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/stephenb/coffee-machine.git" 9 | }, 10 | "main": "./lib/coffee_machine", 11 | "scripts": { 12 | "test": "cake test" 13 | }, 14 | "keywords": ["state machine", "statemachine", "coffee script", "coffeescript"], 15 | "engines": { 16 | "node": ">=v0.4.7" 17 | }, 18 | "dependencies": {}, 19 | "devDependencies": { 20 | "vows": ">=v0.5.8", 21 | "coffee-script": ">=v1.1.1" 22 | } 23 | } -------------------------------------------------------------------------------- /src/coffee_machine.coffee: -------------------------------------------------------------------------------- 1 | root = exports ? window 2 | 3 | # new CoffeeMachine 4 | # states: 5 | # stateName: 6 | # active: true/false (optional, the 1st state defaults to true) 7 | # onEnter: enterMethod (called when successfully entering the state) 8 | # onExit: exitMethod (called when successfully exiting the state) 9 | # guard: guardMethod (stops the change to this state if returns false) 10 | # events: 11 | # eventName: 12 | # from: fromState (should be a defined state, array of defined states, or "any") 13 | # to: toState (should be a defined state) 14 | # onStateChange: changeMethod (called on any state change) 15 | # 16 | root.CoffeeMachine = class CoffeeMachine 17 | 18 | constructor: (@stateMachine = {states:{}, events:{}}) -> 19 | this.defineStateMachine(@stateMachine) 20 | 21 | defineStateMachine: (@stateMachine = {states:{}, events:{}}) -> 22 | # If array setup was used, translate it into the object setup 23 | if @stateMachine.states.constructor.toString().indexOf('Array') isnt -1 24 | states = @stateMachine.states 25 | @stateMachine.states = {} 26 | for state in states 27 | @stateMachine.states[state] = { active: (state is states[0]) } 28 | # Make sure an active state is properly set 29 | activeStates = (state for own state, stateDef of @stateMachine.states when stateDef.active) 30 | if activeStates.length is 0 31 | # Set the 1st state to active 32 | for own state, stateDef of @stateMachine.states 33 | stateDef.active = true 34 | break 35 | else if activeStates.length > 1 36 | # Set only the 1st active state to active 37 | for own state in activeStates 38 | continue if state is activeStates[0] 39 | stateDef.active = false 40 | # Define the event methods 41 | for event, eventDef of @stateMachine.events 42 | do(event, eventDef) => 43 | this[event] = -> this.changeState(eventDef.from, eventDef.to, event) 44 | 45 | currentState: -> 46 | (state for own state, stateDef of @stateMachine.states when stateDef.active)[0] 47 | 48 | availableStates: -> 49 | state for own state of @stateMachine.states 50 | 51 | availableEvents: -> 52 | event for own event of @stateMachine.events 53 | 54 | changeState: (from, to, event=null) -> 55 | # If from is an array, and it contains the currentState, set from to currentState 56 | if from.constructor.toString().indexOf('Array') isnt -1 57 | if from.indexOf(this.currentState()) isnt -1 58 | from = this.currentState() 59 | else 60 | throw "Cannot change from states #{from.join(' or ')}; none are the active state!" 61 | # If using 'any', then set the from to whatever the current state is 62 | if from is 'any' then from = this.currentState() 63 | 64 | fromStateDef = @stateMachine.states[from] 65 | toStateDef = @stateMachine.states[to] 66 | 67 | throw "Cannot change to state '#{to}'; it is undefined!" if toStateDef is undefined 68 | throw "Cannot change from state '#{from}'; it is undefined!" if fromStateDef is undefined 69 | throw "Cannot change from state '#{from}'; it is not the active state!" if fromStateDef.active isnt true 70 | 71 | {onEnter: enterMethod, guard: guardMethod} = toStateDef 72 | {onExit: exitMethod} = fromStateDef 73 | 74 | args = {from: from, to: to, event: event} 75 | return false if guardMethod isnt undefined and guardMethod.call(this, args) is false 76 | exitMethod.call(this, args) if exitMethod isnt undefined 77 | enterMethod.call(this, args) if enterMethod isnt undefined 78 | @stateMachine.onStateChange.call(this, args) if @stateMachine.onStateChange isnt undefined 79 | fromStateDef.active = false 80 | toStateDef.active = true 81 | -------------------------------------------------------------------------------- /lib/state_machine.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var StateMachine, root; 3 | var __hasProp = Object.prototype.hasOwnProperty, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; 4 | root = typeof exports !== "undefined" && exports !== null ? exports : window; 5 | root.StateMachine = StateMachine = (function() { 6 | function StateMachine(stateMachine) { 7 | this.stateMachine = stateMachine != null ? stateMachine : { 8 | states: {}, 9 | events: {} 10 | }; 11 | this.defineStateMachine(this.stateMachine); 12 | } 13 | StateMachine.prototype.defineStateMachine = function(stateMachine) { 14 | var activeStates, event, eventDef, state, stateDef, states, _i, _j, _len, _len2, _ref, _ref2, _results; 15 | this.stateMachine = stateMachine != null ? stateMachine : { 16 | states: {}, 17 | events: {} 18 | }; 19 | if (this.stateMachine.states.constructor.toString().indexOf('Array') !== -1) { 20 | states = this.stateMachine.states; 21 | this.stateMachine.states = {}; 22 | for (_i = 0, _len = states.length; _i < _len; _i++) { 23 | state = states[_i]; 24 | this.stateMachine.states[state] = { 25 | active: state === states[0] 26 | }; 27 | } 28 | } 29 | activeStates = (function() { 30 | var _ref, _results; 31 | _ref = this.stateMachine.states; 32 | _results = []; 33 | for (state in _ref) { 34 | if (!__hasProp.call(_ref, state)) continue; 35 | stateDef = _ref[state]; 36 | if (stateDef.active) { 37 | _results.push(state); 38 | } 39 | } 40 | return _results; 41 | }).call(this); 42 | if (activeStates.length === 0) { 43 | _ref = this.stateMachine.states; 44 | for (state in _ref) { 45 | if (!__hasProp.call(_ref, state)) continue; 46 | stateDef = _ref[state]; 47 | stateDef.active = true; 48 | break; 49 | } 50 | } else if (activeStates.length > 1) { 51 | for (_j = 0, _len2 = activeStates.length; _j < _len2; _j++) { 52 | state = activeStates[_j]; 53 | if (state === activeStates[0]) { 54 | continue; 55 | } 56 | stateDef.active = false; 57 | } 58 | } 59 | _ref2 = this.stateMachine.events; 60 | _results = []; 61 | for (event in _ref2) { 62 | eventDef = _ref2[event]; 63 | _results.push(__bind(function(event, eventDef) { 64 | return this[event] = function() { 65 | return this.changeState(eventDef.from, eventDef.to, event); 66 | }; 67 | }, this)(event, eventDef)); 68 | } 69 | return _results; 70 | }; 71 | StateMachine.prototype.currentState = function() { 72 | var state, stateDef; 73 | return ((function() { 74 | var _ref, _results; 75 | _ref = this.stateMachine.states; 76 | _results = []; 77 | for (state in _ref) { 78 | if (!__hasProp.call(_ref, state)) continue; 79 | stateDef = _ref[state]; 80 | if (stateDef.active) { 81 | _results.push(state); 82 | } 83 | } 84 | return _results; 85 | }).call(this))[0]; 86 | }; 87 | StateMachine.prototype.availableStates = function() { 88 | var state, _ref, _results; 89 | _ref = this.stateMachine.states; 90 | _results = []; 91 | for (state in _ref) { 92 | if (!__hasProp.call(_ref, state)) continue; 93 | _results.push(state); 94 | } 95 | return _results; 96 | }; 97 | StateMachine.prototype.availableEvents = function() { 98 | var event, _ref, _results; 99 | _ref = this.stateMachine.events; 100 | _results = []; 101 | for (event in _ref) { 102 | if (!__hasProp.call(_ref, event)) continue; 103 | _results.push(event); 104 | } 105 | return _results; 106 | }; 107 | StateMachine.prototype.changeState = function(from, to, event) { 108 | var args, enterMethod, exitMethod, fromStateDef, guardMethod, toStateDef; 109 | if (event == null) { 110 | event = null; 111 | } 112 | if (from.constructor.toString().indexOf('Array') !== -1) { 113 | if (from.indexOf(this.currentState()) !== -1) { 114 | from = this.currentState(); 115 | } else { 116 | throw "Cannot change from states " + (from.join(' or ')) + "; none are the active state!"; 117 | } 118 | } 119 | fromStateDef = this.stateMachine.states[from]; 120 | toStateDef = this.stateMachine.states[to]; 121 | if (toStateDef === void 0) { 122 | throw "Cannot change to state '" + to + "'; it is undefined!"; 123 | } 124 | enterMethod = toStateDef.onEnter, guardMethod = toStateDef.guard; 125 | if (from !== 'any') { 126 | if (fromStateDef === void 0) { 127 | throw "Cannot change from state '" + from + "'; it is undefined!"; 128 | } 129 | if (fromStateDef.active !== true) { 130 | throw "Cannot change from state '" + from + "'; it is not the active state!"; 131 | } 132 | } 133 | if (from === 'any') { 134 | fromStateDef = this.stateMachine.states[this.currentState()]; 135 | } 136 | exitMethod = fromStateDef.onExit; 137 | args = { 138 | from: from, 139 | to: to, 140 | event: event 141 | }; 142 | if (guardMethod !== void 0 && guardMethod.call(this, args) === false) { 143 | return false; 144 | } 145 | if (exitMethod !== void 0) { 146 | exitMethod.call(this, args); 147 | } 148 | if (enterMethod !== void 0) { 149 | enterMethod.call(this, args); 150 | } 151 | if (this.stateMachine.onStateChange !== void 0) { 152 | this.stateMachine.onStateChange.call(this, args); 153 | } 154 | fromStateDef.active = false; 155 | return toStateDef.active = true; 156 | }; 157 | return StateMachine; 158 | })(); 159 | }).call(this); 160 | -------------------------------------------------------------------------------- /lib/coffee_machine.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var CoffeeMachine, root; 3 | var __hasProp = Object.prototype.hasOwnProperty, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; 4 | root = typeof exports !== "undefined" && exports !== null ? exports : window; 5 | root.CoffeeMachine = CoffeeMachine = (function() { 6 | function CoffeeMachine(stateMachine) { 7 | this.stateMachine = stateMachine != null ? stateMachine : { 8 | states: {}, 9 | events: {} 10 | }; 11 | this.defineStateMachine(this.stateMachine); 12 | } 13 | CoffeeMachine.prototype.defineStateMachine = function(stateMachine) { 14 | var activeStates, event, eventDef, state, stateDef, states, _i, _j, _len, _len2, _ref, _ref2, _results; 15 | this.stateMachine = stateMachine != null ? stateMachine : { 16 | states: {}, 17 | events: {} 18 | }; 19 | if (this.stateMachine.states.constructor.toString().indexOf('Array') !== -1) { 20 | states = this.stateMachine.states; 21 | this.stateMachine.states = {}; 22 | for (_i = 0, _len = states.length; _i < _len; _i++) { 23 | state = states[_i]; 24 | this.stateMachine.states[state] = { 25 | active: state === states[0] 26 | }; 27 | } 28 | } 29 | activeStates = (function() { 30 | var _ref, _results; 31 | _ref = this.stateMachine.states; 32 | _results = []; 33 | for (state in _ref) { 34 | if (!__hasProp.call(_ref, state)) continue; 35 | stateDef = _ref[state]; 36 | if (stateDef.active) { 37 | _results.push(state); 38 | } 39 | } 40 | return _results; 41 | }).call(this); 42 | if (activeStates.length === 0) { 43 | _ref = this.stateMachine.states; 44 | for (state in _ref) { 45 | if (!__hasProp.call(_ref, state)) continue; 46 | stateDef = _ref[state]; 47 | stateDef.active = true; 48 | break; 49 | } 50 | } else if (activeStates.length > 1) { 51 | for (_j = 0, _len2 = activeStates.length; _j < _len2; _j++) { 52 | state = activeStates[_j]; 53 | if (state === activeStates[0]) { 54 | continue; 55 | } 56 | stateDef.active = false; 57 | } 58 | } 59 | _ref2 = this.stateMachine.events; 60 | _results = []; 61 | for (event in _ref2) { 62 | eventDef = _ref2[event]; 63 | _results.push(__bind(function(event, eventDef) { 64 | return this[event] = function() { 65 | return this.changeState(eventDef.from, eventDef.to, event); 66 | }; 67 | }, this)(event, eventDef)); 68 | } 69 | return _results; 70 | }; 71 | CoffeeMachine.prototype.currentState = function() { 72 | var state, stateDef; 73 | return ((function() { 74 | var _ref, _results; 75 | _ref = this.stateMachine.states; 76 | _results = []; 77 | for (state in _ref) { 78 | if (!__hasProp.call(_ref, state)) continue; 79 | stateDef = _ref[state]; 80 | if (stateDef.active) { 81 | _results.push(state); 82 | } 83 | } 84 | return _results; 85 | }).call(this))[0]; 86 | }; 87 | CoffeeMachine.prototype.availableStates = function() { 88 | var state, _ref, _results; 89 | _ref = this.stateMachine.states; 90 | _results = []; 91 | for (state in _ref) { 92 | if (!__hasProp.call(_ref, state)) continue; 93 | _results.push(state); 94 | } 95 | return _results; 96 | }; 97 | CoffeeMachine.prototype.availableEvents = function() { 98 | var event, _ref, _results; 99 | _ref = this.stateMachine.events; 100 | _results = []; 101 | for (event in _ref) { 102 | if (!__hasProp.call(_ref, event)) continue; 103 | _results.push(event); 104 | } 105 | return _results; 106 | }; 107 | CoffeeMachine.prototype.changeState = function(from, to, event) { 108 | var args, enterMethod, exitMethod, fromStateDef, guardMethod, toStateDef; 109 | if (event == null) { 110 | event = null; 111 | } 112 | if (from.constructor.toString().indexOf('Array') !== -1) { 113 | if (from.indexOf(this.currentState()) !== -1) { 114 | from = this.currentState(); 115 | } else { 116 | throw "Cannot change from states " + (from.join(' or ')) + "; none are the active state!"; 117 | } 118 | } 119 | fromStateDef = this.stateMachine.states[from]; 120 | toStateDef = this.stateMachine.states[to]; 121 | if (toStateDef === void 0) { 122 | throw "Cannot change to state '" + to + "'; it is undefined!"; 123 | } 124 | enterMethod = toStateDef.onEnter, guardMethod = toStateDef.guard; 125 | if (from !== 'any') { 126 | if (fromStateDef === void 0) { 127 | throw "Cannot change from state '" + from + "'; it is undefined!"; 128 | } 129 | if (fromStateDef.active !== true) { 130 | throw "Cannot change from state '" + from + "'; it is not the active state!"; 131 | } 132 | } 133 | if (from === 'any') { 134 | fromStateDef = this.stateMachine.states[this.currentState()]; 135 | } 136 | exitMethod = fromStateDef.onExit; 137 | args = { 138 | from: from, 139 | to: to, 140 | event: event 141 | }; 142 | if (guardMethod !== void 0 && guardMethod.call(this, args) === false) { 143 | return false; 144 | } 145 | if (exitMethod !== void 0) { 146 | exitMethod.call(this, args); 147 | } 148 | if (enterMethod !== void 0) { 149 | enterMethod.call(this, args); 150 | } 151 | if (this.stateMachine.onStateChange !== void 0) { 152 | this.stateMachine.onStateChange.call(this, args); 153 | } 154 | fromStateDef.active = false; 155 | return toStateDef.active = true; 156 | }; 157 | return CoffeeMachine; 158 | })(); 159 | }).call(this); 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Description: 2 | ------------ 3 | 4 | A simple state machine written in CoffeeScript. 5 | 6 | Installation: 7 | ------------- 8 | 9 | npm install coffee-machine 10 | 11 | Usage: 12 | ------ 13 | 14 | A "CoffeeMachine" class is provided that can be used as the basis of your state machine implementation. 15 | The object passed in to the constructor has an expected format that will define the state machine. 16 | The sample stuff below will use a chess game as a basic example. 17 | 18 | Step one will always be to require the state machine: 19 | 20 | {CoffeeMachine} = require 'coffee_machine' 21 | 22 | The CoffeeMachine class' constructor takes in an object that defines the entire state machine. 23 | Here's what it looks like: 24 | 25 | states: 26 | stateName: 27 | active: true/false (optional, the 1st state defaults to true) 28 | onEnter: enterMethod (called when successfully entering the state) 29 | onExit: exitMethod (called when successfully exiting the state) 30 | guard: guardMethod (stops the change to this state if returns false) 31 | stateName2: etc... 32 | events: 33 | eventName: 34 | from: fromState (should be a defined state, or "any") 35 | to: toState (should be a defined state) 36 | eventName2: etc... 37 | onStateChange: changeMethod (called on any state change) 38 | 39 | If you don't need anything fancy on the states, then you can use a basic Array setup: 40 | 41 | game = new CoffeeMachine states: ['whiteToMove', 'blackToMove'] 42 | 43 | game.availableStates() 44 | # outputs: [ 'whiteToMove', 'blackToMove' ] 45 | game.currentState() 46 | # outputs: 'whiteToMove' 47 | 48 | But, you should really define some *events* that will trigger state changes. Each 49 | defined event gives you a method you can call to trigger the state change. 50 | 51 | class ChessGame extends CoffeeMachine 52 | switchSides: -> 53 | # ... 54 | console.log "switchSides called." 55 | 56 | game = new ChessGame 57 | states: 58 | whiteToMove: 59 | onEnter: -> this.switchSides() 60 | blackToMove: 61 | onEnter: -> this.switchSides() 62 | events: 63 | whiteMoved: {from:'whiteToMove', to:'blackToMove'} 64 | blackMoved: {from:'blackToMove', to:'whiteToMove'} 65 | 66 | game.whiteMoved() 67 | # outputs: switchSides called. 68 | 69 | You can also pass the states definition to the defineCoffeeMachine method. So, a more custom 70 | and comprehensive implementation may look like: 71 | 72 | class ChessGame extends CoffeeMachine 73 | constructor: (@board, @pieces) -> 74 | @defineStateMachine 75 | states: 76 | whiteToMove: 77 | # If black was in check, sides can't switch unless they're now not in check 78 | guard: (args) -> not (args.from is 'blackInCheck' and this.blackKingInCheck()) 79 | onEnter: -> this.deliverMessage('white', 'Your move.') 80 | blackToMove: 81 | guard: (args) -> not (args.from is 'whiteInCheck' and this.whiteKingInCheck()) 82 | onEnter: -> this.deliverMessage('black', 'Your move.') 83 | whiteInCheck: 84 | onEnter: -> this.deliverMessage('white', 'Check!') 85 | onExit: -> this.deliverMessage('white', 'Check escaped.') 86 | blackInCheck: 87 | onEnter: -> this.deliverMessage('black', 'Check!') 88 | onExit: -> this.deliverMessage('black', 'Check escaped.') 89 | whiteCheckmated: 90 | onEnter: -> 91 | this.deliverMessage('white', 'Checkmate, you lose :-(') 92 | this.deliverMessage('black', 'Checkmate, you win!') 93 | blackCheckmated: 94 | onEnter: -> 95 | this.deliverMessage('black', 'Checkmate, you lose :-(') 96 | this.deliverMessage('white', 'Checkmate, you win!') 97 | events: 98 | whiteMoved: { from: 'whiteToMove', to: 'blackToMove' } 99 | whiteChecked: { from: ['blackToMove', 'blackInCheck'], to: 'whiteInCheck' } 100 | whiteCheckMated: { from: ['blackToMove', 'blackInCheck'], to: 'whiteCheckmated' } 101 | blackMoved: { from: 'blackToMove', to: 'whiteToMove' } 102 | blackChecked: { from: ['whiteToMove', 'whiteInCheck'], to: 'blackInCheck' } 103 | blackCheckMated: { from: ['whitetoMove', 'whiteInCheck'], to: 'blackCheckmated' } 104 | onStateChange: (args) -> this.logActivity(args.from, args.to, args.event) 105 | 106 | blackKingInCheck: -> 107 | # ... 108 | 109 | whiteKingInCheck: -> 110 | # ... 111 | 112 | deliverMessage: (playerColor, message) -> 113 | console.log "[Message to #{playerColor}] #{message}" 114 | 115 | logActivity: (from, to, event) -> 116 | console.log "Activity: from => #{from}, to => #{to}, event => #{event}" 117 | 118 | ################################## 119 | 120 | game = new ChessGame 121 | 122 | game.whiteMoved() 123 | # outputs: 124 | # [Message to black] Your move. 125 | # Activity: from => whiteToMove, to => blackToMove, event => whiteMoved 126 | 127 | game.blackMoved() 128 | # outputs: 129 | # [Message to white] Your move. 130 | # Activity: from => blackToMove, to => whiteToMove, event => blackMoved 131 | 132 | game.blackChecked() 133 | # outputs: 134 | # [Message to black] Check! 135 | # Activity: from => whiteToMove, to => blackInCheck, event => blackChecked 136 | 137 | game.whiteCheckMated() 138 | # outputs: 139 | # [Message to black] Check escaped. 140 | # [Message to white] Checkmate, you lose :-( 141 | # [Message to black] Checkmate, you win! 142 | # Activity: from => blackInCheck, to => whiteCheckmated, event => whiteCheckMated 143 | 144 | try 145 | game.blackMoved() 146 | catch error 147 | console.log error 148 | # outputs: 149 | # Cannot change from state 'blackToMove'; it is not the active state! 150 | 151 | 152 | Note that each callback method (onEnter, onExit, guard, and onStateChange) gets passed an args object that 153 | has a "from", "to", and "event" key, providing the previous state, new state, and the 154 | event that triggered the state change. 155 | 156 | Tests 157 | ------ 158 | cake test 159 | -------------------------------------------------------------------------------- /test/test_coffee_machine.coffee: -------------------------------------------------------------------------------- 1 | vows = require 'vows' 2 | assert = require 'assert' 3 | {CoffeeMachine} = require '../src/coffee_machine' 4 | 5 | vow = vows.describe('CoffeeMachine') 6 | vow.addBatch 7 | 'State setup using an array': 8 | topic: new CoffeeMachine { 9 | states: ['state1', 'state2', 'state3'] 10 | } 11 | 12 | 'should have available states': (topic) -> 13 | assert.deepEqual topic.availableStates(), ['state1', 'state2', 'state3'] 14 | 15 | 'currentState should be the 1st state': (topic) -> 16 | assert.equal topic.currentState(), 'state1' 17 | 18 | 'changeState should work': (topic) -> 19 | topic.changeState('state1','state2') 20 | assert.equal topic.currentState(), 'state2' 21 | 22 | 'changeState should throw error if trying to change from inactive state': (topic) -> 23 | try 24 | topic.changeState('state1','state3') 25 | catch error 26 | assert.equal topic.currentState(), 'state2' 27 | 28 | vow.addBatch 29 | 'State setup using full object': 30 | topic: new CoffeeMachine 31 | states: 32 | state1: 33 | onEnter: -> 'onEnter state1' 34 | guard: -> 'guard state1' 35 | state2: 36 | onExit: -> 'onExit state2' 37 | state3: 38 | active: true 39 | onEnter: -> 'onEnter state3' 40 | events: 41 | event1: 42 | from: 'state1' 43 | to: 'state2' 44 | 45 | 'should have available states': (topic) -> 46 | assert.deepEqual topic.availableStates(), ['state1', 'state2', 'state3'] 47 | 48 | 'currentState should be state3': (topic) -> 49 | assert.equal topic.currentState(), 'state3' 50 | 51 | 'changeState should work': (topic) -> 52 | topic.changeState('state3','state2') 53 | assert.equal topic.currentState(), 'state2' 54 | 55 | 'changeState should throw error if trying to change from inactive state': (topic) -> 56 | try 57 | topic.changeState('state1','state3') 58 | catch error 59 | assert.equal topic.currentState(), 'state2' 60 | 61 | vow.addBatch 62 | 'onExit': 63 | topic: -> 64 | new CoffeeMachine 65 | states: 66 | state1: 67 | onExit: -> throw 'onExitCalled' 68 | state2: {} 69 | 70 | 'should be setup properly': (topic) -> 71 | assert.isFunction topic.stateMachine.states.state1.onExit 72 | 73 | 'onExit should get called on state change': (topic) -> 74 | try 75 | topic.changeState('state1', 'state2') 76 | catch e 77 | assert.equal e, 'onExitCalled' 78 | 79 | 'onEnter': 80 | topic: -> 81 | new CoffeeMachine 82 | states: 83 | state1: {} 84 | state2: 85 | onEnter: -> throw 'onEnterCalled' 86 | 87 | 'should be setup properly': (topic) -> 88 | assert.isFunction topic.stateMachine.states.state2.onEnter 89 | 90 | 'onEnter should get called on state change': (topic) -> 91 | try 92 | topic.changeState('state1', 'state2') 93 | catch e 94 | assert.equal e, 'onEnterCalled' 95 | 96 | 'guard': 97 | topic: -> 98 | new CoffeeMachine 99 | states: 100 | state1: {} 101 | state2: 102 | guard: -> false 103 | 104 | 'should be setup properly': (topic) -> 105 | assert.isFunction topic.stateMachine.states.state2.guard 106 | 107 | 'state should not change when guard returns false': (topic) -> 108 | topic.changeState('state1', 'state2') 109 | assert.equal topic.currentState(), 'state1' 110 | 111 | 'onStatechange': 112 | topic: -> 113 | new CoffeeMachine 114 | states: 115 | state1: {} 116 | state2: {} 117 | onStateChange: -> throw 'onStateChangeCalled' 118 | 119 | 'should be setup properly': (topic) -> 120 | assert.isFunction topic.stateMachine.onStateChange 121 | 122 | 'onStateChange should get called on state change': (topic) -> 123 | try 124 | topic.changeState('state1', 'state2') 125 | catch e 126 | assert.equal e, 'onStateChangeCalled' 127 | 128 | 'Callbacks should contain state and event info': 129 | topic: -> 130 | new CoffeeMachine 131 | states: 132 | state1: 133 | onExit: (args) -> this.returnedArgs = args 134 | state2: {} 135 | state3: 136 | onEnter: (args) -> this.returnedArgs = args 137 | state4: 138 | guard: (args) -> this.returnedArgs = args 139 | events: 140 | state2to3: {from: 'state2', to: 'state3'} 141 | 142 | 'onExit': (topic) -> 143 | topic.changeState('state1', 'state2') 144 | assert.equal topic.returnedArgs.from, 'state1' 145 | assert.equal topic.returnedArgs.to, 'state2' 146 | 147 | 'onEnter': (topic) -> 148 | topic.state2to3() 149 | assert.equal topic.returnedArgs.from, 'state2' 150 | assert.equal topic.returnedArgs.to, 'state3' 151 | assert.equal topic.returnedArgs.event, 'state2to3' 152 | 153 | 'guard': (topic) -> 154 | topic.changeState('state3', 'state4') 155 | assert.equal topic.returnedArgs.from, 'state3' 156 | assert.equal topic.returnedArgs.to, 'state4' 157 | assert.equal topic.returnedArgs.event, null 158 | 159 | vow.addBatch 160 | 'Events': 161 | topic: -> 162 | new CoffeeMachine 163 | states: ['state1', 'state2', 'state3'] 164 | events: 165 | state1to2: {from:'state1', to:'state2'} 166 | state2to1: {from:'state2', to:'state1'} 167 | anyToState3: {from:'any', to:'state3'} 168 | state2or3toState1: {from:['state2', 'state3'], to:'state1'} 169 | 170 | 'should properly change state': (topic) -> 171 | topic.state1to2() 172 | assert.equal topic.currentState(), 'state2' 173 | topic.state2to1() 174 | assert.equal topic.currentState(), 'state1' 175 | 176 | 'should not change state if the from is different than the defition': (topic) -> 177 | try 178 | topic.state2to1() 179 | assert.equal 1, 2 # hrmmm... don't know how to test the error well 180 | catch e 181 | assert.equal e, "Cannot change from state 'state2'; it is not the active state!" 182 | 183 | 'should change from any state if "any" is the from key': (topic) -> 184 | assert.equal topic.currentState(), 'state1' # We're in state1 185 | topic.anyToState3() 186 | assert.equal topic.currentState(), 'state3' # should change to state3 187 | topic.changeState('state3', 'state2') 188 | assert.equal topic.currentState(), 'state2' # We're now in state2 189 | topic.anyToState3() 190 | assert.equal topic.currentState(), 'state3' # should change to state3 191 | 192 | 'should support an array in the from key': (topic) -> 193 | assert.equal topic.currentState(), 'state3' # We're in state3 194 | topic.state2or3toState1() 195 | assert.equal topic.currentState(), 'state1' 196 | topic.state1to2() 197 | assert.equal topic.currentState(), 'state2' # We're in state2 198 | topic.state2or3toState1() 199 | assert.equal topic.currentState(), 'state1' # We're in state1 200 | try 201 | topic.state2or3toState1() 202 | assert.equal 1, 2 # hrmmm... don't know how to test the error well 203 | catch e 204 | assert.equal e, "Cannot change from states state2 or state3; none are the active state!" 205 | 206 | exports.test_utils = vow --------------------------------------------------------------------------------