From 2bde07e2380c73fe79c18e5227e03acb3f6bdc59 Mon Sep 17 00:00:00 2001 From: taoqf Date: Thu, 28 Jan 2021 09:33:12 +0800 Subject: [PATCH 1/6] fix: issue #11 --- types/state-machine.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/state-machine.d.ts b/types/state-machine.d.ts index faf6f10..4a9a9bb 100644 --- a/types/state-machine.d.ts +++ b/types/state-machine.d.ts @@ -15,8 +15,8 @@ declare class StateMachine { public historyForward(): void; public canHistory(): boolean; public canhistoryForward(): boolean; - protected state: string; - protected history: string[]; + // protected state: string; + // protected history: string[]; } declare namespace StateMachine { From e88096e8e6249779e7105a11f2d0646e7fe2fc6d Mon Sep 17 00:00:00 2001 From: taoqf Date: Thu, 28 Jan 2021 09:34:11 +0800 Subject: [PATCH 2/6] 3.1.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5e817fb..8183c99 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "webpack": "^4.16.5", "webpack-cli": "^3.1.0" }, - "version": "3.1.4", + "version": "3.1.5", "scripts": { "start": "npm run watch", "build": "npm run bundle && npm run minify", From 90075b65d27c0fadfe9eff4eb3968f45c9428f13 Mon Sep 17 00:00:00 2001 From: taoqf Date: Thu, 28 Jan 2021 09:36:53 +0800 Subject: [PATCH 3/6] # 11 --- dist/state-machine-history.min.js | 2 +- dist/state-machine-visualize.min.js | 2 +- dist/state-machine.min.js | 2 +- types/state-machine.d.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dist/state-machine-history.min.js b/dist/state-machine-history.min.js index eb8e81d..f116f2e 100644 --- a/dist/state-machine-history.min.js +++ b/dist/state-machine-history.min.js @@ -1 +1 @@ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.StateMachineHistory=e():t.StateMachineHistory=e()}(this,function(){return o={},r.m=n=[function(t,e,n){"use strict";function r(t){if(0===t.length)return t;var e,n,r=t.split(/[_-]/);if(1===r.length&&r[0][0].toLowerCase()===r[0][0])return t;for(n=r[0].toLowerCase(),e=1;ep&&t[n].shift(),e.transition!==o&&e.transition!==i&&(t[r].length=0))},methods:{},properties:{}};return f.methods[e]=function(){this[n].length=0,this[r].length=0},f.properties[s]={get:function(){return 1p&&t[n].shift(),e.transition!==o&&e.transition!==i&&(t[r].length=0))},methods:{},properties:{}};return t.methods[e]=function(){this[n].length=0,this[r].length=0},t.properties[s]={get:function(){return 1 "+f(t.to)+u.edge.attr(t)+";"}).attr=function(t){var n,r,e,o=Object.keys(t).sort(),i=[];for(n=0,r=o.length;n "+a(t.to)+f.edge.attr(t)+";"},f.edge.attr=function(t){for(var n,r=Object.keys(t).sort(),e=[],o=0,i=r.length;o); public static factory(options: Partial): StateMachine.IFSM; public static factory(instance: T, options: Partial): StateMachine.IFSM | T; - [action: string]: ((...args: any[]) => any) | string | string[]; + [action: string]: ((...args: any[]) => any); public is: StateMachine.StateMachineIs; public can: StateMachine.StateMachineCan; public cannot: StateMachine.StateMachineCan; From 6e5a62492c2fb759a2a52db49f97a978930fbf64 Mon Sep 17 00:00:00 2001 From: taoqf Date: Thu, 28 Jan 2021 09:36:58 +0800 Subject: [PATCH 4/6] 3.1.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8183c99..0416745 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "webpack": "^4.16.5", "webpack-cli": "^3.1.0" }, - "version": "3.1.5", + "version": "3.1.6", "scripts": { "start": "npm run watch", "build": "npm run bundle && npm run minify", From b4f3b0c9dcfe4f755091353593c0133c337ecca6 Mon Sep 17 00:00:00 2001 From: taoqf Date: Thu, 28 Jan 2021 17:06:58 +0800 Subject: [PATCH 5/6] v4.0.0 --- .gitignore | 1 + dist/lib/history.js | 1 - dist/lib/state-machine.js | 1 - dist/lib/visualize.js | 1 - dist/state-machine-history.js | 211 ------- dist/state-machine-history.min.js | 1 - dist/state-machine-visualize.js | 269 -------- dist/state-machine-visualize.min.js | 1 - dist/state-machine.js | 669 -------------------- dist/state-machine.min.js | 1 - package.json | 24 +- src/app.js | 102 --- src/config.js | 161 ----- src/jsm.js | 182 ------ src/plugin.js | 40 -- src/plugin/history.js | 83 --- src/plugin/visualize.js | 161 ----- src/state-machine.ts | 175 ++++++ src/util/camelize.js | 33 - src/util/exception.js | 9 - src/util/mixin.js | 13 - test/basics.js | 156 ++--- test/construction.js | 237 ++----- test/defaults.js | 62 -- test/empty.js | 71 +-- test/errors.js | 209 ++----- test/goto.js | 195 ------ test/helpers/lifecycle_logger.js | 27 - test/introspection.js | 295 ++++----- test/issues.js | 107 +--- test/lifecycle.js | 932 ---------------------------- test/observers.js | 151 ----- test/plugin/history.js | 493 --------------- test/plugin/visualize.js | 443 ------------- test/plugins.js | 151 ----- test/transitions.js | 362 ++--------- test/util/camelize.js | 14 - test/util/mixin.js | 65 -- test/wildcards.js | 366 ++++++----- tsconfig.json | 12 +- types/state-machine.d.ts | 79 --- types/test-commonjs.ts | 60 -- types/test-umd.ts | 19 - webpack.config.js | 2 +- 44 files changed, 761 insertions(+), 5886 deletions(-) delete mode 100644 dist/lib/history.js delete mode 100644 dist/lib/state-machine.js delete mode 100644 dist/lib/visualize.js delete mode 100644 dist/state-machine-history.js delete mode 100644 dist/state-machine-history.min.js delete mode 100644 dist/state-machine-visualize.js delete mode 100644 dist/state-machine-visualize.min.js delete mode 100644 dist/state-machine.js delete mode 100644 dist/state-machine.min.js delete mode 100644 src/app.js delete mode 100644 src/config.js delete mode 100644 src/jsm.js delete mode 100644 src/plugin.js delete mode 100644 src/plugin/history.js delete mode 100644 src/plugin/visualize.js create mode 100644 src/state-machine.ts delete mode 100644 src/util/camelize.js delete mode 100644 src/util/exception.js delete mode 100644 src/util/mixin.js delete mode 100644 test/defaults.js delete mode 100644 test/goto.js delete mode 100644 test/helpers/lifecycle_logger.js delete mode 100644 test/lifecycle.js delete mode 100644 test/observers.js delete mode 100644 test/plugin/history.js delete mode 100644 test/plugin/visualize.js delete mode 100644 test/plugins.js delete mode 100644 test/util/camelize.js delete mode 100644 test/util/mixin.js delete mode 100644 types/state-machine.d.ts delete mode 100644 types/test-commonjs.ts delete mode 100644 types/test-umd.ts diff --git a/.gitignore b/.gitignore index 974b648..3b4f255 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ coverage .nyc_output *.swp yarn.lock +dist diff --git a/dist/lib/history.js b/dist/lib/history.js deleted file mode 100644 index 8c75b0d..0000000 --- a/dist/lib/history.js +++ /dev/null @@ -1 +0,0 @@ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.StateMachineHistory=e():t.StateMachineHistory=e()}(window,(function(){return function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=0)}([function(t,e,n){"use strict";var r=n(1);t.exports=function(t){var e=r((t=t||{}).name||t.past||"history"),n=r(t.future||"future"),o=r.prepended("clear",e),i=r.prepended(e,"back"),u=r.prepended(e,"forward"),s=r.prepended("can",i),f=r.prepended("can",u),p=t.max,c={configure:function(t){t.addTransitionLifecycleNames(i),t.addTransitionLifecycleNames(u)},init:function(t){t[e]=[],t[n]=[]},lifecycle:function(t,r){"onEnterState"===r.event&&(t[e].push(r.to),p&&t[e].length>p&&t[e].shift(),r.transition!==i&&r.transition!==u&&(t[n].length=0))},methods:{},properties:{}};return c.methods[o]=function(){this[e].length=0,this[n].length=0},c.properties[s]={get:function(){return this[e].length>1}},c.properties[f]={get:function(){return this[n].length>0}},c.methods[i]=function(){if(!this[s])throw Error("no history");var t=this[e].pop(),r=this[e].pop();this[n].push(t),this._fsm.transit(i,t,r,[])},c.methods[u]=function(){if(!this[f])throw Error("no history");var t=this.state,e=this[n].pop();this._fsm.transit(u,t,e,[])},c}},function(t,e,n){"use strict";function r(t){if(0===t.length)return t;var e,n,r=t.split(/[_-]/);if(1===r.length&&r[0][0].toLowerCase()===r[0][0])return t;for(n=r[0].toLowerCase(),e=1;e=0:this.state===t},isPending:function(){return this.pending},can:function(t){return!this.isPending()&&!!this.seek(t)},cannot:function(t){return!this.can(t)},allStates:function(){return this.config.allStates()},allTransitions:function(){return this.config.allTransitions()},transitions:function(){return this.config.transitionsFor(this.state)},seek:function(t,n){var i=this.config.defaults.wildcard,e=this.config.transitionFor(this.state,t),s=e&&e.to;return"function"==typeof s?s.apply(this.context,n):s===i?this.state:s},fire:function(t,n){return this.transit(t,this.state,this.seek(t,n),n)},transit:function(t,n,i,e){var s=this.config.lifecycle,r=this.config.options.observeUnchangedState||n!==i;return i?this.isPending()?this.context.onPendingTransition(t,n,i):(this.config.addState(i),this.beginTransit(),e.unshift({transition:t,from:n,to:i,fsm:this.context}),this.observeEvents([this.observersForEvent(s.onBefore.transition),this.observersForEvent(s.onBefore[t]),r?this.observersForEvent(s.onLeave.state):o,r?this.observersForEvent(s.onLeave[n]):o,this.observersForEvent(s.on.transition),r?["doTransit",[this]]:o,r?this.observersForEvent(s.onEnter.state):o,r?this.observersForEvent(s.onEnter[i]):o,r?this.observersForEvent(s.on[i]):o,this.observersForEvent(s.onAfter.transition),this.observersForEvent(s.onAfter[t]),this.observersForEvent(s.on[t])],e)):this.context.onInvalidTransition(t,n,i)},beginTransit:function(){this.pending=!0},endTransit:function(t){return this.pending=!1,t},failTransit:function(t){throw this.pending=!1,t},doTransit:function(t){this.state=t.to},observe:function(t){if(2===t.length){var n={};n[t[0]]=t[1],this.observers.push(n)}else this.observers.push(t[0])},observersForEvent:function(t){for(var n,i=0,e=this.observers.length,s=[];i0&&(a.states=u),f&&f.length>0&&(a.transitions=f),a}function u(t){return" "+t+" "}function f(t){return'"'+t+'"'}function a(t){var n,e,r=(t=t||{}).name||"fsm",o=t.states||[],i=t.transitions||[],u=t.rankdir,s=[];for(s.push("digraph "+f(r)+" {"),u&&s.push(" rankdir="+u+";"),n=0,e=o.length;n "+f(t.to)+a.edge.attr(t)+";"},a.edge.attr=function(t){var n,e,r,o=Object.keys(t).sort(),i=[];for(n=0,e=o.length;n0?" [ "+i.join(" ; ")+" ]":""},o.dotcfg=i,o.dotify=a,t.exports=o},function(t,n,e){"use strict";t.exports=function(t,n){var e,r,o;for(e=1;e max) - instance[past].shift(); - if (lifecycle.transition !== back && lifecycle.transition !== forward) - instance[future].length = 0; - } - }, - - methods: {}, - properties: {} - - } - - plugin.methods[clear] = function() { - this[past].length = 0 - this[future].length = 0 - } - - plugin.properties[canBack] = { - get: function() { - return this[past].length > 1 - } - } - - plugin.properties[canForward] = { - get: function() { - return this[future].length > 0 - } - } - - plugin.methods[back] = function() { - if (!this[canBack]) - throw Error('no history'); - var from = this[past].pop(), - to = this[past].pop(); - this[future].push(from); - this._fsm.transit(back, from, to, []); - } - - plugin.methods[forward] = function() { - if (!this[canForward]) - throw Error('no history'); - var from = this.state, - to = this[future].pop(); - this._fsm.transit(forward, from, to, []); - } - - return plugin; - -} - - -/***/ }) -/******/ ]); -}); \ No newline at end of file diff --git a/dist/state-machine-history.min.js b/dist/state-machine-history.min.js deleted file mode 100644 index f116f2e..0000000 --- a/dist/state-machine-history.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.StateMachineHistory=e():t.StateMachineHistory=e()}(this,function(){return o={},r.m=n=[function(t,e,n){"use strict";function r(t){if(0===t.length)return t;var e,n,r=t.split(/[_-]/);if(1===r.length&&r[0][0].toLowerCase()===r[0][0])return t;for(n=r[0].toLowerCase(),e=1;ep&&t[n].shift(),e.transition!==o&&e.transition!==i&&(t[r].length=0))},methods:{},properties:{}};return t.methods[e]=function(){this[n].length=0,this[r].length=0},t.properties[s]={get:function(){return 1 0) - result.states = states - - if (transitions && transitions.length > 0) - result.transitions = transitions - - return result -} - -//------------------------------------------------------------------------------------------------- - -dotcfg.fetch = function(fsm) { - return (typeof fsm === 'function') ? fsm.prototype._fsm.config - : fsm._fsm.config -} - -dotcfg.rankdir = function(orientation) { - if (orientation === 'horizontal') - return 'LR'; - else if (orientation === 'vertical') - return 'TB'; -} - -dotcfg.states = function(config, options) { - var index, states = config.states; - if (!options.init) { // if not showing init transition, then slice out the implied init :from state - index = states.indexOf(config.init.from); - states = states.slice(0, index).concat(states.slice(index+1)); - } - return states; -} - -dotcfg.transitions = function(config, options) { - var n, max, transition, - init = config.init, - transitions = config.options.transitions || [], // easier to visualize using the ORIGINAL transition declarations rather than our run-time mapping - output = []; - if (options.init && init.active) - dotcfg.transition(init.name, init.from, init.to, init.dot, config, options, output) - for (n = 0, max = transitions.length ; n < max ; n++) { - transition = config.options.transitions[n] - dotcfg.transition(transition.name, transition.from, transition.to, transition.dot, config, options, output) - } - return output -} - -dotcfg.transition = function(name, from, to, dot, config, options, output) { - var n, max, wildcard = config.defaults.wildcard - - if (Array.isArray(from)) { - for(n = 0, max = from.length ; n < max ; n++) - dotcfg.transition(name, from[n], to, dot, config, options, output) - } - else if (from === wildcard || from === undefined) { - for(n = 0, max = config.states.length ; n < max ; n++) - dotcfg.transition(name, config.states[n], to, dot, config, options, output) - } - else if (to === wildcard || to === undefined) { - dotcfg.transition(name, from, from, dot, config, options, output) - } - else if (typeof to === 'function') { - // do nothing, can't display conditional transition - } - else { - output.push(mixin({}, { from: from, to: to, label: pad(name) }, dot || {})) - } - -} - -//------------------------------------------------------------------------------------------------- - -function pad(name) { - return " " + name + " " -} - -function quote(name) { - return "\"" + name + "\"" -} - -function dotify(dotcfg) { - - dotcfg = dotcfg || {}; - - var name = dotcfg.name || 'fsm', - states = dotcfg.states || [], - transitions = dotcfg.transitions || [], - rankdir = dotcfg.rankdir, - output = [], - n, max; - - output.push("digraph " + quote(name) + " {") - if (rankdir) - output.push(" rankdir=" + rankdir + ";") - for(n = 0, max = states.length ; n < max ; n++) - output.push(dotify.state(states[n])) - for(n = 0, max = transitions.length ; n < max ; n++) - output.push(dotify.edge(transitions[n])) - output.push("}") - return output.join("\n") - -} - -dotify.state = function(state) { - return " " + quote(state) + ";" -} - -dotify.edge = function(edge) { - return " " + quote(edge.from) + " -> " + quote(edge.to) + dotify.edge.attr(edge) + ";" -} - -dotify.edge.attr = function(edge) { - var n, max, key, keys = Object.keys(edge).sort(), output = []; - for(n = 0, max = keys.length ; n < max ; n++) { - key = keys[n]; - if (key !== 'from' && key !== 'to') - output.push(key + "=" + quote(edge[key])) - } - return output.length > 0 ? " [ " + output.join(" ; ") + " ]" : "" -} - -//------------------------------------------------------------------------------------------------- - -visualize.dotcfg = dotcfg; -visualize.dotify = dotify; - -//------------------------------------------------------------------------------------------------- - -module.exports = visualize; - -//------------------------------------------------------------------------------------------------- - - -/***/ }) -/******/ ]); -}); \ No newline at end of file diff --git a/dist/state-machine-visualize.min.js b/dist/state-machine-visualize.min.js deleted file mode 100644 index a8957cc..0000000 --- a/dist/state-machine-visualize.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(t,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?exports.StateMachineVisualize=n():t.StateMachineVisualize=n()}(this,function(){return o={},e.m=r=[function(t,n,r){"use strict";t.exports=function(t,n){for(var r,e,o=1;o "+a(t.to)+f.edge.attr(t)+";"},f.edge.attr=function(t){for(var n,r=Object.keys(t).sort(),e=[],o=0,i=r.length;o= 0) : (this.state === state); - }, - - isPending: function() { - return this.pending; - }, - - can: function(transition) { - return !this.isPending() && !!this.seek(transition); - }, - - cannot: function(transition) { - return !this.can(transition); - }, - - allStates: function() { - return this.config.allStates(); - }, - - allTransitions: function() { - return this.config.allTransitions(); - }, - - transitions: function() { - return this.config.transitionsFor(this.state); - }, - - seek: function(transition, args) { - var wildcard = this.config.defaults.wildcard, - entry = this.config.transitionFor(this.state, transition), - to = entry && entry.to; - if (typeof to === 'function') - return to.apply(this.context, args); - else if (to === wildcard) - return this.state - else - return to - }, - - fire: function(transition, args) { - return this.transit(transition, this.state, this.seek(transition, args), args); - }, - - transit: function(transition, from, to, args) { - - var lifecycle = this.config.lifecycle, - changed = this.config.options.observeUnchangedState || (from !== to); - - if (!to) - return this.context.onInvalidTransition(transition, from, to); - - if (this.isPending()) - return this.context.onPendingTransition(transition, from, to); - - this.config.addState(to); // might need to add this state if it's unknown (e.g. conditional transition or goto) - - this.beginTransit(); - - args.unshift({ // this context will be passed to each lifecycle event observer - transition: transition, - from: from, - to: to, - fsm: this.context - }); - - return this.observeEvents([ - this.observersForEvent(lifecycle.onBefore.transition), - this.observersForEvent(lifecycle.onBefore[transition]), - changed ? this.observersForEvent(lifecycle.onLeave.state) : UNOBSERVED, - changed ? this.observersForEvent(lifecycle.onLeave[from]) : UNOBSERVED, - this.observersForEvent(lifecycle.on.transition), - changed ? [ 'doTransit', [ this ] ] : UNOBSERVED, - changed ? this.observersForEvent(lifecycle.onEnter.state) : UNOBSERVED, - changed ? this.observersForEvent(lifecycle.onEnter[to]) : UNOBSERVED, - changed ? this.observersForEvent(lifecycle.on[to]) : UNOBSERVED, - this.observersForEvent(lifecycle.onAfter.transition), - this.observersForEvent(lifecycle.onAfter[transition]), - this.observersForEvent(lifecycle.on[transition]) - ], args); - }, - - beginTransit: function() { this.pending = true; }, - endTransit: function(result) { this.pending = false; return result; }, - failTransit: function(result) { this.pending = false; throw result; }, - doTransit: function(lifecycle) { this.state = lifecycle.to; }, - - observe: function(args) { - if (args.length === 2) { - var observer = {}; - observer[args[0]] = args[1]; - this.observers.push(observer); - } - else { - this.observers.push(args[0]); - } - }, - - observersForEvent: function(event) { // TODO: this could be cached - var n = 0, max = this.observers.length, observer, result = []; - for( ; n < max ; n++) { - observer = this.observers[n]; - if (observer[event]) - result.push(observer); - } - return [ event, result, true ] - }, - - observeEvents: function(events, args, previousEvent, previousResult) { - if (events.length === 0) { - return this.endTransit(previousResult === undefined ? true : previousResult); - } - - var event = events[0][0], - observers = events[0][1], - pluggable = events[0][2]; - - args[0].event = event; - if (event && pluggable && event !== previousEvent) - plugin.hook(this, 'lifecycle', args); - - if (observers.length === 0) { - events.shift(); - return this.observeEvents(events, args, event, previousResult); - } - else { - var observer = observers.shift(), - result = observer[event].apply(observer, args); - if (result && typeof result.then === 'function') { - return result.then(this.observeEvents.bind(this, events, args, event)) - .catch(this.failTransit.bind(this)) - } - else if (result === false) { - return this.endTransit(false); - } - else { - return this.observeEvents(events, args, event, result); - } - } - }, - - onInvalidTransition: function(transition, from, to) { - throw new Exception("transition is invalid in current state", transition, from, to, this.state); - }, - - onPendingTransition: function(transition, from, to) { - throw new Exception("transition is invalid while previous transition is still in progress", transition, from, to, this.state); - } - -}); - -//------------------------------------------------------------------------------------------------- - -module.exports = JSM; - -//------------------------------------------------------------------------------------------------- - - -/***/ }), -/* 5 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -//----------------------------------------------------------------------------------------------- - -var mixin = __webpack_require__(0), - camelize = __webpack_require__(2), - plugin = __webpack_require__(1), - Config = __webpack_require__(3), - JSM = __webpack_require__(4); - -//----------------------------------------------------------------------------------------------- - -var PublicMethods = { - is: function(state) { return this._fsm.is(state) }, - can: function(transition) { return this._fsm.can(transition) }, - cannot: function(transition) { return this._fsm.cannot(transition) }, - observe: function() { return this._fsm.observe(arguments) }, - transitions: function() { return this._fsm.transitions() }, - allTransitions: function() { return this._fsm.allTransitions() }, - allStates: function() { return this._fsm.allStates() }, - onInvalidTransition: function(t, from, to) { return this._fsm.onInvalidTransition(t, from, to) }, - onPendingTransition: function(t, from, to) { return this._fsm.onPendingTransition(t, from, to) }, -} - -var PublicProperties = { - state: { - configurable: false, - enumerable: true, - get: function() { - return this._fsm.state; - }, - set: function(state) { - throw Error('use transitions to change state') - } - } -} - -//----------------------------------------------------------------------------------------------- - -function StateMachine(options) { - return apply(this || {}, options); -} - -function factory() { - var cstor, options; - if (typeof arguments[0] === 'function') { - cstor = arguments[0]; - options = arguments[1] || {}; - } - else { - cstor = function() { this._fsm.apply(this, arguments) }; - options = arguments[0] || {}; - } - var config = new Config(options, StateMachine); - build(cstor.prototype, config); - cstor.prototype._fsm.config = config; // convenience access to shared config without needing an instance - return cstor; -} - -//------------------------------------------------------------------------------------------------- - -function apply(instance, options) { - var config = new Config(options, StateMachine); - build(instance, config); - instance._fsm(); - return instance; -} - -function build(target, config) { - if ((typeof target !== 'object') || Array.isArray(target)) - throw Error('StateMachine can only be applied to objects'); - plugin.build(target, config); - Object.defineProperties(target, PublicProperties); - mixin(target, PublicMethods); - mixin(target, config.methods); - config.allTransitions().forEach(function(transition) { - target[camelize(transition)] = function() { - return this._fsm.fire(transition, [].slice.call(arguments)) - } - }); - target._fsm = function() { - this._fsm = new JSM(this, config); - this._fsm.init(arguments); - } -} - -//----------------------------------------------------------------------------------------------- - -StateMachine.version = '3.0.1'; -StateMachine.factory = factory; -StateMachine.apply = apply; -StateMachine.defaults = { - wildcard: '*', - init: { - name: 'init', - from: 'none' - } -} - -//=============================================================================================== - -module.exports = StateMachine; - - -/***/ }), -/* 6 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -module.exports = function(message, transition, from, to, current) { - this.message = message; - this.transition = transition; - this.from = from; - this.to = to; - this.current = current; -} - - -/***/ }) -/******/ ]); -}); \ No newline at end of file diff --git a/dist/state-machine.min.js b/dist/state-machine.min.js deleted file mode 100644 index 5cb1b81..0000000 --- a/dist/state-machine.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(t,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?exports.StateMachine=n():t.StateMachine=n()}(this,function(){return s={},e.m=i=[function(t,n,i){"use strict";t.exports=function(t,n){for(var i,e,s=1;s= 0) : (this.state === state); - }, - - isPending: function() { - return this.pending; - }, - - can: function(transition) { - return !this.isPending() && !!this.seek(transition); - }, - - cannot: function(transition) { - return !this.can(transition); - }, - - allStates: function() { - return this.config.allStates(); - }, - - allTransitions: function() { - return this.config.allTransitions(); - }, - - transitions: function() { - return this.config.transitionsFor(this.state); - }, - - seek: function(transition, args) { - var wildcard = this.config.defaults.wildcard, - entry = this.config.transitionFor(this.state, transition), - to = entry && entry.to; - if (typeof to === 'function') - return to.apply(this.context, args); - else if (to === wildcard) - return this.state - else - return to - }, - - fire: function(transition, args) { - return this.transit(transition, this.state, this.seek(transition, args), args); - }, - - transit: function(transition, from, to, args) { - - var lifecycle = this.config.lifecycle, - changed = this.config.options.observeUnchangedState || (from !== to); - - if (!to) - return this.context.onInvalidTransition(transition, from, to); - - if (this.isPending()) - return this.context.onPendingTransition(transition, from, to); - - this.config.addState(to); // might need to add this state if it's unknown (e.g. conditional transition or goto) - - this.beginTransit(); - - args.unshift({ // this context will be passed to each lifecycle event observer - transition: transition, - from: from, - to: to, - fsm: this.context - }); - - return this.observeEvents([ - this.observersForEvent(lifecycle.onBefore.transition), - this.observersForEvent(lifecycle.onBefore[transition]), - changed ? this.observersForEvent(lifecycle.onLeave.state) : UNOBSERVED, - changed ? this.observersForEvent(lifecycle.onLeave[from]) : UNOBSERVED, - this.observersForEvent(lifecycle.on.transition), - changed ? [ 'doTransit', [ this ] ] : UNOBSERVED, - changed ? this.observersForEvent(lifecycle.onEnter.state) : UNOBSERVED, - changed ? this.observersForEvent(lifecycle.onEnter[to]) : UNOBSERVED, - changed ? this.observersForEvent(lifecycle.on[to]) : UNOBSERVED, - this.observersForEvent(lifecycle.onAfter.transition), - this.observersForEvent(lifecycle.onAfter[transition]), - this.observersForEvent(lifecycle.on[transition]) - ], args); - }, - - beginTransit: function() { this.pending = true; }, - endTransit: function(result) { this.pending = false; return result; }, - failTransit: function(result) { this.pending = false; throw result; }, - doTransit: function(lifecycle) { this.state = lifecycle.to; }, - - observe: function(args) { - if (args.length === 2) { - var observer = {}; - observer[args[0]] = args[1]; - this.observers.push(observer); - } - else { - this.observers.push(args[0]); - } - }, - - observersForEvent: function(event) { // TODO: this could be cached - var n = 0, max = this.observers.length, observer, result = []; - for( ; n < max ; n++) { - observer = this.observers[n]; - if (observer[event]) - result.push(observer); - } - return [ event, result, true ] - }, - - observeEvents: function(events, args, previousEvent, previousResult) { - if (events.length === 0) { - return this.endTransit(previousResult === undefined ? true : previousResult); - } - - var event = events[0][0], - observers = events[0][1], - pluggable = events[0][2]; - - args[0].event = event; - if (event && pluggable && event !== previousEvent) - plugin.hook(this, 'lifecycle', args); - - if (observers.length === 0) { - events.shift(); - return this.observeEvents(events, args, event, previousResult); - } - else { - var observer = observers.shift(), - result = observer[event].apply(observer, args); - if (result && typeof result.then === 'function') { - return result.then(this.observeEvents.bind(this, events, args, event)) - .catch(this.failTransit.bind(this)) - } - else if (result === false) { - return this.endTransit(false); - } - else { - return this.observeEvents(events, args, event, result); - } - } - }, - - onInvalidTransition: function(transition, from, to) { - throw new Exception("transition is invalid in current state", transition, from, to, this.state); - }, - - onPendingTransition: function(transition, from, to) { - throw new Exception("transition is invalid while previous transition is still in progress", transition, from, to, this.state); - } - -}); - -//------------------------------------------------------------------------------------------------- - -module.exports = JSM; - -//------------------------------------------------------------------------------------------------- diff --git a/src/plugin.js b/src/plugin.js deleted file mode 100644 index 491e25a..0000000 --- a/src/plugin.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict' - -//------------------------------------------------------------------------------------------------- - -var mixin = require('./util/mixin'); - -//------------------------------------------------------------------------------------------------- - -module.exports = { - - build: function(target, config) { - var n, max, plugin, plugins = config.plugins; - for(n = 0, max = plugins.length ; n < max ; n++) { - plugin = plugins[n]; - if (plugin.methods) - mixin(target, plugin.methods); - if (plugin.properties) - Object.defineProperties(target, plugin.properties); - } - }, - - hook: function(fsm, name, additional) { - var n, max, method, plugin, - plugins = fsm.config.plugins, - args = [fsm.context]; - - if (additional) - args = args.concat(additional) - - for(n = 0, max = plugins.length ; n < max ; n++) { - plugin = plugins[n] - method = plugins[n][name] - if (method) - method.apply(plugin, args); - } - } - -} - -//------------------------------------------------------------------------------------------------- diff --git a/src/plugin/history.js b/src/plugin/history.js deleted file mode 100644 index 46799c0..0000000 --- a/src/plugin/history.js +++ /dev/null @@ -1,83 +0,0 @@ -'use strict' - -//------------------------------------------------------------------------------------------------- - -var camelize = require('../util/camelize'); - -//------------------------------------------------------------------------------------------------- - -module.exports = function(options) { options = options || {}; - - var past = camelize(options.name || options.past || 'history'), - future = camelize( options.future || 'future'), - clear = camelize.prepended('clear', past), - back = camelize.prepended(past, 'back'), - forward = camelize.prepended(past, 'forward'), - canBack = camelize.prepended('can', back), - canForward = camelize.prepended('can', forward), - max = options.max; - - var plugin = { - - configure: function(config) { - config.addTransitionLifecycleNames(back); - config.addTransitionLifecycleNames(forward); - }, - - init: function(instance) { - instance[past] = []; - instance[future] = []; - }, - - lifecycle: function(instance, lifecycle) { - if (lifecycle.event === 'onEnterState') { - instance[past].push(lifecycle.to); - if (max && instance[past].length > max) - instance[past].shift(); - if (lifecycle.transition !== back && lifecycle.transition !== forward) - instance[future].length = 0; - } - }, - - methods: {}, - properties: {} - - } - - plugin.methods[clear] = function() { - this[past].length = 0 - this[future].length = 0 - } - - plugin.properties[canBack] = { - get: function() { - return this[past].length > 1 - } - } - - plugin.properties[canForward] = { - get: function() { - return this[future].length > 0 - } - } - - plugin.methods[back] = function() { - if (!this[canBack]) - throw Error('no history'); - var from = this[past].pop(), - to = this[past].pop(); - this[future].push(from); - this._fsm.transit(back, from, to, []); - } - - plugin.methods[forward] = function() { - if (!this[canForward]) - throw Error('no history'); - var from = this.state, - to = this[future].pop(); - this._fsm.transit(forward, from, to, []); - } - - return plugin; - -} diff --git a/src/plugin/visualize.js b/src/plugin/visualize.js deleted file mode 100644 index 0bee32b..0000000 --- a/src/plugin/visualize.js +++ /dev/null @@ -1,161 +0,0 @@ -'use strict' - -//------------------------------------------------------------------------------------------------- - -var mixin = require('../util/mixin') - -//------------------------------------------------------------------------------------------------- - -function visualize(fsm, options) { - return dotify(dotcfg(fsm, options)); -} - -//------------------------------------------------------------------------------------------------- - -function dotcfg(fsm, options) { - - options = options || {} - - var config = dotcfg.fetch(fsm), - name = options.name, - rankdir = dotcfg.rankdir(options.orientation), - states = dotcfg.states(config, options), - transitions = dotcfg.transitions(config, options), - result = { } - - if (name) - result.name = name - - if (rankdir) - result.rankdir = rankdir - - if (states && states.length > 0) - result.states = states - - if (transitions && transitions.length > 0) - result.transitions = transitions - - return result -} - -//------------------------------------------------------------------------------------------------- - -dotcfg.fetch = function(fsm) { - return (typeof fsm === 'function') ? fsm.prototype._fsm.config - : fsm._fsm.config -} - -dotcfg.rankdir = function(orientation) { - if (orientation === 'horizontal') - return 'LR'; - else if (orientation === 'vertical') - return 'TB'; -} - -dotcfg.states = function(config, options) { - var index, states = config.states; - if (!options.init) { // if not showing init transition, then slice out the implied init :from state - index = states.indexOf(config.init.from); - states = states.slice(0, index).concat(states.slice(index+1)); - } - return states; -} - -dotcfg.transitions = function(config, options) { - var n, max, transition, - init = config.init, - transitions = config.options.transitions || [], // easier to visualize using the ORIGINAL transition declarations rather than our run-time mapping - output = []; - if (options.init && init.active) - dotcfg.transition(init.name, init.from, init.to, init.dot, config, options, output) - for (n = 0, max = transitions.length ; n < max ; n++) { - transition = config.options.transitions[n] - dotcfg.transition(transition.name, transition.from, transition.to, transition.dot, config, options, output) - } - return output -} - -dotcfg.transition = function(name, from, to, dot, config, options, output) { - var n, max, wildcard = config.defaults.wildcard - - if (Array.isArray(from)) { - for(n = 0, max = from.length ; n < max ; n++) - dotcfg.transition(name, from[n], to, dot, config, options, output) - } - else if (from === wildcard || from === undefined) { - for(n = 0, max = config.states.length ; n < max ; n++) - dotcfg.transition(name, config.states[n], to, dot, config, options, output) - } - else if (to === wildcard || to === undefined) { - dotcfg.transition(name, from, from, dot, config, options, output) - } - else if (typeof to === 'function') { - // do nothing, can't display conditional transition - } - else { - output.push(mixin({}, { from: from, to: to, label: pad(name) }, dot || {})) - } - -} - -//------------------------------------------------------------------------------------------------- - -function pad(name) { - return " " + name + " " -} - -function quote(name) { - return "\"" + name + "\"" -} - -function dotify(dotcfg) { - - dotcfg = dotcfg || {}; - - var name = dotcfg.name || 'fsm', - states = dotcfg.states || [], - transitions = dotcfg.transitions || [], - rankdir = dotcfg.rankdir, - output = [], - n, max; - - output.push("digraph " + quote(name) + " {") - if (rankdir) - output.push(" rankdir=" + rankdir + ";") - for(n = 0, max = states.length ; n < max ; n++) - output.push(dotify.state(states[n])) - for(n = 0, max = transitions.length ; n < max ; n++) - output.push(dotify.edge(transitions[n])) - output.push("}") - return output.join("\n") - -} - -dotify.state = function(state) { - return " " + quote(state) + ";" -} - -dotify.edge = function(edge) { - return " " + quote(edge.from) + " -> " + quote(edge.to) + dotify.edge.attr(edge) + ";" -} - -dotify.edge.attr = function(edge) { - var n, max, key, keys = Object.keys(edge).sort(), output = []; - for(n = 0, max = keys.length ; n < max ; n++) { - key = keys[n]; - if (key !== 'from' && key !== 'to') - output.push(key + "=" + quote(edge[key])) - } - return output.length > 0 ? " [ " + output.join(" ; ") + " ]" : "" -} - -//------------------------------------------------------------------------------------------------- - -visualize.dotcfg = dotcfg; -visualize.dotify = dotify; - -//------------------------------------------------------------------------------------------------- - -module.exports = visualize; - -//------------------------------------------------------------------------------------------------- diff --git a/src/state-machine.ts b/src/state-machine.ts new file mode 100644 index 0000000..e15b7cd --- /dev/null +++ b/src/state-machine.ts @@ -0,0 +1,175 @@ +export interface Transitions { + [transition: string]: { + from: '*' | string | string[]; + to: string; + } +} + +export interface State { + [transition: string]: { + from: string; + to: string; + } +} + +export interface Callbacks { + onPendingTransition: CallbackWithNoResult; + onBefore: Callback; + onLeave: Callback; + on: Callback; + onEnter: Callback; + onAfter: Callback; +} + +type Callback = (transition: T, from: string, to: string, ...args: unknown[]) => boolean | Promise; +type CallbackWithNoResult = (transition: T, from: string, to: string, ...args: unknown[]) => unknown; + +const wildcard = '*'; + +export default class StateMachine { + public async fire(transition: T, ...args: unknown[]) { + + const from = this._state; + const to = this.seek(transition); + const changed = (from !== to); + + if (this.isPending()) { + return this.callbacks.onPendingTransition(transition, from, to); + } + + this.pending = true; + + try { + let flag = true; + flag = await this.callbacks.onBefore(transition, from, to, ...args); + if (flag === false) { + return; + } + if (changed) { + flag = await this.callbacks.onLeave(transition, from, to, ...args); + if (flag === false) { + return; + } + } + flag = await this.callbacks.on(transition, from, to, ...args); + if (flag === false) { + return; + } + if (changed) { + this._state = to; + flag = await this.callbacks.onEnter(transition, from, to, ...args); + if (flag === false) { + this._state = from; + return; + } + } + flag = await this.callbacks.onAfter(transition, from, to, ...args); + if (flag === false) { + this._state = from; + return; + } + } catch (error) { + this._state = from; + throw error; + } finally { + this.pending = false; + } + } + public is(state: string | string[]) { + return Array.isArray(state) ? (state.indexOf(this._state) >= 0) : (this._state === state); + } + public isPending() { + return this.pending; + } + public can(transition: T | string) { + return !this.isPending() && !!this.seek(transition); + } + public cannot(transition: T | string) { + return !this.can(transition); + } + public allStates() { + return Array.from(Object.keys(this._transitions).reduce((pre, cur) => { + const trans = this._transitions[cur]; + if (Array.isArray(trans.from)) { + trans.from.forEach((it) => { + pre.add(it); + }); + } else { + pre.add(trans.from); + } + pre.add(trans.to); + return pre; + }, new Set())); + } + public allTransitions() { + return Object.keys(this._transitions) as T[]; + } + public transitions() { + const state = this._state; + return Object.keys(this.getTransMap(state)).concat(Object.keys(this.getTransMap(wildcard))); + } + public static factory(init: string, transitions: P, callbacks = {} as Partial>) { + return () => { + return new StateMachine(init, transitions, callbacks); + }; + } + public get state() { + return this._state; + } + public constructor(init: string, private _transitions = {} as P, cbs = {} as Partial>) { + this._state = init; + this.callbacks = { + onPendingTransition() { throw new Error('transition is invalid while previous transition is still in progress'); }, + onBefore() { return true; }, + onLeave() { return true; }, + on() { return true; }, + onEnter() { return true; }, + onAfter() { return true; }, + ...cbs + }; + for (const action in this._transitions) { + if (Object.prototype.hasOwnProperty.call(this._transitions, action)) { + const transition = this._transitions[action]; + const from = (() => { + if (!transition.from) { + return [wildcard]; + } + if (Array.isArray(transition.from)) { + return transition.from; + } + return [transition.from]; + })(); + const to = transition.to || wildcard; + from.forEach((it) => { + this.getTransMap(it)[action] = { + from: it, + to + }; + }) + } + } + } + private seek(transition: T | string) { + const entry = this.transitionFor(this._state, transition); + const to = entry && entry.to; + if (to === wildcard) { + return this._state; + } else { + return to; + } + } + private transitionFor(state: string, transition: T | string) { + return this.getTransMap(state)[transition as string] || + this.getTransMap(wildcard)[transition as string]; + } + private getTransMap(state: string) { + if (!this.map.has(state)) { + this.map.set(state, {}); + } + return this.map.get(state)!; + } + private callbacks = {} as Callbacks; + private _state: string; + private map = new Map(); + private pending = false; +} diff --git a/src/util/camelize.js b/src/util/camelize.js deleted file mode 100644 index b2645fd..0000000 --- a/src/util/camelize.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict' - -//------------------------------------------------------------------------------------------------- - -function camelize(label) { - - if (label.length === 0) - return label; - - var n, result, word, words = label.split(/[_-]/); - - // single word with first character already lowercase, return untouched - if ((words.length === 1) && (words[0][0].toLowerCase() === words[0][0])) - return label; - - result = words[0].toLowerCase(); - for(n = 1 ; n < words.length ; n++) { - result = result + words[n].charAt(0).toUpperCase() + words[n].substring(1).toLowerCase(); - } - - return result; -} - -//------------------------------------------------------------------------------------------------- - -camelize.prepended = function(prepend, label) { - label = camelize(label); - return prepend + label[0].toUpperCase() + label.substring(1); -} - -//------------------------------------------------------------------------------------------------- - -module.exports = camelize; diff --git a/src/util/exception.js b/src/util/exception.js deleted file mode 100644 index cf62304..0000000 --- a/src/util/exception.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict' - -module.exports = function(message, transition, from, to, current) { - this.message = message; - this.transition = transition; - this.from = from; - this.to = to; - this.current = current; -} diff --git a/src/util/mixin.js b/src/util/mixin.js deleted file mode 100644 index 330664b..0000000 --- a/src/util/mixin.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict' - -module.exports = function(target, sources) { - var n, source, key; - for(n = 1 ; n < arguments.length ; n++) { - source = arguments[n]; - for(key in source) { - if (source.hasOwnProperty(key)) - target[key] = source[key]; - } - } - return target; -} diff --git a/test/basics.js b/test/basics.js index d0f6060..c0901b2 100644 --- a/test/basics.js +++ b/test/basics.js @@ -1,130 +1,46 @@ -import test from 'ava'; -import StateMachine from '../src/app'; - -//------------------------------------------------------------------------------------------------- - -test('version', t => { - t.is(StateMachine.version, '3.0.1'); -}); - -//------------------------------------------------------------------------------------------------- - -test('state machine', t => { - - var fsm = new StateMachine({ - init: 'green', - transitions: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ] - }) - - t.is(fsm.state, 'green') - - fsm.warn(); t.is(fsm.state, 'yellow') - fsm.panic(); t.is(fsm.state, 'red') - fsm.calm(); t.is(fsm.state, 'yellow') - fsm.clear(); t.is(fsm.state, 'green') - -}); - -//----------------------------------------------------------------------------- - -test('state machine factory', t => { - - var Alarm = StateMachine.factory({ - init: 'green', - transitions: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ] - }), - a = new Alarm(), - b = new Alarm(); - - t.is(a.state, 'green') - t.is(b.state, 'green') - - a.warn(); t.is(a.state, 'yellow'); t.is(b.state, 'green') - a.panic(); t.is(a.state, 'red'); t.is(b.state, 'green') - a.calm(); t.is(a.state, 'yellow'); t.is(b.state, 'green') - a.clear(); t.is(a.state, 'green'); t.is(b.state, 'green') - - b.warn(); t.is(a.state, 'green'); t.is(b.state, 'yellow') - b.panic(); t.is(a.state, 'green'); t.is(b.state, 'red') - b.calm(); t.is(a.state, 'green'); t.is(b.state, 'yellow') - b.clear(); t.is(a.state, 'green'); t.is(b.state, 'green') - -}); - -//----------------------------------------------------------------------------- - -test('state machine - applied to existing object', t => { - - var obj = { name: 'alarm' } - - StateMachine.apply(obj, { - init: 'green', - transitions: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ] - }); - - t.is(obj.name, 'alarm'); - t.is(obj.state, 'green'); - - obj.warn(); t.is(obj.state, 'yellow') - obj.panic(); t.is(obj.state, 'red') - obj.calm(); t.is(obj.state, 'yellow') - obj.clear(); t.is(obj.state, 'green') - +import test from 'ava'; +import StateMachine from '../dist/state-machine'; + +test('state machine', async (t) => { + var fsm = new StateMachine('green', { + warn: { from: 'green', to: 'yellow' }, + panic: { from: 'yellow', to: 'red' }, + calm: { from: 'red', to: 'yellow' }, + clear: { from: 'yellow', to: 'green' } + }); + + t.is(fsm.state, 'green') + + await fsm.fire('warn'); t.is(fsm.state, 'yellow'); + await fsm.fire('panic'); t.is(fsm.state, 'red'); + await fsm.fire('calm'); t.is(fsm.state, 'yellow'); + await fsm.fire('clear'); t.is(fsm.state, 'green'); }); //----------------------------------------------------------------------------- -test('state machine factory - applied to existing class', t => { - - function Alarm(name) { - this.name = name - this._fsm(); // manual step needed to construct this FSM instance - } +test('state machine factory', async (t) => { - StateMachine.factory(Alarm, { - init: 'green', - transitions: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ] - }); + var Alarm = StateMachine.factory('green', { + warn: { from: 'green', to: 'yellow' }, + panic: { from: 'yellow', to: 'red' }, + calm: { from: 'red', to: 'yellow' }, + clear: { from: 'yellow', to: 'green' } + }), + a = Alarm(), + b = Alarm(); - var a = new Alarm('A'), - b = new Alarm('B'); + t.is(a.state, 'green') + t.is(b.state, 'green') - t.is(a.name, 'A') - t.is(b.name, 'B') + await a.fire('warn'); t.is(a.state, 'yellow'); t.is(b.state, 'green') + await a.fire('panic'); t.is(a.state, 'red'); t.is(b.state, 'green') + await a.fire('calm'); t.is(a.state, 'yellow'); t.is(b.state, 'green') + await a.fire('clear'); t.is(a.state, 'green'); t.is(b.state, 'green') - t.is(a.state, 'green') - t.is(b.state, 'green') - - a.warn(); t.is(a.state, 'yellow'); t.is(b.state, 'green') - a.panic(); t.is(a.state, 'red'); t.is(b.state, 'green') - a.calm(); t.is(a.state, 'yellow'); t.is(b.state, 'green') - a.clear(); t.is(a.state, 'green'); t.is(b.state, 'green') - - b.warn(); t.is(a.state, 'green'); t.is(b.state, 'yellow') - b.panic(); t.is(a.state, 'green'); t.is(b.state, 'red') - b.calm(); t.is(a.state, 'green'); t.is(b.state, 'yellow') - b.clear(); t.is(a.state, 'green'); t.is(b.state, 'green') + await b.fire('warn'); t.is(a.state, 'green'); t.is(b.state, 'yellow') + await b.fire('panic'); t.is(a.state, 'green'); t.is(b.state, 'red') + await b.fire('calm'); t.is(a.state, 'green'); t.is(b.state, 'yellow') + await b.fire('clear'); t.is(a.state, 'green'); t.is(b.state, 'green') }); - -//----------------------------------------------------------------------------- diff --git a/test/construction.js b/test/construction.js index 911b5a3..b1813f7 100644 --- a/test/construction.js +++ b/test/construction.js @@ -1,23 +1,20 @@ -import test from 'ava' -import StateMachine from '../src/app' +import test from 'ava' +import StateMachine from '../dist/state-machine'; //------------------------------------------------------------------------------------------------- test('singleton construction', t => { - var fsm = new StateMachine({ - transitions: [ - { name: 'init', from: 'none', to: 'A' }, - { name: 'step1', from: 'A', to: 'B' }, - { name: 'step2', from: 'B', to: 'C' } - ] - }); + var fsm = new StateMachine('none', { + init: { from: 'none', to: 'A' }, + step1: { from: 'A', to: 'B' }, + step2: { from: 'B', to: 'C' } + }); - t.is(fsm.state, 'none') + t.is(fsm.state, 'none') - t.deepEqual(fsm.allStates(), [ 'none', 'A', 'B', 'C' ]) - t.deepEqual(fsm.allTransitions(), [ 'init', 'step1', 'step2' ]) - t.deepEqual(fsm.transitions(), [ 'init' ]) + t.deepEqual(fsm.allStates(), ['none', 'A', 'B', 'C']) + t.deepEqual(fsm.allTransitions(), ['init', 'step1', 'step2']) }) @@ -26,204 +23,50 @@ test('singleton construction', t => { test('singleton construction - with init state', t => { - var fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step1', from: 'A', to: 'B' }, - { name: 'step2', from: 'B', to: 'C' } - ] - }); + var fsm = new StateMachine('A', { + step1: { from: 'A', to: 'B' }, + step2: { from: 'B', to: 'C' } + }); - t.is(fsm.state, 'A') + t.is(fsm.state, 'A') - t.deepEqual(fsm.allStates(), [ 'none', 'A', 'B', 'C' ]) - t.deepEqual(fsm.allTransitions(), [ 'init', 'step1', 'step2' ]) - t.deepEqual(fsm.transitions(), [ 'step1' ]) + t.deepEqual(fsm.allStates(), ['A', 'B', 'C']) + t.deepEqual(fsm.allTransitions(), ['step1', 'step2']) }) //------------------------------------------------------------------------------------------------- -test('singleton construction - with init state and transition', t => { +test('factory construction', async (t) => { - var fsm = new StateMachine({ - init: { name: 'boot', to: 'A' }, - transitions: [ - { name: 'step1', from: 'A', to: 'B' }, - { name: 'step2', from: 'B', to: 'C' } - ] - }); + var MyClass = StateMachine.factory('A', { + step1: { from: 'A', to: 'B' }, + step2: { from: 'B', to: 'C' } + }); - t.is(fsm.state, 'A') + var fsm1 = MyClass(), + fsm2 = MyClass(), + fsm3 = MyClass(); - t.deepEqual(fsm.allStates(), [ 'none', 'A', 'B', 'C' ]) - t.deepEqual(fsm.allTransitions(), [ 'boot', 'step1', 'step2' ]) - t.deepEqual(fsm.transitions(), [ 'step1' ]) + await fsm2.fire('step1'); + await fsm3.fire('step1'); + await fsm3.fire('step2'); -}) - -//------------------------------------------------------------------------------------------------- - -test('singleton construction - with init state, transition, AND from state', t => { - - var fsm = new StateMachine({ - init: { name: 'boot', from: 'booting', to: 'A' }, - transitions: [ - { name: 'step1', from: 'A', to: 'B' }, - { name: 'step2', from: 'B', to: 'C' } - ] - }); - - t.is(fsm.state, 'A') - - t.deepEqual(fsm.allStates(), [ 'booting', 'A', 'B', 'C' ]) - t.deepEqual(fsm.allTransitions(), [ 'boot', 'step1', 'step2' ]) - t.deepEqual(fsm.transitions(), [ 'step1' ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('singleton construction - with custom data and methods', t => { - - var fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step1', from: 'A', to: 'B' }, - { name: 'step2', from: 'B', to: 'C' } - ], - data: { - value: 42 - }, - methods: { - talk: function() { - return this.state + ' - ' + this.value - } - } - }); - - t.is(fsm.state, 'A') - t.is(fsm.value, 42) - t.is(fsm.talk(), 'A - 42') - - fsm.step1() - - t.is(fsm.state, 'B') - t.is(fsm.value, 42) - t.is(fsm.talk(), 'B - 42') - - fsm.value = 99 + t.is(fsm1.state, 'A') + t.is(fsm2.state, 'B') + t.is(fsm3.state, 'C') - t.is(fsm.state, 'B') - t.is(fsm.value, 99) - t.is(fsm.talk(), 'B - 99') - -}) - -//------------------------------------------------------------------------------------------------- - -test('factory construction', t => { - - var MyClass = StateMachine.factory({ - init: 'A', - transitions: [ - { name: 'step1', from: 'A', to: 'B' }, - { name: 'step2', from: 'B', to: 'C' } - ] - }); - - var fsm1 = new MyClass(), - fsm2 = new MyClass(), - fsm3 = new MyClass(); - - fsm2.step1() - fsm3.step1() - fsm3.step2() - - t.is(fsm1.state, 'A') - t.is(fsm2.state, 'B') - t.is(fsm3.state, 'C') - - t.deepEqual(fsm1.allStates(), [ 'none', 'A', 'B', 'C' ]) - t.deepEqual(fsm2.allStates(), [ 'none', 'A', 'B', 'C' ]) - t.deepEqual(fsm3.allStates(), [ 'none', 'A', 'B', 'C' ]) - - t.deepEqual(fsm1.allTransitions(), [ 'init', 'step1', 'step2' ]) - t.deepEqual(fsm2.allTransitions(), [ 'init', 'step1', 'step2' ]) - t.deepEqual(fsm3.allTransitions(), [ 'init', 'step1', 'step2' ]) - - t.deepEqual(fsm1.transitions(), [ 'step1' ]) - t.deepEqual(fsm2.transitions(), [ 'step2' ]) - t.deepEqual(fsm3.transitions(), [ ]) - - t.is(fsm1.allStates, MyClass.prototype.allStates) - t.is(fsm2.allStates, MyClass.prototype.allStates) - t.is(fsm3.allStates, MyClass.prototype.allStates) - -}) - -//------------------------------------------------------------------------------------------------- + t.deepEqual(fsm1.allStates(), ['A', 'B', 'C']) + t.deepEqual(fsm2.allStates(), ['A', 'B', 'C']) + t.deepEqual(fsm3.allStates(), ['A', 'B', 'C']) -test('factory construction - with custom data and methods', t => { - - var MyClass = StateMachine.factory({ - init: 'A', - transitions: [ - { name: 'step1', from: 'A', to: 'B' }, - { name: 'step2', from: 'B', to: 'C' } - ], - data: function(value) { - return { - value: value - } - }, - methods: { - talk: function() { - return this.state + ' - ' + this.value - } - } - }); - - var fsm1 = new MyClass(1), - fsm2 = new MyClass(2), - fsm3 = new MyClass(3); - - t.is(fsm1.state, 'A') - t.is(fsm2.state, 'A') - t.is(fsm3.state, 'A') - - t.is(fsm1.talk(), 'A - 1') - t.is(fsm2.talk(), 'A - 2') - t.is(fsm3.talk(), 'A - 3') - - fsm2.step1() - fsm3.step1() - fsm3.step2() - - t.is(fsm1.state, 'A') - t.is(fsm2.state, 'B') - t.is(fsm3.state, 'C') - - t.is(fsm1.talk(), 'A - 1') - t.is(fsm2.talk(), 'B - 2') - t.is(fsm3.talk(), 'C - 3') - - t.deepEqual(fsm1.allStates(), [ 'none', 'A', 'B', 'C' ]) - t.deepEqual(fsm2.allStates(), [ 'none', 'A', 'B', 'C' ]) - t.deepEqual(fsm3.allStates(), [ 'none', 'A', 'B', 'C' ]) - - t.deepEqual(fsm1.allTransitions(), [ 'init', 'step1', 'step2' ]) - t.deepEqual(fsm2.allTransitions(), [ 'init', 'step1', 'step2' ]) - t.deepEqual(fsm3.allTransitions(), [ 'init', 'step1', 'step2' ]) - - t.deepEqual(fsm1.transitions(), [ 'step1' ]) - t.deepEqual(fsm2.transitions(), [ 'step2' ]) - t.deepEqual(fsm3.transitions(), [ ]) - - t.is(fsm1.allStates, MyClass.prototype.allStates) - t.is(fsm2.allStates, MyClass.prototype.allStates) - t.is(fsm3.allStates, MyClass.prototype.allStates) + t.deepEqual(fsm1.allTransitions(), ['step1', 'step2']) + t.deepEqual(fsm2.allTransitions(), ['step1', 'step2']) + t.deepEqual(fsm3.allTransitions(), ['step1', 'step2']) + t.deepEqual(fsm1.transitions(), ['step1']) + t.deepEqual(fsm2.transitions(), ['step2']) + t.deepEqual(fsm3.transitions(), []) }) //------------------------------------------------------------------------------------------------- diff --git a/test/defaults.js b/test/defaults.js deleted file mode 100644 index da1bc08..0000000 --- a/test/defaults.js +++ /dev/null @@ -1,62 +0,0 @@ -import test from 'ava'; -import StateMachine from '../src/app'; - -//------------------------------------------------------------------------------------------------- - -const defaults = JSON.stringify(StateMachine.defaults); - -test.afterEach.always('restore defaults', t => { - StateMachine.defaults = JSON.parse(defaults); -}); - -//------------------------------------------------------------------------------------------------- - -test.serial('override global initialization defaults', t => { - - StateMachine.defaults.init = { - name: 'boot', - from: 'booting' - } - - var fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step1', from: 'A', to: 'B' }, - { name: 'step2', from: 'B', to: 'C' } - ] - }); - - t.is(fsm.state, 'A'); - - t.deepEqual(fsm.allStates(), [ 'booting', 'A', 'B', 'C' ]); - t.deepEqual(fsm.allTransitions(), [ 'boot', 'step1', 'step2' ]); - t.deepEqual(fsm.transitions(), [ 'step1' ]); - -}); - -//------------------------------------------------------------------------------------------------- - -test.serial('override global initialization defaults (again)', t => { - - StateMachine.defaults.init = { - name: 'start', - from: 'unknown' - } - - var fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step1', from: 'A', to: 'B' }, - { name: 'step2', from: 'B', to: 'C' } - ] - }); - - t.is(fsm.state, 'A'); - - t.deepEqual(fsm.allStates(), [ 'unknown', 'A', 'B', 'C' ]); - t.deepEqual(fsm.allTransitions(), [ 'start', 'step1', 'step2' ]); - t.deepEqual(fsm.transitions(), [ 'step1' ]); - -}); - -//------------------------------------------------------------------------------------------------- diff --git a/test/empty.js b/test/empty.js index ad2ce62..90eb5da 100644 --- a/test/empty.js +++ b/test/empty.js @@ -1,17 +1,17 @@ -import test from 'ava' -import StateMachine from '../src/app' +import test from 'ava' +import StateMachine from '../dist/state-machine'; //------------------------------------------------------------------------------------------------- test('empty state machine', t => { - var fsm = new StateMachine(); + var fsm = new StateMachine('none', {}); - t.is(fsm.state, 'none') + t.is(fsm.state, 'none') - t.deepEqual(fsm.allStates(), [ 'none' ]) - t.deepEqual(fsm.allTransitions(), [ ]) - t.deepEqual(fsm.transitions(), [ ]) + t.deepEqual(fsm.allStates(), []) + t.deepEqual(fsm.allTransitions(), []) + t.deepEqual(fsm.transitions(), []) }) @@ -19,60 +19,13 @@ test('empty state machine', t => { test('empty state machine - but caller forgot new keyword', t => { - var fsm = StateMachine() // NOTE: missing 'new' + var fsm = new StateMachine() // NOTE: missing 'new' - t.is(fsm.state, 'none') + t.is(fsm.state, undefined) - t.deepEqual(fsm.allStates(), [ 'none' ]) - t.deepEqual(fsm.allTransitions(), [ ]) - t.deepEqual(fsm.transitions(), [ ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('empty state machine - applied to existing object', t => { - - var fsm = {}; - - StateMachine.apply(fsm) - - t.is(fsm.state, 'none') - - t.deepEqual(fsm.allStates(), [ 'none' ]) - t.deepEqual(fsm.allTransitions(), [ ]) - t.deepEqual(fsm.transitions(), [ ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('empty state machine factory', t => { - - var FSM = StateMachine.factory(), - fsm = new FSM(); - - t.is(fsm.state, 'none') - t.deepEqual(fsm.allStates(), [ 'none' ]) - t.deepEqual(fsm.allTransitions(), [ ]) - t.deepEqual(fsm.transitions(), [ ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('empty state machine factory - applied to existing class', t => { - - var FSM = function() { this._fsm() }; - - StateMachine.factory(FSM) - - var fsm = new FSM() - - t.is(fsm.state, 'none') - t.deepEqual(fsm.allStates(), [ 'none' ]) - t.deepEqual(fsm.allTransitions(), [ ]) - t.deepEqual(fsm.transitions(), [ ]) + t.deepEqual(fsm.allStates(), []) + t.deepEqual(fsm.allTransitions(), []) + t.deepEqual(fsm.transitions(), []) }) diff --git a/test/errors.js b/test/errors.js index fb852ca..df3041b 100644 --- a/test/errors.js +++ b/test/errors.js @@ -1,160 +1,87 @@ -import test from 'ava' -import StateMachine from '../src/app' +import test from 'ava' +import StateMachine from '../dist/state-machine'; //------------------------------------------------------------------------------------------------- test('state cannot be modified directly', t => { - var fsm = new StateMachine({ - transitions: [ - { name: 'step', from: 'none', to: 'complete' } - ] - }) + var fsm = new StateMachine('none', { + step: { from: 'none', to: 'complete' } + }) - t.is(fsm.state, 'none') - var error = t.throws(() => { - fsm.state = 'other' - }) - t.is(error.message, 'use transitions to change state') - t.is(fsm.state, 'none') + t.is(fsm.state, 'none') + var error = t.throws(() => { + fsm.state = 'other' + }) + t.is(error.message, 'Cannot set property state of # which has only a getter') + t.is(fsm.state, 'none') }) //------------------------------------------------------------------------------------------------- -test('StateMachine.apply only allowed on objects', t => { - - var config = { - transitions: [ - { name: 'step', from: 'none', to: 'complete' } - ] - }; - - var error = t.throws(() => { - StateMachine.apply(function() {}, config) - }) - t.is(error.message, 'StateMachine can only be applied to objects') - - error = t.throws(() => { - StateMachine.apply([], config) - }) - t.is(error.message, 'StateMachine can only be applied to objects') - - error = t.throws(() => { - StateMachine.apply(42, config) - }) - t.is(error.message, 'StateMachine can only be applied to objects') +test('fire transition while existing transition is still in process raises an exception', async (t) => { + + var fsm = new StateMachine('none', { + step: { from: 'none', to: 'A' }, + other: { from: '*', to: 'X' } + // methods: { + // onBeforeStep: function () { this.other(); }, + // onBeforeOther: function () { t.fail('should never happen') }, + // onEnterX: function () { t.fail('should never happen') } + // } + }, { + onBefore(transition, from, to) { + if (transition === 'step') { + return fsm.fire('other'); + } + } + }); + + t.is(fsm.state, 'none') + t.is(fsm.can('step'), true) + t.is(fsm.can('other'), true) + try { + await fsm.fire('step'); + } catch (error) { + t.is(error.message, 'transition is invalid while previous transition is still in progress') + } + + t.is(fsm.state, 'none', 'entire transition was cancelled by the exception') }) //------------------------------------------------------------------------------------------------- -test('invalid transition raises an exception', t => { - - var fsm = new StateMachine({ - transitions: [ - { name: 'step1', from: 'none', to: 'A' }, - { name: 'step2', from: 'A', to: 'B' } - ] - }); - - t.is(fsm.state, 'none') - t.is(fsm.can('step1'), true) - t.is(fsm.can('step2'), false) - - const error = t.throws(() => { - fsm.step2(); - }) - - t.is(error.message, 'transition is invalid in current state') - t.is(error.transition, 'step2') - t.is(error.from, 'none') - t.is(error.to, undefined) - t.is(error.current, 'none') - -}) - -//------------------------------------------------------------------------------------------------- - -test('invalid transition handler can be customized', t => { - - var fsm = new StateMachine({ - transitions: [ - { name: 'step1', from: 'none', to: 'A' }, - { name: 'step2', from: 'A', to: 'B' } - ], - methods: { - onInvalidTransition: function() { return 'custom error'; } - } - }); - - t.is(fsm.state, 'none') - t.is(fsm.can('step1'), true) - t.is(fsm.can('step2'), false) - t.is(fsm.step2(), 'custom error') - t.is(fsm.state, 'none') - -}) - -//------------------------------------------------------------------------------------------------- - -test('fire transition while existing transition is still in process raises an exception', t => { - - var fsm = new StateMachine({ - transitions: [ - { name: 'step', from: 'none', to: 'A' }, - { name: 'other', from: '*', to: 'X' } - ], - methods: { - onBeforeStep: function() { this.other(); }, - onBeforeOther: function() { t.fail('should never happen') }, - onEnterX: function() { t.fail('should never happen') } - } - }); - - t.is(fsm.state, 'none') - t.is(fsm.can('step'), true) - t.is(fsm.can('other'), true) - - const error = t.throws(() => { - fsm.step() - }) - - t.is(error.message, 'transition is invalid while previous transition is still in progress') - t.is(error.transition, 'other') - t.is(error.from, 'none') - t.is(error.to, 'X') - t.is(error.current, 'none') - - t.is(fsm.state, 'none', 'entire transition was cancelled by the exception') - -}) - -//------------------------------------------------------------------------------------------------- - -test('pending transition handler can be customized', t => { - - var error = "", - fsm = new StateMachine({ - transitions: [ - { name: 'step', from: 'none', to: 'A' }, - { name: 'other', from: '*', to: 'X' } - ], - methods: { - onBeforeStep: function() { error = this.other(); return false }, - onPendingTransition: function() { return 'custom error' }, - onBeforeOther: function() { t.fail('should never happen') }, - onEnterX: function() { t.fail('should never happen') } - } - }); - - t.is(fsm.state, 'none') - t.is(fsm.can('step'), true) - t.is(fsm.can('other'), true) - t.is(fsm.step(), false) - t.is(fsm.state, 'none') - t.is(error, 'custom error') - +test('pending transition handler can be customized', async (t) => { + var fsm = new StateMachine('none', { + step: { from: 'none', to: 'A' }, + other: { from: '*', to: 'X' } + }, { + onPendingTransition() { + throw new Error('custom error'); + }, + onBefore(transition) { + if (transition === 'step') { + return fsm.fire('other'); + } else if (transition === 'other') { + throw new Error('should never happen'); + } + }, + onEnter() { + throw new Error('should never happen'); + } + }); + + t.is(fsm.state, 'none') + t.is(fsm.can('step'), true) + t.is(fsm.can('other'), true); + try { + await fsm.fire('step') + } catch (error) { + t.is(error.message, 'custom error') + } + t.is(fsm.state, 'none') }) //------------------------------------------------------------------------------------------------- diff --git a/test/goto.js b/test/goto.js deleted file mode 100644 index e8fcbcc..0000000 --- a/test/goto.js +++ /dev/null @@ -1,195 +0,0 @@ -import test from 'ava' -import StateMachine from '../src/app' -import LifecycleLogger from './helpers/lifecycle_logger' - -//------------------------------------------------------------------------------------------------- - -function goto(state) { - return state -} - -//------------------------------------------------------------------------------------------------- - -test('goto transition', t => { - - var logger = new LifecycleLogger(), - fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step', from: 'A', to: 'B' }, - { name: 'step', from: 'B', to: 'C' }, - { name: 'step', from: 'C', to: 'D' }, - { name: 'goto', from: '*', to: goto } - ], - methods: { - onBeforeTransition: logger, - onBeforeInit: logger, - onBeforeStep: logger, - onBeforeGoto: logger, - onLeaveState: logger, - onLeaveNone: logger, - onLeaveA: logger, - onLeaveB: logger, - onLeaveC: logger, - onLeaveD: logger, - onTransition: logger, - onEnterState: logger, - onEnterNone: logger, - onEnterA: logger, - onEnterB: logger, - onEnterC: logger, - onEnterD: logger, - onAfterTransition: logger, - onAfterInit: logger, - onAfterStep: logger, - onAfterGoto: logger - } - }); - - t.is(fsm.state, 'A') - t.deepEqual(fsm.allStates(), [ 'none', 'A', 'B', 'C', 'D' ]) - t.deepEqual(fsm.allTransitions(), [ 'init', 'step', 'goto' ]) - - logger.clear() - - fsm.goto('C') - - t.is(fsm.state, 'C') - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'goto', from: 'A', to: 'C', current: 'A', args: [ 'C' ] }, - { event: 'onBeforeGoto', transition: 'goto', from: 'A', to: 'C', current: 'A', args: [ 'C' ] }, - { event: 'onLeaveState', transition: 'goto', from: 'A', to: 'C', current: 'A', args: [ 'C' ] }, - { event: 'onLeaveA', transition: 'goto', from: 'A', to: 'C', current: 'A', args: [ 'C' ] }, - { event: 'onTransition', transition: 'goto', from: 'A', to: 'C', current: 'A', args: [ 'C' ] }, - { event: 'onEnterState', transition: 'goto', from: 'A', to: 'C', current: 'C', args: [ 'C' ] }, - { event: 'onEnterC', transition: 'goto', from: 'A', to: 'C', current: 'C', args: [ 'C' ] }, - { event: 'onAfterTransition', transition: 'goto', from: 'A', to: 'C', current: 'C', args: [ 'C' ] }, - { event: 'onAfterGoto', transition: 'goto', from: 'A', to: 'C', current: 'C', args: [ 'C' ] } - ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('goto can have additional arguments', t => { - - var logger = new LifecycleLogger(), - fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step', from: 'A', to: 'B' }, - { name: 'step', from: 'B', to: 'C' }, - { name: 'step', from: 'C', to: 'D' }, - { name: 'goto', from: '*', to: goto } - ], - methods: { - onStep: logger, - onGoto: logger - } - }); - - t.is(fsm.state, 'A') - t.deepEqual(fsm.allStates(), [ 'none', 'A', 'B', 'C', 'D' ]) - t.deepEqual(fsm.allTransitions(), [ 'init', 'step', 'goto' ]) - - logger.clear() - - fsm.goto('C', 'with', 4, 'additional', 'arguments') - - t.is(fsm.state, 'C') - t.deepEqual(logger.log, [ - { event: 'onGoto', transition: 'goto', from: 'A', to: 'C', current: 'C', args: [ 'C', 'with', 4, 'additional', 'arguments' ] } - ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('goto can go to an unknown state', t => { - - var fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step', from: 'A', to: 'B' }, - { name: 'step', from: 'B', to: 'C' }, - { name: 'step', from: 'C', to: 'D' }, - { name: 'goto', from: '*', to: goto } - ] - }) - - t.is(fsm.state, 'A') - t.deepEqual(fsm.allStates(), [ 'none', 'A', 'B', 'C', 'D' ]); - - fsm.goto('B') - t.is(fsm.state, 'B') - t.deepEqual(fsm.allStates(), [ 'none', 'A', 'B', 'C', 'D' ]); - - fsm.goto('X') - t.is(fsm.state, 'X') - t.deepEqual(fsm.allStates(), [ 'none', 'A', 'B', 'C', 'D', 'X' ]); - -}) - -//------------------------------------------------------------------------------------------------- - -test('goto can be configured with a custom name', t => { - - var logger = new LifecycleLogger(), - fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step', from: 'A', to: 'B' }, - { name: 'step', from: 'B', to: 'C' }, - { name: 'step', from: 'C', to: 'D' }, - { name: 'jump', from: '*', to: goto } - ], - methods: { - onBeforeTransition: logger, - onBeforeInit: logger, - onBeforeStep: logger, - onBeforeJump: logger, - onLeaveState: logger, - onLeaveNone: logger, - onLeaveA: logger, - onLeaveB: logger, - onLeaveC: logger, - onLeaveD: logger, - onTransition: logger, - onEnterState: logger, - onEnterNone: logger, - onEnterA: logger, - onEnterB: logger, - onEnterC: logger, - onEnterD: logger, - onAfterTransition: logger, - onAfterInit: logger, - onAfterStep: logger, - onAfterJump: logger - } - }); - - t.is(fsm.state, 'A') - t.deepEqual(fsm.allStates(), [ 'none', 'A', 'B', 'C', 'D' ]) - t.deepEqual(fsm.allTransitions(), [ 'init', 'step', 'jump' ]) - t.is(fsm.goto, undefined) - - logger.clear() - - fsm.jump('C') - - t.is(fsm.state, 'C') - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'jump', from: 'A', to: 'C', current: 'A', args: [ 'C' ] }, - { event: 'onBeforeJump', transition: 'jump', from: 'A', to: 'C', current: 'A', args: [ 'C' ] }, - { event: 'onLeaveState', transition: 'jump', from: 'A', to: 'C', current: 'A', args: [ 'C' ] }, - { event: 'onLeaveA', transition: 'jump', from: 'A', to: 'C', current: 'A', args: [ 'C' ] }, - { event: 'onTransition', transition: 'jump', from: 'A', to: 'C', current: 'A', args: [ 'C' ] }, - { event: 'onEnterState', transition: 'jump', from: 'A', to: 'C', current: 'C', args: [ 'C' ] }, - { event: 'onEnterC', transition: 'jump', from: 'A', to: 'C', current: 'C', args: [ 'C' ] }, - { event: 'onAfterTransition', transition: 'jump', from: 'A', to: 'C', current: 'C', args: [ 'C' ] }, - { event: 'onAfterJump', transition: 'jump', from: 'A', to: 'C', current: 'C', args: [ 'C' ] } - ]) - -}) - -//------------------------------------------------------------------------------------------------- diff --git a/test/helpers/lifecycle_logger.js b/test/helpers/lifecycle_logger.js deleted file mode 100644 index fc9eb34..0000000 --- a/test/helpers/lifecycle_logger.js +++ /dev/null @@ -1,27 +0,0 @@ - -module.exports = function() { - - 'use strict' - - var entries = [], - logger = function(lifecycle) { - var entry = { - event: lifecycle.event, - transition: lifecycle.transition, - from: lifecycle.from, - to: lifecycle.to, - current: lifecycle.fsm.state - } - if (arguments.length > 1) - entry.args = [].slice.call(arguments, 1); - entries.push(entry); - }; - - logger.clear = function() { - entries.length = 0; - } - - logger.log = entries; - - return logger; -} diff --git a/test/introspection.js b/test/introspection.js index be9e176..f0590d1 100644 --- a/test/introspection.js +++ b/test/introspection.js @@ -1,127 +1,101 @@ -import test from 'ava'; -import StateMachine from '../src/app'; +import test from 'ava'; +import StateMachine from '../dist/state-machine'; //----------------------------------------------------------------------------- -test('is', t => { +test('is', async (t) => { - var fsm = new StateMachine({ - init: 'green', - transitions: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ]}); + var fsm = new StateMachine('green', { + warn: { from: 'green', to: 'yellow' }, + panic: { from: 'yellow', to: 'red' }, + calm: { from: 'red', to: 'yellow' }, + clear: { from: 'yellow', to: 'green' } + }); - t.is(fsm.state, 'green') + t.is(fsm.state, 'green') - t.is(fsm.is('green'), true) - t.is(fsm.is('yellow'), false) - t.is(fsm.is(['green', 'red']), true, 'current state should match when included in array') - t.is(fsm.is(['yellow', 'red']), false, 'current state should NOT match when not included in array') + t.is(fsm.is('green'), true) + t.is(fsm.is('yellow'), false) + t.is(fsm.is(['green', 'red']), true, 'current state should match when included in array') + t.is(fsm.is(['yellow', 'red']), false, 'current state should NOT match when not included in array') - fsm.warn() + await fsm.fire('warn'); - t.is(fsm.state, 'yellow') - t.is(fsm.is('green'), false) - t.is(fsm.is('yellow'), true) - t.is(fsm.is(['green', 'red']), false, 'current state should NOT match when not included in array') - t.is(fsm.is(['yellow', 'red']), true, 'current state should match when included in array') + t.is(fsm.state, 'yellow') + t.is(fsm.is('green'), false) + t.is(fsm.is('yellow'), true) + t.is(fsm.is(['green', 'red']), false, 'current state should NOT match when not included in array') + t.is(fsm.is(['yellow', 'red']), true, 'current state should match when included in array') }); //----------------------------------------------------------------------------- -test('can & cannot', t => { - - var fsm = new StateMachine({ - init: 'green', - transitions: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - ] - }); - - t.is(fsm.state, 'green') - t.is(fsm.can('warn'), true) - t.is(fsm.can('panic'), false) - t.is(fsm.can('calm'), false) - t.is(fsm.cannot('warn'), false) - t.is(fsm.cannot('panic'), true) - t.is(fsm.cannot('calm'), true) - - fsm.warn(); - t.is(fsm.state, 'yellow') - t.is(fsm.can('warn'), false) - t.is(fsm.can('panic'), true) - t.is(fsm.can('calm'), false) - t.is(fsm.cannot('warn'), true) - t.is(fsm.cannot('panic'), false) - t.is(fsm.cannot('calm'), true) - - fsm.panic(); - t.is(fsm.state, 'red') - t.is(fsm.can('warn'), false) - t.is(fsm.can('panic'), false) - t.is(fsm.can('calm'), true) - t.is(fsm.cannot('warn'), true) - t.is(fsm.cannot('panic'), true) - t.is(fsm.cannot('calm'), false) - - t.is(fsm.can('jibber'), false, "unknown event should not crash") - t.is(fsm.cannot('jabber'), true, "unknown event should not crash") +test('can & cannot', async (t) => { + + var fsm = new StateMachine('green', { + warn: { from: 'green', to: 'yellow' }, + panic: { from: 'yellow', to: 'red' }, + calm: { from: 'red', to: 'yellow' }, + clear: { from: 'yellow', to: 'green' } + }); + + t.is(fsm.state, 'green') + t.is(fsm.can('warn'), true) + t.is(fsm.can('panic'), false) + t.is(fsm.can('calm'), false) + t.is(fsm.cannot('warn'), false) + t.is(fsm.cannot('panic'), true) + t.is(fsm.cannot('calm'), true) + + await fsm.fire('warn'); + t.is(fsm.state, 'yellow') + t.is(fsm.can('warn'), false) + t.is(fsm.can('panic'), true) + t.is(fsm.can('calm'), false) + t.is(fsm.cannot('warn'), true) + t.is(fsm.cannot('panic'), false) + t.is(fsm.cannot('calm'), true) + + await fsm.fire('panic') + t.is(fsm.state, 'red') + t.is(fsm.can('warn'), false) + t.is(fsm.can('panic'), false) + t.is(fsm.can('calm'), true) + t.is(fsm.cannot('warn'), true) + t.is(fsm.cannot('panic'), true) + t.is(fsm.cannot('calm'), false) + + t.is(fsm.can('jibber'), false, "unknown event should not crash") + t.is(fsm.cannot('jabber'), true, "unknown event should not crash") }); //----------------------------------------------------------------------------- -test('can is always false during lifecycle events', t => { - - t.plan(81); - - var fsm = new StateMachine({ - init: 'green', - transitions: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - ], - methods: { - assertTransitionsNotAllowed: function() { - t.false(this.can('warn')) - t.false(this.can('panic')) - t.false(this.can('calm')) - }, - onBeforeTransition: function() { this.assertTransitionsNotAllowed(); }, - onBeforeWarn: function() { this.assertTransitionsNotAllowed(); }, - onBeforePanic: function() { this.assertTransitionsNotAllowed(); }, - onBeforeCalm: function() { this.assertTransitionsNotAllowed(); }, - onLeaveState: function() { this.assertTransitionsNotAllowed(); }, - onLeaveNone: function() { this.assertTransitionsNotAllowed(); }, - onLeaveGreen: function() { this.assertTransitionsNotAllowed(); }, - onLeaveYellow: function() { this.assertTransitionsNotAllowed(); }, - onLeaveRed: function() { this.assertTransitionsNotAllowed(); }, - onTransition: function() { this.assertTransitionsNotAllowed(); }, - onEnterState: function() { this.assertTransitionsNotAllowed(); }, - onEnterNone: function() { this.assertTransitionsNotAllowed(); }, - onEnterGreen: function() { this.assertTransitionsNotAllowed(); }, - onEnterYellow: function() { this.assertTransitionsNotAllowed(); }, - onEnterRed: function() { this.assertTransitionsNotAllowed(); }, - onAfterTransition: function() { this.assertTransitionsNotAllowed(); }, - onAfterInit: function() { this.assertTransitionsNotAllowed(); }, - onAfterWarn: function() { this.assertTransitionsNotAllowed(); }, - onAfterPanic: function() { this.assertTransitionsNotAllowed(); }, - onAfterCalm: function() { this.assertTransitionsNotAllowed(); } - } - }); - - t.is(fsm.state, 'green') - fsm.warn() - t.is(fsm.state, 'yellow') - fsm.panic() - t.is(fsm.state, 'red') +test('can is always false during lifecycle events', async (t) => { + t.plan(9); + var fsm = new StateMachine('green', { + warn: { from: 'green', to: 'yellow' }, + panic: { from: 'yellow', to: 'red' }, + calm: { from: 'red', to: 'yellow' }, + clear: { from: 'yellow', to: 'green' } + }, { + onBefore() { + assertTransitionsNotAllowed(); + } + }); + function assertTransitionsNotAllowed() { + t.false(fsm.can('warn')) + t.false(fsm.can('panic')) + t.false(fsm.can('calm')) + } + + t.is(fsm.state, 'green') + await fsm.fire('warn'); + t.is(fsm.state, 'yellow') + await fsm.fire('panic') + t.is(fsm.state, 'red') }); @@ -129,76 +103,65 @@ test('can is always false during lifecycle events', t => { test('all states', t => { - var fsm = new StateMachine({ - init: 'green', - transitions: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' }, - { name: 'finish', from: 'green', to: 'done' }, - ]}); - - t.deepEqual(fsm.allStates(), [ 'none', 'green', 'yellow', 'red', 'done' ]); + var fsm = new StateMachine('green', { + warn: { from: 'green', to: 'yellow' }, + panic: { from: 'yellow', to: 'red' }, + calm: { from: 'red', to: 'yellow' }, + clear: { from: 'yellow', to: 'green' } + }); + t.deepEqual(fsm.allStates(), ['green', 'yellow', 'red']); }); //----------------------------------------------------------------------------- test("all transitions", t => { - - var fsm = new StateMachine({ - init: 'green', - transitions: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' }, - { name: 'finish', from: 'green', to: 'done' }, - ]}); - - t.deepEqual(fsm.allTransitions(), [ - 'init', 'warn', 'panic', 'calm', 'clear', 'finish' - ]); + var fsm = new StateMachine('green', { + warn: { from: 'green', to: 'yellow' }, + panic: { from: 'yellow', to: 'red' }, + calm: { from: 'red', to: 'yellow' }, + clear: { from: 'yellow', to: 'green' }, + finish: { from: 'green', to: 'done' }, + }); + + t.deepEqual(fsm.allTransitions(), [ + 'warn', 'panic', 'calm', 'clear', 'finish' + ]); }) //----------------------------------------------------------------------------- -test("valid transitions", t => { - - var fsm = new StateMachine({ - init: 'green', - transitions: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' }, - { name: 'finish', from: 'green', to: 'done' }, - ]}); - - t.is(fsm.state, 'green') - t.deepEqual(fsm.transitions(), ['warn', 'finish']) - - fsm.warn(); - t.is(fsm.state, 'yellow') - t.deepEqual(fsm.transitions(), ['panic', 'clear']) - - fsm.panic(); - t.is(fsm.state, 'red') - t.deepEqual(fsm.transitions(), ['calm']) - - fsm.calm(); - t.is(fsm.state, 'yellow') - t.deepEqual(fsm.transitions(), ['panic', 'clear']) - - fsm.clear(); - t.is(fsm.state, 'green') - t.deepEqual(fsm.transitions(), ['warn', 'finish']) - - fsm.finish(); - t.is(fsm.state, 'done') - t.deepEqual(fsm.transitions(), []) - +test("valid transitions", async (t) => { + var fsm = new StateMachine('green', { + warn: { from: 'green', to: 'yellow' }, + panic: { from: 'yellow', to: 'red' }, + calm: { from: 'red', to: 'yellow' }, + clear: { from: 'yellow', to: 'green' }, + finish: { from: 'green', to: 'done' }, + }); + + t.is(fsm.state, 'green') + t.deepEqual(fsm.transitions(), ['warn', 'finish']) + + await fsm.fire('warn') + t.is(fsm.state, 'yellow') + t.deepEqual(fsm.transitions(), ['panic', 'clear']) + + await fsm.fire('panic') + t.is(fsm.state, 'red') + t.deepEqual(fsm.transitions(), ['calm']) + + await fsm.fire('calm') + t.is(fsm.state, 'yellow') + t.deepEqual(fsm.transitions(), ['panic', 'clear']) + + await fsm.fire('clear') + t.is(fsm.state, 'green') + t.deepEqual(fsm.transitions(), ['warn', 'finish']) + + await fsm.fire('finish') + t.is(fsm.state, 'done') + t.deepEqual(fsm.transitions(), []) }); //----------------------------------------------------------------------------- diff --git a/test/issues.js b/test/issues.js index ff7741d..6c5e94c 100644 --- a/test/issues.js +++ b/test/issues.js @@ -1,103 +1,36 @@ -import test from 'ava' -import StateMachine from '../src/app' -import LifecycleLogger from './helpers/lifecycle_logger' +import test from 'ava' +import StateMachine from '../dist/state-machine'; //------------------------------------------------------------------------------------------------- -test('github issue #12 - transition return values', t => { +test('github issue #17 - exceptions in lifecycle events are NOT swallowed', async (t) => { - var fsm = new StateMachine({ - transitions: [ - { name: 'init', from: 'none', to: 'A' }, - { name: 'cancelled', from: 'A', to: 'X' }, - { name: 'async', from: 'A', to: 'X' } - ], - methods: { - onBeforeCancelled: function() { return false; }, - onBeforeAsync: function() { return new Promise(function(resolve, reject) {}); } - } - }); + var fsm = new StateMachine('none', { + step: { from: 'none', to: 'complete' } + // onTransition: function () { throw Error('oops') } + }); - t.is(fsm.init(), true, 'successful (synchronous) transition returns true') - t.is(fsm.cancelled(), false, 'cancelled (synchronous) transition returns true') - - var promise = fsm.async(); - t.is(typeof promise.then, 'function', 'asynchronous transition returns a promise'); + t.is(fsm.state, 'none') + try { + await fsm.fire('step') + } catch (error) { + t.is(error.message, 'oops') + } }) //------------------------------------------------------------------------------------------------- -test('github issue #17 - exceptions in lifecycle events are NOT swallowed', t => { - - var fsm = new StateMachine({ - transitions: [ - { name: 'step', from: 'none', to: 'complete' } - ], - methods: { - onTransition: function() { throw Error('oops') } - } - }); - - t.is(fsm.state, 'none') - - const error = t.throws(() => { - fsm.step(); - }) - - t.is(error.message, 'oops') - -}) - -//------------------------------------------------------------------------------------------------- - -test('github issue #19 - lifecycle events have correct this when applying StateMachine to a custom class', t => { - - var FSM = function() { - this.stepped = false; - this._fsm(); - } - - FSM.prototype.onStep = function(lifecycle) { this.stepped = true } - - StateMachine.factory(FSM, { - transitions: [ - { name: 'step', from: 'none', to: 'complete' } - ] - }) - - var a = new FSM(), - b = new FSM(); - - t.is(a.state, 'none') - t.is(b.state, 'none') - t.is(a.stepped, false) - t.is(b.stepped, false) - - a.step(); - - t.is(a.state, 'complete') - t.is(b.state, 'none') - t.is(a.stepped, true) - t.is(b.stepped, false) - -}); - -//------------------------------------------------------------------------------------------------- - -test('github issue #64 - double wildcard transition does not change state', t => { - - var fsm = new StateMachine({ - transitions: [ - { name: 'step', from: '*' /* no-op */ } - ] - }); +test('github issue #64 - double wildcard transition does not change state', async (t) => { - t.is(fsm.state, 'none') + var fsm = new StateMachine('none', { + step: { from: '*' /* no-op */ } + }); - fsm.step(); t.is(fsm.state, 'none') - fsm.step(); t.is(fsm.state, 'none') + t.is(fsm.state, 'none') + await fsm.fire('step'); t.is(fsm.state, 'none') + await fsm.fire('step'); t.is(fsm.state, 'none') }) //------------------------------------------------------------------------------------------------- diff --git a/test/lifecycle.js b/test/lifecycle.js deleted file mode 100644 index 347ebf4..0000000 --- a/test/lifecycle.js +++ /dev/null @@ -1,932 +0,0 @@ -import test from 'ava' -import StateMachine from '../src/app' -import LifecycleLogger from './helpers/lifecycle_logger' - -//------------------------------------------------------------------------------------------------- - -test('lifecycle events occur in correct order', t => { - - var logger = new LifecycleLogger(), - fsm = new StateMachine({ - transitions: [ - { name: 'step', from: 'none', to: 'complete' } - ], - methods: { - onBeforeTransition: logger, - onBeforeStep: logger, - onLeaveState: logger, - onLeaveNone: logger, - onLeaveComplete: logger, - onTransition: logger, - onEnterState: logger, - onEnterNone: logger, - onEnterComplete: logger, - onAfterTransition: logger, - onAfterStep: logger - } - }); - - t.is(fsm.state, 'none') - - fsm.step() - - t.is(fsm.state, 'complete') - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onBeforeStep', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onLeaveState', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onLeaveNone', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onTransition', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onEnterState', transition: 'step', from: 'none', to: 'complete', current: 'complete' }, - { event: 'onEnterComplete', transition: 'step', from: 'none', to: 'complete', current: 'complete' }, - { event: 'onAfterTransition', transition: 'step', from: 'none', to: 'complete', current: 'complete' }, - { event: 'onAfterStep', transition: 'step', from: 'none', to: 'complete', current: 'complete' } - ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('lifecycle events occur in correct order - for same state transition', t => { - - var logger = new LifecycleLogger(), - fsm = new StateMachine({ - transitions: [ - { name: 'noop', from: 'none', to: 'none' } - ], - methods: { - onBeforeTransition: logger, - onBeforeNoop: logger, - onLeaveState: logger, - onLeaveNone: logger, - onLeaveComplete: logger, - onTransition: logger, - onEnterState: logger, - onEnterNone: logger, - onEnterComplete: logger, - onAfterTransition: logger, - onAfterNoop: logger - } - }); - - t.is(fsm.state, 'none') - - fsm.noop() - - t.is(fsm.state, 'none') - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'noop', from: 'none', to: 'none', current: 'none' }, - { event: 'onBeforeNoop', transition: 'noop', from: 'none', to: 'none', current: 'none' }, - { event: 'onTransition', transition: 'noop', from: 'none', to: 'none', current: 'none' }, - { event: 'onAfterTransition', transition: 'noop', from: 'none', to: 'none', current: 'none' }, - { event: 'onAfterNoop', transition: 'noop', from: 'none', to: 'none', current: 'none' } - ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('lifecycle events using shortcut names', t => { - - var logger = new LifecycleLogger(), - fsm = new StateMachine({ - init: 'solid', - transitions: [ - { name: 'melt', from: 'solid', to: 'liquid' }, - { name: 'freeze', from: 'liquid', to: 'solid' }, - { name: 'vaporize', from: 'liquid', to: 'gas' }, - { name: 'condense', from: 'gas', to: 'liquid' } - ], - methods: { - onNone: logger, - onSolid: logger, - onLiquid: logger, - onGas: logger, - onInit: logger, - onMelt: logger, - onFreeze: logger, - onVaporize: logger, - onCondense: logger - } - }); - - t.is(fsm.state, 'solid') - - t.deepEqual(logger.log, [ - { event: "onSolid", transition: "init", from: "none", to: "solid", current: "solid" }, - { event: "onInit", transition: "init", from: "none", to: "solid", current: "solid" } - ]) - - logger.clear() - fsm.melt() - t.is(fsm.state, 'liquid') - - t.deepEqual(logger.log, [ - { event: "onLiquid", transition: "melt", from: "solid", to: "liquid", current: "liquid" }, - { event: "onMelt", transition: "melt", from: "solid", to: "liquid", current: "liquid" } - ]); - -}) - -//------------------------------------------------------------------------------------------------- - -test('lifecycle events with dash or underscore are camelized', t => { - - var logger = new LifecycleLogger(), - fsm = new StateMachine({ - init: 'has-dash', - transitions: [ - { name: 'do-with-dash', from: 'has-dash', to: 'has_underscore' }, - { name: 'do_with_underscore', from: 'has_underscore', to: 'alreadyCamelized' }, - { name: 'doAlreadyCamelized', from: 'alreadyCamelized', to: 'has-dash' } - ], - methods: { - onBeforeTransition: logger, - onBeforeInit: logger, - onBeforeDoWithDash: logger, - onBeforeDoWithUnderscore: logger, - onBeforeDoAlreadyCamelized: logger, - onLeaveState: logger, - onLeaveNone: logger, - onLeaveHasDash: logger, - onLeaveHasUnderscore: logger, - onLeaveAlreadyCamelized: logger, - onTransition: logger, - onEnterState: logger, - onEnterNone: logger, - onEnterHasDash: logger, - onEnterHasUnderscore: logger, - onEnterAlreadyCamelized: logger, - onAfterTransition: logger, - onAfterInit: logger, - onAfterDoWithDash: logger, - onAfterDoWithUnderscore: logger, - onAfterDoAlreadyCamelized: logger - } - }); - - t.is(fsm.state, 'has-dash') - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'init', from: 'none', to: 'has-dash', current: 'none' }, - { event: 'onBeforeInit', transition: 'init', from: 'none', to: 'has-dash', current: 'none' }, - { event: 'onLeaveState', transition: 'init', from: 'none', to: 'has-dash', current: 'none' }, - { event: 'onLeaveNone', transition: 'init', from: 'none', to: 'has-dash', current: 'none' }, - { event: 'onTransition', transition: 'init', from: 'none', to: 'has-dash', current: 'none' }, - { event: 'onEnterState', transition: 'init', from: 'none', to: 'has-dash', current: 'has-dash' }, - { event: 'onEnterHasDash', transition: 'init', from: 'none', to: 'has-dash', current: 'has-dash' }, - { event: 'onAfterTransition', transition: 'init', from: 'none', to: 'has-dash', current: 'has-dash' }, - { event: 'onAfterInit', transition: 'init', from: 'none', to: 'has-dash', current: 'has-dash' } - ]) - - logger.clear() - fsm.doWithDash() - t.is(fsm.state, 'has_underscore') - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'do-with-dash', from: 'has-dash', to: 'has_underscore', current: 'has-dash' }, - { event: 'onBeforeDoWithDash', transition: 'do-with-dash', from: 'has-dash', to: 'has_underscore', current: 'has-dash' }, - { event: 'onLeaveState', transition: 'do-with-dash', from: 'has-dash', to: 'has_underscore', current: 'has-dash' }, - { event: 'onLeaveHasDash', transition: 'do-with-dash', from: 'has-dash', to: 'has_underscore', current: 'has-dash' }, - { event: 'onTransition', transition: 'do-with-dash', from: 'has-dash', to: 'has_underscore', current: 'has-dash' }, - { event: 'onEnterState', transition: 'do-with-dash', from: 'has-dash', to: 'has_underscore', current: 'has_underscore' }, - { event: 'onEnterHasUnderscore', transition: 'do-with-dash', from: 'has-dash', to: 'has_underscore', current: 'has_underscore' }, - { event: 'onAfterTransition', transition: 'do-with-dash', from: 'has-dash', to: 'has_underscore', current: 'has_underscore' }, - { event: 'onAfterDoWithDash', transition: 'do-with-dash', from: 'has-dash', to: 'has_underscore', current: 'has_underscore' } - ]) - - logger.clear() - fsm.doWithUnderscore() - t.is(fsm.state, 'alreadyCamelized') - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'do_with_underscore', from: 'has_underscore', to: 'alreadyCamelized', current: 'has_underscore' }, - { event: 'onBeforeDoWithUnderscore', transition: 'do_with_underscore', from: 'has_underscore', to: 'alreadyCamelized', current: 'has_underscore' }, - { event: 'onLeaveState', transition: 'do_with_underscore', from: 'has_underscore', to: 'alreadyCamelized', current: 'has_underscore' }, - { event: 'onLeaveHasUnderscore', transition: 'do_with_underscore', from: 'has_underscore', to: 'alreadyCamelized', current: 'has_underscore' }, - { event: 'onTransition', transition: 'do_with_underscore', from: 'has_underscore', to: 'alreadyCamelized', current: 'has_underscore' }, - { event: 'onEnterState', transition: 'do_with_underscore', from: 'has_underscore', to: 'alreadyCamelized', current: 'alreadyCamelized' }, - { event: 'onEnterAlreadyCamelized', transition: 'do_with_underscore', from: 'has_underscore', to: 'alreadyCamelized', current: 'alreadyCamelized' }, - { event: 'onAfterTransition', transition: 'do_with_underscore', from: 'has_underscore', to: 'alreadyCamelized', current: 'alreadyCamelized' }, - { event: 'onAfterDoWithUnderscore', transition: 'do_with_underscore', from: 'has_underscore', to: 'alreadyCamelized', current: 'alreadyCamelized' } - ]) - - logger.clear() - fsm.doAlreadyCamelized() - t.is(fsm.state, 'has-dash') - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'doAlreadyCamelized', from: 'alreadyCamelized', to: 'has-dash', current: 'alreadyCamelized' }, - { event: 'onBeforeDoAlreadyCamelized', transition: 'doAlreadyCamelized', from: 'alreadyCamelized', to: 'has-dash', current: 'alreadyCamelized' }, - { event: 'onLeaveState', transition: 'doAlreadyCamelized', from: 'alreadyCamelized', to: 'has-dash', current: 'alreadyCamelized' }, - { event: 'onLeaveAlreadyCamelized', transition: 'doAlreadyCamelized', from: 'alreadyCamelized', to: 'has-dash', current: 'alreadyCamelized' }, - { event: 'onTransition', transition: 'doAlreadyCamelized', from: 'alreadyCamelized', to: 'has-dash', current: 'alreadyCamelized' }, - { event: 'onEnterState', transition: 'doAlreadyCamelized', from: 'alreadyCamelized', to: 'has-dash', current: 'has-dash' }, - { event: 'onEnterHasDash', transition: 'doAlreadyCamelized', from: 'alreadyCamelized', to: 'has-dash', current: 'has-dash' }, - { event: 'onAfterTransition', transition: 'doAlreadyCamelized', from: 'alreadyCamelized', to: 'has-dash', current: 'has-dash' }, - { event: 'onAfterDoAlreadyCamelized', transition: 'doAlreadyCamelized', from: 'alreadyCamelized', to: 'has-dash', current: 'has-dash' } - ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('lifecycle event names that are all uppercase are camelized', t => { - - var logger = new LifecycleLogger(), - fsm = new StateMachine({ - init: 'FIRST', - transitions: [ - { name: 'GO', from: 'FIRST', to: 'SECOND_STATE' }, - { name: 'DO_IT', from: 'SECOND_STATE', to: 'FIRST' } - ], - methods: { - onBeforeGo: logger, - onBeforeDoIt: logger, - onLeaveFirst: logger, - onLeaveSecondState: logger, - onEnterFirst: logger, - onEnterSecondState: logger, - onAfterGo: logger, - onAfterDoIt: logger - } - }); - - t.is(fsm.state, 'FIRST') - t.deepEqual(logger.log, [ - { event: 'onEnterFirst', transition: 'init', from: 'none', to: 'FIRST', current: 'FIRST' }, - ]) - - logger.clear() - fsm.go() - t.is(fsm.state, 'SECOND_STATE') - t.deepEqual(logger.log, [ - { event: 'onBeforeGo', transition: 'GO', from: 'FIRST', to: 'SECOND_STATE', current: 'FIRST' }, - { event: 'onLeaveFirst', transition: 'GO', from: 'FIRST', to: 'SECOND_STATE', current: 'FIRST' }, - { event: 'onEnterSecondState', transition: 'GO', from: 'FIRST', to: 'SECOND_STATE', current: 'SECOND_STATE' }, - { event: 'onAfterGo', transition: 'GO', from: 'FIRST', to: 'SECOND_STATE', current: 'SECOND_STATE' } - ]) - - logger.clear(); - fsm.doIt(); - t.is(fsm.state, 'FIRST') - t.deepEqual(logger.log, [ - { event: 'onBeforeDoIt', transition: 'DO_IT', from: 'SECOND_STATE', to: 'FIRST', current: 'SECOND_STATE' }, - { event: 'onLeaveSecondState', transition: 'DO_IT', from: 'SECOND_STATE', to: 'FIRST', current: 'SECOND_STATE' }, - { event: 'onEnterFirst', transition: 'DO_IT', from: 'SECOND_STATE', to: 'FIRST', current: 'FIRST' }, - { event: 'onAfterDoIt', transition: 'DO_IT', from: 'SECOND_STATE', to: 'FIRST', current: 'FIRST' } - ]) - -}); - -//------------------------------------------------------------------------------------------------- - -test('lifecycle events receive arbitrary transition arguments', t => { - - var logger = new LifecycleLogger(), - fsm = new StateMachine({ - transitions: [ - { name: 'init', from: 'none', to: 'A' }, - { name: 'step', from: 'A', to: 'B' } - ], - methods: { - onBeforeTransition: logger, - onBeforeInit: logger, - onBeforeStep: logger, - onLeaveState: logger, - onLeaveNone: logger, - onLeaveA: logger, - onLeaveB: logger, - onTransition: logger, - onEnterState: logger, - onEnterNone: logger, - onEnterA: logger, - onEnterB: logger, - onAfterTransition: logger, - onAfterInit: logger, - onAfterStep: logger - } - }); - - t.is(fsm.state, 'none') - t.deepEqual(logger.log, []) - - fsm.init() - - t.is(fsm.state, 'A') - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'init', from: 'none', to: 'A', current: 'none' }, - { event: 'onBeforeInit', transition: 'init', from: 'none', to: 'A', current: 'none' }, - { event: 'onLeaveState', transition: 'init', from: 'none', to: 'A', current: 'none' }, - { event: 'onLeaveNone', transition: 'init', from: 'none', to: 'A', current: 'none' }, - { event: 'onTransition', transition: 'init', from: 'none', to: 'A', current: 'none' }, - { event: 'onEnterState', transition: 'init', from: 'none', to: 'A', current: 'A' }, - { event: 'onEnterA', transition: 'init', from: 'none', to: 'A', current: 'A' }, - { event: 'onAfterTransition', transition: 'init', from: 'none', to: 'A', current: 'A' }, - { event: 'onAfterInit', transition: 'init', from: 'none', to: 'A', current: 'A' } - ]) - logger.clear() - - fsm.step('with', 4, 'more', 'arguments') - - t.is(fsm.state, 'B') - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'step', from: 'A', to: 'B', current: 'A', args: [ 'with', 4, 'more', 'arguments' ] }, - { event: 'onBeforeStep', transition: 'step', from: 'A', to: 'B', current: 'A', args: [ 'with', 4, 'more', 'arguments' ] }, - { event: 'onLeaveState', transition: 'step', from: 'A', to: 'B', current: 'A', args: [ 'with', 4, 'more', 'arguments' ] }, - { event: 'onLeaveA', transition: 'step', from: 'A', to: 'B', current: 'A', args: [ 'with', 4, 'more', 'arguments' ] }, - { event: 'onTransition', transition: 'step', from: 'A', to: 'B', current: 'A', args: [ 'with', 4, 'more', 'arguments' ] }, - { event: 'onEnterState', transition: 'step', from: 'A', to: 'B', current: 'B', args: [ 'with', 4, 'more', 'arguments' ] }, - { event: 'onEnterB', transition: 'step', from: 'A', to: 'B', current: 'B', args: [ 'with', 4, 'more', 'arguments' ] }, - { event: 'onAfterTransition', transition: 'step', from: 'A', to: 'B', current: 'B', args: [ 'with', 4, 'more', 'arguments' ] }, - { event: 'onAfterStep', transition: 'step', from: 'A', to: 'B', current: 'B', args: [ 'with', 4, 'more', 'arguments' ] } - ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('lifecycle events are cancelable', t => { - - var FSM = StateMachine.factory({ - transitions: [ - { name: 'step', from: 'none', to: 'complete' } - ], - data: function(cancel) { - return { - cancel: cancel, - logger: new LifecycleLogger() - } - }, - methods: { - onBeforeTransition: function(lifecycle) { this.logger(lifecycle); return lifecycle.event !== this.cancel }, - onBeforeStep: function(lifecycle) { this.logger(lifecycle); return lifecycle.event !== this.cancel }, - onEnterState: function(lifecycle) { this.logger(lifecycle); return lifecycle.event !== this.cancel }, - onEnterNone: function(lifecycle) { this.logger(lifecycle); return lifecycle.event !== this.cancel }, - onEnterComplete: function(lifecycle) { this.logger(lifecycle); return lifecycle.event !== this.cancel }, - onTransition: function(lifecycle) { this.logger(lifecycle); return lifecycle.event !== this.cancel }, - onLeaveState: function(lifecycle) { this.logger(lifecycle); return lifecycle.event !== this.cancel }, - onLeaveNone: function(lifecycle) { this.logger(lifecycle); return lifecycle.event !== this.cancel }, - onLeaveComplete: function(lifecycle) { this.logger(lifecycle); return lifecycle.event !== this.cancel }, - onAfterTransition: function(lifecycle) { this.logger(lifecycle); return lifecycle.event !== this.cancel }, - onAfterStep: function(lifecycle) { this.logger(lifecycle); return lifecycle.event !== this.cancel } - } - }); - - var cancelledBeforeTransition = new FSM('onBeforeTransition'), - cancelledBeforeStep = new FSM('onBeforeStep'), - cancelledLeaveState = new FSM('onLeaveState'), - cancelledLeaveNone = new FSM('onLeaveNone'), - cancelledTransition = new FSM('onTransition'), - cancelledEnterState = new FSM('onEnterState'), - cancelledEnterComplete = new FSM('onEnterComplete'), - cancelledAfterTransition = new FSM('onAfterTransition'), - cancelledAfterStep = new FSM('onAfterStep'); - - t.is(cancelledBeforeTransition.state, 'none') - t.is(cancelledBeforeStep.state, 'none') - t.is(cancelledLeaveState.state, 'none') - t.is(cancelledLeaveNone.state, 'none') - t.is(cancelledTransition.state, 'none') - t.is(cancelledEnterState.state, 'none') - t.is(cancelledEnterComplete.state, 'none') - t.is(cancelledAfterTransition.state, 'none') - t.is(cancelledAfterStep.state, 'none') - - cancelledBeforeTransition.step() - cancelledBeforeStep.step() - cancelledLeaveState.step() - cancelledLeaveNone.step() - cancelledTransition.step() - cancelledEnterState.step() - cancelledEnterComplete.step() - cancelledAfterTransition.step() - cancelledAfterStep.step() - - t.is(cancelledBeforeTransition.state, 'none') - t.is(cancelledBeforeStep.state, 'none') - t.is(cancelledLeaveState.state, 'none') - t.is(cancelledLeaveNone.state, 'none') - t.is(cancelledTransition.state, 'none') - t.is(cancelledEnterState.state, 'complete') - t.is(cancelledEnterComplete.state, 'complete') - t.is(cancelledAfterTransition.state, 'complete') - t.is(cancelledAfterStep.state, 'complete') - - t.deepEqual(cancelledBeforeTransition.logger.log, [ - { event: 'onBeforeTransition', transition: 'step', from: 'none', to: 'complete', current: 'none' } - ]) - - t.deepEqual(cancelledBeforeStep.logger.log, [ - { event: 'onBeforeTransition', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onBeforeStep', transition: 'step', from: 'none', to: 'complete', current: 'none' } - ]) - - t.deepEqual(cancelledLeaveState.logger.log, [ - { event: 'onBeforeTransition', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onBeforeStep', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onLeaveState', transition: 'step', from: 'none', to: 'complete', current: 'none' } - ]) - - t.deepEqual(cancelledLeaveNone.logger.log, [ - { event: 'onBeforeTransition', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onBeforeStep', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onLeaveState', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onLeaveNone', transition: 'step', from: 'none', to: 'complete', current: 'none' } - ]) - - t.deepEqual(cancelledTransition.logger.log, [ - { event: 'onBeforeTransition', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onBeforeStep', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onLeaveState', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onLeaveNone', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onTransition', transition: 'step', from: 'none', to: 'complete', current: 'none' } - ]) - - t.deepEqual(cancelledEnterState.logger.log, [ - { event: 'onBeforeTransition', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onBeforeStep', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onLeaveState', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onLeaveNone', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onTransition', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onEnterState', transition: 'step', from: 'none', to: 'complete', current: 'complete' } - ]) - - t.deepEqual(cancelledEnterComplete.logger.log, [ - { event: 'onBeforeTransition', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onBeforeStep', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onLeaveState', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onLeaveNone', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onTransition', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onEnterState', transition: 'step', from: 'none', to: 'complete', current: 'complete' }, - { event: 'onEnterComplete', transition: 'step', from: 'none', to: 'complete', current: 'complete' } - ]) - - t.deepEqual(cancelledAfterTransition.logger.log, [ - { event: 'onBeforeTransition', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onBeforeStep', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onLeaveState', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onLeaveNone', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onTransition', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onEnterState', transition: 'step', from: 'none', to: 'complete', current: 'complete' }, - { event: 'onEnterComplete', transition: 'step', from: 'none', to: 'complete', current: 'complete' }, - { event: 'onAfterTransition', transition: 'step', from: 'none', to: 'complete', current: 'complete' } - ]) - - t.deepEqual(cancelledAfterStep.logger.log, [ - { event: 'onBeforeTransition', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onBeforeStep', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onLeaveState', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onLeaveNone', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onTransition', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onEnterState', transition: 'step', from: 'none', to: 'complete', current: 'complete' }, - { event: 'onEnterComplete', transition: 'step', from: 'none', to: 'complete', current: 'complete' }, - { event: 'onAfterTransition', transition: 'step', from: 'none', to: 'complete', current: 'complete' }, - { event: 'onAfterStep', transition: 'step', from: 'none', to: 'complete', current: 'complete' } - ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('lifecycle events can be deferred using a promise', t => { - return new Promise(function(resolveTest, rejectTest) { - - var logger = new LifecycleLogger(), - start = Date.now(), - pause = function(ms) { return new Promise(function(resolve, reject) { setTimeout(function() { resolve('resolved') }, ms); }); }, - fsm = new StateMachine({ - transitions: [ - { name: 'step', from: 'none', to: 'complete' } - ], - methods: { - onBeforeTransition: function(lifecycle, a, b) { logger(lifecycle, a, b); return pause(100); }, - onBeforeStep: function(lifecycle, a, b) { logger(lifecycle, a, b); return pause(100); }, - onEnterState: function(lifecycle, a, b) { logger(lifecycle, a, b); return pause(100); }, - onEnterNone: function(lifecycle, a, b) { logger(lifecycle, a, b); return pause(100); }, - onEnterComplete: function(lifecycle, a, b) { logger(lifecycle, a, b); return pause(100); }, - onTransition: function(lifecycle, a, b) { logger(lifecycle, a, b); return pause(100); }, - onLeaveState: function(lifecycle, a, b) { logger(lifecycle, a, b); return pause(100); }, - onLeaveNone: function(lifecycle, a, b) { logger(lifecycle, a, b); return pause(100); }, - onLeaveComplete: function(lifecycle, a, b) { logger(lifecycle, a, b); return pause(100); }, - onAfterTransition: function(lifecycle, a, b) { logger(lifecycle, a, b); return pause(100); }, - onAfterStep: function(lifecycle, a, b) { logger(lifecycle, a, b); return pause(100); } - } - }); - - function done(answer) { - var duration = Date.now() - start; - t.is(fsm.state, 'complete') - t.is(duration > 600, true) - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onBeforeStep', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onLeaveState', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onLeaveNone', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onTransition', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onEnterState', transition: 'step', from: 'none', to: 'complete', current: 'complete', args: [ 'additional', 'arguments' ] }, - { event: 'onEnterComplete', transition: 'step', from: 'none', to: 'complete', current: 'complete', args: [ 'additional', 'arguments' ] }, - { event: 'onAfterTransition', transition: 'step', from: 'none', to: 'complete', current: 'complete', args: [ 'additional', 'arguments' ] }, - { event: 'onAfterStep', transition: 'step', from: 'none', to: 'complete', current: 'complete', args: [ 'additional', 'arguments' ] }, - ]) - t.is(answer, 'resolved'); - resolveTest() - } - - fsm.step('additional', 'arguments') - .then(done); - - }); -}); - -//------------------------------------------------------------------------------------------------- - -test('lifecycle events can be cancelled using a promise', t => { - return new Promise(function(resolveTest, rejectTest) { - - var logger = new LifecycleLogger(), - start = Date.now(), - pause = function(ms) { - return new Promise(function(resolve, reject) { - setTimeout(function() { resolve('resolved') }, ms); - }); - }, - cancel = function(ms) { - return new Promise(function(resolve, reject) { - setTimeout(function() { reject('rejected'); }, ms); - }); - }, - fsm = new StateMachine({ - transitions: [ - { name: 'step', from: 'none', to: 'complete' } - ], - methods: { - onBeforeTransition: function(lifecycle, a, b) { logger(lifecycle, a, b); return pause(100); }, - onBeforeStep: function(lifecycle, a, b) { logger(lifecycle, a, b); return pause(100); }, - onEnterState: function(lifecycle, a, b) { logger(lifecycle, a, b); return pause(100); }, - onEnterNone: function(lifecycle, a, b) { logger(lifecycle, a, b); return pause(100); }, - onEnterComplete: function(lifecycle, a, b) { logger(lifecycle, a, b); return pause(100); }, - onTransition: function(lifecycle, a, b) { logger(lifecycle, a, b); return cancel(100); }, - onLeaveState: function(lifecycle, a, b) { logger(lifecycle, a, b); }, - onLeaveNone: function(lifecycle, a, b) { logger(lifecycle, a, b); }, - onLeaveComplete: function(lifecycle, a, b) { logger(lifecycle, a, b); }, - onAfterTransition: function(lifecycle, a, b) { logger(lifecycle, a, b); }, - onAfterStep: function(lifecycle, a, b) { logger(lifecycle, a, b); } - } - }); - - function done(answer) { - var duration = Date.now() - start; - t.is(fsm.state, 'none'); - t.is(duration > 300, true); - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onBeforeStep', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onLeaveState', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onLeaveNone', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onTransition', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] } - ]); - t.is(answer, 'rejected'); - resolveTest(); - } - - fsm.step('additional', 'arguments') - .then(function() { done('promise was rejected so this should never happen'); }) - .catch(done) - - }) -}) - -//------------------------------------------------------------------------------------------------- - -test('transition cannot fire while lifecycle event is in progress', t => { - - t.plan(20); - - var fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step', from: 'A', to: 'B' }, - { name: 'other', from: '*', to: 'X' } - ], - methods: { - - onBeforeStep: function(lifecycle) { - t.false(this.can('other')); - const error = t.throws(function() { - fsm.other(); - }); - t.is(error.message, 'transition is invalid while previous transition is still in progress'); - t.is(error.transition, 'other'); - t.is(error.from, 'A'); - t.is(error.to, 'X'); - t.is(error.current, 'A'); - }, - - onAfterStep: function(lifecycle) { - t.false(this.can('other')); - const error = t.throws(function() { - fsm.other(); - }); - t.is(error.message, 'transition is invalid while previous transition is still in progress'); - t.is(error.transition, 'other'); - t.is(error.from, 'B'); - t.is(error.to, 'X'); - t.is(error.current, 'B'); - }, - - onBeforeOther: function(lifecycle) { t.fail('should never happen') }, - onAfterOther: function(lifecycle) { t.fail('should never happen') }, - onLeaveA: function(lifecycle) { t.false(this.can('other')) }, - onEnterB: function(lifecycle) { t.false(this.can('other')) }, - onLeaveB: function(lifecycle) { t.fail('should never happen') }, - onEnterX: function(lifecycle) { t.fail('should never happen') }, - onLeaveX: function(lifecycle) { t.fail('should never happen') } - - } - }); - - t.is(fsm.state, 'A') - t.true(fsm.can('other')) - - fsm.step() - - t.is(fsm.state, 'B') - t.true(fsm.can('other')) - -}) - -//------------------------------------------------------------------------------------------------- - -test('transition cannot fire while asynchronous lifecycle event is in progress', t => { - return new Promise(function(resolveTest, rejectTest) { - - t.plan(20); - - var fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step', from: 'A', to: 'B' }, - { name: 'other', from: '*', to: 'X' } - ], - methods: { - - onBeforeStep: function(lifecycle) { - return new Promise(function(resolve, reject) { - setTimeout(function() { - t.false(fsm.can('other')); - const error = t.throws(function() { - fsm.other(); - }); - t.is(error.message, 'transition is invalid while previous transition is still in progress'); - t.is(error.transition, 'other'); - t.is(error.from, 'A'); - t.is(error.to, 'X'); - t.is(error.current, 'A'); - resolve(); - }, 200); - }); - }, - - onAfterStep: function(lifecycle) { - return new Promise(function(resolve, reject) { - setTimeout(function() { - t.false(fsm.can('other')); - const error = t.throws(function() { - fsm.other(); - }); - t.is(error.message, 'transition is invalid while previous transition is still in progress'); - t.is(error.transition, 'other'); - t.is(error.from, 'B'); - t.is(error.to, 'X'); - t.is(error.current, 'B'); - resolve(); - setTimeout(done, 0); // HACK - let lifecycle finish before calling done() - }, 200); - }); - }, - - onBeforeOther: function(lifecycle) { t.fail('should never happen') }, - onAfterOther: function(lifecycle) { t.fail('should never happen') }, - onLeaveA: function(lifecycle) { t.false(this.can('other')) }, - onEnterB: function(lifecycle) { t.false(this.can('other')) }, - onLeaveB: function(lifecycle) { t.fail('should never happen') }, - onEnterX: function(lifecycle) { t.fail('should never happen') }, - onLeaveX: function(lifecycle) { t.fail('should never happen') } - - } - }); - - t.is(fsm.state, 'A') - t.true(fsm.can('other')) - - function done() { - t.is(fsm.state, 'B'); - t.true(fsm.can('other')); - resolveTest(); - } - - fsm.step(); // kick off the async behavior - - }) -}) - -//------------------------------------------------------------------------------------------------- - -test('lifecycle events for transitions with multiple :from or :to states', t => { - - var logger = new LifecycleLogger(), - fsm = new StateMachine({ - init: 'hungry', - transitions: [ - { name: 'eat', from: 'hungry', to: 'satisfied' }, - { name: 'eat', from: 'satisfied', to: 'full' }, - { name: 'rest', from: [ 'satisfied', 'full' ], to: 'hungry' } - ], - methods: { - onBeforeTransition: logger, - onBeforeEat: logger, - onBeforeRest: logger, - onLeaveState: logger, - onLeaveHungry: logger, - onLeaveSatisfied: logger, - onLeaveFull: logger, - onTransition: logger, - onEnterState: logger, - onEnterHungry: logger, - onEnterSatisfied: logger, - onEnterFull: logger, - onAfterTransition: logger, - onAfterEat: logger, - onAfterRest: logger - } - }); - - - t.is(fsm.state, 'hungry') - logger.clear() - - fsm.eat() - t.is(fsm.state, 'satisfied') - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'eat', from: 'hungry', to: 'satisfied', current: 'hungry' }, - { event: 'onBeforeEat', transition: 'eat', from: 'hungry', to: 'satisfied', current: 'hungry' }, - { event: 'onLeaveState', transition: 'eat', from: 'hungry', to: 'satisfied', current: 'hungry' }, - { event: 'onLeaveHungry', transition: 'eat', from: 'hungry', to: 'satisfied', current: 'hungry' }, - { event: 'onTransition', transition: 'eat', from: 'hungry', to: 'satisfied', current: 'hungry' }, - { event: 'onEnterState', transition: 'eat', from: 'hungry', to: 'satisfied', current: 'satisfied' }, - { event: 'onEnterSatisfied', transition: 'eat', from: 'hungry', to: 'satisfied', current: 'satisfied' }, - { event: 'onAfterTransition', transition: 'eat', from: 'hungry', to: 'satisfied', current: 'satisfied' }, - { event: 'onAfterEat', transition: 'eat', from: 'hungry', to: 'satisfied', current: 'satisfied' } - ]) - - logger.clear() - fsm.eat() - t.is(fsm.state, 'full') - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'eat', from: 'satisfied', to: 'full', current: 'satisfied' }, - { event: 'onBeforeEat', transition: 'eat', from: 'satisfied', to: 'full', current: 'satisfied' }, - { event: 'onLeaveState', transition: 'eat', from: 'satisfied', to: 'full', current: 'satisfied' }, - { event: 'onLeaveSatisfied', transition: 'eat', from: 'satisfied', to: 'full', current: 'satisfied' }, - { event: 'onTransition', transition: 'eat', from: 'satisfied', to: 'full', current: 'satisfied' }, - { event: 'onEnterState', transition: 'eat', from: 'satisfied', to: 'full', current: 'full' }, - { event: 'onEnterFull', transition: 'eat', from: 'satisfied', to: 'full', current: 'full' }, - { event: 'onAfterTransition', transition: 'eat', from: 'satisfied', to: 'full', current: 'full' }, - { event: 'onAfterEat', transition: 'eat', from: 'satisfied', to: 'full', current: 'full' } - ]) - - logger.clear() - fsm.rest() - t.is(fsm.state, 'hungry') - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'rest', from: 'full', to: 'hungry', current: 'full' }, - { event: 'onBeforeRest', transition: 'rest', from: 'full', to: 'hungry', current: 'full' }, - { event: 'onLeaveState', transition: 'rest', from: 'full', to: 'hungry', current: 'full' }, - { event: 'onLeaveFull', transition: 'rest', from: 'full', to: 'hungry', current: 'full' }, - { event: 'onTransition', transition: 'rest', from: 'full', to: 'hungry', current: 'full' }, - { event: 'onEnterState', transition: 'rest', from: 'full', to: 'hungry', current: 'hungry' }, - { event: 'onEnterHungry', transition: 'rest', from: 'full', to: 'hungry', current: 'hungry' }, - { event: 'onAfterTransition', transition: 'rest', from: 'full', to: 'hungry', current: 'hungry' }, - { event: 'onAfterRest', transition: 'rest', from: 'full', to: 'hungry', current: 'hungry' } - ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('lifecycle events for factory generated state machines', t => { - - var FSM = StateMachine.factory({ - transitions: [ - { name: 'stepA', from: 'none', to: 'A' }, - { name: 'stepB', from: 'none', to: 'B' } - ], - data: function(name) { - return { - name: name, - logger: new LifecycleLogger() - } - }, - methods: { - onBeforeTransition: function(lifecycle) { this.logger(lifecycle) }, - onBeforeStepA: function(lifecycle) { this.logger(lifecycle) }, - onBeforeStepB: function(lifecycle) { this.logger(lifecycle) }, - onLeaveState: function(lifecycle) { this.logger(lifecycle) }, - onLeaveNone: function(lifecycle) { this.logger(lifecycle) }, - onLeaveA: function(lifecycle) { this.logger(lifecycle) }, - onLeaveB: function(lifecycle) { this.logger(lifecycle) }, - onTransition: function(lifecycle) { this.logger(lifecycle) }, - onEnterState: function(lifecycle) { this.logger(lifecycle) }, - onEnterNone: function(lifecycle) { this.logger(lifecycle) }, - onEnterA: function(lifecycle) { this.logger(lifecycle) }, - onEnterB: function(lifecycle) { this.logger(lifecycle) }, - onAfterTransition: function(lifecycle) { this.logger(lifecycle) }, - onAfterStepA: function(lifecycle) { this.logger(lifecycle) }, - onAfterStepB: function(lifecycle) { this.logger(lifecycle) } - } - }); - - var a = new FSM('a'), - b = new FSM('b'); - - t.is(a.state, 'none') - t.is(b.state, 'none') - - t.deepEqual(a.logger.log, []) - t.deepEqual(b.logger.log, []) - - a.stepA() - b.stepB() - - t.is(a.state, 'A') - t.is(b.state, 'B') - - t.deepEqual(a.logger.log, [ - { event: 'onBeforeTransition', transition: 'stepA', from: 'none', to: 'A', current: 'none' }, - { event: 'onBeforeStepA', transition: 'stepA', from: 'none', to: 'A', current: 'none' }, - { event: 'onLeaveState', transition: 'stepA', from: 'none', to: 'A', current: 'none' }, - { event: 'onLeaveNone', transition: 'stepA', from: 'none', to: 'A', current: 'none' }, - { event: 'onTransition', transition: 'stepA', from: 'none', to: 'A', current: 'none' }, - { event: 'onEnterState', transition: 'stepA', from: 'none', to: 'A', current: 'A' }, - { event: 'onEnterA', transition: 'stepA', from: 'none', to: 'A', current: 'A' }, - { event: 'onAfterTransition', transition: 'stepA', from: 'none', to: 'A', current: 'A' }, - { event: 'onAfterStepA', transition: 'stepA', from: 'none', to: 'A', current: 'A' } - ]) - - t.deepEqual(b.logger.log, [ - { event: 'onBeforeTransition', transition: 'stepB', from: 'none', to: 'B', current: 'none' }, - { event: 'onBeforeStepB', transition: 'stepB', from: 'none', to: 'B', current: 'none' }, - { event: 'onLeaveState', transition: 'stepB', from: 'none', to: 'B', current: 'none' }, - { event: 'onLeaveNone', transition: 'stepB', from: 'none', to: 'B', current: 'none' }, - { event: 'onTransition', transition: 'stepB', from: 'none', to: 'B', current: 'none' }, - { event: 'onEnterState', transition: 'stepB', from: 'none', to: 'B', current: 'B' }, - { event: 'onEnterB', transition: 'stepB', from: 'none', to: 'B', current: 'B' }, - { event: 'onAfterTransition', transition: 'stepB', from: 'none', to: 'B', current: 'B' }, - { event: 'onAfterStepB', transition: 'stepB', from: 'none', to: 'B', current: 'B' } - ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('lifecycle events for custom init transition', t => { - - var logger = new LifecycleLogger(), - fsm = new StateMachine({ - init: { name: 'boot', from: 'booting', to: 'complete' }, - methods: { - onBeforeTransition: logger, - onBeforeInit: logger, - onBeforeBoot: logger, - onLeaveState: logger, - onLeaveNone: logger, - onLeaveBooting: logger, - onLeaveComplete: logger, - onTransition: logger, - onEnterState: logger, - onEnterNone: logger, - onEnterBooting: logger, - onEnterComplete: logger, - onAfterTransition: logger, - onAfterInit: logger, - onAfterBoot: logger - } - }); - - t.is(fsm.state, 'complete') - - t.deepEqual(fsm.allStates(), [ 'booting', 'complete' ]) - t.deepEqual(fsm.allTransitions(), [ 'boot' ]) - t.deepEqual(fsm.transitions(), [ ]) - - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'boot', from: 'booting', to: 'complete', current: 'booting' }, - { event: 'onBeforeBoot', transition: 'boot', from: 'booting', to: 'complete', current: 'booting' }, - { event: 'onLeaveState', transition: 'boot', from: 'booting', to: 'complete', current: 'booting' }, - { event: 'onLeaveBooting', transition: 'boot', from: 'booting', to: 'complete', current: 'booting' }, - { event: 'onTransition', transition: 'boot', from: 'booting', to: 'complete', current: 'booting' }, - { event: 'onEnterState', transition: 'boot', from: 'booting', to: 'complete', current: 'complete' }, - { event: 'onEnterComplete', transition: 'boot', from: 'booting', to: 'complete', current: 'complete' }, - { event: 'onAfterTransition', transition: 'boot', from: 'booting', to: 'complete', current: 'complete' }, - { event: 'onAfterBoot', transition: 'boot', from: 'booting', to: 'complete', current: 'complete' } - ]) - -}) - -//------------------------------------------------------------------------------------------------- diff --git a/test/observers.js b/test/observers.js deleted file mode 100644 index f20ab6f..0000000 --- a/test/observers.js +++ /dev/null @@ -1,151 +0,0 @@ -import test from 'ava' -import StateMachine from '../src/app' -import LifecycleLogger from './helpers/lifecycle_logger' - -//------------------------------------------------------------------------------------------------- - -test('lifecycle events can be observed by external observer methods', t => { - - var logger = new LifecycleLogger(), - fsm = new StateMachine({ - transitions: [ - { name: 'step', from: 'none', to: 'complete' } - ] - }); - - fsm.observe("onBeforeTransition", logger) - fsm.observe("onBeforeStep", logger) - fsm.observe("onLeaveState", logger) - fsm.observe("onLeaveNone", logger) - fsm.observe("onLeaveComplete", logger) - fsm.observe("onTransition", logger) - fsm.observe("onEnterState", logger) - fsm.observe("onEnterNone", logger) - fsm.observe("onEnterComplete", logger) - fsm.observe("onAfterTransition", logger) - fsm.observe("onAfterStep", logger) - - fsm.step('additional', 'arguments') - - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onBeforeStep', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onLeaveState', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onLeaveNone', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onTransition', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onEnterState', transition: 'step', from: 'none', to: 'complete', current: 'complete', args: [ 'additional', 'arguments' ] }, - { event: 'onEnterComplete', transition: 'step', from: 'none', to: 'complete', current: 'complete', args: [ 'additional', 'arguments' ] }, - { event: 'onAfterTransition', transition: 'step', from: 'none', to: 'complete', current: 'complete', args: [ 'additional', 'arguments' ] }, - { event: 'onAfterStep', transition: 'step', from: 'none', to: 'complete', current: 'complete', args: [ 'additional', 'arguments' ] } - ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('lifecycle events can be observed by external observer classes', t => { - - var logger = new LifecycleLogger(), - fsm = new StateMachine({ - transitions: [ - { name: 'step', from: 'none', to: 'complete' } - ] - }); - - fsm.observe({ - "onBeforeTransition": logger, - "onBeforeStep": logger, - "onLeaveState": logger, - "onLeaveNone": logger, - "onLeaveComplete": logger, - "onTransition": logger, - "onEnterState": logger, - "onEnterNone": logger, - "onEnterComplete": logger, - "onAfterTransition": logger, - "onAfterStep": logger, - }) - - fsm.step('additional', 'arguments') - - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onBeforeStep', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onLeaveState', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onLeaveNone', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onTransition', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onEnterState', transition: 'step', from: 'none', to: 'complete', current: 'complete', args: [ 'additional', 'arguments' ] }, - { event: 'onEnterComplete', transition: 'step', from: 'none', to: 'complete', current: 'complete', args: [ 'additional', 'arguments' ] }, - { event: 'onAfterTransition', transition: 'step', from: 'none', to: 'complete', current: 'complete', args: [ 'additional', 'arguments' ] }, - { event: 'onAfterStep', transition: 'step', from: 'none', to: 'complete', current: 'complete', args: [ 'additional', 'arguments' ] } - ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('lifecycle events can be observed by multiple observers', t => { - - var logger1 = new LifecycleLogger(), - logger2 = new LifecycleLogger(), - fsm = new StateMachine({ - transitions: [ - { name: 'step', from: 'none', to: 'complete' } - ] - }); - - fsm.observe("onBeforeTransition", logger1) - fsm.observe("onBeforeStep", logger1) - fsm.observe("onLeaveState", logger1) - fsm.observe("onLeaveNone", logger1) - fsm.observe("onLeaveComplete", logger1) - fsm.observe("onTransition", logger1) - fsm.observe("onEnterState", logger1) - fsm.observe("onEnterNone", logger1) - fsm.observe("onEnterComplete", logger1) - fsm.observe("onAfterTransition", logger1) - fsm.observe("onAfterStep", logger1) - - fsm.observe({ - "onBeforeTransition": logger2, - "onBeforeStep": logger2, - "onLeaveState": logger2, - "onLeaveNone": logger2, - "onLeaveComplete": logger2, - "onTransition": logger2, - "onEnterState": logger2, - "onEnterNone": logger2, - "onEnterComplete": logger2, - "onAfterTransition": logger2, - "onAfterStep": logger2, - }) - - fsm.step('additional', 'arguments') - - t.deepEqual(logger1.log, [ - { event: 'onBeforeTransition', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onBeforeStep', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onLeaveState', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onLeaveNone', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onTransition', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onEnterState', transition: 'step', from: 'none', to: 'complete', current: 'complete', args: [ 'additional', 'arguments' ] }, - { event: 'onEnterComplete', transition: 'step', from: 'none', to: 'complete', current: 'complete', args: [ 'additional', 'arguments' ] }, - { event: 'onAfterTransition', transition: 'step', from: 'none', to: 'complete', current: 'complete', args: [ 'additional', 'arguments' ] }, - { event: 'onAfterStep', transition: 'step', from: 'none', to: 'complete', current: 'complete', args: [ 'additional', 'arguments' ] } - ]) - - t.deepEqual(logger2.log, [ - { event: 'onBeforeTransition', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onBeforeStep', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onLeaveState', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onLeaveNone', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onTransition', transition: 'step', from: 'none', to: 'complete', current: 'none', args: [ 'additional', 'arguments' ] }, - { event: 'onEnterState', transition: 'step', from: 'none', to: 'complete', current: 'complete', args: [ 'additional', 'arguments' ] }, - { event: 'onEnterComplete', transition: 'step', from: 'none', to: 'complete', current: 'complete', args: [ 'additional', 'arguments' ] }, - { event: 'onAfterTransition', transition: 'step', from: 'none', to: 'complete', current: 'complete', args: [ 'additional', 'arguments' ] }, - { event: 'onAfterStep', transition: 'step', from: 'none', to: 'complete', current: 'complete', args: [ 'additional', 'arguments' ] } - ]) - -}) - -//------------------------------------------------------------------------------------------------- diff --git a/test/plugin/history.js b/test/plugin/history.js deleted file mode 100644 index 00f73ef..0000000 --- a/test/plugin/history.js +++ /dev/null @@ -1,493 +0,0 @@ -import test from 'ava' -import StateMachine from '../../src/app' -import StateMachineHistory from '../../src/plugin/history' -import LifecycleLogger from '../helpers/lifecycle_logger' - -//------------------------------------------------------------------------------------------------- - -test('history', t => { - - var fsm = new StateMachine({ - init: 'solid', - transitions: [ - { name: 'melt', from: 'solid', to: 'liquid' }, - { name: 'freeze', from: 'liquid', to: 'solid' }, - { name: 'vaporize', from: 'liquid', to: 'gas' }, - { name: 'condense', from: 'gas', to: 'liquid' } - ], - plugins: [ - StateMachineHistory - ] - }) - - t.is(fsm.state, 'solid'); t.deepEqual(fsm.history, [ 'solid' ]) - fsm.melt(); t.is(fsm.state, 'liquid'); t.deepEqual(fsm.history, [ 'solid', 'liquid' ]) - fsm.vaporize(); t.is(fsm.state, 'gas'); t.deepEqual(fsm.history, [ 'solid', 'liquid', 'gas' ]) - fsm.condense(); t.is(fsm.state, 'liquid'); t.deepEqual(fsm.history, [ 'solid', 'liquid', 'gas', 'liquid' ]); - -}) - -//------------------------------------------------------------------------------------------------- - -test('history can be cleared', t => { - - var fsm = new StateMachine({ - transitions: [ - { name: 'init', from: 'none', to: 'A' }, - { name: 'step', from: 'A', to: 'B' }, - { name: 'step', from: 'B', to: 'C' }, - { name: 'step', from: 'C', to: 'A' } - ], - plugins: [ - StateMachineHistory - ] - }) - - fsm.init() - fsm.step() - - t.is(fsm.state, 'B') - t.deepEqual(fsm.history, ['A', 'B']) - - fsm.clearHistory() - - t.is(fsm.state, 'B') - t.deepEqual(fsm.history, []) - -}) - -//------------------------------------------------------------------------------------------------- - -test('history does not record no-op transitions', t => { - - var fsm = new StateMachine({ - init: 'solid', - transitions: [ - { name: 'melt', from: 'solid', to: 'liquid' }, - { name: 'freeze', from: 'liquid', to: 'solid' }, - { name: 'vaporize', from: 'liquid', to: 'gas' }, - { name: 'condense', from: 'gas', to: 'liquid' }, - { name: 'noop', from: '*', to: '*' } - ], - plugins: [ - StateMachineHistory - ] - }) - - t.is(fsm.state, 'solid'); t.deepEqual(fsm.history, [ 'solid' ]) - fsm.noop(); t.is(fsm.state, 'solid'); t.deepEqual(fsm.history, [ 'solid' ]) - fsm.melt(); t.is(fsm.state, 'liquid'); t.deepEqual(fsm.history, [ 'solid', 'liquid' ]) - fsm.noop(); t.is(fsm.state, 'liquid'); t.deepEqual(fsm.history, [ 'solid', 'liquid' ]) - fsm.vaporize(); t.is(fsm.state, 'gas'); t.deepEqual(fsm.history, [ 'solid', 'liquid', 'gas' ]) - fsm.noop(); t.is(fsm.state, 'gas'); t.deepEqual(fsm.history, [ 'solid', 'liquid', 'gas' ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('history with configurable names', t => { - - var fsm = new StateMachine({ - init: 'solid', - transitions: [ - { name: 'melt', from: 'solid', to: 'liquid' }, - { name: 'freeze', from: 'liquid', to: 'solid' }, - { name: 'vaporize', from: 'liquid', to: 'gas' }, - { name: 'condense', from: 'gas', to: 'liquid' } - ], - plugins: [ - new StateMachineHistory({ name: 'memory', future: 'yonder' }) - ] - }) - - t.is(fsm.state, 'solid'); t.deepEqual(fsm.memory, [ 'solid' ]) - fsm.melt(); t.is(fsm.state, 'liquid'); t.deepEqual(fsm.memory, [ 'solid', 'liquid' ]) - fsm.vaporize(); t.is(fsm.state, 'gas'); t.deepEqual(fsm.memory, [ 'solid', 'liquid', 'gas' ]) - fsm.condense(); t.is(fsm.state, 'liquid'); t.deepEqual(fsm.memory, [ 'solid', 'liquid', 'gas', 'liquid' ]) - - t.is(fsm.canMemoryBack, true) - t.is(fsm.canMemoryForward, false) - t.deepEqual(fsm.yonder, [ ]) - - fsm.memoryBack() - - t.is(fsm.state, 'gas') - t.deepEqual(fsm.memory, [ 'solid', 'liquid', 'gas' ]) - t.deepEqual(fsm.yonder, [ 'liquid' ]) - - fsm.clearMemory() - t.deepEqual(fsm.memory, []) - t.deepEqual(fsm.yonder, []) - -}) - -//------------------------------------------------------------------------------------------------- - -test('history, by default, just keeps growing', t => { - - var fsm = new StateMachine({ - init: 'solid', - transitions: [ - { name: 'melt', from: 'solid', to: 'liquid' }, - { name: 'freeze', from: 'liquid', to: 'solid' }, - { name: 'vaporize', from: 'liquid', to: 'gas' }, - { name: 'condense', from: 'gas', to: 'liquid' } - ], - plugins: [ - new StateMachineHistory() - ] - }) - - t.is(fsm.state, 'solid') - t.deepEqual(fsm.history, [ 'solid' ]) - - fsm.melt(); t.deepEqual(fsm.history, [ 'solid', 'liquid' ]) - fsm.vaporize(); t.deepEqual(fsm.history, [ 'solid', 'liquid', 'gas' ]) - fsm.condense(); t.deepEqual(fsm.history, [ 'solid', 'liquid', 'gas', 'liquid' ]) - fsm.freeze(); t.deepEqual(fsm.history, [ 'solid', 'liquid', 'gas', 'liquid', 'solid' ]) - fsm.melt(); t.deepEqual(fsm.history, [ 'solid', 'liquid', 'gas', 'liquid', 'solid', 'liquid' ]) - fsm.vaporize(); t.deepEqual(fsm.history, [ 'solid', 'liquid', 'gas', 'liquid', 'solid', 'liquid', 'gas' ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('history can be limited to N entries', t => { - - var fsm = new StateMachine({ - init: 'solid', - transitions: [ - { name: 'melt', from: 'solid', to: 'liquid' }, - { name: 'freeze', from: 'liquid', to: 'solid' }, - { name: 'vaporize', from: 'liquid', to: 'gas' }, - { name: 'condense', from: 'gas', to: 'liquid' } - ], - plugins: [ - new StateMachineHistory({ max: 3 }) - ] - }) - - t.is(fsm.state, 'solid') - t.deepEqual(fsm.history, [ 'solid' ]) - - fsm.melt(); t.deepEqual(fsm.history, [ 'solid', 'liquid' ]) - fsm.vaporize(); t.deepEqual(fsm.history, [ 'solid', 'liquid', 'gas' ]) - fsm.condense(); t.deepEqual(fsm.history, [ 'liquid', 'gas', 'liquid' ]) - fsm.freeze(); t.deepEqual(fsm.history, [ 'gas', 'liquid', 'solid' ]) - fsm.melt(); t.deepEqual(fsm.history, [ 'liquid', 'solid', 'liquid' ]) - fsm.vaporize(); t.deepEqual(fsm.history, [ 'solid', 'liquid', 'gas' ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('history back and forward', t => { - - var fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step', from: 'A', to: 'B' }, - { name: 'step', from: 'B', to: 'C' }, - { name: 'step', from: 'C', to: 'D' } - ], - plugins: [ - StateMachineHistory - ] - }) - - t.is(fsm.state, 'A') - t.is(fsm.canHistoryBack, false) - t.deepEqual(fsm.history, [ 'A' ]) - t.deepEqual(fsm.future, [ ]) - - var error = t.throws(() => { - fsm.historyBack() - }) - t.is(error.message, 'no history') - - fsm.step() - fsm.step() - fsm.step() - - t.is(fsm.state, 'D') - t.is(fsm.canHistoryBack, true) - t.is(fsm.canHistoryForward, false) - t.deepEqual(fsm.history, [ 'A', 'B', 'C', 'D' ]) - t.deepEqual(fsm.future, []) - - fsm.historyBack() - t.is(fsm.state, 'C') - t.deepEqual(fsm.history, [ 'A', 'B', 'C' ]) - t.deepEqual(fsm.future, [ 'D' ]) - t.is(fsm.canHistoryBack, true) - t.is(fsm.canHistoryForward, true) - - fsm.historyBack() - t.is(fsm.state, 'B') - t.deepEqual(fsm.history, [ 'A', 'B' ]) - t.deepEqual(fsm.future, [ 'D', 'C' ]) - t.is(fsm.canHistoryBack, true) - t.is(fsm.canHistoryForward, true) - - fsm.historyBack() - t.is(fsm.state, 'A') - t.deepEqual(fsm.history, [ 'A' ]) - t.deepEqual(fsm.future, [ 'D', 'C', 'B' ]) - t.is(fsm.canHistoryBack, false) - t.is(fsm.canHistoryForward, true) - - fsm.historyForward() - t.is(fsm.state, 'B') - t.deepEqual(fsm.history, [ 'A', 'B' ]) - t.deepEqual(fsm.future, [ 'D', 'C' ]) - t.is(fsm.canHistoryBack, true) - t.is(fsm.canHistoryForward, true) - - fsm.historyForward() - t.is(fsm.state, 'C') - t.deepEqual(fsm.history, [ 'A', 'B', 'C' ]) - t.deepEqual(fsm.future, [ 'D' ]) - t.is(fsm.canHistoryBack, true) - t.is(fsm.canHistoryForward, true) - - fsm.step() - t.is(fsm.state, 'D') - t.deepEqual(fsm.history, [ 'A', 'B', 'C', 'D' ]) - t.deepEqual(fsm.future, [ ]) - t.is(fsm.canHistoryBack, true) - t.is(fsm.canHistoryForward, false) - - error = t.throws(() => { - fsm.historyForward() - }) - t.is(error.message, 'no history') - -}) - -//------------------------------------------------------------------------------------------------- - -test('history back and forward lifecycle events', t => { - - var logger = new LifecycleLogger(), - fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step', from: 'A', to: 'B' }, - { name: 'step', from: 'B', to: 'C' }, - { name: 'step', from: 'C', to: 'D' } - ], - methods: { - onBeforeTransition: logger, - onBeforeStep: logger, - onBeforeHistoryBack: logger, - onBeforeHistoryForward: logger, - onLeaveState: logger, - onLeaveA: logger, - onLeaveB: logger, - onLeaveC: logger, - onLeaveD: logger, - onTransition: logger, - onEnterState: logger, - onEnterA: logger, - onEnterB: logger, - onEnterC: logger, - onEnterD: logger, - onAfterTransition: logger, - onAfterStep: logger, - onAfterHistoryBack: logger, - onAfterHistoryForward: logger - }, - plugins: [ - StateMachineHistory - ] - }) - - fsm.step() - fsm.step() - fsm.step() - logger.clear() - - t.is(fsm.state, 'D') - t.deepEqual(fsm.history, [ 'A', 'B', 'C', 'D' ]) - t.deepEqual(fsm.future, [ ]) - - fsm.historyBack() - - t.is(fsm.state, 'C') - t.deepEqual(fsm.history, [ 'A', 'B', 'C' ]) - t.deepEqual(fsm.future, [ 'D' ]) - - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'historyBack', from: 'D', to: 'C', current: 'D' }, - { event: 'onBeforeHistoryBack', transition: 'historyBack', from: 'D', to: 'C', current: 'D' }, - { event: 'onLeaveState', transition: 'historyBack', from: 'D', to: 'C', current: 'D' }, - { event: 'onLeaveD', transition: 'historyBack', from: 'D', to: 'C', current: 'D' }, - { event: 'onTransition', transition: 'historyBack', from: 'D', to: 'C', current: 'D' }, - { event: 'onEnterState', transition: 'historyBack', from: 'D', to: 'C', current: 'C' }, - { event: 'onEnterC', transition: 'historyBack', from: 'D', to: 'C', current: 'C' }, - { event: 'onAfterTransition', transition: 'historyBack', from: 'D', to: 'C', current: 'C' }, - { event: 'onAfterHistoryBack', transition: 'historyBack', from: 'D', to: 'C', current: 'C' } - ]) - - logger.clear() - - fsm.historyForward() - - t.is(fsm.state, 'D') - t.deepEqual(fsm.history, [ 'A', 'B', 'C', 'D' ]) - t.deepEqual(fsm.future, [ ]) - - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'historyForward', from: 'C', to: 'D', current: 'C' }, - { event: 'onBeforeHistoryForward', transition: 'historyForward', from: 'C', to: 'D', current: 'C' }, - { event: 'onLeaveState', transition: 'historyForward', from: 'C', to: 'D', current: 'C' }, - { event: 'onLeaveC', transition: 'historyForward', from: 'C', to: 'D', current: 'C' }, - { event: 'onTransition', transition: 'historyForward', from: 'C', to: 'D', current: 'C' }, - { event: 'onEnterState', transition: 'historyForward', from: 'C', to: 'D', current: 'D' }, - { event: 'onEnterD', transition: 'historyForward', from: 'C', to: 'D', current: 'D' }, - { event: 'onAfterTransition', transition: 'historyForward', from: 'C', to: 'D', current: 'D' }, - { event: 'onAfterHistoryForward', transition: 'historyForward', from: 'C', to: 'D', current: 'D' } - ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('history can be used with a state machine factory', t => { - - var FSM = StateMachine.factory({ - init: 'solid', - transitions: [ - { name: 'melt', from: 'solid', to: 'liquid' }, - { name: 'freeze', from: 'liquid', to: 'solid' }, - { name: 'vaporize', from: 'liquid', to: 'gas' }, - { name: 'condense', from: 'gas', to: 'liquid' } - ], - plugins: [ - StateMachineHistory - ] - }) - - var a = new FSM(), - b = new FSM(); - - t.is(a.state, 'solid') - t.is(b.state, 'solid') - t.deepEqual(a.history, [ 'solid' ]) - t.deepEqual(b.history, [ 'solid' ]) - - a.melt() - a.vaporize() - a.condense() - a.freeze() - - t.is(a.state, 'solid') - t.is(b.state, 'solid') - t.deepEqual(a.history, [ 'solid', 'liquid', 'gas', 'liquid', 'solid' ]) - t.deepEqual(b.history, [ 'solid' ]) - - b.melt() - b.freeze() - - t.is(a.state, 'solid') - t.is(b.state, 'solid') - - t.deepEqual(a.history, [ 'solid', 'liquid', 'gas', 'liquid', 'solid' ]) - t.deepEqual(b.history, [ 'solid', 'liquid', 'solid' ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('history can be used with a singleton state machine applied to existing object', t => { - - var fsm = { - name: 'jake' - } - - StateMachine.apply(fsm, { - init: 'solid', - transitions: [ - { name: 'melt', from: 'solid', to: 'liquid' }, - { name: 'freeze', from: 'liquid', to: 'solid' }, - { name: 'vaporize', from: 'liquid', to: 'gas' }, - { name: 'condense', from: 'gas', to: 'liquid' } - ], - plugins: [ - StateMachineHistory - ] - }) - - t.is(fsm.name, 'jake') - t.is(fsm.state, 'solid') - t.deepEqual(fsm.history, [ 'solid' ]) - - fsm.melt(); - t.is(fsm.state, 'liquid') - t.deepEqual(fsm.history, [ 'solid', 'liquid' ]) - - fsm.vaporize(); - t.is(fsm.state, 'gas') - t.deepEqual(fsm.history, [ 'solid', 'liquid', 'gas' ]) - - fsm.condense() - t.is(fsm.state, 'liquid') - t.deepEqual(fsm.history, [ 'solid', 'liquid', 'gas', 'liquid' ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test('history can be used with a state machine factory applied to existing class', t => { - - function FSM(name) { - this.name = name - this._fsm() - } - - StateMachine.factory(FSM, { - init: 'solid', - transitions: [ - { name: 'melt', from: 'solid', to: 'liquid' }, - { name: 'freeze', from: 'liquid', to: 'solid' }, - { name: 'vaporize', from: 'liquid', to: 'gas' }, - { name: 'condense', from: 'gas', to: 'liquid' } - ], - plugins: [ - StateMachineHistory - ] - }) - - var a = new FSM('A'), - b = new FSM('B'); - - t.is(a.name, 'A') - t.is(b.name, 'B') - - t.is(a.state, 'solid') - t.is(b.state, 'solid') - t.deepEqual(a.history, [ 'solid' ]) - t.deepEqual(b.history, [ 'solid' ]) - - a.melt() - a.vaporize() - a.condense() - a.freeze() - - t.is(a.state, 'solid') - t.is(b.state, 'solid') - t.deepEqual(a.history, [ 'solid', 'liquid', 'gas', 'liquid', 'solid' ]) - t.deepEqual(b.history, [ 'solid' ]) - - b.melt() - b.freeze() - - t.is(a.state, 'solid') - t.is(b.state, 'solid') - - t.deepEqual(a.history, [ 'solid', 'liquid', 'gas', 'liquid', 'solid' ]) - t.deepEqual(b.history, [ 'solid', 'liquid', 'solid' ]) - -}) - -//------------------------------------------------------------------------------------------------- diff --git a/test/plugin/visualize.js b/test/plugin/visualize.js deleted file mode 100644 index a2b9388..0000000 --- a/test/plugin/visualize.js +++ /dev/null @@ -1,443 +0,0 @@ -import test from 'ava' -import StateMachine from '../../src/app' -import visualize from '../../src/plugin/visualize' - -var dotcfg = visualize.dotcfg, // converts FSM to DOT CONFIG - dotify = visualize.dotify; // converts DOT CONFIG to DOT OUTPUT - -//------------------------------------------------------------------------------------------------- - -test('visualize state machine', t => { - - var fsm = new StateMachine({ - init: 'solid', - transitions: [ - { name: 'melt', from: 'solid', to: 'liquid' }, - { name: 'freeze', from: 'liquid', to: 'solid' }, - { name: 'vaporize', from: 'liquid', to: 'gas' }, - { name: 'condense', from: 'gas', to: 'liquid' } - ] - }) - - t.is(visualize(fsm), `digraph "fsm" { - "solid"; - "liquid"; - "gas"; - "solid" -> "liquid" [ label=" melt " ]; - "liquid" -> "solid" [ label=" freeze " ]; - "liquid" -> "gas" [ label=" vaporize " ]; - "gas" -> "liquid" [ label=" condense " ]; -}`) -}) - -//------------------------------------------------------------------------------------------------- - -test('visualize state machine factory', t => { - - var FSM = StateMachine.factory({ - init: 'solid', - transitions: [ - { name: 'melt', from: 'solid', to: 'liquid' }, - { name: 'freeze', from: 'liquid', to: 'solid' }, - { name: 'vaporize', from: 'liquid', to: 'gas' }, - { name: 'condense', from: 'gas', to: 'liquid' } - ] - }) - - t.is(visualize(FSM), `digraph "fsm" { - "solid"; - "liquid"; - "gas"; - "solid" -> "liquid" [ label=" melt " ]; - "liquid" -> "solid" [ label=" freeze " ]; - "liquid" -> "gas" [ label=" vaporize " ]; - "gas" -> "liquid" [ label=" condense " ]; -}`) -}) - -//------------------------------------------------------------------------------------------------- - -test('visualize with custom .dot markup', t => { - - var fsm = new StateMachine({ - init: 'solid', - transitions: [ - { name: 'melt', from: 'solid', to: 'liquid', dot: { color: 'red', headport: 'nw', tailport: 'ne' } }, - { name: 'freeze', from: 'liquid', to: 'solid', dot: { color: 'grey', headport: 'se', tailport: 'sw' } }, - { name: 'vaporize', from: 'liquid', to: 'gas', dot: { color: 'yellow', headport: 'nw', tailport: 'ne' } }, - { name: 'condense', from: 'gas', to: 'liquid', dot: { color: 'brown', headport: 'se', tailport: 'sw' } } - ] - }) - - t.is(visualize(fsm, { name: 'matter', orientation: 'horizontal' }), `digraph "matter" { - rankdir=LR; - "solid"; - "liquid"; - "gas"; - "solid" -> "liquid" [ color="red" ; headport="nw" ; label=" melt " ; tailport="ne" ]; - "liquid" -> "solid" [ color="grey" ; headport="se" ; label=" freeze " ; tailport="sw" ]; - "liquid" -> "gas" [ color="yellow" ; headport="nw" ; label=" vaporize " ; tailport="ne" ]; - "gas" -> "liquid" [ color="brown" ; headport="se" ; label=" condense " ; tailport="sw" ]; -}`) -}) - -//================================================================================================= -// TEST FSM => DOTCFG -//================================================================================================= - -test('dotcfg simple state machine', t => { - - var fsm = new StateMachine({ - init: 'solid', - transitions: [ - { name: 'melt', from: 'solid', to: 'liquid' }, - { name: 'freeze', from: 'liquid', to: 'solid' }, - { name: 'vaporize', from: 'liquid', to: 'gas' }, - { name: 'condense', from: 'gas', to: 'liquid' } - ] - }) - - t.deepEqual(dotcfg(fsm), { - states: [ 'solid', 'liquid', 'gas' ], - transitions: [ - { from: 'solid', to: 'liquid', label: ' melt ' }, - { from: 'liquid', to: 'solid', label: ' freeze ' }, - { from: 'liquid', to: 'gas', label: ' vaporize ' }, - { from: 'gas', to: 'liquid', label: ' condense ' } - ] - }) - -}) - -//------------------------------------------------------------------------------------------------- - -test('dotcfg for state machine - optionally include :init transition', t => { - - var fsm = new StateMachine({ - init: { name: 'boot', from: 'booting', to: 'ready', dot: { color: 'red' } } - }) - - t.deepEqual(dotcfg(fsm, { init: false }), { - states: [ 'ready' ] - }) - - t.deepEqual(dotcfg(fsm, { init: true }), { - states: [ 'booting', 'ready' ], - transitions: [ - { from: 'booting', to: 'ready', label: ' boot ', color: 'red' } - ] - }) - -}) - -//------------------------------------------------------------------------------------------------- - -test('dotcfg for fsm with multiple transitions with same :name', t => { - - var fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step', from: 'A', to: 'B' }, - { name: 'step', from: 'B', to: 'C' }, - { name: 'step', from: 'C', to: 'D' } - ] - }) - - t.deepEqual(dotcfg(fsm), { - states: [ 'A', 'B', 'C', 'D' ], - transitions: [ - { from: 'A', to: 'B', label: ' step ' }, - { from: 'B', to: 'C', label: ' step ' }, - { from: 'C', to: 'D', label: ' step ' } - ] - }) - -}) - -//------------------------------------------------------------------------------------------------- - -test('dotcfg for fsm transition with multiple :from', t => { - - var fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step', from: 'A', to: 'B' }, - { name: 'step', from: 'B', to: 'C' }, - { name: 'step', from: 'C', to: 'D' }, - { name: 'reset', from: [ 'A', 'B' ], to: 'A' } - ] - }) - - t.deepEqual(dotcfg(fsm), { - states: [ 'A', 'B', 'C', 'D' ], - transitions: [ - { from: 'A', to: 'B', label: ' step ' }, - { from: 'B', to: 'C', label: ' step ' }, - { from: 'C', to: 'D', label: ' step ' }, - { from: 'A', to: 'A', label: ' reset ' }, - { from: 'B', to: 'A', label: ' reset ' } - ] - }) - -}) - -//------------------------------------------------------------------------------------------------- - -test('dotcfg for fsm with wildcard/missing :from', t => { - - var fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step', from: 'A', to: 'B' }, - { name: 'step', from: 'B', to: 'C' }, - { name: 'step', from: 'C', to: 'D' }, - { name: 'reset', from: '*', to: 'A' }, - { name: 'finish', /* missing */ to: 'X' } - ] - }) - - t.deepEqual(dotcfg(fsm), { - states: [ 'A', 'B', 'C', 'D', 'X' ], - transitions: [ - { from: 'A', to: 'B', label: ' step ' }, - { from: 'B', to: 'C', label: ' step ' }, - { from: 'C', to: 'D', label: ' step ' }, - { from: 'none', to: 'A', label: ' reset ' }, - { from: 'A', to: 'A', label: ' reset ' }, - { from: 'B', to: 'A', label: ' reset ' }, - { from: 'C', to: 'A', label: ' reset ' }, - { from: 'D', to: 'A', label: ' reset ' }, - { from: 'X', to: 'A', label: ' reset ' }, - { from: 'none', to: 'X', label: ' finish ' }, - { from: 'A', to: 'X', label: ' finish ' }, - { from: 'B', to: 'X', label: ' finish ' }, - { from: 'C', to: 'X', label: ' finish ' }, - { from: 'D', to: 'X', label: ' finish ' }, - { from: 'X', to: 'X', label: ' finish ' } - ] - }) - -}) - -//------------------------------------------------------------------------------------------------- - -test('dotcfg for fsm with wildcard/missing :to', t => { - - var fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step', from: 'A', to: 'B' }, - { name: 'step', from: 'B', to: 'C' }, - { name: 'step', from: 'C', to: 'D' }, - { name: 'stay', from: 'A', to: 'A' }, - { name: 'stay', from: 'B', to: '*' }, - { name: 'stay', from: 'C' /* missing */ }, - { name: 'noop', from: '*', to: '*' } - ] - }) - - t.deepEqual(dotcfg(fsm), { - states: [ 'A', 'B', 'C', 'D' ], - transitions: [ - { from: 'A', to: 'B', label: ' step ' }, - { from: 'B', to: 'C', label: ' step ' }, - { from: 'C', to: 'D', label: ' step ' }, - { from: 'A', to: 'A', label: ' stay ' }, - { from: 'B', to: 'B', label: ' stay ' }, - { from: 'C', to: 'C', label: ' stay ' }, - { from: 'none', to: 'none', label: ' noop ' }, - { from: 'A', to: 'A', label: ' noop ' }, - { from: 'B', to: 'B', label: ' noop ' }, - { from: 'C', to: 'C', label: ' noop ' }, - { from: 'D', to: 'D', label: ' noop ' } - ] - }) - -}) - -//------------------------------------------------------------------------------------------------- - -test('dotcfg for fsm - conditional transition is not displayed', t => { - - var fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step', from: '*', to: function(n) { return this.skip(n) } }, - ], - methods: { - skip: function(amount) { - var code = this.state.charCodeAt(0); - return String.fromCharCode(code + (amount || 1)); - } - } - }); - - t.deepEqual(dotcfg(fsm), { - states: [ 'A' ] - }) - -}) - -//------------------------------------------------------------------------------------------------- - -test('dotcfg with custom transition .dot edge markup', t => { - - var fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step', from: 'A', to: 'B', dot: { color: "red", headport: 'nw', tailport: 'ne', label: 'A2B' } }, - { name: 'step', from: 'B', to: 'C', dot: { color: "green", headport: 'sw', tailport: 'se', label: 'B2C' } } - ] - }) - - t.deepEqual(dotcfg(fsm), { - states: [ 'A', 'B', 'C' ], - transitions: [ - { from: 'A', to: 'B', label: 'A2B', color: "red", headport: "nw", tailport: "ne" }, - { from: 'B', to: 'C', label: 'B2C', color: "green", headport: "sw", tailport: "se" } - ] - }) - -}) - -//------------------------------------------------------------------------------------------------- - -test('dotcfg with custom name', t => { - - var fsm = new StateMachine(); - - t.deepEqual(dotcfg(fsm, { name: 'bob' }), { - name: 'bob', - }) - -}) - -//------------------------------------------------------------------------------------------------- - -test('dotcfg with custom orientation', t => { - - var fsm = new StateMachine(); - - t.deepEqual(dotcfg(fsm, { orientation: 'horizontal' }), { - rankdir: 'LR', - }) - - t.deepEqual(dotcfg(fsm, { orientation: 'vertical' }), { - rankdir: 'TB', - }) - -}) - -//------------------------------------------------------------------------------------------------- - -test('dotcfg for empty state machine', t => { - - var fsm = new StateMachine(); - - t.deepEqual(dotcfg(fsm), {}) - -}) - -//================================================================================================= -// TEST DOTCFG => DOT OUTPUT -//================================================================================================= - -test('dotify empty', t => { - var expected = `digraph "fsm" { -}` - t.is(dotify(), expected) - t.is(dotify({}), expected) -}) - -//------------------------------------------------------------------------------------------------- - -test('dotify name', t => { - t.is(dotify({ name: 'bob' }), `digraph "bob" { -}`) -}) - -//------------------------------------------------------------------------------------------------- - -test('dotify rankdir', t => { - t.is(dotify({ rankdir: 'LR' }), `digraph "fsm" { - rankdir=LR; -}`) -}) - -//------------------------------------------------------------------------------------------------- - -test('dotify states', t => { - var states = [ 'A', 'B' ]; - t.is(dotify({ states: states }), `digraph "fsm" { - "A"; - "B"; -}`) -}) - -//------------------------------------------------------------------------------------------------- - -test('dotify transitions', t => { - var transitions = [ - { from: 'A', to: 'B' }, - { from: 'B', to: 'C' }, - ]; - t.is(dotify({ transitions: transitions }), `digraph "fsm" { - "A" -> "B"; - "B" -> "C"; -}`) -}) - -//------------------------------------------------------------------------------------------------- - -test('dotify transitions with labels', t => { - var transitions = [ - { from: 'A', to: 'B', label: 'first' }, - { from: 'B', to: 'C', label: 'second' } - ]; - t.is(dotify({ transitions: transitions }), `digraph "fsm" { - "A" -> "B" [ label="first" ]; - "B" -> "C" [ label="second" ]; -}`) -}) - -//------------------------------------------------------------------------------------------------- - -test('dotify transitions with custom .dot edge markup', t => { - var transitions = [ - { from: 'A', to: 'B', label: 'first', color: 'red', headport: 'nw', tailport: 'ne' }, - { from: 'B', to: 'A', label: 'second', color: 'green', headport: 'se', tailport: 'sw' } - ] - t.is(dotify({ transitions: transitions }), `digraph "fsm" { - "A" -> "B" [ color="red" ; headport="nw" ; label="first" ; tailport="ne" ]; - "B" -> "A" [ color="green" ; headport="se" ; label="second" ; tailport="sw" ]; -}`) -}) - -//------------------------------------------------------------------------------------------------- - -test('dotify kitchen sink', t => { - var name = "my fsm", - rankdir = "LR", - states = [ 'none', 'solid', 'liquid', 'gas' ], - transitions = [ - { from: 'none', to: 'solid', color: 'red', label: 'init' }, - { from: 'solid', to: 'liquid', color: 'red', label: 'melt' }, - { from: 'liquid', to: 'solid', color: 'green', label: 'freeze' }, - { from: 'liquid', to: 'gas', color: 'red', label: 'vaporize' }, - { from: 'gas', to: 'liquid', color: 'green', label: 'condense' } - ]; - t.is(dotify({ name: name, rankdir: rankdir, states: states, transitions: transitions }), `digraph "my fsm" { - rankdir=LR; - "none"; - "solid"; - "liquid"; - "gas"; - "none" -> "solid" [ color="red" ; label="init" ]; - "solid" -> "liquid" [ color="red" ; label="melt" ]; - "liquid" -> "solid" [ color="green" ; label="freeze" ]; - "liquid" -> "gas" [ color="red" ; label="vaporize" ]; - "gas" -> "liquid" [ color="green" ; label="condense" ]; -}`) -}) - -//================================================================================================= diff --git a/test/plugins.js b/test/plugins.js deleted file mode 100644 index 486125b..0000000 --- a/test/plugins.js +++ /dev/null @@ -1,151 +0,0 @@ -import test from 'ava' -import StateMachine from '../src/app' -import LifecycleLogger from './helpers/lifecycle_logger' - -//------------------------------------------------------------------------------------------------- - -test('an empty plugin object', t => { - - var plugin = { - init: function(instance) { - instance.plugged = true - } - }; - - var fsm = new StateMachine({ - plugins: [ plugin ] - }); - - t.is(fsm.state, 'none') - t.is(fsm.plugged, true) - -}) - -//------------------------------------------------------------------------------------------------- - -test('an empty plugin function', t => { - - var plugin = function() { - return { - init: function(instance) { - instance.plugged = true - } - } - }; - - var fsm = new StateMachine({ - plugins: [ plugin ] - }); - - t.is(fsm.state, 'none') - t.is(fsm.plugged, true) - -}) - -//------------------------------------------------------------------------------------------------- - -test('an empty plugin function with configuration', t => { - - var plugin = function(value) { - return { - init: function(instance) { - instance.plugged = value - } - } - }; - - var fsm = new StateMachine({ - plugins: [ new plugin(42) ] - }); - - t.is(fsm.state, 'none') - t.is(fsm.plugged, 42) - -}) - -//------------------------------------------------------------------------------------------------- - -test('plugin can add methods', t => { - - var plugin = { - methods: { - foo: function() { return 'FOO' }, - bar: function() { return 'BAR' } - } - }; - - var fsm = new StateMachine({ - plugins: [ plugin ] - }); - - t.is(fsm.state, 'none') - t.is(fsm.foo(), 'FOO') - t.is(fsm.bar(), 'BAR') - -}) - -//------------------------------------------------------------------------------------------------- - -test('plugin can add properties', t => { - - var plugin = { - properties: { - color: { get: function() { return 'red' } } - } - }; - - var fsm = new StateMachine({ - plugins: [ plugin ] - }); - - t.is(fsm.state, 'none') - t.is(fsm.color, 'red') - -}) - -//------------------------------------------------------------------------------------------------- - -test('plugin lifecycle hook', t => { - - var plugin = { - - init: function(instance) { - instance.logger = new LifecycleLogger(); - }, - - lifecycle: function(instance, lifecycle) { - instance.logger(lifecycle) - } - - }; - - var fsm = new StateMachine({ - transitions: [ - { name: 'step', from: 'none', to: 'complete' } - ], - plugins: [ plugin ] - }); - - t.is(fsm.state, 'none') - t.deepEqual(fsm.logger.log, []) - - fsm.step() - - t.is(fsm.state, 'complete') - t.deepEqual(fsm.logger.log, [ - { event: 'onBeforeTransition', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onBeforeStep', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onLeaveState', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onLeaveNone', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onTransition', transition: 'step', from: 'none', to: 'complete', current: 'none' }, - { event: 'onEnterState', transition: 'step', from: 'none', to: 'complete', current: 'complete' }, - { event: 'onEnterComplete', transition: 'step', from: 'none', to: 'complete', current: 'complete' }, - { event: 'onComplete', transition: 'step', from: 'none', to: 'complete', current: 'complete' }, - { event: 'onAfterTransition', transition: 'step', from: 'none', to: 'complete', current: 'complete' }, - { event: 'onAfterStep', transition: 'step', from: 'none', to: 'complete', current: 'complete' }, - { event: 'onStep', transition: 'step', from: 'none', to: 'complete', current: 'complete' } - ]) - -}) - -//------------------------------------------------------------------------------------------------- diff --git a/test/transitions.js b/test/transitions.js index 942a3b4..f0ea4dc 100644 --- a/test/transitions.js +++ b/test/transitions.js @@ -1,310 +1,78 @@ -import test from 'ava' -import StateMachine from '../src/app' -import LifecycleLogger from './helpers/lifecycle_logger' +import test from 'ava' +import StateMachine from '../dist/state-machine'; //----------------------------------------------------------------------------- -test('basic transition from state to state', t => { +test('basic transition from state to state', async (t) => { - var fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step1', from: 'A', to: 'B' }, - { name: 'step2', from: 'B', to: 'C' }, - { name: 'step3', from: 'C', to: 'D' } - ] - }); + var fsm = new StateMachine('A', { + step1: { from: 'A', to: 'B' }, + step2: { from: 'B', to: 'C' }, + step3: { from: 'C', to: 'D' } + }); - t.is(fsm.state, 'A') - - fsm.step1(); t.is(fsm.state, 'B') - fsm.step2(); t.is(fsm.state, 'C') - fsm.step3(); t.is(fsm.state, 'D') - -}) - -//----------------------------------------------------------------------------- - -test('multiple transitions with same name', t => { - - var fsm = new StateMachine({ - init: 'hungry', - transitions: [ - { name: 'eat', from: 'hungry', to: 'satisfied' }, - { name: 'eat', from: 'satisfied', to: 'full' }, - { name: 'eat', from: 'full', to: 'sick' }, - { name: 'rest', from: '*', to: 'hungry' } - ] - }); - - t.is(fsm.state, 'hungry') - t.is(fsm.can('eat'), true) - t.is(fsm.can('rest'), true) - - fsm.eat() - - t.is(fsm.state, 'satisfied') - t.is(fsm.can('eat'), true) - t.is(fsm.can('rest'), true) - - fsm.eat() - - t.is(fsm.state, 'full') - t.is(fsm.can('eat'), true) - t.is(fsm.can('rest'), true) - - fsm.eat() - - t.is(fsm.state, 'sick') - t.is(fsm.can('eat'), false) - t.is(fsm.can('rest'), true) - - fsm.rest() - - t.is(fsm.state, 'hungry') - t.is(fsm.can('eat'), true) - t.is(fsm.can('rest'), true) + t.is(fsm.state, 'A') + await fsm.fire('step1'); + t.is(fsm.state, 'B') + await fsm.fire('step2'); + t.is(fsm.state, 'C') + await fsm.fire('step3'); + t.is(fsm.state, 'D') }) //----------------------------------------------------------------------------- -test('transitions with multiple from states', t => { - - var fsm = new StateMachine({ - transitions: [ - { name: 'start', from: 'none', to: 'green' }, - { name: 'warn', from: ['green', 'red'], to: 'yellow' }, - { name: 'panic', from: ['green', 'yellow'], to: 'red' }, - { name: 'clear', from: ['red', 'yellow'], to: 'green' } - ] - }); - - t.deepEqual(fsm.allStates(), [ 'none', 'green', 'yellow', 'red' ]) - t.deepEqual(fsm.allTransitions(), [ 'start', 'warn', 'panic', 'clear' ]) - - t.is(fsm.state, 'none') - t.is(fsm.can('start'), true) - t.is(fsm.can('warn'), false) - t.is(fsm.can('panic'), false) - t.is(fsm.can('clear'), false) - t.deepEqual(fsm.transitions(), ['start']) - - fsm.start() - t.is(fsm.state, 'green') - t.is(fsm.can('start'), false) - t.is(fsm.can('warn'), true) - t.is(fsm.can('panic'), true) - t.is(fsm.can('clear'), false) - t.deepEqual(fsm.transitions(), ['warn', 'panic']) - - fsm.warn() - t.is(fsm.state, 'yellow') - t.is(fsm.can('start'), false) - t.is(fsm.can('warn'), false) - t.is(fsm.can('panic'), true) - t.is(fsm.can('clear'), true) - t.deepEqual(fsm.transitions(), ['panic', 'clear']) - - fsm.panic() - t.is(fsm.state, 'red') - t.is(fsm.can('start'), false) - t.is(fsm.can('warn'), true) - t.is(fsm.can('panic'), false) - t.is(fsm.can('clear'), true) - t.deepEqual(fsm.transitions(), ['warn', 'clear']) - - fsm.clear() - t.is(fsm.state, 'green') - t.is(fsm.can('start'), false) - t.is(fsm.can('warn'), true) - t.is(fsm.can('panic'), true) - t.is(fsm.can('clear'), false) - t.deepEqual(fsm.transitions(), ['warn', 'panic']) - -}) - -//------------------------------------------------------------------------------------------------- - -test("transitions that dont change state, dont trigger enter/leave lifecycle events", t => { - - var logger = new LifecycleLogger(), - fsm = new StateMachine({ - transitions: [ - { name: 'noop', from: 'none', to: 'none' } - ], - methods: { - onBeforeTransition: logger, - onBeforeNoop: logger, - onLeaveState: logger, - onLeaveNone: logger, - onTransition: logger, - onEnterState: logger, - onEnterNone: logger, - onNone: logger, - onAfterTransition: logger, - onAfterNoop: logger, - onNoop: logger - } - }) - - t.is(fsm.state, 'none') - t.deepEqual(logger.log, []) - - fsm.noop() - - t.is(fsm.state, 'none') - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'noop', from: 'none', to: 'none', current: 'none' }, - { event: 'onBeforeNoop', transition: 'noop', from: 'none', to: 'none', current: 'none' }, - { event: 'onTransition', transition: 'noop', from: 'none', to: 'none', current: 'none' }, - { event: 'onAfterTransition', transition: 'noop', from: 'none', to: 'none', current: 'none' }, - { event: 'onAfterNoop', transition: 'noop', from: 'none', to: 'none', current: 'none' }, - { event: 'onNoop', transition: 'noop', from: 'none', to: 'none', current: 'none' } - ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test("transitions that dont change state, can be configured to trigger enter/leave lifecycle events", t => { - - var logger = new LifecycleLogger(), - fsm = new StateMachine({ - observeUnchangedState: true, - transitions: [ - { name: 'noop', from: 'none', to: 'none' } - ], - methods: { - onBeforeTransition: logger, - onBeforeNoop: logger, - onLeaveState: logger, - onLeaveNone: logger, - onTransition: logger, - onEnterState: logger, - onEnterNone: logger, - onNone: logger, - onAfterTransition: logger, - onAfterNoop: logger, - onNoop: logger - } - }) - - t.is(fsm.state, 'none') - t.deepEqual(logger.log, []) - - fsm.noop() - - t.is(fsm.state, 'none') - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'noop', from: 'none', to: 'none', current: 'none' }, - { event: 'onBeforeNoop', transition: 'noop', from: 'none', to: 'none', current: 'none' }, - { event: 'onLeaveState', transition: 'noop', from: 'none', to: 'none', current: 'none' }, - { event: 'onLeaveNone', transition: 'noop', from: 'none', to: 'none', current: 'none' }, - { event: 'onTransition', transition: 'noop', from: 'none', to: 'none', current: 'none' }, - { event: 'onEnterState', transition: 'noop', from: 'none', to: 'none', current: 'none' }, - { event: 'onEnterNone', transition: 'noop', from: 'none', to: 'none', current: 'none' }, - { event: 'onNone', transition: 'noop', from: 'none', to: 'none', current: 'none' }, - { event: 'onAfterTransition', transition: 'noop', from: 'none', to: 'none', current: 'none' }, - { event: 'onAfterNoop', transition: 'noop', from: 'none', to: 'none', current: 'none' }, - { event: 'onNoop', transition: 'noop', from: 'none', to: 'none', current: 'none' } - ]) - -}) - -//------------------------------------------------------------------------------------------------- - -test("transition methods with dash or underscore are camelized", t => { - - var fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'do-with-dash', from: 'A', to: 'B' }, - { name: 'do_with_underscore', from: 'B', to: 'C' }, - { name: 'doAlreadyCamelized', from: 'C', to: 'D' } - ] - }); - - t.is(fsm.state, 'A') - fsm.doWithDash(); t.is(fsm.state, 'B') - fsm.doWithUnderscore(); t.is(fsm.state, 'C') - fsm.doAlreadyCamelized(); t.is(fsm.state, 'D') - -}) - -//------------------------------------------------------------------------------------------------- - -test('conditional transitions', t => { - - var logger = new LifecycleLogger(), - fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step', from: '*', to: function(n) { return this.skip(n) } }, - ], - methods: { - skip: function(amount) { - var code = this.state.charCodeAt(0); - return String.fromCharCode(code + (amount || 1)); - }, - onBeforeTransition: logger, - onBeforeInit: logger, - onBeforeStep: logger, - onLeaveState: logger, - onLeaveNone: logger, - onLeaveA: logger, - onLeaveB: logger, - onLeaveG: logger, - onTransition: logger, - onEnterState: logger, - onEnterNone: logger, - onEnterA: logger, - onEnterB: logger, - onEnterG: logger, - onAfterTransition: logger, - onAfterInit: logger, - onAfterStep: logger - } - }); - - t.is(fsm.state, 'A') - - t.deepEqual(fsm.allStates(), [ 'none', 'A' ]); - - fsm.step(); t.is(fsm.state, 'B'); t.deepEqual(fsm.allStates(), [ 'none', 'A', 'B' ]) - fsm.step(5); t.is(fsm.state, 'G'); t.deepEqual(fsm.allStates(), [ 'none', 'A', 'B', 'G' ]) - - t.deepEqual(logger.log, [ - { event: 'onBeforeTransition', transition: 'init', from: 'none', to: 'A', current: 'none' }, - { event: 'onBeforeInit', transition: 'init', from: 'none', to: 'A', current: 'none' }, - { event: 'onLeaveState', transition: 'init', from: 'none', to: 'A', current: 'none' }, - { event: 'onLeaveNone', transition: 'init', from: 'none', to: 'A', current: 'none' }, - { event: 'onTransition', transition: 'init', from: 'none', to: 'A', current: 'none' }, - { event: 'onEnterState', transition: 'init', from: 'none', to: 'A', current: 'A' }, - { event: 'onEnterA', transition: 'init', from: 'none', to: 'A', current: 'A' }, - { event: 'onAfterTransition', transition: 'init', from: 'none', to: 'A', current: 'A' }, - { event: 'onAfterInit', transition: 'init', from: 'none', to: 'A', current: 'A' }, - - { event: 'onBeforeTransition', transition: 'step', from: 'A', to: 'B', current: 'A' }, - { event: 'onBeforeStep', transition: 'step', from: 'A', to: 'B', current: 'A' }, - { event: 'onLeaveState', transition: 'step', from: 'A', to: 'B', current: 'A' }, - { event: 'onLeaveA', transition: 'step', from: 'A', to: 'B', current: 'A' }, - { event: 'onTransition', transition: 'step', from: 'A', to: 'B', current: 'A' }, - { event: 'onEnterState', transition: 'step', from: 'A', to: 'B', current: 'B' }, - { event: 'onEnterB', transition: 'step', from: 'A', to: 'B', current: 'B' }, - { event: 'onAfterTransition', transition: 'step', from: 'A', to: 'B', current: 'B' }, - { event: 'onAfterStep', transition: 'step', from: 'A', to: 'B', current: 'B' }, - - { event: 'onBeforeTransition', transition: 'step', from: 'B', to: 'G', current: 'B', args: [ 5 ] }, - { event: 'onBeforeStep', transition: 'step', from: 'B', to: 'G', current: 'B', args: [ 5 ] }, - { event: 'onLeaveState', transition: 'step', from: 'B', to: 'G', current: 'B', args: [ 5 ] }, - { event: 'onLeaveB', transition: 'step', from: 'B', to: 'G', current: 'B', args: [ 5 ] }, - { event: 'onTransition', transition: 'step', from: 'B', to: 'G', current: 'B', args: [ 5 ] }, - { event: 'onEnterState', transition: 'step', from: 'B', to: 'G', current: 'G', args: [ 5 ] }, - { event: 'onEnterG', transition: 'step', from: 'B', to: 'G', current: 'G', args: [ 5 ] }, - { event: 'onAfterTransition', transition: 'step', from: 'B', to: 'G', current: 'G', args: [ 5 ] }, - { event: 'onAfterStep', transition: 'step', from: 'B', to: 'G', current: 'G', args: [ 5 ] }, - ]) +test('transitions with multiple from states', async (t) => { + + var fsm = new StateMachine('none', { + start: { from: 'none', to: 'green' }, + warn: { from: ['green', 'red'], to: 'yellow' }, + panic: { from: ['green', 'yellow'], to: 'red' }, + clear: { from: ['red', 'yellow'], to: 'green' } + }); + + t.deepEqual(fsm.allStates(), ['none', 'green', 'red', 'yellow']) + t.deepEqual(fsm.allTransitions(), ['start', 'warn', 'panic', 'clear']) + + t.is(fsm.state, 'none') + t.is(fsm.can('start'), true) + t.is(fsm.can('warn'), false) + t.is(fsm.can('panic'), false) + t.is(fsm.can('clear'), false) + t.deepEqual(fsm.transitions(), ['start']) + + await fsm.fire('start') + t.is(fsm.state, 'green') + t.is(fsm.can('start'), false) + t.is(fsm.can('warn'), true) + t.is(fsm.can('panic'), true) + t.is(fsm.can('clear'), false) + t.deepEqual(fsm.transitions(), ['warn', 'panic']) + + await fsm.fire('warn') + t.is(fsm.state, 'yellow') + t.is(fsm.can('start'), false) + t.is(fsm.can('warn'), false) + t.is(fsm.can('panic'), true) + t.is(fsm.can('clear'), true) + t.deepEqual(fsm.transitions(), ['panic', 'clear']) + + await fsm.fire('panic') + t.is(fsm.state, 'red') + t.is(fsm.can('start'), false) + t.is(fsm.can('warn'), true) + t.is(fsm.can('panic'), false) + t.is(fsm.can('clear'), true) + t.deepEqual(fsm.transitions(), ['warn', 'clear']) + + await fsm.fire('clear') + t.is(fsm.state, 'green') + t.is(fsm.can('start'), false) + t.is(fsm.can('warn'), true) + t.is(fsm.can('panic'), true) + t.is(fsm.can('clear'), false) + t.deepEqual(fsm.transitions(), ['warn', 'panic']) }) diff --git a/test/util/camelize.js b/test/util/camelize.js deleted file mode 100644 index 84318a2..0000000 --- a/test/util/camelize.js +++ /dev/null @@ -1,14 +0,0 @@ -import test from 'ava'; -import camelize from '../../src/util/camelize'; - -test('camelize', t => { - t.is(camelize(""), ""); - t.is(camelize("word"), "word"); - t.is(camelize("Word"), "word"); - t.is(camelize("WORD"), "word"); - t.is(camelize("word-with-dash"), "wordWithDash"); - t.is(camelize("word_with_underscore"), "wordWithUnderscore"); - t.is(camelize("word--with--double--dash"), "wordWithDoubleDash"); - t.is(camelize("word_WITH_mixed_CASE"), "wordWithMixedCase"); - t.is(camelize("alreadyCamelizedString"), "alreadyCamelizedString"); -}); diff --git a/test/util/mixin.js b/test/util/mixin.js deleted file mode 100644 index 0fb3561..0000000 --- a/test/util/mixin.js +++ /dev/null @@ -1,65 +0,0 @@ -import test from 'ava'; -import mixin from '../../src/util/mixin'; - -//------------------------------------------------------------------------------------------------- - -test('mixin', t => { - - var a = { first: 'Jake', key: 'a' }, - b = { last: 'Gordon', key: 'b' }; - - t.deepEqual(mixin({}, a), { first: 'Jake', key: 'a' }); - t.deepEqual(mixin({}, b), { last: 'Gordon', key: 'b' }); - t.deepEqual(mixin({}, a, b), { first: 'Jake', last: 'Gordon', key: 'b' }); - t.deepEqual(mixin({}, b, a), { first: 'Jake', last: 'Gordon', key: 'a' }); - -}); - -//------------------------------------------------------------------------------------------------- - -test('mixin only mixes in owned properties', t => { - - var MyClass = function(name) { this.name = name } - - MyClass.prototype = { - answer: 42 - } - - var a = new MyClass('a'), - b = new MyClass('b'); - - t.is(a.name, 'a'); - t.is(a.answer, 42); - t.is(b.name, 'b'); - t.is(b.answer, 42); - - t.is(a.hasOwnProperty('name'), true); - t.is(a.hasOwnProperty('answer'), false); - t.is(b.hasOwnProperty('name'), true); - t.is(b.hasOwnProperty('answer'), false); - - t.deepEqual(mixin({}, a), { name: 'a' }); - t.deepEqual(mixin({}, b), { name: 'b' }); - t.deepEqual(mixin({}, a, b), { name: 'b' }); - t.deepEqual(mixin({}, b, a), { name: 'a' }); - - b.answer = 99; - - t.is(a.name, 'a'); - t.is(a.answer, 42); - t.is(b.name, 'b'); - t.is(b.answer, 99); - - t.is(a.hasOwnProperty('name'), true); - t.is(a.hasOwnProperty('answer'), false); - t.is(b.hasOwnProperty('name'), true); - t.is(b.hasOwnProperty('answer'), true); - - t.deepEqual(mixin({}, a), { name: 'a' }); - t.deepEqual(mixin({}, b), { name: 'b', answer: 99 }); - t.deepEqual(mixin({}, a, b), { name: 'b', answer: 99 }); - t.deepEqual(mixin({}, b, a), { name: 'a', answer: 99 }); - -}); - -//------------------------------------------------------------------------------------------------- diff --git a/test/wildcards.js b/test/wildcards.js index a97b2af..0c13a9a 100644 --- a/test/wildcards.js +++ b/test/wildcards.js @@ -1,211 +1,235 @@ -import test from 'ava' -import StateMachine from '../src/app' +import test from 'ava' +import StateMachine from '../dist/state-machine'; //----------------------------------------------------------------------------- -test('wildcard :from allows transition from any state', t => { - - var fsm = new StateMachine({ - init: 'stopped', - transitions: [ - { name: 'prepare', from: 'stopped', to: 'ready' }, - { name: 'start', from: 'ready', to: 'running' }, - { name: 'resume', from: 'paused', to: 'running' }, - { name: 'pause', from: 'running', to: 'paused' }, - { name: 'stop', from: '*', to: 'stopped' } - ]}); - - t.is(fsm.state, 'stopped', "initial state should be stopped"); - - fsm.prepare(); t.is(fsm.state, 'ready') - fsm.stop(); t.is(fsm.state, 'stopped') - - fsm.prepare(); t.is(fsm.state, 'ready') - fsm.start(); t.is(fsm.state, 'running') - fsm.stop(); t.is(fsm.state, 'stopped') - - fsm.prepare(); t.is(fsm.state, 'ready') - fsm.start(); t.is(fsm.state, 'running') - fsm.pause(); t.is(fsm.state, 'paused') - fsm.stop(); t.is(fsm.state, 'stopped') - fsm.stop(); t.is(fsm.state, 'stopped') - - t.deepEqual(fsm.transitions(), ["prepare", "stop"], "ensure wildcard transition (stop) is included in available transitions") - fsm.prepare(); t.deepEqual(fsm.transitions(), ["start", "stop"], "ensure wildcard transition (stop) is included in available transitions") - fsm.start(); t.deepEqual(fsm.transitions(), ["pause", "stop"], "ensure wildcard transition (stop) is included in available transitions") - fsm.stop(); t.deepEqual(fsm.transitions(), ["prepare", "stop"], "ensure wildcard transition (stop) is included in available transitions") +test('wildcard :from allows transition from any state', async (t) => { + + var fsm = new StateMachine('stopped', { + prepare: { from: 'stopped', to: 'ready' }, + start: { from: 'ready', to: 'running' }, + resume: { from: 'paused', to: 'running' }, + pause: { from: 'running', to: 'paused' }, + stop: { from: '*', to: 'stopped' } + }); + + t.is(fsm.state, 'stopped', "initial state should be stopped"); + + await fsm.fire('prepare'); + t.is(fsm.state, 'ready') + await fsm.fire('stop') + t.is(fsm.state, 'stopped') + + await fsm.fire('prepare') + t.is(fsm.state, 'ready') + await fsm.fire('start') + t.is(fsm.state, 'running') + await fsm.fire('stop') + t.is(fsm.state, 'stopped') + + await fsm.fire('prepare') + t.is(fsm.state, 'ready') + await fsm.fire('start') + t.is(fsm.state, 'running') + await fsm.fire('pause') + t.is(fsm.state, 'paused') + await fsm.fire('stop') + t.is(fsm.state, 'stopped') + await fsm.fire('stop') + t.is(fsm.state, 'stopped') + + t.deepEqual(fsm.transitions(), ["prepare", "stop"], "ensure wildcard transition (stop) is included in available transitions") + await fsm.fire('prepare') + t.deepEqual(fsm.transitions(), ["start", "stop"], "ensure wildcard transition (stop) is included in available transitions") + await fsm.fire('start'); + t.deepEqual(fsm.transitions(), ["pause", "stop"], "ensure wildcard transition (stop) is included in available transitions") + await fsm.fire('stop') + t.deepEqual(fsm.transitions(), ["prepare", "stop"], "ensure wildcard transition (stop) is included in available transitions") }) //----------------------------------------------------------------------------- -test('missing :from allows transition from any state', t => { - - var fsm = new StateMachine({ - init: 'stopped', - transitions: [ - { name: 'prepare', from: 'stopped', to: 'ready' }, - { name: 'start', from: 'ready', to: 'running' }, - { name: 'resume', from: 'paused', to: 'running' }, - { name: 'pause', from: 'running', to: 'paused' }, - { name: 'stop', /* any from state */ to: 'stopped' } - ]}); - - t.is(fsm.state, 'stopped', "initial state should be stopped") - - fsm.prepare(); t.is(fsm.state, 'ready') - fsm.stop(); t.is(fsm.state, 'stopped') - - fsm.prepare(); t.is(fsm.state, 'ready') - fsm.start(); t.is(fsm.state, 'running') - fsm.stop(); t.is(fsm.state, 'stopped') - - fsm.prepare(); t.is(fsm.state, 'ready') - fsm.start(); t.is(fsm.state, 'running') - fsm.pause(); t.is(fsm.state, 'paused') - fsm.stop(); t.is(fsm.state, 'stopped') - - t.deepEqual(fsm.transitions(), ["prepare", "stop"], "ensure missing :from transition (stop) is included in available transitions") - fsm.prepare(); t.deepEqual(fsm.transitions(), ["start", "stop"], "ensure missing :from transition (stop) is included in available transitions") - fsm.start(); t.deepEqual(fsm.transitions(), ["pause", "stop"], "ensure missing :from transition (stop) is included in available transitions") - fsm.stop(); t.deepEqual(fsm.transitions(), ["prepare", "stop"], "ensure missing :from transition (stop) is included in available transitions") +test('missing :from allows transition from any state', async (t) => { + + var fsm = new StateMachine('stopped', { + prepare: { from: 'stopped', to: 'ready' }, + start: { from: 'ready', to: 'running' }, + resume: { from: 'paused', to: 'running' }, + pause: { from: 'running', to: 'paused' }, + stop: { /* any from state */ to: 'stopped' } + }); + + t.is(fsm.state, 'stopped', "initial state should be stopped") + + await fsm.fire('prepare') + t.is(fsm.state, 'ready') + await fsm.fire('stop') + t.is(fsm.state, 'stopped') + + await fsm.fire('prepare') + t.is(fsm.state, 'ready') + await fsm.fire('start') + t.is(fsm.state, 'running') + await fsm.fire('stop') + t.is(fsm.state, 'stopped') + + await fsm.fire('prepare') + t.is(fsm.state, 'ready') + await fsm.fire('start') + t.is(fsm.state, 'running') + await fsm.fire('pause') + t.is(fsm.state, 'paused') + await fsm.fire('stop') + t.is(fsm.state, 'stopped') + + t.deepEqual(fsm.transitions(), ["prepare", "stop"], "ensure missing :from transition (stop) is included in available transitions") + await fsm.fire('prepare') + t.deepEqual(fsm.transitions(), ["start", "stop"], "ensure missing :from transition (stop) is included in available transitions") + await fsm.fire('start') + t.deepEqual(fsm.transitions(), ["pause", "stop"], "ensure missing :from transition (stop) is included in available transitions") + await fsm.fire('stop') + t.deepEqual(fsm.transitions(), ["prepare", "stop"], "ensure missing :from transition (stop) is included in available transitions") }) //----------------------------------------------------------------------------- -test('wildcard :from allows transition to a state that is never declared in any other :from transition ', t => { +test('wildcard :from allows transition to a state that is never declared in any other :from transition ', async (t) => { - var fsm = new StateMachine({ - transitions: [ - { name: 'step', from: 'none', to: 'mystery' }, // NOTE: 'mystery' is only ever declared in :to, never :from - { name: 'other', from: '*', to: 'complete' } - ] - }); + var fsm = new StateMachine('none', { + step: { from: 'none', to: 'mystery' }, // NOTE: 'mystery' is only ever declared in :to, never :from + other: { from: '*', to: 'complete' } + }); - t.is(fsm.state, 'none') - t.is(fsm.can('step'), true) - t.is(fsm.can('other'), true) + t.is(fsm.state, 'none') + t.is(fsm.can('step'), true) + t.is(fsm.can('other'), true) - fsm.step() + await fsm.fire('step') - t.is(fsm.state, 'mystery') - t.is(fsm.can('step'), false) - t.is(fsm.can('other'), true) + t.is(fsm.state, 'mystery') + t.is(fsm.can('step'), false) + t.is(fsm.can('other'), true) }) //----------------------------------------------------------------------------- -test('wildcard :to allows no-op transitions', t => { +test('wildcard :to allows no-op transitions', async (t) => { - var fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'stayA', from: 'A', to: '*' }, - { name: 'stayB', from: 'B', to: '*' }, - { name: 'noop', from: '*', to: '*' }, - { name: 'step', from: 'A', to: 'B' } - ] - }); + var fsm = new StateMachine('A', { + stayA: { from: 'A', to: '*' }, + stayB: { from: 'B', to: '*' }, + noop: { from: '*', to: '*' }, + step: { from: 'A', to: 'B' } + }); - t.is(fsm.state, 'A') - t.is(fsm.can('noop'), true) - t.is(fsm.can('step'), true) - t.is(fsm.can('stayA'), true) - t.is(fsm.can('stayB'), false) + t.is(fsm.state, 'A') + t.is(fsm.can('noop'), true) + t.is(fsm.can('step'), true) + t.is(fsm.can('stayA'), true) + t.is(fsm.can('stayB'), false) - fsm.stayA(); t.is(fsm.state, 'A') - fsm.noop(); t.is(fsm.state, 'A') + await fsm.fire('stayA') + t.is(fsm.state, 'A') + await fsm.fire('noop'); + t.is(fsm.state, 'A') - fsm.step(); + await fsm.fire('step') - t.is(fsm.state, 'B') - t.is(fsm.can('noop'), true) - t.is(fsm.can('step'), false) - t.is(fsm.can('stayA'), false) - t.is(fsm.can('stayB'), true) + t.is(fsm.state, 'B') + t.is(fsm.can('noop'), true) + t.is(fsm.can('step'), false) + t.is(fsm.can('stayA'), false) + t.is(fsm.can('stayB'), true) - fsm.stayB(); t.is(fsm.state, 'B') - fsm.noop(); t.is(fsm.state, 'B') + await fsm.fire('stayB') + t.is(fsm.state, 'B') + await fsm.fire('noop') + t.is(fsm.state, 'B') }) //----------------------------------------------------------------------------- -test('missing :to allows no-op transitions', t => { - - var fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'stayA', from: 'A' /* no-op */ }, - { name: 'stayB', from: 'B' /* no-op */ }, - { name: 'noop', from: '*' /* no-op */ }, - { name: 'step', from: 'A', to: 'B' } - ] - }); - - t.is(fsm.state, 'A') - t.is(fsm.can('noop'), true) - t.is(fsm.can('step'), true) - t.is(fsm.can('stayA'), true) - t.is(fsm.can('stayB'), false) - - fsm.stayA(); t.is(fsm.state, 'A') - fsm.noop(); t.is(fsm.state, 'A') - - fsm.step(); - - t.is(fsm.state, 'B') - t.is(fsm.can('noop'), true) - t.is(fsm.can('step'), false) - t.is(fsm.can('stayA'), false) - t.is(fsm.can('stayB'), true) - - fsm.stayB(); t.is(fsm.state, 'B') - fsm.noop(); t.is(fsm.state, 'B') - +test('missing :to allows no-op transitions', async (t) => { + var fsm = new StateMachine('A', { + stayA: { from: 'A' /* no-op */ }, + stayB: { from: 'B' /* no-op */ }, + noop: { from: '*' /* no-op */ }, + step: { from: 'A', to: 'B' } + }); + + t.is(fsm.state, 'A') + t.is(fsm.can('noop'), true) + t.is(fsm.can('step'), true) + t.is(fsm.can('stayA'), true) + t.is(fsm.can('stayB'), false) + + await fsm.fire('stayA'); + t.is(fsm.state, 'A') + await fsm.fire('noop'); + t.is(fsm.state, 'A') + + await fsm.fire('step'); + + t.is(fsm.state, 'B') + t.is(fsm.can('noop'), true) + t.is(fsm.can('step'), false) + t.is(fsm.can('stayA'), false) + t.is(fsm.can('stayB'), true) + + await fsm.fire('stayB'); + t.is(fsm.state, 'B') + await fsm.fire('noop'); + t.is(fsm.state, 'B') }) //----------------------------------------------------------------------------- -test('no-op transitions with multiple from states', t => { - - var fsm = new StateMachine({ - init: 'A', - transitions: [ - { name: 'step', from: 'A', to: 'B' }, - { name: 'noop1', from: ['A', 'B'] /* no-op */ }, - { name: 'noop2', from: '*' /* no-op */ }, - { name: 'noop3', from: ['A', 'B'], to: '*' }, - { name: 'noop4', from: '*', to: '*' } - ] - }); - - t.is(fsm.state, 'A') - t.is(fsm.can('step'), true) - t.is(fsm.can('noop1'), true) - t.is(fsm.can('noop2'), true) - t.is(fsm.can('noop3'), true) - t.is(fsm.can('noop4'), true) - - fsm.noop1(); t.is(fsm.state, 'A') - fsm.noop2(); t.is(fsm.state, 'A') - fsm.noop3(); t.is(fsm.state, 'A') - fsm.noop4(); t.is(fsm.state, 'A') - - fsm.step(); - t.is(fsm.state, 'B') - t.is(fsm.can('step'), false) - t.is(fsm.can('noop1'), true) - t.is(fsm.can('noop2'), true) - t.is(fsm.can('noop3'), true) - t.is(fsm.can('noop4'), true) - - fsm.noop1(); t.is(fsm.state, 'B') - fsm.noop2(); t.is(fsm.state, 'B') - fsm.noop3(); t.is(fsm.state, 'B') - fsm.noop4(); t.is(fsm.state, 'B') +test('no-op transitions with multiple from states', async (t) => { + + var fsm = new StateMachine('A', { + step: { from: 'A', to: 'B' }, + noop1: { from: ['A', 'B'] /* no-op */ }, + noop2: { from: '*' /* no-op */ }, + noop3: { from: ['A', 'B'], to: '*' }, + noop4: { from: '*', to: '*' } + }); + + t.is(fsm.state, 'A') + t.is(fsm.can('step'), true) + t.is(fsm.can('noop1'), true) + t.is(fsm.can('noop2'), true) + t.is(fsm.can('noop3'), true) + t.is(fsm.can('noop4'), true) + + await fsm.fire('noop1') + t.is(fsm.state, 'A') + await fsm.fire('noop2') + t.is(fsm.state, 'A') + await fsm.fire('noop3') + t.is(fsm.state, 'A') + await fsm.fire('noop4') + t.is(fsm.state, 'A') + + await fsm.fire('step') + t.is(fsm.state, 'B') + t.is(fsm.can('step'), false) + t.is(fsm.can('noop1'), true) + t.is(fsm.can('noop2'), true) + t.is(fsm.can('noop3'), true) + t.is(fsm.can('noop4'), true) + + await fsm.fire('noop1') + t.is(fsm.state, 'B') + await fsm.fire('noop2') + t.is(fsm.state, 'B') + await fsm.fire('noop3') + t.is(fsm.state, 'B') + await fsm.fire('noop4') + t.is(fsm.state, 'B') }) diff --git a/tsconfig.json b/tsconfig.json index 2e9b92a..d24f8d8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1,10 @@ { + "include": [ + "./src/**/*.ts" + ], + "exclude": [ + "node_modules" + ], "compilerOptions": { /* Basic Options */ "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ @@ -7,12 +13,12 @@ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - // "declaration": true, /* Generates corresponding '.d.ts' file. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ - // "outDir": "./", /* Redirect output structure to the directory. */ - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + "outDir": "./dist/", /* Redirect output structure to the directory. */ + "rootDir": "./src/", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "removeComments": true, /* Do not emit comments to output. */ // "noEmit": true, /* Do not emit outputs. */ diff --git a/types/state-machine.d.ts b/types/state-machine.d.ts deleted file mode 100644 index d3df890..0000000 --- a/types/state-machine.d.ts +++ /dev/null @@ -1,79 +0,0 @@ -declare class StateMachine { - public constructor(options: Partial); - public static factory(options: Partial): StateMachine.IFSM; - public static factory(instance: T, options: Partial): StateMachine.IFSM | T; - [action: string]: ((...args: any[]) => any); - public is: StateMachine.StateMachineIs; - public can: StateMachine.StateMachineCan; - public cannot: StateMachine.StateMachineCan; - public transitions: StateMachine.StateMachineTransitions; - public allTransitions: StateMachine.StateMachineTransitions; - public allStates: StateMachine.StateMachineStates; - public observe: StateMachine.Observe; - public clearHistory(): void; - public historyBack(): void; - public historyForward(): void; - public canHistory(): boolean; - public canhistoryForward(): boolean; - // protected state: string; - // protected history: string[]; -} - -declare namespace StateMachine { - const VERSION: string; // = "3.x.x" - const defaults: { - wildcard: '*', - init: { - name: 'init', - from: 'none' - } - }; - // types - type StateMachineIs = (state: string) => boolean; - type StateMachineCan = (evt: string) => boolean; - type StateMachineTransitions = () => string[]; - type StateMachineStates = () => string[]; - type Callback = (...args: any[]) => any; - interface Observe { - (event: string, callback: Callback): void; - [name: string]: Callback; - } - - interface LifeCycle { - transition: string; - from: string; - to: string; - } - - interface Options { - name: string; - past: string; - future: string; - init: string; - max: number; // max history - state: string; - transitions: { - name: string; - from: string | string[] | '*'; - to: string | ((...args: any[]) => string); - }[]; - methods: { - [method: string]: Callback | undefined; - onBeforeTransition?(lifecycle: LifeCycle, ...args: any[]): boolean | Promise; // 1 - onLeaveState?(lifecycle: LifeCycle, ...args: any[]): boolean | Promise; // 2 - onTransition?(lifecycle: LifeCycle, ...args: any[]): boolean | Promise; // 3 - onEnterState?(lifecycle: LifeCycle, ...args: any[]): any | Promise; // 4 - onAfterTransition?(lifecycle: LifeCycle, ...args: any[]): any | Promise; // 5 - onPendingTransition?(transition: string, from: string, to: string): any | Promise; - }; - data: any; // {} | any[] | ((...args: any[]) => {} | any[]); - plugins: any[]; - } - - interface IFSM { - new(...data: any[]): StateMachine; - } -} - -export = StateMachine; -export as namespace StateMachine; diff --git a/types/test-commonjs.ts b/types/test-commonjs.ts deleted file mode 100644 index a3efc2c..0000000 --- a/types/test-commonjs.ts +++ /dev/null @@ -1,60 +0,0 @@ -import * as StateMachine from './state-machine'; - -var fsm = new StateMachine({ - transitions: [ - { name: 'melt', from: 'solid', to: 'liquid' }, - { name: 'freeze', from: 'liquid', to: 'solid' }, - { name: 'vaporize', from: 'liquid', to: 'gas' }, - { name: 'condense', from: 'gas', to: 'liquid' } - ], - methods: { - onMelt: function () { console.log('I melted') }, - onFreeze: function () { console.log('I froze') }, - onVaporize: function () { console.log('I vaporized') }, - onCondense: function () { console.log('I condensed') } - } -}); - -var Matter = StateMachine.factory({ // <-- the factory is constructed here - init: 'solid', - transitions: [ - { name: 'melt', from: 'solid', to: 'liquid' }, - { name: 'freeze', from: 'liquid', to: 'solid' }, - { name: 'vaporize', from: 'liquid', to: 'gas' }, - { name: 'condense', from: 'gas', to: 'liquid' } - ] -}); - -var a = new Matter(), // <-- instances are constructed here - b = new Matter(), - c = new Matter(); - -function Person(name) { - this.name = name; - this._fsm(); // <-- IMPORTANT -} - -Person.prototype = { - speak: function () { - console.log('my name is ' + this.name + ' and I am ' + this.state); - } -} - -StateMachine.factory(Person, { - init: 'idle', - transitions: [ - { name: 'sleep', from: 'idle', to: 'sleeping' }, - { name: 'wake', from: 'sleeping', to: 'idle' } - ] -}); - -var amy = new Person('amy'), - bob = new Person('bob'); - -bob.sleep(); - -amy.state; // 'idle' -bob.state; // 'sleeping' - -amy.speak(); // 'my name is amy and I am idle' -bob.speak(); // 'my name is bob and I am sleeping' \ No newline at end of file diff --git a/types/test-umd.ts b/types/test-umd.ts deleted file mode 100644 index e576e3d..0000000 --- a/types/test-umd.ts +++ /dev/null @@ -1,19 +0,0 @@ -const fsm = new StateMachine({ - transitions: [ - { name: 'melt', from: 'solid', to: 'liquid' }, - { name: 'freeze', from: 'liquid', to: 'solid' }, - { name: 'vaporize', from: 'liquid', to: 'gas' }, - { name: 'condense', from: 'gas', to: 'liquid' } - ], - methods: { - onMelt: function () { console.log('I melted') }, - onFreeze: function () { console.log('I froze') }, - onVaporize: function () { console.log('I vaporized') }, - onCondense: function () { console.log('I condensed') } - } -}); - -const FSM = StateMachine.factory({}); - -const f = new FSM(); -f.abc(1); diff --git a/webpack.config.js b/webpack.config.js index 5759158..3fe1549 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -35,7 +35,7 @@ module.exports = function (env) { output: { filename: path.join(output, cfg.name + '.js'), library: cfg.library, - libraryTarget: 'umd', + libraryTarget: 'commonjs-module', umdNamedDefine: false } } From a75b2214080e841cfb685062c25c32968bed4a29 Mon Sep 17 00:00:00 2001 From: taoqf Date: Thu, 28 Jan 2021 21:53:12 +0800 Subject: [PATCH 6/6] 4.0 --- README.md | 58 ++++++++++++++++++++++--------------------------------- 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index db34450..71d55c4 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ A library for finite state machines.
-### NOTE for existing users +## NOTE for existing users > **VERSION 3.0** Is a significant rewrite from earlier versions. Existing 2.x users should be sure to read the [Upgrade Guide](docs/upgrading-from-v2.md). @@ -23,7 +23,7 @@ A library for finite state machines.
-# Installation +## Installation In a browser: @@ -47,24 +47,20 @@ In Node.js: var StateMachine = require('@taoqf/javascript-state-machine'); ``` -# Usage +## Usage A state machine can be constructed using: ```javascript - var fsm = new StateMachine({ - init: 'solid', - transitions: [ - { name: 'melt', from: 'solid', to: 'liquid' }, - { name: 'freeze', from: 'liquid', to: 'solid' }, - { name: 'vaporize', from: 'liquid', to: 'gas' }, - { name: 'condense', from: 'gas', to: 'liquid' } - ], - methods: { - onMelt: function() { console.log('I melted') }, - onFreeze: function() { console.log('I froze') }, - onVaporize: function() { console.log('I vaporized') }, - onCondense: function() { console.log('I condensed') } + var fsm = new StateMachine('solid',{ + melt: { from: 'solid', to: 'liquid' }, + freeze: { from: 'liquid', to: 'solid' }, + vaporize: { from: 'liquid', to: 'gas' }, + condense: { from: 'gas', to: 'liquid' } + },{ + on(transition, from, to) { + if(transition === 'melt'){ + console.log('I melted'); } }); ``` @@ -75,17 +71,10 @@ A state machine can be constructed using: ... methods to transition to a different state: - * `fsm.melt()` - * `fsm.freeze()` - * `fsm.vaporize()` - * `fsm.condense()` - -... observer methods called automatically during the lifecycle of a transition: - - * `onMelt()` - * `onFreeze()` - * `onVaporize()` - * `onCondense()` + * `await fsm.fire('melt')` + * `await fsm.fire('freeze')` + * `await fsm.fire('vaporize')` + * `await fsm.fire('condense')` ... along with the following helper methods: @@ -96,7 +85,7 @@ A state machine can be constructed using: * `fsm.allTransitions()` - return list of all possible transitions * `fsm.allStates()` - return list of all possible states -# Terminology +## Terminology A state machine consists of a set of [**States**](docs/states-and-transitions.md) @@ -123,7 +112,7 @@ A state machine can also have arbitrary [**Data and Methods**](docs/data-and-met Multiple instances of a state machine can be created using a [**State Machine Factory**](docs/state-machine-factory.md). -# Documentation +## Documentation Read more about @@ -139,20 +128,19 @@ Read more about * [Upgrading from 2.x](docs/upgrading-from-v2.md) * [TypeScript](docs/typescript.md) -# Contributing +## Contributing You can [Contribute](docs/contributing.md) to this project with issues or pull requests. -# Release Notes +## Release Notes See [RELEASE NOTES](RELEASE_NOTES.md) file. -# License +## License See [MIT LICENSE](https://github.com/jakesgordon/javascript-state-machine/blob/master/LICENSE) file. -# Contact +## Contact If you have any ideas, feedback, requests or bug reports, you can reach me at -[jake@codeincomplete.com](mailto:jake@codeincomplete.com), or via -my website: [Code inComplete](http://codeincomplete.com/) +[tao_qiufeng@126.com](mailto:tao_qiufeng@126.com)