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/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)
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 eb8e81d..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 f.methods[e]=function(){this[n].length=0,this[r].length=0},f.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 6e7b953..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){var r,e,o;for(r=1;r "+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= 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 45c8e37..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){var i,e,s;for(i=1;i= 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 faf6f10..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) | string | string[];
- 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
}
}