diff --git a/.ackrc b/.ackrc new file mode 100644 index 0000000..df1e9a8 --- /dev/null +++ b/.ackrc @@ -0,0 +1,4 @@ +--ignore-dir=coverage +--ignore-dir=node_modules +--ignore-dir=.nyc_output + diff --git a/.gitignore b/.gitignore index 3c3629e..89d2c73 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ node_modules +coverage +.nyc_output +*.swp diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c86adcc --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: + - '4' + - '6' +sudo: false diff --git a/LICENSE b/LICENSE index 01f6314..b01dd40 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012, 2013, 2014, 2015, 2016, Jake Gordon and contributors +Copyright (c) 2012, 2013, 2014, 2015, 2016, 2017, 2018, Jake Gordon and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 8020ce1..a2c4add 100644 --- a/README.md +++ b/README.md @@ -1,367 +1,148 @@ -Javascript Finite State Machine (v2.3.5) -======================================== +# Javascript State Machine -This standalone javascript micro-framework provides a finite state machine for your pleasure. +[![NPM version](https://badge.fury.io/js/javascript-state-machine.svg)](https://badge.fury.io/js/javascript-state-machine) +[![Build Status](https://travis-ci.org/jakesgordon/javascript-state-machine.svg?branch=master)](https://travis-ci.org/jakesgordon/javascript-state-machine) - * You can find the [code here](https://github.com/jakesgordon/javascript-state-machine) - * You can find a [description here](http://codeincomplete.com/posts/2013/1/26/javascript_state_machine_v2_2_0/) - * You can find a [working demo here](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/example/) +A library for finite state machines. -This library has also been ported to: +![matter state machine](examples/matter.png) - * [Go](https://github.com/looplab/fsm) by @maxpersson - * [Python](https://github.com/oxplot/fysom) by @oxplot +
-Download -======== +### NOTE for existing users -You can download [state-machine.js](https://github.com/jakesgordon/javascript-state-machine/raw/master/state-machine.js), -or the [minified version](https://github.com/jakesgordon/javascript-state-machine/raw/master/state-machine.min.js) +> **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). -Alternatively: +
- git clone git@github.com:jakesgordon/javascript-state-machine +# Installation +In a browser: - * All code is in state-machine.js - * Minified version provided in state-machine.min.js - * No 3rd party library is required - * Demo can be found in /index.html - * QUnit (browser) tests can be found in /test/index.html - * QUnit (headless) tests can be run with "node test/runner.js" (after installing node-qunit with "npm install") +```html + +``` -Usage -===== +> after downloading the [source](dist/state-machine.js) or the [minified version](dist/state-machine.min.js) -Include `state-machine.js` in your web application, or, for nodejs `require("javascript-state-machine.js")`. +Using npm: -In its simplest form, create a standalone state machine using: +```shell + npm install --save-dev javascript-state-machine +``` - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ]}); +In Node.js: -... will create an object with a method for each event: +```javascript + var StateMachine = require('javascript-state-machine'); +``` - * fsm.warn() - transition from 'green' to 'yellow' - * fsm.panic() - transition from 'yellow' to 'red' - * fsm.calm() - transition from 'red' to 'yellow' - * fsm.clear() - transition from 'yellow' to 'green' +# Usage -along with the following members: +A state machine can be constructed using: - * fsm.current - contains the current state - * fsm.is(s) - return true if state `s` is the current state - * fsm.can(e) - return true if event `e` can be fired in the current state - * fsm.cannot(e) - return true if event `e` cannot be fired in the current state - * fsm.transitions() - return list of events that are allowed from the current state +```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') } + } + }); +``` -Multiple 'from' and 'to' states for a single event -================================================== +... which creates an object with a current state property: -If an event is allowed **from** multiple states, and always transitions to the same -state, then simply provide an array of states in the `from` attribute of an event. However, -if an event is allowed from multiple states, but should transition **to** a different -state depending on the current state, then provide multiple event entries with -the same name: + * `fsm.state` - var fsm = StateMachine.create({ - initial: 'hungry', - events: [ - { name: 'eat', from: 'hungry', to: 'satisfied' }, - { name: 'eat', from: 'satisfied', to: 'full' }, - { name: 'eat', from: 'full', to: 'sick' }, - { name: 'rest', from: ['hungry', 'satisfied', 'full', 'sick'], to: 'hungry' }, - ]}); +... methods to transition to a different state: -This example will create an object with 2 event methods: + * `fsm.melt()` + * `fsm.freeze()` + * `fsm.vaporize()` + * `fsm.condense()` - * fsm.eat() - * fsm.rest() +... observer methods called automatically during the lifecycle of a transition: -The `rest` event will always transition to the `hungry` state, while the `eat` event -will transition to a state that is dependent on the current state. + * `onMelt()` + * `onFreeze()` + * `onVaporize()` + * `onCondense()` ->> NOTE: The `rest` event could use a wildcard '*' for the 'from' state if it should be -allowed from any current state. +... along with the following helper methods: ->> NOTE: The `rest` event in the above example can also be specified as multiple events with -the same name if you prefer the verbose approach. + * `fsm.is(s)` - return true if state `s` is the current state + * `fsm.can(t)` - return true if transition `t` can occur from the current state + * `fsm.cannot(t)` - return true if transition `t` cannot occur from the current state + * `fsm.transitions()` - return list of transitions that are allowed from the current state + * `fsm.allTransitions()` - return list of all possible transitions + * `fsm.allStates()` - return list of all possible states -Callbacks -========= +# Terminology -4 types of callback are available by attaching methods to your StateMachine using the following naming conventions: +A state machine consists of a set of [**States**](docs/states-and-transitions.md) - * `onbeforeEVENT` - fired before the event - * `onleaveSTATE` - fired when leaving the old state - * `onenterSTATE` - fired when entering the new state - * `onafterEVENT` - fired after the event + * solid + * liquid + * gas ->> (using your **specific** EVENT and STATE names) +A state machine changes state by using [**Transitions**](docs/states-and-transitions.md) -For convenience, the 2 most useful callbacks can be shortened: + * melt + * freeze + * vaporize + * condense - * `onEVENT` - convenience shorthand for `onafterEVENT` - * `onSTATE` - convenience shorthand for `onenterSTATE` +A state machine can perform actions during a transition by observing [**Lifecycle Events**](docs/lifecycle-events.md) -In addition, 4 general-purpose callbacks can be used to capture **all** event and state changes: + * onBeforeMelt + * onAfterMelt + * onLeaveSolid + * onEnterLiquid + * ... - * `onbeforeevent` - fired before *any* event - * `onleavestate` - fired when leaving *any* state - * `onenterstate` - fired when entering *any* state - * `onafterevent` - fired after *any* event +A state machine can also have arbitrary [**Data and Methods**](docs/data-and-methods.md). -All callbacks will be passed the same arguments: +Multiple instances of a state machine can be created using a [**State Machine Factory**](docs/state-machine-factory.md). - * **event** name - * **from** state - * **to** state - * _(followed by any arguments you passed into the original event method)_ +# Documentation -Callbacks can be specified when the state machine is first created: +Read more about - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ], - callbacks: { - onpanic: function(event, from, to, msg) { alert('panic! ' + msg); }, - onclear: function(event, from, to, msg) { alert('thanks to ' + msg); }, - ongreen: function(event, from, to) { document.body.className = 'green'; }, - onyellow: function(event, from, to) { document.body.className = 'yellow'; }, - onred: function(event, from, to) { document.body.className = 'red'; }, - } - }); + * [States and Transitions](docs/states-and-transitions.md) + * [Data and Methods](docs/data-and-methods.md) + * [Lifecycle Events](docs/lifecycle-events.md) + * [Asynchronous Transitions](docs/async-transitions.md) + * [Initialization](docs/initialization.md) + * [Error Handling](docs/error-handling.md) + * [State History](docs/state-history.md) + * [Visualization](docs/visualization.md) + * [State Machine Factory](docs/state-machine-factory.md) + * [Upgrading from 2.x](docs/upgrading-from-v2.md) + +# Contributing - fsm.panic('killer bees'); - fsm.clear('sedatives in the honey pots'); - ... +You can [Contribute](docs/contributing.md) to this project with issues or pull requests. -Additionally, they can be added and removed from the state machine at any time: +# Release Notes - fsm.ongreen = null; - fsm.onyellow = null; - fsm.onred = null; - fsm.onenterstate = function(event, from, to) { document.body.className = to; }; +See [RELEASE NOTES](RELEASE_NOTES.md) file. +# License -The order in which callbacks occur is as follows: +See [MIT LICENSE](https://github.com/jakesgordon/javascript-state-machine/blob/master/LICENSE) file. ->> assume event **go** transitions from **red** state to **green** - - * `onbeforego` - specific handler for the **go** event only - * `onbeforeevent` - generic handler for all events - * `onleavered` - specific handler for the **red** state only - * `onleavestate` - generic handler for all states - * `onentergreen` - specific handler for the **green** state only - * `onenterstate` - generic handler for all states - * `onaftergo` - specific handler for the **go** event only - * `onafterevent` - generic handler for all events - ->> NOTE: the legacy `onchangestate` handler has been deprecated and will be removed in a future version - -You can affect the event in 3 ways: - - * return `false` from an `onbeforeEVENT` handler to cancel the event. - * return `false` from an `onleaveSTATE` handler to cancel the event. - * return `ASYNC` from an `onleaveSTATE` handler to perform an asynchronous state transition (see next section) - -Asynchronous State Transitions -============================== - -Sometimes, you need to execute some asynchronous code during a state transition and ensure the -new state is not entered until your code has completed. - -A good example of this is when you transition out of a `menu` state, perhaps you want to gradually -fade the menu away, or slide it off the screen and don't want to transition to your `game` state -until after that animation has been performed. - -You can now return `StateMachine.ASYNC` from your `onleavestate` handler and the state machine -will be _'put on hold'_ until you are ready to trigger the transition using the new `transition()` -method. - -For example, using jQuery effects: - - var fsm = StateMachine.create({ - - initial: 'menu', - - events: [ - { name: 'play', from: 'menu', to: 'game' }, - { name: 'quit', from: 'game', to: 'menu' } - ], - - callbacks: { - - onentermenu: function() { $('#menu').show(); }, - onentergame: function() { $('#game').show(); }, - - onleavemenu: function() { - $('#menu').fadeOut('fast', function() { - fsm.transition(); - }); - return StateMachine.ASYNC; // tell StateMachine to defer next state until we call transition (in fadeOut callback above) - }, - - onleavegame: function() { - $('#game').slideDown('slow', function() { - fsm.transition(); - }; - return StateMachine.ASYNC; // tell StateMachine to defer next state until we call transition (in slideDown callback above) - } - - } - }); - ->> _NOTE: If you decide to cancel the ASYNC event, you can call `fsm.transition.cancel();` - -State Machine Classes -===================== - -You can also turn all instances of a _class_ into an FSM by applying -the state machine functionality to the prototype, including your callbacks -in your prototype, and providing a `startup` event for use when constructing -instances: - - MyFSM = function() { // my constructor function - this.startup(); - }; - - MyFSM.prototype = { - - onpanic: function(event, from, to) { alert('panic'); }, - onclear: function(event, from, to) { alert('all is clear'); }, - - // my other prototype methods - - }; - - StateMachine.create({ - target: MyFSM.prototype, - events: [ - { name: 'startup', from: 'none', to: 'green' }, - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ]}); - - -This should be easy to adjust to fit your appropriate mechanism for object construction. - ->> _NOTE: the `startup` event can be given any name, but it must be present in some form to - ensure that each instance constructed is initialized with its own unique `current` state._ - -Initialization Options -====================== - -How the state machine should initialize can depend on your application requirements, so -the library provides a number of simple options. - -By default, if you don't specify any initial state, the state machine will be in the `'none'` -state and you would need to provide an event to take it out of this state: - - var fsm = StateMachine.create({ - events: [ - { name: 'startup', from: 'none', to: 'green' }, - { name: 'panic', from: 'green', to: 'red' }, - { name: 'calm', from: 'red', to: 'green' }, - ]}); - alert(fsm.current); // "none" - fsm.startup(); - alert(fsm.current); // "green" - -If you specify the name of your initial state (as in all the earlier examples), then an -implicit `startup` event will be created for you and fired when the state machine is constructed. - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'panic', from: 'green', to: 'red' }, - { name: 'calm', from: 'red', to: 'green' }, - ]}); - alert(fsm.current); // "green" - -If your object already has a `startup` method you can use a different name for the initial event - - var fsm = StateMachine.create({ - initial: { state: 'green', event: 'init' }, - events: [ - { name: 'panic', from: 'green', to: 'red' }, - { name: 'calm', from: 'red', to: 'green' }, - ]}); - alert(fsm.current); // "green" - -Finally, if you want to wait to call the initial state transition event until a later date you -can `defer` it: - - var fsm = StateMachine.create({ - initial: { state: 'green', event: 'init', defer: true }, - events: [ - { name: 'panic', from: 'green', to: 'red' }, - { name: 'calm', from: 'red', to: 'green' }, - ]}); - alert(fsm.current); // "none" - fsm.init(); - alert(fsm.current); // "green" - -Of course, we have now come full circle, this last example is pretty much functionally the -same as the first example in this section where you simply define your own startup event. - -So you have a number of choices available to you when initializing your state machine. - ->> _IMPORTANT NOTE: if you are using the pattern described in the previous section "State Machine - Classes", and wish to declare an `initial` state in this manner, you MUST use the `defer: true` - attribute and manually call the starting event in your constructor function. This will ensure - that each instance gets its own unique `current` state, rather than an (unwanted) shared - `current` state on the prototype object itself._ - -Handling Failures -====================== - -By default, if you try to call an event method that is not allowed in the current state, the -state machine will throw an exception. If you prefer to handle the problem yourself, you can -define a custom `error` handler: - - var fsm = StateMachine.create({ - initial: 'green', - error: function(eventName, from, to, args, errorCode, errorMessage) { - return 'event ' + eventName + ' was naughty :- ' + errorMessage; - }, - events: [ - { name: 'panic', from: 'green', to: 'red' }, - { name: 'calm', from: 'red', to: 'green' }, - ]}); - alert(fsm.calm()); // "event calm was naughty :- event not allowed in current state green" - -Release Notes -============= - -See [RELEASE NOTES](https://github.com/jakesgordon/javascript-state-machine/blob/master/RELEASE_NOTES.md) file. - -License -======= - -See [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/) - - - - - +[jakesgordon@gmail.com](mailto:jakesgordon@gmail.com), or via +my website: [jakesgordon.com](https://jakesgordon.com/) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 7e9fd70..67de268 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,50 @@ +Version 3.1.0 (July 12th 2018) +------------------------------ + + * Changed back to MIT license + +Version 3.0.1 (June 10th 2017) +------------------------------ + + * First 3.x release - see 3.0.0-rc.1 release notes below + + * fix issue #109 - rejection from async lifecycle method does not reject transitions promise + * fix issue #106 - async transition: forward resolved value + * fix issue #107 - lifecycle event name breaks for all uppercase + +Version 3.0.0-rc.1 (January 10 2017) +------------------------------------ + +**IMPORTANT NOTE**: this version includes **breaking changes** that will require code updates. + +Please read [UPGRADING FROM 2.x](docs/upgrading-from-v2.md) for details. Highlights include: + + * Improved Construction. + * Arbitrary Data and Methods. + * Observable Transitions + * Conditional Transitions + * Promise-based Asynchronous Transitions + * Improved Transition Lifecycle Events + * State History + * Visualization + * Webpack build system + * ... + +
+
+ +Version 2.4.0 (November 20 2016) +-------------------------------- + + * added npm install instructions to readme + * fix for javascript error when running in jasmine/node (issue #88) + * exclude build files from bower install (pull request #75) + * ensure WILDCARD events are included in list of available transitions() (issue #93) + * fix FSM getting stuck into "*" state when using double wildcard (issue #64) + * function (fsm.states) returning list of all available states in the machine would help automated testing (issue #54) + * state machine hides callback exceptions (issue #62) + * replaced (dev dependency) YUI compressor with uglify-js for building minified version + Version 2.3.5 (January 20 2014) ------------------------------- @@ -34,7 +81,7 @@ Version 2.2.0 (January 26th 2013) * Added generic state callbacks 'onleavestate' and 'onenterstate' (issue #28) * Fixed 'undefined' event return codes (issue #34) - pull from gentooboontoo (thanks!) * Allow async event transition to be cancelled (issue #22) - * [read more...](http://codeincomplete.com/posts/2013/1/26/javascript_state_machine_v2_2_0/) + * [read more...](https://jakesgordon.com/writing/javascript-state-machine-v2-2-0/) Version 2.1.0 (January 7th 2012) -------------------------------- @@ -57,14 +104,14 @@ Version 2.0.0 (August 19th 2011) * added a generic `onchangestate(event,from,to)` callback to detect all state changes with a single function. * allow callbacks to be declared at creation time (instead of having to attach them afterwards) * renamed 'hooks' => 'callbacks' - * [read more...](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/) + * [read more...](https://jakesgordon.com/writing/javascript-state-machine-v2/) Version 1.2.0 (June 21st 2011) ------------------------------ * allows the same event to transition to different states, depending on the current state (see 'Multiple...' section in README.md) - * [read more...](http://codeincomplete.com/posts/2011/6/21/javascript_state_machine_v1_2_0/) + * [read more...](https://jakesgordon.com/writing/javascript-state-machine-v1-2-0/) Version 1.0.0 (June 1st 2011) ----------------------------- * initial version - * [read more...](http://codeincomplete.com/posts/2011/6/1/javascript_state_machine/) + * [read more...](https://jakesgordon.com/writing/javascript-state-machine/) diff --git a/Rakefile b/Rakefile deleted file mode 100644 index beb8702..0000000 --- a/Rakefile +++ /dev/null @@ -1,8 +0,0 @@ - -desc "create minified version of state-machine.js" -task :minify do - require File.expand_path(File.join(File.dirname(__FILE__), 'minifier/minifier')) - Minifier.enabled = true - Minifier.minify('state-machine.js') -end - diff --git a/bin/examples b/bin/examples new file mode 100755 index 0000000..e726661 --- /dev/null +++ b/bin/examples @@ -0,0 +1,60 @@ +#!/usr/bin/env node + +//================================================================================================= +// +// This script is used to regenerate the example visualizations +// +//================================================================================================= + +var fs = require('fs'), + path = require('path'), + child = require('child_process'); + +//------------------------------------------------------------------------------------------------- + +fs.readdirSync('examples') + .filter(function(file) { return path.extname(file) === ".js" }) + .map(visualize); + +//------------------------------------------------------------------------------------------------- + +function visualize(example) { + var name = path.basename(example, '.js'), + fsm = require('../examples/' + example), + dot = fsm.visualize(), + svg = dot2svg(dot), + png = dot2png(dot); + console.log('visualizing examples/' + example); + fs.writeFileSync('examples/' + name + '.dot', dot); + fs.writeFileSync('examples/' + name + '.svg', svg); + fs.writeFileSync('examples/' + name + '.png', png, 'binary'); +} + +//------------------------------------------------------------------------------------------------- + +function dot2svg(dot) { + var result = child.spawnSync("dot", ["-Tsvg"], { input: dot }); + if (result.error) + dotError(result.error.errno); + return result.stdout.toString(); +} + +//------------------------------------------------------------------------------------------------- + +function dot2png(dot) { + var result = child.spawnSync("dot", ["-Tpng"], { input: dot }); + if (result.error) + dotError(result.error.errno); + return result.stdout; +} + +//------------------------------------------------------------------------------------------------- + +function dotError(errno) { + if (errno === 'ENOENT') + throw new Error("dot program not found. Install graphviz (http://graphviz.org)") + else + throw new Error("unexpected error: " + errno) +} + +//------------------------------------------------------------------------------------------------- diff --git a/bin/minify b/bin/minify new file mode 100755 index 0000000..88fe76c --- /dev/null +++ b/bin/minify @@ -0,0 +1,40 @@ +#!/usr/bin/env node + +//================================================================================================= +// +// This script is used (by npm run build) to minify the distributed source code +// +//================================================================================================= + +var fs = require('fs-sync'), + path = require('path'), + uglify = require('uglify-js'), + target = 'dist'; + +//------------------------------------------------------------------------------------------------- + +fs.expand("lib/**/*.js") + .map(minify); + +//------------------------------------------------------------------------------------------------- + +function minify(file) { + var name = output_name(file), + expanded = path.join(target, name + '.js'), + minified = path.join(target, name + '.min.js') + + console.log('copied ' + file + ' to ' + expanded + ' and minified as ' + minified); + + fs.copy(file, expanded, { force: true }); + fs.write(minified, uglify.minify(expanded).code); +} + +function output_name(file) { + var name = path.basename(file, '.js'); + if (name === 'state-machine') + return 'state-machine' + else + return 'state-machine-' + name +} + +//------------------------------------------------------------------------------------------------- diff --git a/bower.json b/bower.json deleted file mode 100644 index 116db62..0000000 --- a/bower.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "javascript-state-machine", - "version": "2.3.5", - "homepage": "https://github.com/jakesgordon/javascript-state-machine", - "authors": [ - "Jake Gordon " - ], - "description": "a simple finite state machine library", - "main": "state-machine.js", - "moduleType": [ - "amd", - "globals", - "node" - ], - "keywords": [ - "state machine", - "server", - "client" - ], - "license": "MIT", - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "test", - "tests" - ] -} diff --git a/demo/demo.js b/demo/demo.js deleted file mode 100644 index 4789c85..0000000 --- a/demo/demo.js +++ /dev/null @@ -1,78 +0,0 @@ -Demo = function() { - - var output = document.getElementById('output'), - demo = document.getElementById('demo'), - panic = document.getElementById('panic'), - warn = document.getElementById('warn'), - calm = document.getElementById('calm'), - clear = document.getElementById('clear'), - count = 0; - - var log = function(msg, separate) { - count = count + (separate ? 1 : 0); - output.value = count + ": " + msg + "\n" + (separate ? "\n" : "") + output.value; - demo.className = fsm.current; - panic.disabled = fsm.cannot('panic'); - warn.disabled = fsm.cannot('warn'); - calm.disabled = fsm.cannot('calm'); - clear.disabled = fsm.cannot('clear'); - }; - - var fsm = StateMachine.create({ - - events: [ - { name: 'start', from: 'none', to: 'green' }, - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'green', to: 'red' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'red', to: 'green' }, - { name: 'clear', from: 'yellow', to: 'green' }, - ], - - callbacks: { - onbeforestart: function(event, from, to) { log("STARTING UP"); }, - onstart: function(event, from, to) { log("READY"); }, - - onbeforewarn: function(event, from, to) { log("START EVENT: warn!", true); }, - onbeforepanic: function(event, from, to) { log("START EVENT: panic!", true); }, - onbeforecalm: function(event, from, to) { log("START EVENT: calm!", true); }, - onbeforeclear: function(event, from, to) { log("START EVENT: clear!", true); }, - - onwarn: function(event, from, to) { log("FINISH EVENT: warn!"); }, - onpanic: function(event, from, to) { log("FINISH EVENT: panic!"); }, - oncalm: function(event, from, to) { log("FINISH EVENT: calm!"); }, - onclear: function(event, from, to) { log("FINISH EVENT: clear!"); }, - - onleavegreen: function(event, from, to) { log("LEAVE STATE: green"); }, - onleaveyellow: function(event, from, to) { log("LEAVE STATE: yellow"); }, - onleavered: function(event, from, to) { log("LEAVE STATE: red"); async(to); return StateMachine.ASYNC; }, - - ongreen: function(event, from, to) { log("ENTER STATE: green"); }, - onyellow: function(event, from, to) { log("ENTER STATE: yellow"); }, - onred: function(event, from, to) { log("ENTER STATE: red"); }, - - onchangestate: function(event, from, to) { log("CHANGED STATE: " + from + " to " + to); } - } - }); - - var async = function(to) { - pending(to, 3); - setTimeout(function() { - pending(to, 2); - setTimeout(function() { - pending(to, 1); - setTimeout(function() { - fsm.transition(); // trigger deferred state transition - }, 1000); - }, 1000); - }, 1000); - }; - - var pending = function(to, n) { log("PENDING STATE: " + to + " in ..." + n); }; - - fsm.start(); - return fsm; - -}(); - diff --git a/dist/state-machine-history.js b/dist/state-machine-history.js new file mode 100644 index 0000000..19f7c4f --- /dev/null +++ b/dist/state-machine-history.js @@ -0,0 +1,211 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if(typeof define === 'function' && define.amd) + define("StateMachineHistory", [], factory); + else if(typeof exports === 'object') + exports["StateMachineHistory"] = factory(); + else + root["StateMachineHistory"] = factory(); +})(this, function() { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // identity function for calling harmony imports with the correct context +/******/ __webpack_require__.i = function(value) { return value; }; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 1); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +"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; + + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +//------------------------------------------------------------------------------------------------- + +var camelize = __webpack_require__(0); + +//------------------------------------------------------------------------------------------------- + +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; + +} + + +/***/ }) +/******/ ]); +}); \ No newline at end of file diff --git a/dist/state-machine-history.min.js b/dist/state-machine-history.min.js new file mode 100644 index 0000000..9c7fb3f --- /dev/null +++ b/dist/state-machine-history.min.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("StateMachineHistory",[],e):"object"==typeof exports?exports.StateMachineHistory=e():t.StateMachineHistory=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,e),o.l=!0,o.exports}var n={};return e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,r){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:r})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=1)}([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;ec&&t[e].shift(),r.transition!==i&&r.transition!==s&&(t[n].length=0))},methods:{},properties:{}};return f.methods[o]=function(){this[e].length=0,this[n].length=0},f.properties[u]={get:function(){return this[e].length>1}},f.properties[p]={get:function(){return this[n].length>0}},f.methods[i]=function(){if(!this[u])throw Error("no history");var t=this[e].pop(),r=this[e].pop();this[n].push(t),this._fsm.transit(i,t,r,[])},f.methods[s]=function(){if(!this[p])throw Error("no history");var t=this.state,e=this[n].pop();this._fsm.transit(s,t,e,[])},f}}])}); \ No newline at end of file diff --git a/dist/state-machine-visualize.js b/dist/state-machine-visualize.js new file mode 100644 index 0000000..9c18e13 --- /dev/null +++ b/dist/state-machine-visualize.js @@ -0,0 +1,269 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if(typeof define === 'function' && define.amd) + define("StateMachineVisualize", [], factory); + else if(typeof exports === 'object') + exports["StateMachineVisualize"] = factory(); + else + root["StateMachineVisualize"] = factory(); +})(this, function() { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // identity function for calling harmony imports with the correct context +/******/ __webpack_require__.i = function(value) { return value; }; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 1); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +"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; +} + + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +//------------------------------------------------------------------------------------------------- + +var mixin = __webpack_require__(0) + +//------------------------------------------------------------------------------------------------- + +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; + +//------------------------------------------------------------------------------------------------- + + +/***/ }) +/******/ ]); +}); \ No newline at end of file diff --git a/dist/state-machine-visualize.min.js b/dist/state-machine-visualize.min.js new file mode 100644 index 0000000..e517d3b --- /dev/null +++ b/dist/state-machine-visualize.min.js @@ -0,0 +1 @@ +!function(t,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define("StateMachineVisualize",[],n):"object"==typeof exports?exports.StateMachineVisualize=n():t.StateMachineVisualize=n()}(this,function(){return function(t){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}var e={};return n.m=t,n.c=e,n.i=function(t){return t},n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get: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,n){return Object.prototype.hasOwnProperty.call(t,n)},n.p="",n(n.s=1)}([function(t,n,e){"use strict";t.exports=function(t,n){var e,r,o;for(e=1;e0&&(u.states=s),a&&a.length>0&&(u.transitions=a),u}function i(t){return" "+t+" "}function s(t){return'"'+t+'"'}function a(t){t=t||{};var n,e,r=t.name||"fsm",o=t.states||[],i=t.transitions||[],u=t.rankdir,f=[];for(f.push("digraph "+s(r)+" {"),u&&f.push(" rankdir="+u+";"),n=0,e=o.length;n "+s(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(" ; ")+" ]":""},r.dotcfg=o,r.dotify=a,t.exports=r}])}); \ No newline at end of file diff --git a/dist/state-machine.js b/dist/state-machine.js new file mode 100644 index 0000000..b8b9e37 --- /dev/null +++ b/dist/state-machine.js @@ -0,0 +1,669 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if(typeof define === 'function' && define.amd) + define("StateMachine", [], factory); + else if(typeof exports === 'object') + exports["StateMachine"] = factory(); + else + root["StateMachine"] = factory(); +})(this, function() { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // identity function for calling harmony imports with the correct context +/******/ __webpack_require__.i = function(value) { return value; }; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 5); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +"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; +} + + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +//------------------------------------------------------------------------------------------------- + +var mixin = __webpack_require__(0); + +//------------------------------------------------------------------------------------------------- + +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); + } + } + +} + +//------------------------------------------------------------------------------------------------- + + +/***/ }), +/* 2 */ +/***/ (function(module, exports, __webpack_require__) { + +"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; + + +/***/ }), +/* 3 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +//------------------------------------------------------------------------------------------------- + +var mixin = __webpack_require__(0), + camelize = __webpack_require__(2); + +//------------------------------------------------------------------------------------------------- + +function Config(options, StateMachine) { + + options = options || {}; + + this.options = options; // preserving original options can be useful (e.g visualize plugin) + this.defaults = StateMachine.defaults; + this.states = []; + this.transitions = []; + this.map = {}; + this.lifecycle = this.configureLifecycle(); + this.init = this.configureInitTransition(options.init); + this.data = this.configureData(options.data); + this.methods = this.configureMethods(options.methods); + + this.map[this.defaults.wildcard] = {}; + + this.configureTransitions(options.transitions || []); + + this.plugins = this.configurePlugins(options.plugins, StateMachine.plugin); + +} + +//------------------------------------------------------------------------------------------------- + +mixin(Config.prototype, { + + addState: function(name) { + if (!this.map[name]) { + this.states.push(name); + this.addStateLifecycleNames(name); + this.map[name] = {}; + } + }, + + addStateLifecycleNames: function(name) { + this.lifecycle.onEnter[name] = camelize.prepended('onEnter', name); + this.lifecycle.onLeave[name] = camelize.prepended('onLeave', name); + this.lifecycle.on[name] = camelize.prepended('on', name); + }, + + addTransition: function(name) { + if (this.transitions.indexOf(name) < 0) { + this.transitions.push(name); + this.addTransitionLifecycleNames(name); + } + }, + + addTransitionLifecycleNames: function(name) { + this.lifecycle.onBefore[name] = camelize.prepended('onBefore', name); + this.lifecycle.onAfter[name] = camelize.prepended('onAfter', name); + this.lifecycle.on[name] = camelize.prepended('on', name); + }, + + mapTransition: function(transition) { + var name = transition.name, + from = transition.from, + to = transition.to; + this.addState(from); + if (typeof to !== 'function') + this.addState(to); + this.addTransition(name); + this.map[from][name] = transition; + return transition; + }, + + configureLifecycle: function() { + return { + onBefore: { transition: 'onBeforeTransition' }, + onAfter: { transition: 'onAfterTransition' }, + onEnter: { state: 'onEnterState' }, + onLeave: { state: 'onLeaveState' }, + on: { transition: 'onTransition' } + }; + }, + + configureInitTransition: function(init) { + if (typeof init === 'string') { + return this.mapTransition(mixin({}, this.defaults.init, { to: init, active: true })); + } + else if (typeof init === 'object') { + return this.mapTransition(mixin({}, this.defaults.init, init, { active: true })); + } + else { + this.addState(this.defaults.init.from); + return this.defaults.init; + } + }, + + configureData: function(data) { + if (typeof data === 'function') + return data; + else if (typeof data === 'object') + return function() { return data; } + else + return function() { return {}; } + }, + + configureMethods: function(methods) { + return methods || {}; + }, + + configurePlugins: function(plugins, builtin) { + plugins = plugins || []; + var n, max, plugin; + for(n = 0, max = plugins.length ; n < max ; n++) { + plugin = plugins[n]; + if (typeof plugin === 'function') + plugins[n] = plugin = plugin() + if (plugin.configure) + plugin.configure(this); + } + return plugins + }, + + configureTransitions: function(transitions) { + var i, n, transition, from, to, wildcard = this.defaults.wildcard; + for(n = 0 ; n < transitions.length ; n++) { + transition = transitions[n]; + from = Array.isArray(transition.from) ? transition.from : [transition.from || wildcard] + to = transition.to || wildcard; + for(i = 0 ; i < from.length ; i++) { + this.mapTransition({ name: transition.name, from: from[i], to: to }); + } + } + }, + + transitionFor: function(state, transition) { + var wildcard = this.defaults.wildcard; + return this.map[state][transition] || + this.map[wildcard][transition]; + }, + + transitionsFor: function(state) { + var wildcard = this.defaults.wildcard; + return Object.keys(this.map[state]).concat(Object.keys(this.map[wildcard])); + }, + + allStates: function() { + return this.states; + }, + + allTransitions: function() { + return this.transitions; + } + +}); + +//------------------------------------------------------------------------------------------------- + +module.exports = Config; + +//------------------------------------------------------------------------------------------------- + + +/***/ }), +/* 4 */ +/***/ (function(module, exports, __webpack_require__) { + + +var mixin = __webpack_require__(0), + Exception = __webpack_require__(6), + plugin = __webpack_require__(1), + UNOBSERVED = [ null, [] ]; + +//------------------------------------------------------------------------------------------------- + +function JSM(context, config) { + this.context = context; + this.config = config; + this.state = config.init.from; + this.observers = [context]; +} + +//------------------------------------------------------------------------------------------------- + +mixin(JSM.prototype, { + + init: function(args) { + mixin(this.context, this.config.data.apply(this.context, args)); + plugin.hook(this, 'init'); + if (this.config.init.active) + return this.fire(this.config.init.name, []); + }, + + is: function(state) { + return Array.isArray(state) ? (state.indexOf(this.state) >= 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 new file mode 100644 index 0000000..b9439bc --- /dev/null +++ b/dist/state-machine.min.js @@ -0,0 +1 @@ +!function(t,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define("StateMachine",[],n):"object"==typeof exports?exports.StateMachine=n():t.StateMachine=n()}(this,function(){return function(t){function n(e){if(i[e])return i[e].exports;var s=i[e]={i:e,l:!1,exports:{}};return t[e].call(s.exports,s,s.exports,n),s.l=!0,s.exports}var i={};return n.m=t,n.c=i,n.i=function(t){return t},n.d=function(t,i,e){n.o(t,i)||Object.defineProperty(t,i,{configurable:!1,enumerable:!0,get:e})},n.n=function(t){var i=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(i,"a",i),i},n.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},n.p="",n(n.s=5)}([function(t,n,i){"use strict";t.exports=function(t,n){var i,e,s;for(i=1;i=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):a,r?this.observersForEvent(s.onLeave[n]):a,this.observersForEvent(s.on.transition),r?["doTransit",[this]]:a,r?this.observersForEvent(s.onEnter.state):a,r?this.observersForEvent(s.onEnter[i]):a,r?this.observersForEvent(s.on[i]):a,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=[];i You should be familiar with the state machine [Lifecycle Events](lifecycle-events.md) before reading this article. + +Sometimes, you need to execute some asynchronous code during a state transition and ensure the new +state is not entered until your code has completed. A good example of this is when you transition +out of a state and want to gradually fade a UI component away, or slide it off the screen, and +don't want to transition to the next state until after that animation has completed. + +You can achieve this by returning a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) +object from any of the [Lifecycle Events](lifecycle-events.md). + +Returning a Promise from a lifecycle event will cause the lifecycle for that transition to +pause. It can be continued by resolving the promise, or cancelled by rejecting the promise. + +For example (using jQuery effects): + +```javascript + var fsm = new StateMachine({ + + init: 'menu', + + transitions: [ + { name: 'play', from: 'menu', to: 'game' }, + { name: 'quit', from: 'game', to: 'menu' } + ], + + methods: { + + onEnterMenu: function() { + return new Promise(function(resolve, reject) { + $('#menu').fadeIn('fast', resolve) + }) + }, + + onEnterGame: function() { + return new Promise(function(resolve, reject) { + $('#game').fadeIn('fast', resolve) + }) + }, + + onLeaveMenu: function() { + return new Promise(function(resolve, reject) { + $('#menu').fadeOut('fast', resolve) + }) + }, + + onLeaveGame: function() { + return new Promise(function(resolve, reject) { + $('#game').fadeOut('fast', resolve) + }) + } + } + + }) +``` + +> Be sure that you always resolve (or reject) your Promise eventually, otherwise the state + machine will be stuck forever within that pending transition. diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..01e5e74 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,59 @@ +# Contributing + +The `javascript-state-machine` library is built using: + + * [Webpack 2](https://webpack.js.org/concepts/) - for bundling javascript modules together + * [UglifyJS2](https://github.com/mishoo/UglifyJS2) - for minifying bundled javascript files + * [Ava](https://github.com/avajs/ava) - for testing + +The directory structure includes: + +```shell + /bin # - build scripts + /dist # - minified bundles for distribution + /docs # - documentation + /examples # - example visualizations + /lib # - bundled source code for npm + /src # - source code + /test # - unit tests + + package.json # - npm configuration + webpack.config.js # - webpack configuration + + LICENSE # - the project licensing terms + README.md # - the project readme + RELEASE_NOTES.md # - the project release notes + +``` + +Build time dependencies can be installed using npm: + +```shell + > npm install +``` + +A number of npm scripts are available: + +```shell + > npm run test # run unit tests + > npm run build # bundle and minify files for distribution + > npm run watch # run tests if source files change +``` + +## Source Code + +The source code is written in es5 syntax and should be supported by all [es5 compatible browsers](http://caniuse.com/#feat=es5). +[Babel](https://babeljs.io/) is **NOT** used for this project. Webpack is used to +bundle modules together for distribution. + +## Submitting Pull Requests + +Generally speaking, please raise an issue first and lets discuss the problem and the +proposed solution. The next step would be a pull-request - fantastic and thank you for helping out - but +please try to... + + * ensure the tests pass (`npm test`). + * rebuild distribution files (`npm run build`). + * include tests for your changes. + * include documentation for your changes. + * include a great commit message. diff --git a/docs/data-and-methods.md b/docs/data-and-methods.md new file mode 100644 index 0000000..28aa7a6 --- /dev/null +++ b/docs/data-and-methods.md @@ -0,0 +1,64 @@ +# Data and Methods + +In addition to [States](states-and-transitions.md) and [Transitions](states-and-transitions.md), a state machine can +also contain arbitrary data and methods: + +```javascript + var fsm = new StateMachine({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B' } + ], + data: { + color: 'red' + }, + methods: { + describe: function() { + console.log('I am ' + this.color); + } + } + }); + + fsm.state; // 'A' + fsm.color; // 'red' + fsm.describe(); // 'I am red' +``` + +## Data and State Machine Factories + +If you are constructing multiple instances from a [State Machine Factory](state-machine-factory.md) then the +`data` object will be shared amongst them. This is almost certainly **NOT** what you want! To +ensure that each instance gets unique data you should use a `data` method instead: + +```javascript + var FSM = StateMachine.factory({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B' } + ], + data: function(color) { // <-- use a method that can be called for each instance + return { + color: color + } + }, + methods: { + describe: function() { + console.log('I am ' + this.color); + } + } + }); + + var a = new FSM('red'), + b = new FSM('blue'); + + a.state; // 'A' + b.state; // 'A' + + a.color; // 'red' + b.color; // 'blue' + + a.describe(); // 'I am red' + b.describe(); // 'I am blue' +``` + +> NOTE: that arguments used when constructing each instance are passed thru to the `data` method directly. diff --git a/docs/error-handling.md b/docs/error-handling.md new file mode 100644 index 0000000..c79f14c --- /dev/null +++ b/docs/error-handling.md @@ -0,0 +1,56 @@ +# Error Handling + +## Invalid Transitions + +By default, if you try to fire a transition that is not allowed in the current state, the +state machine will throw an exception. If you prefer to handle the problem yourself, you can +define a custom `onInvalidTransition` handler: + +```javascript + var fsm = new StateMachine({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B' }, + { name: 'reset', from: 'B', to: 'A' } + ], + methods: { + onInvalidTransition: function(transition, from, to) { + throw new Exception("transition not allowed from that state"); + } + } + }); + + fsm.state; // 'A' + fsm.can('step'); // true + fsm.can('reset'); // false + + fsm.reset(); // <-- throws "transition not allowed from that state" +``` + +## Pending Transitions + +By default, if you try to fire a transition during a [Lifecycle Event](lifecycle-events.md) for a +pending transition, the state machine will throw an exception. If you prefer to handle the problem +yourself, you can define a custom `onPendingTransition` handler: + +```javascript + var fsm = new StateMachine({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B' }, + { name: 'step', from: 'B', to: 'C' } + ], + methods: { + onLeaveA: function() { + this.step(); // <-- uh oh, trying to transition from within a lifecycle event is not allowed + }, + onPendingTransition: function(transition, from, to) { + throw new Exception("transition already in progress"); + } + } + }); + + fsm.state; // 'A' + fsm.can('step'), // true + fsm.step(); // <-- throws "transition already in progress" +``` diff --git a/docs/initialization.md b/docs/initialization.md new file mode 100644 index 0000000..39151ff --- /dev/null +++ b/docs/initialization.md @@ -0,0 +1,57 @@ +# Initialization Options + +## Explicit Init Transition + +By default, if you don't specify an initial state, the state machine will be in the `none` +state, no lifecycle events will fire during construction, and you will need to provide an +explicit transition to advance out of this state: + +```javascript + var fsm = new StateMachine({ + transitions: [ + { name: 'init', from: 'none', to: 'A' }, + { name: 'step', from: 'A', to: 'B' }, + { name: 'step', from: 'B', to: 'C' } + ] + }); + fsm.state; // 'none' + fsm.init(); // 'init()' transition is fired explicitly + fsm.state; // 'A' +``` + +## Implicit Init Transition + +If you specify the name of your initial state (as in most of the examples in this documentation), +then an implicit `init` transition will be created for you and fired (along with appropriate +lifecycle events) when the state machine is constructed. + +> This is the most common initialization strategy, and the one you should use 90% of the time + +```javascript + var fsm = new StateMachine({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B' }, + { name: 'step', from: 'B', to: 'C' } + ] + }); // 'init()' transition fires from 'none' to 'A' during construction + fsm.state; // 'A' +``` + +## Initialization and State Machine Factories + +For [State Machine Factories](state-machine-factory.md), the `init` transition +is triggered for each constructed instance. + +```javascript + var FSM = StateMachine.factory({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B' }, + { name: 'step', from: 'B', to: 'C' } + ] + }); + + var fsm1 = new FSM(), // 'init()' transition fires from 'none' to 'A' for fsm1 + fsm2 = new FSM(); // 'init()' transition fires from 'none' to 'A' for fsm2 +``` diff --git a/docs/lifecycle-events.md b/docs/lifecycle-events.md new file mode 100644 index 0000000..bb3381b --- /dev/null +++ b/docs/lifecycle-events.md @@ -0,0 +1,148 @@ +# Lifecycle Events + +In order to track or perform an action when a transition occurs, five +general-purpose lifecycle events can be observed: + + * `onBeforeTransition` - fired before any transition + * `onLeaveState` - fired when leaving any state + * `onTransition` - fired during any transition + * `onEnterState` - fired when entering any state + * `onAfterTransition` - fired after any transition + +In addition to the general-purpose events, transitions can be observed +using your specific transition and state names: + + * `onBefore` - fired before a specific TRANSITION begins + * `onLeave` - fired when leaving a specific STATE + * `onEnter` - fired when entering a specific STATE + * `onAfter` - fired after a specific TRANSITION completes + +For convenience, the 2 most useful events can be shortened: + + * `on` - convenience shorthand for `onAfter` + * `on` - convenience shorthand for `onEnter` + +## Observing Lifecycle Events + +Individual lifecycle events can be observed using an observer method: + +```javascript + fsm.observe('onStep', function() { + console.log('stepped'); + }); +``` + +Multiple events can be observed using an observer object: + +```javascript + fsm.observe({ + onStep: function() { console.log('stepped'); } + onA: function() { console.log('entered state A'); } + onB: function() { console.log('entered state B'); } + }); +``` + +A state machine always observes its own lifecycle events: + +```javascript + var fsm = new StateMachine({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B' } + ], + methods: { + onStep: function() { console.log('stepped'); } + onA: function() { console.log('entered state A'); } + onB: function() { console.log('entered state B'); } + } + }); +``` + +## Lifecycle Event Arguments + +Observers will be passed a single argument containing a `lifecycle` object with the following attributes: + + * **transition** - the transition name + * **from** - the previous state + * **to** - the next state + +In addition to the `lifecycle` argument, the observer will receive any arbitrary arguments passed +into the transition method + +```javascript + var fsm = new StateMachine({ + transitions: [ + { name: 'step', from: 'A', to: 'B' } + ], + methods: { + onTransition: function(lifecycle, arg1, arg2) { + console.log(lifecycle.transition); // 'step' + console.log(lifecycle.from); // 'A' + console.log(lifecycle.to); // 'B' + console.log(arg1); // 42 + console.log(arg2); // 'hello' + } + } + }); + + fsm.step(42, 'hello'); +``` + +## Lifecycle Event Names + +Lifecycle event names always use standard javascipt camelCase, even if your transition and +state names do not: + +```javascript + var fsm = new StateMachine({ + transitions: [ + { name: 'do-with-dash', from: 'has-dash', to: 'has_underscore' }, + { name: 'do_with_underscore', from: 'has_underscore', to: 'alreadyCamelized' }, + { name: 'doAlreadyCamelized', from: 'alreadyCamelize', to: 'has-dash' } + ], + methods: { + onBeforeDoWithDash: function() { /* ... */ }, + onBeforeDoWithUnderscore: function() { /* ... */ }, + onBeforeDoAlreadyCamelized: function() { /* ... */ }, + onLeaveHasDash: function() { /* ... */ }, + onLeaveHasUnderscore: function() { /* ... */ }, + onLeaveAlreadyCamelized: function() { /* ... */ }, + onEnterHasDash: function() { /* ... */ }, + onEnterHasUnderscore: function() { /* ... */ }, + onEnterAlreadyCamelized: function() { /* ... */ }, + onAfterDoWithDash: function() { /* ... */ }, + onAfterDoWithUnderscore: function() { /* ... */ }, + onAfterDoAlreadyCamelized: function() { /* ... */ } + } + }); +``` + +# Lifecycle Events Listed in Order + +To recap, the lifecycle of a transition occurs in the following order: + + * `onBeforeTransition` - fired before any transition + * `onBefore` - fired before a specific TRANSITION + * `onLeaveState` - fired when leaving any state + * `onLeave` - fired when leaving a specific STATE + * `onTransition` - fired during any transition + * `onEnterState` - fired when entering any state + * `onEnter` - fired when entering a specific STATE + * `on` - convenience shorthand for `onEnter` + * `onAfterTransition` - fired after any transition + * `onAfter` - fired after a specific TRANSITION + * `on` - convenience shorthand for `onAfter` + +# Cancelling a Transition + +Any observer can cancel a transition by explicitly returning `false` during any of the following +lifecycle events: + + * `onBeforeTransition` + * `onBefore` + * `onLeaveState` + * `onLeave` + * `onTransition` + +All subsequent lifecycle events will be cancelled and the state will remain unchanged. + diff --git a/docs/state-history.md b/docs/state-history.md new file mode 100644 index 0000000..7e2ac85 --- /dev/null +++ b/docs/state-history.md @@ -0,0 +1,127 @@ +# Remembering State History + +By default, a state machine only tracks its current state. If you wish to track the state history +you can extend the state machine with the `state-machine-history` plugin. + +```javascript + var StateMachineHistory = require('javascript-state-machine/lib/history') +``` + +```javascript + + 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: [ + new StateMachineHistory() // <-- plugin enabled here + ] + }) + + fsm.history; // [ 'A' ] + fsm.step(); + fsm.history; // [ 'A', 'B' ] + fsm.step(); + fsm.history; // [ 'A', 'B', 'C' ] + + fsm.clearHistory(); + + fsm.history; // [ ] + +``` +## Traversing History + +You can traverse back through history using the `historyBack` and `historyForward` methods: + +```javascript + 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' } + ] + }) + + fsm.step(); + fsm.step(); + fsm.step(); + + fsm.state; // 'D' + fsm.history; // [ 'A', 'B', 'C', 'D' ] + + fsm.historyBack(); + + fsm.state; // 'C' + fsm.history; // [ 'A', 'B', 'C' ] + + fsm.historyBack(); + + fsm.state; // 'B' + fsm.history; // [ 'A', 'B' ] + + fsm.historyForward(); + + fsm.state; // 'C' + fsm.history; // [ 'A', 'B', 'C' ] +``` + +You can test if history traversal is allowed using the following properties: + +```javascript + fsm.canHistoryBack; // true/false + fsm.canHistoryForward; // true/false +``` + +A full set of [Lifecycle Events](lifecycle-events.md) will still apply when traversing history with +`historyBack` and `historyForward`. + +## Limiting History + +By default, the state machine history is unbounded and will continue to grow until cleared. You +can limit storage to only the last N states by configuring the plugin: + +``` javascript + var fsm = new StateMachine({ + plugins: [ + new StateMachineHistory({ max: 100 }) // <-- plugin configuration + ] + }) +``` + +## Customizing History + +If the `history` terminology clashes with your existing state machine attributes or methods, you +can enable the plugin with a different name: + +```javascript + 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: [ + new StateMachineHistory({ name: 'memory' }) + ] + }) + + fsm.step(); + fsm.step(); + + fsm.memory; // [ 'A', 'B', 'C' ] + + fsm.memoryBack(); + fsm.memory; // [ 'A', 'B' ] + + fsm.memoryForward(); + fsm.memory; // [ 'A', 'B', 'C' ] + + fsm.clearMemory(); + fsm.memory; // [ ] +``` + diff --git a/docs/state-machine-factory.md b/docs/state-machine-factory.md new file mode 100644 index 0000000..9f5e906 --- /dev/null +++ b/docs/state-machine-factory.md @@ -0,0 +1,104 @@ +# State Machine Factory + +Most examples in this documentation construct a single state machine instance, for example: + +```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' } + ] + }); +``` + +If you wish to construct multiple instances using the same configuration you should use a State +Machine Factory. A State Machine Factory provides a javascript constructor function (e.g. a 'class') +that can be instantiated multiple times: + +```javascript + 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(); + + b.melt(); + c.melt(); + c.vaporize(); + + a.state; // solid + b.state; // liquid + c.state; // gas +``` + +Using the factory, each state machine instance is a unique javascript object. Each instance manages +its own `state` property, but methods are shared via the normal javascript prototype mechanism. + +> NOTE: be aware of special case handling required for [Data and State Machine Factories](data-and-methods.md#data-and-state-machine-factories) + +## Applying State Machine Behavior to Existing Objects + +Occasionally, you may wish to apply state machine behavior to an already existing +object (e.g. a react component). You can achieve this using the `StateMachine.apply` method: + +```javascript + var component = { /* ... */ }; + + StateMachine.apply(component, { + init: 'A', + transitions: { + { name: 'step', from: 'A', to: 'B' } + } + }); +``` + +> Be careful not to use state or transition names that will clash with existing object properties. + +## Applying State Machine Factory Behavior to Existing Classes + +You can also apply state machine factory behavior to an existing class, however you must now +take responsibility for initialization by calling `this._fsm()` from within your class +constructor method: + +```javascript + 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' +``` diff --git a/docs/states-and-transitions.md b/docs/states-and-transitions.md new file mode 100644 index 0000000..0581379 --- /dev/null +++ b/docs/states-and-transitions.md @@ -0,0 +1,156 @@ +# States and Transitions + +![matter state machine](../examples/matter.png) + +A state machine consists of a set of **states**, e.g: + + * solid + * liquid + * gas + +.. and a set of **transitions**, e.g: + + * melt + * freeze + * vaporize + * condense + +```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' } + ] + }); + + fsm.state; // 'solid' + fsm.melt(); + fsm.state; // 'liquid' + fsm.vaporize(); + fsm.state; // 'gas' +``` + +## Multiple states for a transition + +![wizard state machine](../examples/wizard.png) + +If a transition is allowed `from` multiple states then declare the transitions with the same name: + +```javascript + { name: 'step', from: 'A', to: 'B' }, + { name: 'step', from: 'B', to: 'C' }, + { name: 'step', from: 'C', to: 'D' } +``` + +If a transition with multiple `from` states always transitions `to` the same state, e.g: + +```javascript + { name: 'reset', from: 'B', to: 'A' }, + { name: 'reset', from: 'C', to: 'A' }, + { name: 'reset', from: 'D', to: 'A' } +``` + +... then it can be abbreviated using an array of `from` states: + +```javascript + { name: 'reset', from: [ 'B', 'C', 'D' ], to: 'A' } +``` + +Combining these into a single example: + +```javascript + 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: [ 'B', 'C', 'D' ], to: 'A' } + ] + }) +``` + +This example will create an object with 2 transition methods: + + * `fsm.step()` + * `fsm.reset()` + +The `reset` transition will always end up in the `A` state, while the `step` transition +will end up in a state that is dependent on the current state. + +## Wildcard Transitions + +If a transition is appropriate from **any** state, then a wildcard '*' `from` state can be used: + +```javascript + var fsm = new StateMachine({ + transitions: [ + // ... + { name: 'reset', from: '*', to: 'A' } + ] + }); +``` + +## Conditional Transitions + +A transition can choose the target state at run-time by providing a function as the `to` attribute: + +```javascript + var fsm = new StateMachine({ + init: 'A', + transitions: [ + { name: 'step', from: '*', to: function(n) { return increaseCharacter(this.state, n || 1) } } + ] + }); + + fsm.state; // A + fsm.step(); + fsm.state; // B + fsm.step(5); + fsm.state; // G + + // helper method to perform (c = c + n) on the 1st character in str + function increaseCharacter(str, n) { + return String.fromCharCode(str.charCodeAt(0) + n); + } +``` + +The `allStates` method will only include conditional states once they have been seen at run-time: + +```javascript + fsm.state; // A + fsm.allStates(); // [ 'A' ] + fsm.step(); + fsm.state; // B + fsm.allStates(); // [ 'A', 'B' ] + fsm.step(5); + fsm.state; // G + fsm.allStates(); // [ 'A', 'B', 'G' ] +``` + +## GOTO - Changing State Without a Transition + +You can use a conditional transition, combined with a wildcard `from`, to implement +arbitrary `goto` behavior: + +```javascript + 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: function(s) { return s } } + ] + }) + + fsm.state; // 'A' + fsm.goto('D'); + fsm.state; // 'D' +``` + +A full set of [Lifecycle Events](lifecycle-events.md) still apply when using `goto`. + diff --git a/docs/upgrading-from-v2.md b/docs/upgrading-from-v2.md new file mode 100644 index 0000000..42f8150 --- /dev/null +++ b/docs/upgrading-from-v2.md @@ -0,0 +1,378 @@ +# Upgrading from Version 2.x + +Version 3.0 is a significant rewrite from earlier versions in order to support more +advanced use cases and to improve the existing use cases. Unfortunately, many of these +updates are incompatible with earlier versions, so changes are required in your code when you upgrade +to version 3.x. We want to tackle those all in one swoop and avoid any more big-bang changes +in the future. + +Please read this article carefully if you are upgrading from version 2.x to 3.x. + +> A [summary](#upgrade-summary) of the changes required can be found at the end of the article. + +### Table of Contents + + * [**Construction**](#construction) - constructing single instances follows a more idomatic javascript pattern. + * [**State Machine Factory**](#state-machine-factory) - constructing multiple instances from a class has been simplified. + * [**Data and Methods**](#data-and-methods) - A state machine can now have additional data and methods. + * [**Renamed Terminology**](#renamed-terminology) - A more consistent terminology has been applied. + * [**Lifecycle Events**](#lifecycle-events) - (previously called 'callbacks') are camelCased and observable. + * [**Async Transitions**](#promise-based-asynchronous-transitions) - Asynchronous transitions now use standard Promises. + * [**Conditional Transitions**](#conditional-transitions) - A transition can now dynamically choose its target state at run-time. + * [**Goto**](#goto) - The state can be changed without a defined transition using `goto`. + * [**State History**](#state-history) - The state history can now be retained and traversed with back/forward semantics. + * [**Visualization**](#visualization) - A state machine can now be visualized using GraphViz. + * [**Build System**](#build-system) - A new webpack-based build system has been implemented. + +## Construction + +Constructing a single state machine now follows a more idiomatic javascript pattern: + +Version 2.x: + +```javascript + var fsm = StateMachine.create({ /* ... */ }) +``` + +**Version 3.x**: + +```javascript + var fsm = new StateMachine({ /* ... */ }) // <-- more idomatic +``` + +## State Machine Factory + +Constructing multiple instances from a state machine 'class' has been simplified: + +Version 2.x: + +```javascript + function FSM() { } + + StateMachine.create({ + target: FSM.prototype, + // ... + }) + + var a = new FSM(), + b = new FSM(); +``` + +**Version 3.x**: + +```javascript + var FSM = StateMachine.factory({ /* ... */ }), // <-- generate a factory (a constructor function) + a = new FSM(), // <-- then create instances + b = new FSM(); +``` + +## Data and Methods + +A state machine can now have additional (arbitrary) data and methods defined: + +Version 2.x: _not supported_. + +**Version 3.x**: + +```javascript + var fsm = new StateMachine({ + data: { + color: 'red' + }, + methods: { + speak: function() { console.log('hello') } + } + }); + + fsm.color; // 'red' + fsm.speak(); // 'hello' +``` + +## Renamed Terminology + +A more consistent terminology has been applied: + + * A state machine consists of a set of [**States**](states-and-transitions.md). + * A state machine changes state by using [**Transitions**](states-and-transitions.md). + * A state machine can perform actions during a transition by observing [**Lifecycle Events**](lifecycle-events.md). + * A state machine can also have arbitrary [**Data and Methods**](data-and-methods.md). + +Version 2.x: + +```javascript + var fsm = StateMachine.create({ + initial: 'ready', + events: [ /* ... */ ], + callbacks: { /* ... */ } + }); + + fsm.current; // 'ready' +``` + +**Version 3.x**: + +```javascript + var fsm = new StateMachine({ + init: 'ready', // <-- renamed s/initial/init/ + transitions: [ /* ... */ ], // <-- renamed s/events/transitions/ + data: { /* ... */ }, // <-- new + methods: { /* ... */ } // <-- renamed s/callbacks/methods/ + // ... which can contain arbitrary methods AND lifecycle event callbacks + }); + + fsm.state; // 'ready' // <-- renamed s/current/state/ +``` + +## Lifecycle Events + +**Callbacks** have been renamed **Lifecycle Events** and are now declared as `methods` on the +state machine using a more traditional javascript camelCase for the method names: + +Version 2.x: + +```javascript + var fsm = StateMachine.create({ + initial: 'initial-state', + events: [ + { name: 'do-something', from: 'initial-state', to: 'final-state' } + ], + callbacks: { + onbeforedosomething: function() { /* ... */ }, + onleaveinitialstate: function() { /* ... */ }, + onenterfinalstate: function() { /* ... */ }, + onafterdosomething: function() { /* ... */ } + } + }) +``` + +**Version 3.x**: + +```javascript + var fsm = new StateMachine({ + init: 'initial-state', + transitions: [ + { name: 'do-something', from: 'initial-state', to: 'final-state' } + ], + methods: { // <-- renamed s/callbacks/methods/ + onBeforeDoSomething: function() { /* ... */ }, // <-- camelCase naming convention + onLeaveInitialState: function() { /* ... */ }, // <-- + onEnterFinalState: function() { /* ... */ }, // <-- + onAfterDoSomething: function() { /* ... */ } // <-- + } + }) +``` + +
+Lifecycle events are now passed information in a single `lifecycle` argument: + +Version 2.x: + +```javascript + var fsm = StateMachine.create({ + events: [ + { name: 'step', from: 'none', to: 'complete' } + ], + callbacks: { + onbeforestep: function(event, from, to) { + console.log('event: ' + event); // 'step' + console.log('from: ' + from); // 'none' + console.log('to: ' + to); // 'complete' + }, + } + }); +``` + +**Version 3.x**: + +```javascript + var fsm = new StateMachine({ + transitions: [ + { name: 'step', from: 'none', to: 'complete' } + ], + methods: { + onBeforeStep: function(lifecycle) { // <-- combined into a single argument + console.log('transition: ' + lifecycle.transition); // 'step' + console.log('from: ' + lifecycle.from); // 'none' + console.log('to: ' + lifecycle.to); // 'complete' + } + } + }); +``` + +> This change allows us to include additional information in the future without having to have a ridiculous +number of arguments to lifecycle event observer methods + +
+Lifecycle events are also now observable by others: + +Version 2.x: _not supported_. + +**Version 3.x**: + +```javascript + var fsm = new StateMachine({ /* ... */ }); + + // observe individual lifecycle events with observer methods + fsm.observe('onBeforeTransition', function() { /* ... */ }); + fsm.observe('onLeaveState', function() { /* ... */ }); + + // or observe multiple lifecycle events with an observer object + fsm.observe({ + onBeforeTransition: function() { /* ... */ }, + onLeaveState: function() { /* ... */ } + }); +``` + +
+The general purpose lifecycle events now use the word `transition` instead of `event` and +occur **before** their specialized versions: + +Version 2.x, the lifecycle order was: + + * `onbefore` + * `onbeforeevent` + * `onleave` + * `onleavestate` + * `onenter` + * `onenterstate` + * `on` + * `onafter` + * `onafterevent` + * `on` + +**Version 3.x**, the lifecycle order is: + + * `onBeforeTransition` - fired before any transition + * `onBefore` - fired before a specific TRANSITION + * `onLeaveState` - fired when leaving any state + * `onLeave` - fired when leaving a specific STATE + * `onTransition` - fired during any transition + * `onEnterState` - fired when entering any state + * `onEnter` - fired when entering a specific STATE + * `on` - convenience shorthand for `onEnter` + * `onAfterTransition` - fired after any transition + * `onAfter` - fired after a specific TRANSITION + * `on` - convenience shorthand for `onAfter` + +> For more details, read [Lifecycle Events](lifecycle-events.md) + +## Promise-Based Asynchronous Transitions + +Asynchronous transitions are now implemented using standard javascript [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). + +If you return a Promise from **any** lifecycle event then the entire lifecycle for that transition +is put on hold until that Promise gets resolved. If the promise is rejected then the transition +is cancelled. + +Version 2.x: + +```javascript + var fsm = StateMachine.create({ + events: [ + { name: 'step', from: 'none', to: 'complete' } + ], + callbacks: { + onbeforestep: function() { + $('#ui').fadeOut('fast', function() { + fsm.transition(); + }); + return StateMachine.ASYNC; + } + } + }); +``` + +**Version 3.x**: + +```javascript + var fsm = new StateMachine({ + transitions: [ + { name: 'step', from: 'none', to: 'complete' } + ], + methods: { + onBeforeStep: function() { + return new Promise(function(resolve, reject) { // <-- return a Promise instead of StateMachine.ASYNC + $('#ui').fadeOut('fast', resolve); // <-- resolve the promise instead of calling .transition() + }); + } + } + }); +``` + +> For more details, read [Asynchronous Transitions](async-transitions.md) + +## Conditional Transitions + +A transition can now be conditional and choose the target state at run-time by providing a function +as the `to` attribute. + +Version 2.x: _not supported_. + +**Version 3.x**: See [Conditional Transitions](states-and-transitions.md#conditional-transitions) + +## Goto + +The state can now be changed without the need for a predefined transition using a conditional `goto` +transition: + +Version 2.x: _not_supported_. + +**Version 3.x**: See [Goto](states-and-transitions.md#goto---changing-state-without-a-transition) + +## State History + +A state machine can now track and traverse (back/forward) its state history. + +Version 2.x: _not supported_. + +**Version 3.x**: See [State History](state-history.md) + +## Visualization + +A state machine can now be visualized as a directed graph using GraphViz `.dot` syntax. + +Version 2.x: _not_supported_. + +**Version 3.x**: See [Visualization](visualization.md) + +## Build System + +A new [Webpack](https://webpack.js.org/concepts/) based build system has been provided along +with an [Ava](https://github.com/avajs/ava) based unit test suite. + +Version 2.x: _not_supported_. + +**Version 3.x**: See [Contributing](contributing.md) + +## Other Breaking Changes in Version 3.0 + +`isFinished` is no longer built-in, you can easily add it to your state machine with a custom method: + +```javascript + var fsm = new StateMachine({ + methods: { + isFinished: function() { return this.state === 'done' } + } + }) +``` + +# UPGRADE SUMMARY + +The following list summarizes the above changes you might need when upgrading to version 3.0 + + * replace `StateMachine.create()` with `new StateMachine()` + * rename: + * `initial` to `init` + * `events` to `transitions` + * `callbacks` to `methods` + * `fsm.current` to `fsm.state` + * update your callback methods: + * rename them to use traditional javascript `camelCasing` + * refactor them to use the single `lifecycle` argument instead of individual `event,from,to` arguments + * update any asynchronous callback methods: + * return a `Promise` instead of `StateMachine.ASYNC` + * `resolve()` the promise when ready instead of calling `fsm.transition()` + * replace `StateMachine.create({ target: FOO })` with: + * if FOO is a class - `StateMachine.factory(FOO, {})` + * if FOO is an object - `StateMachine.apply(FOO, {})` + diff --git a/docs/visualization.md b/docs/visualization.md new file mode 100644 index 0000000..e944dcf --- /dev/null +++ b/docs/visualization.md @@ -0,0 +1,211 @@ +# Visualization + +It can be very helpful to visualize your state machine as a directed graph. This is possible +with the open source [GraphViz](http://www.graphviz.org/) library if we convert from our +state machine configuration to the `.dot` language expected by GraphViz using the +`visualize` method: + +```javascript + var visualize = require('javascript-state-machine/lib/visualize'); + + var fsm = new StateMachine({ + init: 'open', + transitions: [ + { name: 'close', from: 'open', to: 'closed' }, + { name: 'open', from: 'closed', to: 'open' } + ] + }); + + visualize(fsm) +``` + +Generates the following .dot syntax: + +```dot + digraph "fsm" { + "closed"; + "open"; + "closed" -> "open" [ label=" open " ]; + "open" -> "closed" [ label=" close " ]; + } +``` + +Which GraphViz displays as: + +![door](../examples/vertical_door.png) + +## Enhanced Visualization + +You can customize the generated `.dot` output - and hence the graphviz visualization - by attaching +`dot` attributes to your transitions and (optionally) declaring an `orientation`: + +```javascript + var fsm = new StateMachine({ + init: 'closed', + transitions: [ + { name: 'open', from: 'closed', to: 'open', dot: { color: 'blue', headport: 'n', tailport: 'n' } }, + { name: 'close', from: 'open', to: 'closed', dot: { color: 'red', headport: 's', tailport: 's' } } + ] + }); + visualize(fsm, { name: 'door', orientation: 'horizontal' }); +``` + +Generates the following (enhanced) `.dot` syntax: + +```dot + digraph "door" { + rankdir=LR; + "closed"; + "open"; + "closed" -> "open" [ color="blue" ; headport="n" ; label=" open " ; tailport="n" ]; + "open" -> "closed" [ color="red" ; headport="s" ; label=" close " ; tailport="s" ]; + } +``` + +Which GraphViz displays as: + +![door](../examples/horizontal_door.png) + +## Visualizing State Machine Factories + +You can use the same `visualize` method to generate `.dot` output for a state machine factory: + +```javascript + var Matter = StateMachine.factory({ + init: 'solid', + transitions: [ + { name: 'melt', from: 'solid', to: 'liquid', dot: { headport: 'nw' } }, + { name: 'freeze', from: 'liquid', to: 'solid', dot: { headport: 'se' } }, + { name: 'vaporize', from: 'liquid', to: 'gas', dot: { headport: 'nw' } }, + { name: 'condense', from: 'gas', to: 'liquid', dot: { headport: 'se' } } + ] + }); + + visualize(Matter, { name: 'matter', orientation: 'horizontal' }) +``` + +Generates the following .dot syntax: + +```dot + digraph "matter" { + rankdir=LR; + "solid"; + "liquid"; + "gas"; + "solid" -> "liquid" [ headport="nw" ; label=" melt " ]; + "liquid" -> "solid" [ headport="se" ; label=" freeze " ]; + "liquid" -> "gas" [ headport="nw" ; label=" vaporize " ]; + "gas" -> "liquid" [ headport="se" ; label=" condense " ]; + } +``` + +Which GraphViz displays as: + +![matter](../examples/matter.png) + +## Other Examples + +```javascript + var Wizard = StateMachine.factory({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B', dot: { headport: 'w', tailport: 'ne' } }, + { name: 'step', from: 'B', to: 'C', dot: { headport: 'w', tailport: 'e' } }, + { name: 'step', from: 'C', to: 'D', dot: { headport: 'w', tailport: 'e' } }, + { name: 'reset', from: [ 'B', 'C', 'D' ], to: 'A', dot: { headport: 'se', tailport: 's' } } + ] + }); + + visualize(Wizard, { orientation: 'horizontal' }) +``` + +Generates: + +```dot + digraph "wizard" { + rankdir=LR; + "A"; + "B"; + "C"; + "D"; + "A" -> "B" [ headport="w" ; label=" step " ; tailport="ne" ]; + "B" -> "C" [ headport="w" ; label=" step " ; tailport="e" ]; + "C" -> "D" [ headport="w" ; label=" step " ; tailport="e" ]; + "B" -> "A" [ headport="se" ; label=" reset " ; tailport="s" ]; + "C" -> "A" [ headport="se" ; label=" reset " ; tailport="s" ]; + "D" -> "A" [ headport="se" ; label=" reset " ; tailport="s" ]; + } +``` + +Displays: + +![wizard](../examples/wizard.png) + +```javascript + var ATM = StateMachine.factory({ + init: 'ready', + transitions: [ + { name: 'insert-card', from: 'ready', to: 'pin' }, + { name: 'confirm', from: 'pin', to: 'action' }, + { name: 'reject', from: 'pin', to: 'return-card' }, + { name: 'withdraw', from: 'return-card', to: 'ready' }, + + { name: 'deposit', from: 'action', to: 'deposit-account' }, + { name: 'provide', from: 'deposit-account', to: 'deposit-amount' }, + { name: 'provide', from: 'deposit-amount', to: 'confirm-deposit' }, + { name: 'confirm', from: 'confirm-deposit', to: 'collect-envelope' }, + { name: 'provide', from: 'collect-envelope', to: 'continue' }, + + { name: 'withdraw', from: 'action', to: 'withdrawal-account' }, + { name: 'provide', from: 'withdrawal-account', to: 'withdrawal-amount' }, + { name: 'provide', from: 'withdrawal-amount', to: 'confirm-withdrawal' }, + { name: 'confirm', from: 'confirm-withdrawal', to: 'dispense-cash' }, + { name: 'withdraw', from: 'dispense-cash', to: 'continue' }, + + { name: 'continue', from: 'continue', to: 'action' }, + { name: 'finish', from: 'continue', to: 'return-card' } + ] + }) + + visualize(ATM) +``` + +Generates: + +```dot + digraph "ATM" { + "ready"; + "pin"; + "action"; + "return-card"; + "deposit-account"; + "deposit-amount"; + "confirm-deposit"; + "collect-envelope"; + "continue"; + "withdrawal-account"; + "withdrawal-amount"; + "confirm-withdrawal"; + "dispense-cash"; + "ready" -> "pin" [ label=" insert-card " ]; + "pin" -> "action" [ label=" confirm " ]; + "pin" -> "return-card" [ label=" reject " ]; + "return-card" -> "ready" [ label=" withdraw " ]; + "action" -> "deposit-account" [ label=" deposit " ]; + "deposit-account" -> "deposit-amount" [ label=" provide " ]; + "deposit-amount" -> "confirm-deposit" [ label=" provide " ]; + "confirm-deposit" -> "collect-envelope" [ label=" confirm " ]; + "collect-envelope" -> "continue" [ label=" provide " ]; + "action" -> "withdrawal-account" [ label=" withdraw " ]; + "withdrawal-account" -> "withdrawal-amount" [ label=" provide " ]; + "withdrawal-amount" -> "confirm-withdrawal" [ label=" provide " ]; + "confirm-withdrawal" -> "dispense-cash" [ label=" confirm " ]; + "dispense-cash" -> "continue" [ label=" withdraw " ]; + "continue" -> "action" [ label=" continue " ]; + "continue" -> "return-card" [ label=" finish " ]; + } +``` + +Displays: + +![atm](../examples/atm.png) diff --git a/examples/atm.dot b/examples/atm.dot new file mode 100644 index 0000000..9f76543 --- /dev/null +++ b/examples/atm.dot @@ -0,0 +1,31 @@ +digraph "ATM" { + "ready"; + "pin"; + "action"; + "return-card"; + "deposit-account"; + "deposit-amount"; + "confirm-deposit"; + "collect-envelope"; + "continue"; + "withdrawal-account"; + "withdrawal-amount"; + "confirm-withdrawal"; + "dispense-cash"; + "ready" -> "pin" [ label=" insert-card " ]; + "pin" -> "action" [ label=" confirm " ]; + "pin" -> "return-card" [ label=" reject " ]; + "return-card" -> "ready" [ label=" withdraw " ]; + "action" -> "deposit-account" [ label=" deposit " ]; + "deposit-account" -> "deposit-amount" [ label=" provide " ]; + "deposit-amount" -> "confirm-deposit" [ label=" provide " ]; + "confirm-deposit" -> "collect-envelope" [ label=" confirm " ]; + "collect-envelope" -> "continue" [ label=" provide " ]; + "action" -> "withdrawal-account" [ label=" withdraw " ]; + "withdrawal-account" -> "withdrawal-amount" [ label=" provide " ]; + "withdrawal-amount" -> "confirm-withdrawal" [ label=" provide " ]; + "confirm-withdrawal" -> "dispense-cash" [ label=" confirm " ]; + "dispense-cash" -> "continue" [ label=" withdraw " ]; + "continue" -> "action" [ label=" continue " ]; + "continue" -> "return-card" [ label=" finish " ]; +} \ No newline at end of file diff --git a/examples/atm.js b/examples/atm.js new file mode 100644 index 0000000..0113e55 --- /dev/null +++ b/examples/atm.js @@ -0,0 +1,33 @@ +var StateMachine = require('../src/app'), + visualize = require('../src/plugin/visualize'); + +var ATM = StateMachine.factory({ + init: 'ready', + transitions: [ + { name: 'insert-card', from: 'ready', to: 'pin' }, + { name: 'confirm', from: 'pin', to: 'action' }, + { name: 'reject', from: 'pin', to: 'return-card' }, + { name: 'withdraw', from: 'return-card', to: 'ready' }, + + { name: 'deposit', from: 'action', to: 'deposit-account' }, + { name: 'provide', from: 'deposit-account', to: 'deposit-amount' }, + { name: 'provide', from: 'deposit-amount', to: 'confirm-deposit' }, + { name: 'confirm', from: 'confirm-deposit', to: 'collect-envelope' }, + { name: 'provide', from: 'collect-envelope', to: 'continue' }, + + { name: 'withdraw', from: 'action', to: 'withdrawal-account' }, + { name: 'provide', from: 'withdrawal-account', to: 'withdrawal-amount' }, + { name: 'provide', from: 'withdrawal-amount', to: 'confirm-withdrawal' }, + { name: 'confirm', from: 'confirm-withdrawal', to: 'dispense-cash' }, + { name: 'withdraw', from: 'dispense-cash', to: 'continue' }, + + { name: 'continue', from: 'continue', to: 'action' }, + { name: 'finish', from: 'continue', to: 'return-card' } + ] +}) + +ATM.visualize = function() { + return visualize(ATM, { name: 'ATM' }) +} + +module.exports = ATM diff --git a/examples/atm.png b/examples/atm.png new file mode 100644 index 0000000..a699d39 Binary files /dev/null and b/examples/atm.png differ diff --git a/examples/atm.svg b/examples/atm.svg new file mode 100644 index 0000000..e3bf071 --- /dev/null +++ b/examples/atm.svg @@ -0,0 +1,174 @@ + + + + + + +ATM + + +ready + +ready + + +pin + +pin + + +ready->pin + + + insert-card + + +action + +action + + +pin->action + + + confirm + + +return-card + +return-card + + +pin->return-card + + + reject + + +deposit-account + +deposit-account + + +action->deposit-account + + + deposit + + +withdrawal-account + +withdrawal-account + + +action->withdrawal-account + + + withdraw + + +return-card->ready + + + withdraw + + +deposit-amount + +deposit-amount + + +deposit-account->deposit-amount + + + provide + + +confirm-deposit + +confirm-deposit + + +deposit-amount->confirm-deposit + + + provide + + +collect-envelope + +collect-envelope + + +confirm-deposit->collect-envelope + + + confirm + + +continue + +continue + + +collect-envelope->continue + + + provide + + +continue->action + + + continue + + +continue->return-card + + + finish + + +withdrawal-amount + +withdrawal-amount + + +withdrawal-account->withdrawal-amount + + + provide + + +confirm-withdrawal + +confirm-withdrawal + + +withdrawal-amount->confirm-withdrawal + + + provide + + +dispense-cash + +dispense-cash + + +confirm-withdrawal->dispense-cash + + + confirm + + +dispense-cash->continue + + + withdraw + + + diff --git a/demo/demo.css b/examples/demo/demo.css similarity index 100% rename from demo/demo.css rename to examples/demo/demo.css diff --git a/examples/demo/demo.js b/examples/demo/demo.js new file mode 100644 index 0000000..f2b2b95 --- /dev/null +++ b/examples/demo/demo.js @@ -0,0 +1,84 @@ +Demo = function() { + + var output = document.getElementById('output'), + demo = document.getElementById('demo'), + panic = document.getElementById('panic'), + warn = document.getElementById('warn'), + calm = document.getElementById('calm'), + clear = document.getElementById('clear'), + count = 0; + + var log = function(msg, separate) { + count = count + (separate ? 1 : 0); + output.value = count + ": " + msg + "\n" + (separate ? "\n" : "") + output.value; + refreshUI(); + }; + + var refreshUI = function() { + setTimeout(function() { + demo.className = fsm.state; + panic.disabled = fsm.cannot('panic', true); + warn.disabled = fsm.cannot('warn', true); + calm.disabled = fsm.cannot('calm', true); + clear.disabled = fsm.cannot('clear', true); + }, 0); // defer until end of current tick to allow fsm to complete transaction + }; + + var fsm = new StateMachine({ + + transitions: [ + { name: 'start', from: 'none', to: 'green' }, + { name: 'warn', from: 'green', to: 'yellow' }, + { name: 'panic', from: 'green', to: 'red' }, + { name: 'panic', from: 'yellow', to: 'red' }, + { name: 'calm', from: 'red', to: 'yellow' }, + { name: 'clear', from: 'red', to: 'green' }, + { name: 'clear', from: 'yellow', to: 'green' }, + ], + + methods: { + + onBeforeTransition: function(lifecycle) { + log("BEFORE: " + lifecycle.transition, true); + }, + + onLeaveState: function(lifecycle) { + log("LEAVE: " + lifecycle.from); + }, + + onEnterState: function(lifecycle) { + log("ENTER: " + lifecycle.to); + }, + + onAfterTransition: function(lifecycle) { + log("AFTER: " + lifecycle.transition); + }, + + onTransition: function(lifecycle) { + log("DURING: " + lifecycle.transition + " (from " + lifecycle.from + " to " + lifecycle.to + ")"); + }, + + onLeaveRed: function(lifecycle) { + return new Promise(function(resolve, reject) { + var msg = lifecycle.transition + ' to ' + lifecycle.to; + log("PENDING " + msg + " in ...3"); + setTimeout(function() { + log("PENDING " + msg + " in ...2"); + setTimeout(function() { + log("PENDING " + msg + " in ...1"); + setTimeout(function() { + resolve(); + }, 1000); + }, 1000); + }, 1000); + }); + } + + } + }); + + fsm.start(); + return fsm; + +}(); + diff --git a/demo/images/alerts.green.png b/examples/demo/images/alerts.green.png similarity index 100% rename from demo/images/alerts.green.png rename to examples/demo/images/alerts.green.png diff --git a/demo/images/alerts.red.png b/examples/demo/images/alerts.red.png similarity index 100% rename from demo/images/alerts.red.png rename to examples/demo/images/alerts.red.png diff --git a/demo/images/alerts.yellow.png b/examples/demo/images/alerts.yellow.png similarity index 100% rename from demo/images/alerts.yellow.png rename to examples/demo/images/alerts.yellow.png diff --git a/examples/horizontal_door.dot b/examples/horizontal_door.dot new file mode 100644 index 0000000..008113f --- /dev/null +++ b/examples/horizontal_door.dot @@ -0,0 +1,7 @@ +digraph "door" { + rankdir=LR; + "closed"; + "open"; + "closed" -> "open" [ color="blue" ; headport="n" ; label=" open " ; tailport="n" ]; + "open" -> "closed" [ color="red" ; headport="s" ; label=" close " ; tailport="s" ]; +} \ No newline at end of file diff --git a/examples/horizontal_door.js b/examples/horizontal_door.js new file mode 100644 index 0000000..1a2f676 --- /dev/null +++ b/examples/horizontal_door.js @@ -0,0 +1,16 @@ +var StateMachine = require('../src/app'), + visualize = require('../src/plugin/visualize'); + +var Door = StateMachine.factory({ + init: 'closed', + transitions: [ + { name: 'open', from: 'closed', to: 'open', dot: { color: 'blue', headport: 'n', tailport: 'n' } }, + { name: 'close', from: 'open', to: 'closed', dot: { color: 'red', headport: 's', tailport: 's' } } + ] +}); + +Door.visualize = function() { + return visualize(Door, { name: 'door', orientation: 'horizontal' }) +} + +module.exports = Door diff --git a/examples/horizontal_door.png b/examples/horizontal_door.png new file mode 100644 index 0000000..65d8ddb Binary files /dev/null and b/examples/horizontal_door.png differ diff --git a/examples/horizontal_door.svg b/examples/horizontal_door.svg new file mode 100644 index 0000000..217e038 --- /dev/null +++ b/examples/horizontal_door.svg @@ -0,0 +1,35 @@ + + + + + + +door + + +closed + +closed + + +open + +open + + +closed:n->open:n + + + open + + +open:s->closed:s + + + close + + + diff --git a/examples/matter.dot b/examples/matter.dot new file mode 100644 index 0000000..9a5b12e --- /dev/null +++ b/examples/matter.dot @@ -0,0 +1,10 @@ +digraph "matter" { + rankdir=LR; + "solid"; + "liquid"; + "gas"; + "solid" -> "liquid" [ headport="nw" ; label=" melt " ]; + "liquid" -> "solid" [ headport="se" ; label=" freeze " ]; + "liquid" -> "gas" [ headport="nw" ; label=" vaporize " ]; + "gas" -> "liquid" [ headport="se" ; label=" condense " ]; +} \ No newline at end of file diff --git a/examples/matter.js b/examples/matter.js new file mode 100644 index 0000000..c3b960f --- /dev/null +++ b/examples/matter.js @@ -0,0 +1,18 @@ +var StateMachine = require('../src/app'), + visualize = require('../src/plugin/visualize'); + +var Matter = StateMachine.factory({ + init: 'solid', + transitions: [ + { name: 'melt', from: 'solid', to: 'liquid', dot: { headport: 'nw' } }, + { name: 'freeze', from: 'liquid', to: 'solid', dot: { headport: 'se' } }, + { name: 'vaporize', from: 'liquid', to: 'gas', dot: { headport: 'nw' } }, + { name: 'condense', from: 'gas', to: 'liquid', dot: { headport: 'se' } } + ] +}); + +Matter.visualize = function() { + return visualize(Matter, { name: 'matter', orientation: 'horizontal' }) +} + +module.exports = Matter diff --git a/examples/matter.png b/examples/matter.png new file mode 100644 index 0000000..cde3b89 Binary files /dev/null and b/examples/matter.png differ diff --git a/examples/matter.svg b/examples/matter.svg new file mode 100644 index 0000000..9ccd86f --- /dev/null +++ b/examples/matter.svg @@ -0,0 +1,52 @@ + + + + + + +matter + + +solid + +solid + + +liquid + +liquid + + +solid->liquid:nw + + + melt + + +liquid->solid:se + + + freeze + + +gas + +gas + + +liquid->gas:nw + + + vaporize + + +gas->liquid:se + + + condense + + + diff --git a/examples/vertical_door.dot b/examples/vertical_door.dot new file mode 100644 index 0000000..822dad8 --- /dev/null +++ b/examples/vertical_door.dot @@ -0,0 +1,6 @@ +digraph "fsm" { + "closed"; + "open"; + "closed" -> "open" [ label=" open " ]; + "open" -> "closed" [ label=" close " ]; +} \ No newline at end of file diff --git a/examples/vertical_door.js b/examples/vertical_door.js new file mode 100644 index 0000000..b619195 --- /dev/null +++ b/examples/vertical_door.js @@ -0,0 +1,16 @@ +var StateMachine = require('../src/app'), + visualize = require('../src/plugin/visualize'); + +var Door = StateMachine.factory({ + init: 'closed', + transitions: [ + { name: 'open', from: 'closed', to: 'open' }, + { name: 'close', from: 'open', to: 'closed' } + ] +}); + +Door.visualize = function() { + return visualize(Door) +} + +module.exports = Door diff --git a/examples/vertical_door.png b/examples/vertical_door.png new file mode 100644 index 0000000..c29023d Binary files /dev/null and b/examples/vertical_door.png differ diff --git a/examples/vertical_door.svg b/examples/vertical_door.svg new file mode 100644 index 0000000..12dc09b --- /dev/null +++ b/examples/vertical_door.svg @@ -0,0 +1,35 @@ + + + + + + +fsm + + +closed + +closed + + +open + +open + + +closed->open + + + open + + +open->closed + + + close + + + diff --git a/examples/wizard.dot b/examples/wizard.dot new file mode 100644 index 0000000..c7e6f25 --- /dev/null +++ b/examples/wizard.dot @@ -0,0 +1,13 @@ +digraph "wizard" { + rankdir=LR; + "A"; + "B"; + "C"; + "D"; + "A" -> "B" [ headport="w" ; label=" step " ; tailport="ne" ]; + "B" -> "C" [ headport="w" ; label=" step " ; tailport="e" ]; + "C" -> "D" [ headport="w" ; label=" step " ; tailport="e" ]; + "B" -> "A" [ headport="se" ; label=" reset " ; tailport="s" ]; + "C" -> "A" [ headport="se" ; label=" reset " ; tailport="s" ]; + "D" -> "A" [ headport="se" ; label=" reset " ; tailport="s" ]; +} \ No newline at end of file diff --git a/examples/wizard.js b/examples/wizard.js new file mode 100644 index 0000000..8d1aa4c --- /dev/null +++ b/examples/wizard.js @@ -0,0 +1,18 @@ +var StateMachine = require('../src/app'), + visualize = require('../src/plugin/visualize'); + +var Wizard = StateMachine.factory({ + init: 'A', + transitions: [ + { name: 'step', from: 'A', to: 'B', dot: { headport: 'w', tailport: 'ne' } }, + { name: 'step', from: 'B', to: 'C', dot: { headport: 'w', tailport: 'e' } }, + { name: 'step', from: 'C', to: 'D', dot: { headport: 'w', tailport: 'e' } }, + { name: 'reset', from: [ 'B', 'C', 'D' ], to: 'A', dot: { headport: 'se', tailport: 's' } } + ] +}); + +Wizard.visualize = function() { + return visualize(Wizard, { name: 'wizard', orientation: 'horizontal' }) +} + +module.exports = Wizard diff --git a/examples/wizard.png b/examples/wizard.png new file mode 100644 index 0000000..74945ca Binary files /dev/null and b/examples/wizard.png differ diff --git a/examples/wizard.svg b/examples/wizard.svg new file mode 100644 index 0000000..46edc96 --- /dev/null +++ b/examples/wizard.svg @@ -0,0 +1,69 @@ + + + + + + +wizard + + +A + +A + + +B + +B + + +A:ne->B:w + + + step + + +B:s->A:se + + + reset + + +C + +C + + +B:e->C:w + + + step + + +C:s->A:se + + + reset + + +D + +D + + +C:e->D:w + + + step + + +D:s->A:se + + + reset + + + diff --git a/index.html b/index.html index 2d6cb62..7beb6b8 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ Javascript Finite State Machine - + @@ -32,8 +32,8 @@

Finite State Machine

- - + + diff --git a/lib/history.js b/lib/history.js new file mode 100644 index 0000000..19f7c4f --- /dev/null +++ b/lib/history.js @@ -0,0 +1,211 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if(typeof define === 'function' && define.amd) + define("StateMachineHistory", [], factory); + else if(typeof exports === 'object') + exports["StateMachineHistory"] = factory(); + else + root["StateMachineHistory"] = factory(); +})(this, function() { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // identity function for calling harmony imports with the correct context +/******/ __webpack_require__.i = function(value) { return value; }; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 1); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +"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; + + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +//------------------------------------------------------------------------------------------------- + +var camelize = __webpack_require__(0); + +//------------------------------------------------------------------------------------------------- + +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; + +} + + +/***/ }) +/******/ ]); +}); \ No newline at end of file diff --git a/lib/state-machine.js b/lib/state-machine.js new file mode 100644 index 0000000..b8b9e37 --- /dev/null +++ b/lib/state-machine.js @@ -0,0 +1,669 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if(typeof define === 'function' && define.amd) + define("StateMachine", [], factory); + else if(typeof exports === 'object') + exports["StateMachine"] = factory(); + else + root["StateMachine"] = factory(); +})(this, function() { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // identity function for calling harmony imports with the correct context +/******/ __webpack_require__.i = function(value) { return value; }; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 5); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +"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; +} + + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +//------------------------------------------------------------------------------------------------- + +var mixin = __webpack_require__(0); + +//------------------------------------------------------------------------------------------------- + +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); + } + } + +} + +//------------------------------------------------------------------------------------------------- + + +/***/ }), +/* 2 */ +/***/ (function(module, exports, __webpack_require__) { + +"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; + + +/***/ }), +/* 3 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +//------------------------------------------------------------------------------------------------- + +var mixin = __webpack_require__(0), + camelize = __webpack_require__(2); + +//------------------------------------------------------------------------------------------------- + +function Config(options, StateMachine) { + + options = options || {}; + + this.options = options; // preserving original options can be useful (e.g visualize plugin) + this.defaults = StateMachine.defaults; + this.states = []; + this.transitions = []; + this.map = {}; + this.lifecycle = this.configureLifecycle(); + this.init = this.configureInitTransition(options.init); + this.data = this.configureData(options.data); + this.methods = this.configureMethods(options.methods); + + this.map[this.defaults.wildcard] = {}; + + this.configureTransitions(options.transitions || []); + + this.plugins = this.configurePlugins(options.plugins, StateMachine.plugin); + +} + +//------------------------------------------------------------------------------------------------- + +mixin(Config.prototype, { + + addState: function(name) { + if (!this.map[name]) { + this.states.push(name); + this.addStateLifecycleNames(name); + this.map[name] = {}; + } + }, + + addStateLifecycleNames: function(name) { + this.lifecycle.onEnter[name] = camelize.prepended('onEnter', name); + this.lifecycle.onLeave[name] = camelize.prepended('onLeave', name); + this.lifecycle.on[name] = camelize.prepended('on', name); + }, + + addTransition: function(name) { + if (this.transitions.indexOf(name) < 0) { + this.transitions.push(name); + this.addTransitionLifecycleNames(name); + } + }, + + addTransitionLifecycleNames: function(name) { + this.lifecycle.onBefore[name] = camelize.prepended('onBefore', name); + this.lifecycle.onAfter[name] = camelize.prepended('onAfter', name); + this.lifecycle.on[name] = camelize.prepended('on', name); + }, + + mapTransition: function(transition) { + var name = transition.name, + from = transition.from, + to = transition.to; + this.addState(from); + if (typeof to !== 'function') + this.addState(to); + this.addTransition(name); + this.map[from][name] = transition; + return transition; + }, + + configureLifecycle: function() { + return { + onBefore: { transition: 'onBeforeTransition' }, + onAfter: { transition: 'onAfterTransition' }, + onEnter: { state: 'onEnterState' }, + onLeave: { state: 'onLeaveState' }, + on: { transition: 'onTransition' } + }; + }, + + configureInitTransition: function(init) { + if (typeof init === 'string') { + return this.mapTransition(mixin({}, this.defaults.init, { to: init, active: true })); + } + else if (typeof init === 'object') { + return this.mapTransition(mixin({}, this.defaults.init, init, { active: true })); + } + else { + this.addState(this.defaults.init.from); + return this.defaults.init; + } + }, + + configureData: function(data) { + if (typeof data === 'function') + return data; + else if (typeof data === 'object') + return function() { return data; } + else + return function() { return {}; } + }, + + configureMethods: function(methods) { + return methods || {}; + }, + + configurePlugins: function(plugins, builtin) { + plugins = plugins || []; + var n, max, plugin; + for(n = 0, max = plugins.length ; n < max ; n++) { + plugin = plugins[n]; + if (typeof plugin === 'function') + plugins[n] = plugin = plugin() + if (plugin.configure) + plugin.configure(this); + } + return plugins + }, + + configureTransitions: function(transitions) { + var i, n, transition, from, to, wildcard = this.defaults.wildcard; + for(n = 0 ; n < transitions.length ; n++) { + transition = transitions[n]; + from = Array.isArray(transition.from) ? transition.from : [transition.from || wildcard] + to = transition.to || wildcard; + for(i = 0 ; i < from.length ; i++) { + this.mapTransition({ name: transition.name, from: from[i], to: to }); + } + } + }, + + transitionFor: function(state, transition) { + var wildcard = this.defaults.wildcard; + return this.map[state][transition] || + this.map[wildcard][transition]; + }, + + transitionsFor: function(state) { + var wildcard = this.defaults.wildcard; + return Object.keys(this.map[state]).concat(Object.keys(this.map[wildcard])); + }, + + allStates: function() { + return this.states; + }, + + allTransitions: function() { + return this.transitions; + } + +}); + +//------------------------------------------------------------------------------------------------- + +module.exports = Config; + +//------------------------------------------------------------------------------------------------- + + +/***/ }), +/* 4 */ +/***/ (function(module, exports, __webpack_require__) { + + +var mixin = __webpack_require__(0), + Exception = __webpack_require__(6), + plugin = __webpack_require__(1), + UNOBSERVED = [ null, [] ]; + +//------------------------------------------------------------------------------------------------- + +function JSM(context, config) { + this.context = context; + this.config = config; + this.state = config.init.from; + this.observers = [context]; +} + +//------------------------------------------------------------------------------------------------- + +mixin(JSM.prototype, { + + init: function(args) { + mixin(this.context, this.config.data.apply(this.context, args)); + plugin.hook(this, 'init'); + if (this.config.init.active) + return this.fire(this.config.init.name, []); + }, + + is: function(state) { + return Array.isArray(state) ? (state.indexOf(this.state) >= 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/lib/visualize.js b/lib/visualize.js new file mode 100644 index 0000000..9c18e13 --- /dev/null +++ b/lib/visualize.js @@ -0,0 +1,269 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if(typeof define === 'function' && define.amd) + define("StateMachineVisualize", [], factory); + else if(typeof exports === 'object') + exports["StateMachineVisualize"] = factory(); + else + root["StateMachineVisualize"] = factory(); +})(this, function() { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // identity function for calling harmony imports with the correct context +/******/ __webpack_require__.i = function(value) { return value; }; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 1); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +"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; +} + + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +//------------------------------------------------------------------------------------------------- + +var mixin = __webpack_require__(0) + +//------------------------------------------------------------------------------------------------- + +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; + +//------------------------------------------------------------------------------------------------- + + +/***/ }) +/******/ ]); +}); \ No newline at end of file diff --git a/minifier/LICENSE.TXT b/minifier/LICENSE.TXT deleted file mode 100755 index b12bbbf..0000000 --- a/minifier/LICENSE.TXT +++ /dev/null @@ -1,54 +0,0 @@ -YUI Compressor Copyright License Agreement (BSD License) - -Copyright (c) 2010, Yahoo! Inc. -All rights reserved. - -Redistribution and use of this software in source and binary forms, -with or without modification, are permitted provided that the following -conditions are met: - -* Redistributions of source code must retain the above - copyright notice, this list of conditions and the - following disclaimer. - -* Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the - following disclaimer in the documentation and/or other - materials provided with the distribution. - -* Neither the name of Yahoo! Inc. nor the names of its - contributors may be used to endorse or promote products - derived from this software without specific prior - written permission of Yahoo! Inc. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -This software also requires access to software from the following sources: - -The Jarg Library v 1.0 ( http://jargs.sourceforge.net/ ) is available -under a BSD License – Copyright (c) 2001-2003 Steve Purcell, -Copyright (c) 2002 Vidar Holen, Copyright (c) 2002 Michal Ceresna and -Copyright (c) 2005 Ewan Mellor. - -The Rhino Library ( http://www.mozilla.org/rhino/ ) is dually available -under an MPL 1.1/GPL 2.0 license, with portions subject to a BSD license. - -Additionally, this software contains modified versions of the following -component files from the Rhino Library: - -[org/mozilla/javascript/Decompiler.java] -[org/mozilla/javascript/Parser.java] -[org/mozilla/javascript/Token.java] -[org/mozilla/javascript/TokenStream.java] - -The modified versions of these files are distributed under the MPL v 1.1 -( http://www.mozilla.org/MPL/MPL-1.1.html ) diff --git a/minifier/minifier.rb b/minifier/minifier.rb deleted file mode 100644 index 001cf35..0000000 --- a/minifier/minifier.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Minifier - - class << self - attr_accessor :enabled - attr_accessor :extensions - end - - self.extensions = ['.js', '.css'] - - def self.available? - @available ||= !`which java`.empty? # warning: linux only way of checking if java is available - end - - def self.enabled?(name = nil) - enabled && available? && (name.nil? || extensions.include?(File.extname(name))) - end - - def self.minified_name(name) - if enabled?(name) - ext = File.extname(name) - name.sub(ext, ".min#{ext}") - else - name - end - end - - def self.minify(name) - if name && enabled?(name) && File.exists?(name) - minified_name = minified_name(name) - `java -jar "#{File.dirname(__FILE__)}/yuicompressor-2.4.6.jar" "#{name}" -o "#{minified_name}"` - end - end - -end diff --git a/minifier/yuicompressor-2.4.6.jar b/minifier/yuicompressor-2.4.6.jar deleted file mode 100755 index 61f6318..0000000 Binary files a/minifier/yuicompressor-2.4.6.jar and /dev/null differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b860621 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6766 @@ +{ + "name": "javascript-state-machine", + "version": "3.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "acorn": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.0.3.tgz", + "integrity": "sha1-xGDfCEkUY/AozLguqzcwvwEIez0=", + "dev": true + }, + "acorn-dynamic-import": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz", + "integrity": "sha1-x1K9IQvvZ5UBtsbLf8hPj0cVjMQ=", + "dev": true, + "requires": { + "acorn": "4.0.13" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", + "dev": true + } + } + }, + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ajv-keywords": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", + "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", + "dev": true + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } + }, + "ansi-align": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-1.1.0.tgz", + "integrity": "sha1-LwwWWIKXOa3V67FeawxuNCPwFro=", + "dev": true, + "requires": { + "string-width": "1.0.2" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "anymatch": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.0.tgz", + "integrity": "sha1-o+Uvo5FoyCX/V7AkgSbOWo/5VQc=", + "dev": true, + "requires": { + "arrify": "1.0.1", + "micromatch": "2.3.11" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "1.0.3" + } + }, + "arr-exclude": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/arr-exclude/-/arr-exclude-1.0.0.tgz", + "integrity": "sha1-38fC5VKicHI8zaBM8xKMjL/lxjE=", + "dev": true + }, + "arr-flatten": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.0.3.tgz", + "integrity": "sha1-onTthawIhJtr14R8RYB0XcUa37E=", + "dev": true + }, + "array-differ": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", + "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "1.0.3" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asn1.js": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.1.tgz", + "integrity": "sha1-SLokC0WpKA6UdImQull9IWYX/UA=", + "dev": true, + "requires": { + "bn.js": "4.11.6", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0" + } + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "dev": true, + "requires": { + "util": "0.10.3" + } + }, + "async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.4.1.tgz", + "integrity": "sha1-YqVrJ5yYoR0JhwlqAcw+6463u9c=", + "dev": true, + "requires": { + "lodash": "4.17.4" + } + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, + "auto-bind": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-0.1.0.tgz", + "integrity": "sha1-einvyMI4jT1XjgL8LfUxyB/8HuE=", + "dev": true + }, + "ava": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/ava/-/ava-0.17.0.tgz", + "integrity": "sha1-NZ4qiWFoAe8Dkpw88QqdT45FHQI=", + "dev": true, + "requires": { + "arr-flatten": "1.0.3", + "array-union": "1.0.2", + "array-uniq": "1.0.3", + "arrify": "1.0.1", + "auto-bind": "0.1.0", + "ava-files": "0.2.0", + "ava-init": "0.1.6", + "babel-code-frame": "6.22.0", + "babel-core": "6.25.0", + "babel-plugin-ava-throws-helper": "0.1.0", + "babel-plugin-detective": "2.0.0", + "babel-plugin-espower": "2.3.2", + "babel-plugin-transform-runtime": "6.23.0", + "babel-preset-es2015": "6.24.1", + "babel-preset-es2015-node4": "2.1.1", + "babel-preset-stage-2": "6.24.1", + "babel-runtime": "6.23.0", + "bluebird": "3.5.0", + "caching-transform": "1.0.1", + "chalk": "1.1.3", + "chokidar": "1.7.0", + "clean-yaml-object": "0.1.0", + "cli-cursor": "1.0.2", + "cli-spinners": "0.1.2", + "cli-truncate": "0.2.1", + "co-with-promise": "4.6.0", + "common-path-prefix": "1.0.0", + "convert-source-map": "1.5.0", + "core-assert": "0.2.1", + "currently-unhandled": "0.4.1", + "debug": "2.6.8", + "empower-core": "0.6.2", + "figures": "1.7.0", + "find-cache-dir": "0.1.1", + "fn-name": "2.0.1", + "get-port": "2.1.0", + "has-flag": "2.0.0", + "ignore-by-default": "1.0.1", + "is-ci": "1.0.10", + "is-generator-fn": "1.0.0", + "is-obj": "1.0.1", + "is-observable": "0.2.0", + "is-promise": "2.1.0", + "last-line-stream": "1.0.0", + "lodash.debounce": "4.0.8", + "lodash.difference": "4.5.0", + "lodash.isequal": "4.5.0", + "loud-rejection": "1.6.0", + "matcher": "0.1.2", + "max-timeout": "1.0.0", + "md5-hex": "1.3.0", + "meow": "3.7.0", + "ms": "0.7.3", + "object-assign": "4.1.1", + "observable-to-promise": "0.4.0", + "option-chain": "0.1.1", + "package-hash": "1.2.0", + "pkg-conf": "1.1.3", + "plur": "2.1.2", + "power-assert-context-formatter": "1.1.1", + "power-assert-renderer-assertion": "1.1.1", + "power-assert-renderer-succinct": "1.1.1", + "pretty-ms": "2.1.0", + "repeating": "2.0.1", + "require-precompiled": "0.1.0", + "resolve-cwd": "1.0.0", + "semver": "5.3.0", + "set-immediate-shim": "1.0.1", + "source-map-support": "0.4.15", + "stack-utils": "0.4.0", + "strip-ansi": "3.0.1", + "strip-bom": "2.0.0", + "time-require": "0.1.2", + "unique-temp-dir": "1.0.0", + "update-notifier": "1.0.3" + } + }, + "ava-files": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ava-files/-/ava-files-0.2.0.tgz", + "integrity": "sha1-x7i24uDOpjtXpuJ+DbFFx8Gc/iA=", + "dev": true, + "requires": { + "auto-bind": "0.1.0", + "bluebird": "3.5.0", + "globby": "6.1.0", + "ignore-by-default": "1.0.1", + "lodash.flatten": "4.4.0", + "multimatch": "2.1.0", + "slash": "1.0.0" + } + }, + "ava-init": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ava-init/-/ava-init-0.1.6.tgz", + "integrity": "sha1-7xntCyS2vzWdrW+63xoF2DY5XJE=", + "dev": true, + "requires": { + "arr-exclude": "1.0.0", + "cross-spawn": "4.0.2", + "pinkie-promise": "2.0.1", + "read-pkg-up": "1.0.1", + "the-argv": "1.0.0", + "write-pkg": "1.0.0" + } + }, + "babel-code-frame": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", + "integrity": "sha1-AnYgvuVnqIwyVhV05/0IAdMxGOQ=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.1" + } + }, + "babel-core": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.25.0.tgz", + "integrity": "sha1-fdQrBGPHQunVKW3rPsZ6kyLa1yk=", + "dev": true, + "requires": { + "babel-code-frame": "6.22.0", + "babel-generator": "6.25.0", + "babel-helpers": "6.24.1", + "babel-messages": "6.23.0", + "babel-register": "6.24.1", + "babel-runtime": "6.23.0", + "babel-template": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0", + "babylon": "6.17.3", + "convert-source-map": "1.5.0", + "debug": "2.6.8", + "json5": "0.5.1", + "lodash": "4.17.4", + "minimatch": "3.0.4", + "path-is-absolute": "1.0.1", + "private": "0.1.7", + "slash": "1.0.0", + "source-map": "0.5.6" + } + }, + "babel-generator": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.25.0.tgz", + "integrity": "sha1-M6GvcNXyiQrrRlpKd5PB32qeqfw=", + "dev": true, + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.23.0", + "babel-types": "6.25.0", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.4", + "source-map": "0.5.6", + "trim-right": "1.0.1" + } + }, + "babel-helper-bindify-decorators": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz", + "integrity": "sha1-FMGeXxQte0fxmlJDHlKxzLxAozA=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0" + } + }, + "babel-helper-builder-binary-assignment-operator-visitor": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", + "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", + "dev": true, + "requires": { + "babel-helper-explode-assignable-expression": "6.24.1", + "babel-runtime": "6.23.0", + "babel-types": "6.25.0" + } + }, + "babel-helper-call-delegate": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "6.24.1", + "babel-runtime": "6.23.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0" + } + }, + "babel-helper-define-map": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.24.1.tgz", + "integrity": "sha1-epdH8ljYlH0y1RX2qhx70CIEoIA=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.23.0", + "babel-types": "6.25.0", + "lodash": "4.17.4" + } + }, + "babel-helper-explode-assignable-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", + "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0" + } + }, + "babel-helper-explode-class": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz", + "integrity": "sha1-fcKjkQ3uAHBW4eMdZAztPVTqqes=", + "dev": true, + "requires": { + "babel-helper-bindify-decorators": "6.24.1", + "babel-runtime": "6.23.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0" + } + }, + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "dev": true, + "requires": { + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.23.0", + "babel-template": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0" + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "babel-types": "6.25.0" + } + }, + "babel-helper-hoist-variables": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "babel-types": "6.25.0" + } + }, + "babel-helper-optimise-call-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "babel-types": "6.25.0" + } + }, + "babel-helper-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.24.1.tgz", + "integrity": "sha1-024i+rEAjXnYhkjjIRaGgShFbOg=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "babel-types": "6.25.0", + "lodash": "4.17.4" + } + }, + "babel-helper-remap-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", + "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.23.0", + "babel-template": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0" + } + }, + "babel-helper-replace-supers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", + "dev": true, + "requires": { + "babel-helper-optimise-call-expression": "6.24.1", + "babel-messages": "6.23.0", + "babel-runtime": "6.23.0", + "babel-template": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0" + } + }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "babel-template": "6.25.0" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0" + } + }, + "babel-plugin-ava-throws-helper": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-ava-throws-helper/-/babel-plugin-ava-throws-helper-0.1.0.tgz", + "integrity": "sha1-lREHcIoSIIAmv4ykzvGKh7ybDP4=", + "dev": true, + "requires": { + "babel-template": "6.25.0", + "babel-types": "6.25.0" + } + }, + "babel-plugin-check-es2015-constants": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0" + } + }, + "babel-plugin-detective": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-detective/-/babel-plugin-detective-2.0.0.tgz", + "integrity": "sha1-bmQug8IqM1J5dU6+LXVNJjX0nxM=", + "dev": true + }, + "babel-plugin-espower": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/babel-plugin-espower/-/babel-plugin-espower-2.3.2.tgz", + "integrity": "sha1-VRa4/NsmyfDh2BYHSfbkxl5xJx4=", + "dev": true, + "requires": { + "babel-generator": "6.25.0", + "babylon": "6.17.3", + "call-matcher": "1.0.1", + "core-js": "2.4.1", + "espower-location-detector": "1.0.0", + "espurify": "1.7.0", + "estraverse": "4.2.0" + } + }, + "babel-plugin-syntax-async-functions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", + "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", + "dev": true + }, + "babel-plugin-syntax-async-generators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz", + "integrity": "sha1-a8lj67FuzLrmuStZbrfzXDQqi5o=", + "dev": true + }, + "babel-plugin-syntax-class-properties": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", + "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=", + "dev": true + }, + "babel-plugin-syntax-decorators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", + "integrity": "sha1-MSVjtNvePMgGzuPkFszurd0RrAs=", + "dev": true + }, + "babel-plugin-syntax-dynamic-import": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", + "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=", + "dev": true + }, + "babel-plugin-syntax-exponentiation-operator": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", + "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=", + "dev": true + }, + "babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", + "dev": true + }, + "babel-plugin-syntax-trailing-function-commas": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", + "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=", + "dev": true + }, + "babel-plugin-transform-async-generator-functions": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz", + "integrity": "sha1-8FiQAUX9PpkHpt3yjaWfIVJYpds=", + "dev": true, + "requires": { + "babel-helper-remap-async-to-generator": "6.24.1", + "babel-plugin-syntax-async-generators": "6.13.0", + "babel-runtime": "6.23.0" + } + }, + "babel-plugin-transform-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", + "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", + "dev": true, + "requires": { + "babel-helper-remap-async-to-generator": "6.24.1", + "babel-plugin-syntax-async-functions": "6.13.0", + "babel-runtime": "6.23.0" + } + }, + "babel-plugin-transform-class-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", + "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-plugin-syntax-class-properties": "6.13.0", + "babel-runtime": "6.23.0", + "babel-template": "6.25.0" + } + }, + "babel-plugin-transform-decorators": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz", + "integrity": "sha1-eIAT2PjGtSIr33s0Q5Df13Vp4k0=", + "dev": true, + "requires": { + "babel-helper-explode-class": "6.24.1", + "babel-plugin-syntax-decorators": "6.13.0", + "babel-runtime": "6.23.0", + "babel-template": "6.25.0", + "babel-types": "6.25.0" + } + }, + "babel-plugin-transform-es2015-arrow-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0" + } + }, + "babel-plugin-transform-es2015-block-scoped-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0" + } + }, + "babel-plugin-transform-es2015-block-scoping": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.24.1.tgz", + "integrity": "sha1-dsKV3DpHQbFmWt/TFnIV3P8ypXY=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "babel-template": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0", + "lodash": "4.17.4" + } + }, + "babel-plugin-transform-es2015-classes": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", + "dev": true, + "requires": { + "babel-helper-define-map": "6.24.1", + "babel-helper-function-name": "6.24.1", + "babel-helper-optimise-call-expression": "6.24.1", + "babel-helper-replace-supers": "6.24.1", + "babel-messages": "6.23.0", + "babel-runtime": "6.23.0", + "babel-template": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0" + } + }, + "babel-plugin-transform-es2015-computed-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "babel-template": "6.25.0" + } + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0" + } + }, + "babel-plugin-transform-es2015-duplicate-keys": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "babel-types": "6.25.0" + } + }, + "babel-plugin-transform-es2015-for-of": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0" + } + }, + "babel-plugin-transform-es2015-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.23.0", + "babel-types": "6.25.0" + } + }, + "babel-plugin-transform-es2015-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0" + } + }, + "babel-plugin-transform-es2015-modules-amd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-commonjs": "6.24.1", + "babel-runtime": "6.23.0", + "babel-template": "6.25.0" + } + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.1.tgz", + "integrity": "sha1-0+MQtA72ZKNmIiAAl8bUQCmPK/4=", + "dev": true, + "requires": { + "babel-plugin-transform-strict-mode": "6.24.1", + "babel-runtime": "6.23.0", + "babel-template": "6.25.0", + "babel-types": "6.25.0" + } + }, + "babel-plugin-transform-es2015-modules-systemjs": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "6.24.1", + "babel-runtime": "6.23.0", + "babel-template": "6.25.0" + } + }, + "babel-plugin-transform-es2015-modules-umd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-amd": "6.24.1", + "babel-runtime": "6.23.0", + "babel-template": "6.25.0" + } + }, + "babel-plugin-transform-es2015-object-super": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", + "dev": true, + "requires": { + "babel-helper-replace-supers": "6.24.1", + "babel-runtime": "6.23.0" + } + }, + "babel-plugin-transform-es2015-parameters": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", + "dev": true, + "requires": { + "babel-helper-call-delegate": "6.24.1", + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.23.0", + "babel-template": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0" + } + }, + "babel-plugin-transform-es2015-shorthand-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "babel-types": "6.25.0" + } + }, + "babel-plugin-transform-es2015-spread": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0" + } + }, + "babel-plugin-transform-es2015-sticky-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", + "dev": true, + "requires": { + "babel-helper-regex": "6.24.1", + "babel-runtime": "6.23.0", + "babel-types": "6.25.0" + } + }, + "babel-plugin-transform-es2015-template-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0" + } + }, + "babel-plugin-transform-es2015-typeof-symbol": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0" + } + }, + "babel-plugin-transform-es2015-unicode-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", + "dev": true, + "requires": { + "babel-helper-regex": "6.24.1", + "babel-runtime": "6.23.0", + "regexpu-core": "2.0.0" + } + }, + "babel-plugin-transform-exponentiation-operator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", + "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", + "dev": true, + "requires": { + "babel-helper-builder-binary-assignment-operator-visitor": "6.24.1", + "babel-plugin-syntax-exponentiation-operator": "6.13.0", + "babel-runtime": "6.23.0" + } + }, + "babel-plugin-transform-object-rest-spread": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz", + "integrity": "sha1-h11ryb52HFiirj/u5dxIldjH+SE=", + "dev": true, + "requires": { + "babel-plugin-syntax-object-rest-spread": "6.13.0", + "babel-runtime": "6.23.0" + } + }, + "babel-plugin-transform-regenerator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.24.1.tgz", + "integrity": "sha1-uNowWtQ8PJm0hI5P5AN7dw0jxBg=", + "dev": true, + "requires": { + "regenerator-transform": "0.9.11" + } + }, + "babel-plugin-transform-runtime": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz", + "integrity": "sha1-iEkNRGUC6puOfvsP4J7E2ZR5se4=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0" + } + }, + "babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "babel-types": "6.25.0" + } + }, + "babel-preset-es2015": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", + "integrity": "sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=", + "dev": true, + "requires": { + "babel-plugin-check-es2015-constants": "6.22.0", + "babel-plugin-transform-es2015-arrow-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoping": "6.24.1", + "babel-plugin-transform-es2015-classes": "6.24.1", + "babel-plugin-transform-es2015-computed-properties": "6.24.1", + "babel-plugin-transform-es2015-destructuring": "6.23.0", + "babel-plugin-transform-es2015-duplicate-keys": "6.24.1", + "babel-plugin-transform-es2015-for-of": "6.23.0", + "babel-plugin-transform-es2015-function-name": "6.24.1", + "babel-plugin-transform-es2015-literals": "6.22.0", + "babel-plugin-transform-es2015-modules-amd": "6.24.1", + "babel-plugin-transform-es2015-modules-commonjs": "6.24.1", + "babel-plugin-transform-es2015-modules-systemjs": "6.24.1", + "babel-plugin-transform-es2015-modules-umd": "6.24.1", + "babel-plugin-transform-es2015-object-super": "6.24.1", + "babel-plugin-transform-es2015-parameters": "6.24.1", + "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", + "babel-plugin-transform-es2015-spread": "6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "6.24.1", + "babel-plugin-transform-es2015-template-literals": "6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "6.23.0", + "babel-plugin-transform-es2015-unicode-regex": "6.24.1", + "babel-plugin-transform-regenerator": "6.24.1" + } + }, + "babel-preset-es2015-node4": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/babel-preset-es2015-node4/-/babel-preset-es2015-node4-2.1.1.tgz", + "integrity": "sha1-4x8pCFm1hhnIz6JB0bC8kA+UHNs=", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-destructuring": "6.23.0", + "babel-plugin-transform-es2015-function-name": "6.24.1", + "babel-plugin-transform-es2015-modules-commonjs": "6.24.1", + "babel-plugin-transform-es2015-parameters": "6.24.1", + "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", + "babel-plugin-transform-es2015-spread": "6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "6.24.1", + "babel-plugin-transform-es2015-unicode-regex": "6.24.1" + } + }, + "babel-preset-stage-2": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz", + "integrity": "sha1-2eKWD7PXEYfw5k7sYrwHdnIZvcE=", + "dev": true, + "requires": { + "babel-plugin-syntax-dynamic-import": "6.18.0", + "babel-plugin-transform-class-properties": "6.24.1", + "babel-plugin-transform-decorators": "6.24.1", + "babel-preset-stage-3": "6.24.1" + } + }, + "babel-preset-stage-3": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz", + "integrity": "sha1-g2raCp56f6N8sTj7kyb4eTSkg5U=", + "dev": true, + "requires": { + "babel-plugin-syntax-trailing-function-commas": "6.22.0", + "babel-plugin-transform-async-generator-functions": "6.24.1", + "babel-plugin-transform-async-to-generator": "6.24.1", + "babel-plugin-transform-exponentiation-operator": "6.24.1", + "babel-plugin-transform-object-rest-spread": "6.23.0" + } + }, + "babel-register": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.24.1.tgz", + "integrity": "sha1-fhDhOi9xBlvfrVoXh7pFvKbe118=", + "dev": true, + "requires": { + "babel-core": "6.25.0", + "babel-runtime": "6.23.0", + "core-js": "2.4.1", + "home-or-tmp": "2.0.0", + "lodash": "4.17.4", + "mkdirp": "0.5.1", + "source-map-support": "0.4.15" + } + }, + "babel-runtime": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.23.0.tgz", + "integrity": "sha1-CpSJ8UTecO+zzkMArM2zKeL8VDs=", + "dev": true, + "requires": { + "core-js": "2.4.1", + "regenerator-runtime": "0.10.5" + } + }, + "babel-template": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.25.0.tgz", + "integrity": "sha1-ZlJBFmt8KqTGGdceGSlpVSsQwHE=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0", + "babylon": "6.17.3", + "lodash": "4.17.4" + } + }, + "babel-traverse": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.25.0.tgz", + "integrity": "sha1-IldJfi/NGbie3BPEyROB+VEklvE=", + "dev": true, + "requires": { + "babel-code-frame": "6.22.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.23.0", + "babel-types": "6.25.0", + "babylon": "6.17.3", + "debug": "2.6.8", + "globals": "9.18.0", + "invariant": "2.2.2", + "lodash": "4.17.4" + } + }, + "babel-types": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.25.0.tgz", + "integrity": "sha1-cK+ySNVmDl0Y+BHZHIMDtUE0oY4=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "esutils": "2.0.2", + "lodash": "4.17.4", + "to-fast-properties": "1.0.3" + } + }, + "babylon": { + "version": "6.17.3", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.17.3.tgz", + "integrity": "sha512-mq0x3HCAGGmQyZXviOVe5TRsw37Ijy3D43jCqt/9WVf+onx2dUgW3PosnqCbScAFhRO9DGs8nxoMzU0iiosMqQ==", + "dev": true + }, + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + }, + "base64-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.0.tgz", + "integrity": "sha1-o5mS1yNYSBGYK+XikLtqU9hnAPE=", + "dev": true + }, + "big.js": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.1.3.tgz", + "integrity": "sha1-TK2iGTZS6zyp7I5VyQFWacmAaXg=", + "dev": true + }, + "binary-extensions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.8.0.tgz", + "integrity": "sha1-SOyNFt9Dd+rl+liEaCSAr02Vx3Q=", + "dev": true + }, + "bluebird": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", + "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=", + "dev": true + }, + "bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU=", + "dev": true + }, + "boxen": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-0.6.0.tgz", + "integrity": "sha1-g2TUJIrDT/DvGy8r9JpsYM4NgbY=", + "dev": true, + "requires": { + "ansi-align": "1.1.0", + "camelcase": "2.1.1", + "chalk": "1.1.3", + "cli-boxes": "1.0.0", + "filled-array": "1.1.0", + "object-assign": "4.1.1", + "repeating": "2.0.1", + "string-width": "1.0.2", + "widest-line": "1.0.0" + } + }, + "brace-expansion": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", + "integrity": "sha1-Pv/DxQ4ABTH7cg6v+A8K6O8jz1k=", + "dev": true, + "requires": { + "balanced-match": "0.4.2", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browserify-aes": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.0.6.tgz", + "integrity": "sha1-Xncl297x/Vkw1OurSFZ85FHEigo=", + "dev": true, + "requires": { + "buffer-xor": "1.0.3", + "cipher-base": "1.0.3", + "create-hash": "1.1.3", + "evp_bytestokey": "1.0.0", + "inherits": "2.0.3" + } + }, + "browserify-cipher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz", + "integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=", + "dev": true, + "requires": { + "browserify-aes": "1.0.6", + "browserify-des": "1.0.0", + "evp_bytestokey": "1.0.0" + } + }, + "browserify-des": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz", + "integrity": "sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=", + "dev": true, + "requires": { + "cipher-base": "1.0.3", + "des.js": "1.0.0", + "inherits": "2.0.3" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "4.11.6", + "randombytes": "2.0.5" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "4.11.6", + "browserify-rsa": "4.0.1", + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "elliptic": "6.4.0", + "inherits": "2.0.3", + "parse-asn1": "5.1.0" + } + }, + "browserify-zlib": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", + "dev": true, + "requires": { + "pako": "0.2.9" + } + }, + "buf-compare": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buf-compare/-/buf-compare-1.0.1.tgz", + "integrity": "sha1-/vKNqLgROgoNtEMLC2Rntpcws0o=", + "dev": true + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true, + "requires": { + "base64-js": "1.2.0", + "ieee754": "1.1.8", + "isarray": "1.0.0" + } + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "caching-transform": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-1.0.1.tgz", + "integrity": "sha1-bb2y8g+Nj7znnz6U6dF0Lc31wKE=", + "dev": true, + "requires": { + "md5-hex": "1.3.0", + "mkdirp": "0.5.1", + "write-file-atomic": "1.3.4" + } + }, + "call-matcher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-matcher/-/call-matcher-1.0.1.tgz", + "integrity": "sha1-UTTQd5hPcSpU2tPL9i3ijc5BbKg=", + "dev": true, + "requires": { + "core-js": "2.4.1", + "deep-equal": "1.0.1", + "espurify": "1.7.0", + "estraverse": "4.2.0" + } + }, + "call-signature": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/call-signature/-/call-signature-0.0.2.tgz", + "integrity": "sha1-qEq8glpV70yysCi9dOIFpluaSZY=", + "dev": true + }, + "camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", + "dev": true, + "requires": { + "no-case": "2.3.1", + "upper-case": "1.1.3" + } + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "2.1.1", + "map-obj": "1.0.1" + } + }, + "capture-stack-trace": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", + "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", + "dev": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "1.3.0", + "async-each": "1.0.1", + "fsevents": "1.1.1", + "glob-parent": "2.0.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "2.0.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0" + } + }, + "ci-info": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.0.0.tgz", + "integrity": "sha1-3FKF8rTiUYIWg2gcOBwziPRuxTQ=", + "dev": true + }, + "cipher-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.3.tgz", + "integrity": "sha1-7qvxlEGc6QDaMBjCB9IS8qbfCgc=", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "clean-yaml-object": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/clean-yaml-object/-/clean-yaml-object-0.1.0.tgz", + "integrity": "sha1-Y/sRDcLOGoTcIfbZM0h20BCui2g=", + "dev": true + }, + "cli-boxes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", + "dev": true + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "1.0.1" + } + }, + "cli-spinners": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-0.1.2.tgz", + "integrity": "sha1-u3ZNiOGF+54eaiofGXcjGPYF4xw=", + "dev": true + }, + "cli-truncate": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", + "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=", + "dev": true, + "requires": { + "slice-ansi": "0.0.4", + "string-width": "1.0.2" + } + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "co-with-promise": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co-with-promise/-/co-with-promise-4.6.0.tgz", + "integrity": "sha1-QT59tvWJOmC5Qs9JLEvsk9tBWrc=", + "dev": true, + "requires": { + "pinkie-promise": "1.0.0" + }, + "dependencies": { + "pinkie": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-1.0.0.tgz", + "integrity": "sha1-Wkfyi6EBXQIBvae/DzWOR77Ix+Q=", + "dev": true + }, + "pinkie-promise": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-1.0.0.tgz", + "integrity": "sha1-0dpn9UglY7t89X8oauKCLs+/NnA=", + "dev": true, + "requires": { + "pinkie": "1.0.0" + } + } + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "common-path-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-1.0.0.tgz", + "integrity": "sha1-zVL28HEuC6q5fW+XModPIvR3UsA=", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "configstore": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-2.1.0.tgz", + "integrity": "sha1-c3o6cDbpiGECqmCZ5HuzOrGroaE=", + "dev": true, + "requires": { + "dot-prop": "3.0.0", + "graceful-fs": "4.1.11", + "mkdirp": "0.5.1", + "object-assign": "4.1.1", + "os-tmpdir": "1.0.2", + "osenv": "0.1.4", + "uuid": "2.0.3", + "write-file-atomic": "1.3.4", + "xdg-basedir": "2.0.0" + } + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "0.1.4" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "convert-source-map": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.0.tgz", + "integrity": "sha1-ms1whRxtXf3ZPZKC5e35SgP/RrU=", + "dev": true + }, + "core-assert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/core-assert/-/core-assert-0.2.1.tgz", + "integrity": "sha1-+F4s+b/tKPdzzIs/pcW2m9wC/j8=", + "dev": true, + "requires": { + "buf-compare": "1.0.1", + "is-error": "2.2.1" + } + }, + "core-js": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", + "integrity": "sha1-TekR5mew6ukSTjQlS1OupvxhjT4=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "create-ecdh": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", + "integrity": "sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=", + "dev": true, + "requires": { + "bn.js": "4.11.6", + "elliptic": "6.4.0" + } + }, + "create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "dev": true, + "requires": { + "capture-stack-trace": "1.0.0" + } + }, + "create-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", + "integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=", + "dev": true, + "requires": { + "cipher-base": "1.0.3", + "inherits": "2.0.3", + "ripemd160": "2.0.1", + "sha.js": "2.4.8" + } + }, + "create-hmac": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz", + "integrity": "sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=", + "dev": true, + "requires": { + "cipher-base": "1.0.3", + "create-hash": "1.1.3", + "inherits": "2.0.3", + "ripemd160": "2.0.1", + "safe-buffer": "5.0.1", + "sha.js": "2.4.8" + } + }, + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "dev": true, + "requires": { + "lru-cache": "4.1.0", + "which": "1.2.14" + } + }, + "crypto-browserify": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.11.0.tgz", + "integrity": "sha1-NlKgkGq5sqfgw85mpAjpV6JIVSI=", + "dev": true, + "requires": { + "browserify-cipher": "1.0.0", + "browserify-sign": "4.0.4", + "create-ecdh": "4.0.0", + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "diffie-hellman": "5.0.2", + "inherits": "2.0.3", + "pbkdf2": "3.0.12", + "public-encrypt": "4.0.0", + "randombytes": "2.0.5" + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "1.0.2" + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "date-time": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/date-time/-/date-time-0.1.1.tgz", + "integrity": "sha1-7S9tk9l5DOL9ZtW1/z7dW7y/Owc=", + "dev": true + }, + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, + "deep-extend": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", + "dev": true + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0" + } + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "diffie-hellman": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", + "integrity": "sha1-tYNXOScM/ias9jIJn97SoH8gnl4=", + "dev": true, + "requires": { + "bn.js": "4.11.6", + "miller-rabin": "4.0.0", + "randombytes": "2.0.5" + } + }, + "domain-browser": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", + "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=", + "dev": true + }, + "dot-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz", + "integrity": "sha1-G3CK8JSknJoOfbyteQq6U52sEXc=", + "dev": true, + "requires": { + "is-obj": "1.0.1" + } + }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dev": true, + "requires": { + "readable-stream": "2.2.11" + } + }, + "eastasianwidth": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.1.1.tgz", + "integrity": "sha1-RNZW3p2kFWlEZzNTZfsxR7hXK3w=", + "dev": true + }, + "elliptic": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", + "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "dev": true, + "requires": { + "bn.js": "4.11.6", + "brorand": "1.1.0", + "hash.js": "1.0.3", + "hmac-drbg": "1.0.1", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "empower-core": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/empower-core/-/empower-core-0.6.2.tgz", + "integrity": "sha1-Wt71ZgiOMfuoC6CjbfR9cJQWkUQ=", + "dev": true, + "requires": { + "call-signature": "0.0.2", + "core-js": "2.4.1" + } + }, + "enhanced-resolve": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.1.0.tgz", + "integrity": "sha1-n0tib1dyRe3PSyrYPYbhf09CHew=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "memory-fs": "0.4.1", + "object-assign": "4.1.1", + "tapable": "0.2.6" + } + }, + "errno": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz", + "integrity": "sha1-uJbiOp5ei6M4cfyZar02NfyaHH0=", + "dev": true, + "requires": { + "prr": "0.0.0" + } + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "espower-location-detector": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/espower-location-detector/-/espower-location-detector-1.0.0.tgz", + "integrity": "sha1-oXt+zFnTDheeK+9z+0E3cEyzMbU=", + "dev": true, + "requires": { + "is-url": "1.2.2", + "path-is-absolute": "1.0.1", + "source-map": "0.5.6", + "xtend": "4.0.1" + } + }, + "espurify": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/espurify/-/espurify-1.7.0.tgz", + "integrity": "sha1-HFz2y8zDLm9jk4C9T5kfq5up0iY=", + "dev": true, + "requires": { + "core-js": "2.4.1" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "dev": true + }, + "evp_bytestokey": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.0.tgz", + "integrity": "sha1-SXtmrZ/vZc18CKYYCCS6FHa2blM=", + "dev": true, + "requires": { + "create-hash": "1.1.3" + } + }, + "exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", + "dev": true + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "2.2.3" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5", + "object-assign": "4.1.1" + } + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "dev": true, + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.7", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "filled-array": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filled-array/-/filled-array-1.1.0.tgz", + "integrity": "sha1-w8T2xmO5I0WamqKZEtLQMfFQf4Q=", + "dev": true + }, + "find-cache-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-0.1.1.tgz", + "integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=", + "dev": true, + "requires": { + "commondir": "1.0.1", + "mkdirp": "0.5.1", + "pkg-dir": "1.0.0" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "fn-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fn-name/-/fn-name-2.0.1.tgz", + "integrity": "sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc=", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, + "fs-sync": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fs-sync/-/fs-sync-1.0.4.tgz", + "integrity": "sha1-L5Tq3jGGLsCp8zocJUbfsaPz0a4=", + "dev": true, + "requires": { + "glob": "7.1.2", + "iconv-lite": "0.4.17", + "lodash": "4.17.4", + "mkdirp": "0.5.1", + "rimraf": "2.6.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.1.tgz", + "integrity": "sha1-8Z/Sj0Pur3YWgOUZogPE0LPTGv8=", + "dev": true, + "optional": true, + "requires": { + "nan": "2.6.2", + "node-pre-gyp": "0.6.33" + }, + "dependencies": { + "abbrev": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "bundled": true, + "dev": true, + "optional": true + }, + "aproba": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.2.2" + } + }, + "asn1": { + "version": "0.2.3", + "bundled": true, + "dev": true, + "optional": true + }, + "assert-plus": { + "version": "0.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true, + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "aws4": { + "version": "1.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "balanced-match": { + "version": "0.4.2", + "bundled": true, + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "block-stream": { + "version": "0.0.9", + "bundled": true, + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "boom": { + "version": "2.10.1", + "bundled": true, + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.6", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "0.4.2", + "concat-map": "0.0.1" + } + }, + "buffer-shims": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "caseless": { + "version": "0.11.0", + "bundled": true, + "dev": true, + "optional": true + }, + "chalk": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "combined-stream": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.9.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "graceful-readlink": "1.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "cryptiles": { + "version": "2.0.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "debug": { + "version": "2.2.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "0.7.1" + } + }, + "deep-extend": { + "version": "0.4.1", + "bundled": true, + "dev": true, + "optional": true + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "extend": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "extsprintf": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true, + "dev": true, + "optional": true + }, + "form-data": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.14" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "fstream": { + "version": "1.0.10", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.5.4" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fstream": "1.0.10", + "inherits": "2.0.3", + "minimatch": "3.0.3" + } + }, + "gauge": { + "version": "2.7.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "1.1.1", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.0" + } + }, + "generate-function": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "generate-object-property": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "is-property": "1.0.2" + } + }, + "getpass": { + "version": "0.1.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "glob": { + "version": "7.1.1", + "bundled": true, + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.3", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true, + "dev": true + }, + "graceful-readlink": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "har-validator": { + "version": "2.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chalk": "1.1.3", + "commander": "2.9.0", + "is-my-json-valid": "2.15.0", + "pinkie-promise": "2.0.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "hawk": { + "version": "3.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "bundled": true, + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.3.1", + "sshpk": "1.10.2" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.4", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-my-json-valid": { + "version": "2.15.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "generate-function": "2.0.0", + "generate-object-property": "1.2.0", + "jsonpointer": "4.0.1", + "xtend": "4.0.1" + } + }, + "is-property": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "jodid25519": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "jsbn": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true, + "dev": true, + "optional": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "jsonpointer": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "jsprim": { + "version": "1.3.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "extsprintf": "1.0.2", + "json-schema": "0.2.3", + "verror": "1.3.6" + } + }, + "mime-db": { + "version": "1.26.0", + "bundled": true, + "dev": true + }, + "mime-types": { + "version": "2.1.14", + "bundled": true, + "dev": true, + "requires": { + "mime-db": "1.26.0" + } + }, + "minimatch": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "1.1.6" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "0.7.1", + "bundled": true, + "dev": true, + "optional": true + }, + "node-pre-gyp": { + "version": "0.6.33", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "mkdirp": "0.5.1", + "nopt": "3.0.6", + "npmlog": "4.0.2", + "rc": "1.1.7", + "request": "2.79.0", + "rimraf": "2.5.4", + "semver": "5.3.0", + "tar": "2.2.1", + "tar-pack": "3.3.0" + } + }, + "nopt": { + "version": "3.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1.1.0" + } + }, + "npmlog": { + "version": "4.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.2", + "console-control-strings": "1.1.0", + "gauge": "2.7.3", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "bundled": true, + "dev": true, + "optional": true + }, + "pinkie-promise": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "process-nextick-args": { + "version": "1.0.7", + "bundled": true, + "dev": true + }, + "punycode": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true + }, + "qs": { + "version": "6.3.1", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.1.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "0.4.1", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.2.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "buffer-shims": "1.0.0", + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "0.10.31", + "util-deprecate": "1.0.2" + } + }, + "request": { + "version": "2.79.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.11.0", + "combined-stream": "1.0.5", + "extend": "3.0.0", + "forever-agent": "0.6.1", + "form-data": "2.1.2", + "har-validator": "2.0.6", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.14", + "oauth-sign": "0.8.2", + "qs": "6.3.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.2", + "tunnel-agent": "0.4.3", + "uuid": "3.0.1" + } + }, + "rimraf": { + "version": "2.5.4", + "bundled": true, + "dev": true, + "requires": { + "glob": "7.1.1" + } + }, + "semver": { + "version": "5.3.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sntp": { + "version": "1.0.9", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "hoek": "2.16.3" + } + }, + "sshpk": { + "version": "1.10.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.6", + "jodid25519": "1.0.2", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "0.10.31", + "bundled": true, + "dev": true + }, + "stringstream": { + "version": "0.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "supports-color": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "2.2.1", + "bundled": true, + "dev": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.10", + "inherits": "2.0.3" + } + }, + "tar-pack": { + "version": "3.3.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "2.2.0", + "fstream": "1.0.10", + "fstream-ignore": "1.0.5", + "once": "1.3.3", + "readable-stream": "2.1.5", + "rimraf": "2.5.4", + "tar": "2.2.1", + "uid-number": "0.0.6" + }, + "dependencies": { + "once": { + "version": "1.3.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "readable-stream": { + "version": "2.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "buffer-shims": "1.0.0", + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "0.10.31", + "util-deprecate": "1.0.2" + } + } + } + }, + "tough-cookie": { + "version": "2.3.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.4.3", + "bundled": true, + "dev": true, + "optional": true + }, + "tweetnacl": { + "version": "0.14.5", + "bundled": true, + "dev": true, + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "uuid": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "verror": { + "version": "1.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "extsprintf": "1.0.2" + } + }, + "wide-align": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "xtend": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", + "dev": true + }, + "get-port": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-2.1.0.tgz", + "integrity": "sha1-h4P53OvR7qSVozThpqJR54iHqxo=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "2.0.1" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "got": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/got/-/got-5.7.1.tgz", + "integrity": "sha1-X4FjWmHkplifGAVp6k44FoClHzU=", + "dev": true, + "requires": { + "create-error-class": "3.0.2", + "duplexer2": "0.1.4", + "is-redirect": "1.0.0", + "is-retry-allowed": "1.1.0", + "is-stream": "1.1.0", + "lowercase-keys": "1.0.0", + "node-status-codes": "1.0.0", + "object-assign": "4.1.1", + "parse-json": "2.2.0", + "pinkie-promise": "2.0.1", + "read-all-stream": "3.1.0", + "readable-stream": "2.2.11", + "timed-out": "3.1.3", + "unzip-response": "1.0.2", + "url-parse-lax": "1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-color": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", + "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", + "dev": true + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "hash-base": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", + "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "hash.js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.0.3.tgz", + "integrity": "sha1-EzL/ABVsCg/92CNgE9B7d6BFFXM=", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "1.0.3", + "minimalistic-assert": "1.0.0", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "dev": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "hosted-git-info": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.4.2.tgz", + "integrity": "sha1-AHa59GonBQbduq6lZJaJdGBhKmc=", + "dev": true + }, + "https-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz", + "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.17.tgz", + "integrity": "sha1-T9qjs4rLwsAxsEXQ7c3+HsqxjI0=", + "dev": true + }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=", + "dev": true + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", + "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", + "dev": true + }, + "interpret": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.0.3.tgz", + "integrity": "sha1-y8NcYu7uc/Gat7EKgBURQBr8D5A=", + "dev": true + }, + "invariant": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", + "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=", + "dev": true, + "requires": { + "loose-envify": "1.3.1" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "irregular-plurals": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-1.2.0.tgz", + "integrity": "sha1-OPKZg0uowAwwvpxVThNyaXUv86w=", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "1.8.0" + } + }, + "is-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-ci": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.0.10.tgz", + "integrity": "sha1-9zkzayYyNlBhqdSCcM1WrjNpMY4=", + "dev": true, + "requires": { + "ci-info": "1.0.0" + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-error": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-error/-/is-error-2.2.1.tgz", + "integrity": "sha1-aEqW2EB2V3yY9M20DG0mpRI78Zw=", + "dev": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-generator-fn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-1.0.0.tgz", + "integrity": "sha1-lp1J4bszKfa7fwkIm+JleLLd1Go=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-npm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", + "dev": true + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-observable": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-0.2.0.tgz", + "integrity": "sha1-s2ExHYPG5dcmyr9eJQsCNxBvWuI=", + "dev": true, + "requires": { + "symbol-observable": "0.2.4" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", + "dev": true + }, + "is-retry-allowed": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", + "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-url": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.2.tgz", + "integrity": "sha1-SYkFpZO/R8wtnn9zg3K792lsfyY=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "js-tokens": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.1.tgz", + "integrity": "sha1-COnxMkhKLEWjCQfp3E1VZ7fxFNc=", + "dev": true + }, + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + }, + "json-loader": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.4.tgz", + "integrity": "sha1-i6oTZaYy9Yo8RtIBdfxgAsluN94=", + "dev": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.5" + } + }, + "last-line-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/last-line-stream/-/last-line-stream-1.0.0.tgz", + "integrity": "sha1-0bZNafhv8kry0EiDos7uFFIKVgA=", + "dev": true, + "requires": { + "through2": "2.0.3" + } + }, + "latest-version": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-2.0.0.tgz", + "integrity": "sha1-VvjWE5YghHuAF/jx9NeOIRMkFos=", + "dev": true, + "requires": { + "package-json": "2.4.0" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true + }, + "lazy-req": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/lazy-req/-/lazy-req-1.1.0.tgz", + "integrity": "sha1-va6+rTD42CQDnODOFJ1Nqge6H6w=", + "dev": true + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "1.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "loader-runner": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.0.tgz", + "integrity": "sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI=", + "dev": true + }, + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "3.1.3", + "emojis-list": "2.1.0", + "json5": "0.5.1", + "object-assign": "4.1.1" + } + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "dev": true + }, + "lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=", + "dev": true + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "dev": true + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "dev": true, + "requires": { + "js-tokens": "3.0.1" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "0.4.1", + "signal-exit": "3.0.2" + } + }, + "lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", + "dev": true + }, + "lowercase-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", + "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=", + "dev": true + }, + "lru-cache": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.0.tgz", + "integrity": "sha512-aHGs865JXz6bkB4AHL+3AhyvTFKL3iZamKVWjIUKnXOXyasJvqPK8WAjOnAQKQZVpeXDVz19u1DD0r/12bWAdQ==", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "matcher": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-0.1.2.tgz", + "integrity": "sha1-7yDL3mTCTFDMYa9bg+4LG4/wAQE=", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5" + } + }, + "max-timeout": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/max-timeout/-/max-timeout-1.0.0.tgz", + "integrity": "sha1-to9povmeC0dv1Msj4gWcp1BxXh8=", + "dev": true + }, + "md5-hex": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-1.3.0.tgz", + "integrity": "sha1-0sSv6YPENwZiF5uMrRRSGRNQRsQ=", + "dev": true, + "requires": { + "md5-o-matic": "0.1.1" + } + }, + "md5-o-matic": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/md5-o-matic/-/md5-o-matic-0.1.1.tgz", + "integrity": "sha1-givM1l4RfFFPqxdrJZRdVBAKA8M=", + "dev": true + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "0.1.4", + "readable-stream": "2.2.11" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "2.1.0", + "decamelize": "1.2.0", + "loud-rejection": "1.6.0", + "map-obj": "1.0.1", + "minimist": "1.2.0", + "normalize-package-data": "2.3.8", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "redent": "1.0.0", + "trim-newlines": "1.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.3" + } + }, + "miller-rabin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.0.tgz", + "integrity": "sha1-SmL7HUKTPAVYOYL0xxb2+55sbT0=", + "dev": true, + "requires": { + "bn.js": "4.11.6", + "brorand": "1.1.0" + } + }, + "minimalistic-assert": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", + "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.3.tgz", + "integrity": "sha1-cIFVpeROM/X9D8U+gdDUCpG+H/8=", + "dev": true + }, + "multimatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", + "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=", + "dev": true, + "requires": { + "array-differ": "1.0.0", + "array-union": "1.0.2", + "arrify": "1.0.1", + "minimatch": "3.0.4" + } + }, + "nan": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz", + "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=", + "dev": true, + "optional": true + }, + "no-case": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.1.tgz", + "integrity": "sha1-euuhxzpSGEJlVUt9wDuvcg34AIE=", + "dev": true, + "requires": { + "lower-case": "1.1.4" + } + }, + "node-libs-browser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.0.0.tgz", + "integrity": "sha1-o6WeyXAkmFtG6Vg3lkb5bEthZkY=", + "dev": true, + "requires": { + "assert": "1.4.1", + "browserify-zlib": "0.1.4", + "buffer": "4.9.1", + "console-browserify": "1.1.0", + "constants-browserify": "1.0.0", + "crypto-browserify": "3.11.0", + "domain-browser": "1.1.7", + "events": "1.1.1", + "https-browserify": "0.0.1", + "os-browserify": "0.2.1", + "path-browserify": "0.0.0", + "process": "0.11.10", + "punycode": "1.4.1", + "querystring-es3": "0.2.1", + "readable-stream": "2.2.11", + "stream-browserify": "2.0.1", + "stream-http": "2.7.1", + "string_decoder": "0.10.31", + "timers-browserify": "2.0.2", + "tty-browserify": "0.0.0", + "url": "0.11.0", + "util": "0.10.3", + "vm-browserify": "0.0.4" + }, + "dependencies": { + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "node-status-codes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz", + "integrity": "sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8=", + "dev": true + }, + "normalize-package-data": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.3.8.tgz", + "integrity": "sha1-2Bntoqne29H/pWPqQHHZNngilbs=", + "dev": true, + "requires": { + "hosted-git-info": "2.4.2", + "is-builtin-module": "1.0.0", + "semver": "5.3.0", + "validate-npm-package-license": "3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "1.0.2" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "nyc": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-10.3.2.tgz", + "integrity": "sha1-8n9NkfKp2zbCT1dP9cbv/wIz3kY=", + "dev": true, + "requires": { + "archy": "1.0.0", + "arrify": "1.0.1", + "caching-transform": "1.0.1", + "convert-source-map": "1.5.0", + "debug-log": "1.0.1", + "default-require-extensions": "1.0.0", + "find-cache-dir": "0.1.1", + "find-up": "1.1.2", + "foreground-child": "1.5.6", + "glob": "7.1.1", + "istanbul-lib-coverage": "1.1.0", + "istanbul-lib-hook": "1.0.6", + "istanbul-lib-instrument": "1.7.1", + "istanbul-lib-report": "1.1.0", + "istanbul-lib-source-maps": "1.2.0", + "istanbul-reports": "1.1.0", + "md5-hex": "1.3.0", + "merge-source-map": "1.0.3", + "micromatch": "2.3.11", + "mkdirp": "0.5.1", + "resolve-from": "2.0.0", + "rimraf": "2.6.1", + "signal-exit": "3.0.2", + "spawn-wrap": "1.2.4", + "test-exclude": "4.1.0", + "yargs": "7.1.0", + "yargs-parser": "5.0.0" + }, + "dependencies": { + "align-text": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.0", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } + }, + "amdefine": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "bundled": true, + "dev": true + }, + "append-transform": { + "version": "0.4.0", + "bundled": true, + "dev": true, + "requires": { + "default-require-extensions": "1.0.0" + } + }, + "archy": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "arr-diff": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "arr-flatten": "1.0.3" + } + }, + "arr-flatten": { + "version": "1.0.3", + "bundled": true, + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "bundled": true, + "dev": true + }, + "arrify": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "async": { + "version": "1.5.2", + "bundled": true, + "dev": true + }, + "babel-code-frame": { + "version": "6.22.0", + "bundled": true, + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.1" + } + }, + "babel-generator": { + "version": "6.24.1", + "bundled": true, + "dev": true, + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.23.0", + "babel-types": "6.24.1", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.4", + "source-map": "0.5.6", + "trim-right": "1.0.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "6.23.0" + } + }, + "babel-runtime": { + "version": "6.23.0", + "bundled": true, + "dev": true, + "requires": { + "core-js": "2.4.1", + "regenerator-runtime": "0.10.5" + } + }, + "babel-template": { + "version": "6.24.1", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "babel-traverse": "6.24.1", + "babel-types": "6.24.1", + "babylon": "6.17.0", + "lodash": "4.17.4" + } + }, + "babel-traverse": { + "version": "6.24.1", + "bundled": true, + "dev": true, + "requires": { + "babel-code-frame": "6.22.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.23.0", + "babel-types": "6.24.1", + "babylon": "6.17.0", + "debug": "2.6.6", + "globals": "9.17.0", + "invariant": "2.2.2", + "lodash": "4.17.4" + } + }, + "babel-types": { + "version": "6.24.1", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "esutils": "2.0.2", + "lodash": "4.17.4", + "to-fast-properties": "1.0.3" + } + }, + "babylon": { + "version": "6.17.0", + "bundled": true, + "dev": true + }, + "balanced-match": { + "version": "0.4.2", + "bundled": true, + "dev": true + }, + "brace-expansion": { + "version": "1.1.7", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "0.4.2", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "bundled": true, + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "builtin-modules": { + "version": "1.1.1", + "bundled": true, + "dev": true + }, + "caching-transform": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "md5-hex": "1.3.0", + "mkdirp": "0.5.1", + "write-file-atomic": "1.3.4" + } + }, + "camelcase": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true + }, + "center-align": { + "version": "0.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "chalk": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "cliui": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "commondir": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "convert-source-map": { + "version": "1.5.0", + "bundled": true, + "dev": true + }, + "core-js": { + "version": "2.4.1", + "bundled": true, + "dev": true + }, + "cross-spawn": { + "version": "4.0.2", + "bundled": true, + "dev": true, + "requires": { + "lru-cache": "4.0.2", + "which": "1.2.14" + } + }, + "debug": { + "version": "2.6.6", + "bundled": true, + "dev": true, + "requires": { + "ms": "0.7.3" + } + }, + "debug-log": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "decamelize": { + "version": "1.2.0", + "bundled": true, + "dev": true + }, + "default-require-extensions": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "strip-bom": "2.0.0" + } + }, + "detect-indent": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "error-ex": { + "version": "1.3.1", + "bundled": true, + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "bundled": true, + "dev": true + }, + "esutils": { + "version": "2.0.2", + "bundled": true, + "dev": true + }, + "expand-brackets": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "bundled": true, + "dev": true, + "requires": { + "fill-range": "2.2.3" + } + }, + "extglob": { + "version": "0.3.2", + "bundled": true, + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "filename-regex": { + "version": "2.0.1", + "bundled": true, + "dev": true + }, + "fill-range": { + "version": "2.2.3", + "bundled": true, + "dev": true, + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.6", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "find-cache-dir": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "requires": { + "commondir": "1.0.1", + "mkdirp": "0.5.1", + "pkg-dir": "1.0.0" + } + }, + "find-up": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "for-in": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "for-own": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, + "foreground-child": { + "version": "1.5.6", + "bundled": true, + "dev": true, + "requires": { + "cross-spawn": "4.0.2", + "signal-exit": "3.0.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "get-caller-file": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "glob": { + "version": "7.1.1", + "bundled": true, + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.3", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-base": { + "version": "0.3.0", + "bundled": true, + "dev": true, + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } + }, + "glob-parent": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-glob": "2.0.1" + } + }, + "globals": { + "version": "9.17.0", + "bundled": true, + "dev": true + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true, + "dev": true + }, + "handlebars": { + "version": "4.0.8", + "bundled": true, + "dev": true, + "requires": { + "async": "1.5.2", + "optimist": "0.6.1", + "source-map": "0.4.4", + "uglify-js": "2.8.22" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "bundled": true, + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "has-ansi": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "hosted-git-info": { + "version": "2.4.2", + "bundled": true, + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "bundled": true, + "dev": true + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "invariant": { + "version": "2.2.2", + "bundled": true, + "dev": true, + "requires": { + "loose-envify": "1.3.1" + } + }, + "invert-kv": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "bundled": true, + "dev": true + }, + "is-buffer": { + "version": "1.1.5", + "bundled": true, + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-dotfile": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "bundled": true, + "dev": true, + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-glob": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-number": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.0" + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "bundled": true, + "dev": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "isexe": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "isobject": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "istanbul-lib-coverage": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "istanbul-lib-hook": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "requires": { + "append-transform": "0.4.0" + } + }, + "istanbul-lib-instrument": { + "version": "1.7.1", + "bundled": true, + "dev": true, + "requires": { + "babel-generator": "6.24.1", + "babel-template": "6.24.1", + "babel-traverse": "6.24.1", + "babel-types": "6.24.1", + "babylon": "6.17.0", + "istanbul-lib-coverage": "1.1.0", + "semver": "5.3.0" + } + }, + "istanbul-lib-report": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "istanbul-lib-coverage": "1.1.0", + "mkdirp": "0.5.1", + "path-parse": "1.0.5", + "supports-color": "3.2.3" + }, + "dependencies": { + "supports-color": { + "version": "3.2.3", + "bundled": true, + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "requires": { + "debug": "2.6.6", + "istanbul-lib-coverage": "1.1.0", + "mkdirp": "0.5.1", + "rimraf": "2.6.1", + "source-map": "0.5.6" + } + }, + "istanbul-reports": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "handlebars": "4.0.8" + } + }, + "js-tokens": { + "version": "3.0.1", + "bundled": true, + "dev": true + }, + "jsesc": { + "version": "1.3.0", + "bundled": true, + "dev": true + }, + "kind-of": { + "version": "3.2.0", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "1.1.5" + } + }, + "lazy-cache": { + "version": "1.0.4", + "bundled": true, + "dev": true, + "optional": true + }, + "lcid": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "invert-kv": "1.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "lodash": { + "version": "4.17.4", + "bundled": true, + "dev": true + }, + "longest": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "loose-envify": { + "version": "1.3.1", + "bundled": true, + "dev": true, + "requires": { + "js-tokens": "3.0.1" + } + }, + "lru-cache": { + "version": "4.0.2", + "bundled": true, + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "md5-hex": { + "version": "1.3.0", + "bundled": true, + "dev": true, + "requires": { + "md5-o-matic": "0.1.1" + } + }, + "md5-o-matic": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "merge-source-map": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "requires": { + "source-map": "0.5.6" + } + }, + "micromatch": { + "version": "2.3.11", + "bundled": true, + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.0", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.3" + } + }, + "minimatch": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "0.7.3", + "bundled": true, + "dev": true + }, + "normalize-package-data": { + "version": "2.3.8", + "bundled": true, + "dev": true, + "requires": { + "hosted-git-info": "2.4.2", + "is-builtin-module": "1.0.0", + "semver": "5.3.0", + "validate-npm-package-license": "3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "requires": { + "remove-trailing-separator": "1.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true + }, + "object.omit": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "optimist": { + "version": "0.6.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8", + "wordwrap": "0.0.3" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "os-locale": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "lcid": "1.0.0" + } + }, + "parse-glob": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.2", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } + }, + "parse-json": { + "version": "2.2.0", + "bundled": true, + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, + "path-exists": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "bundled": true, + "dev": true + }, + "path-type": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "pify": { + "version": "2.3.0", + "bundled": true, + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "bundled": true, + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "pkg-dir": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "find-up": "1.1.2" + } + }, + "preserve": { + "version": "0.2.0", + "bundled": true, + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "randomatic": { + "version": "1.1.6", + "bundled": true, + "dev": true, + "requires": { + "is-number": "2.1.0", + "kind-of": "3.2.0" + } + }, + "read-pkg": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.3.8", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "regenerator-runtime": { + "version": "0.10.5", + "bundled": true, + "dev": true + }, + "regex-cache": { + "version": "0.4.3", + "bundled": true, + "dev": true, + "requires": { + "is-equal-shallow": "0.1.3", + "is-primitive": "2.0.0" + } + }, + "remove-trailing-separator": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "repeat-element": { + "version": "1.1.2", + "bundled": true, + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "bundled": true, + "dev": true + }, + "repeating": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-finite": "1.0.2" + } + }, + "require-directory": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "resolve-from": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "right-align": { + "version": "0.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.6.1", + "bundled": true, + "dev": true, + "requires": { + "glob": "7.1.1" + } + }, + "semver": { + "version": "5.3.0", + "bundled": true, + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true + }, + "slide": { + "version": "1.1.6", + "bundled": true, + "dev": true + }, + "source-map": { + "version": "0.5.6", + "bundled": true, + "dev": true + }, + "spawn-wrap": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "requires": { + "foreground-child": "1.5.6", + "mkdirp": "0.5.1", + "os-homedir": "1.0.2", + "rimraf": "2.6.1", + "signal-exit": "2.1.2", + "which": "1.2.14" + }, + "dependencies": { + "signal-exit": { + "version": "2.1.2", + "bundled": true, + "dev": true + } + } + }, + "spdx-correct": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "spdx-license-ids": "1.2.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "bundled": true, + "dev": true + }, + "spdx-license-ids": { + "version": "1.2.2", + "bundled": true, + "dev": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "supports-color": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "test-exclude": { + "version": "4.1.0", + "bundled": true, + "dev": true, + "requires": { + "arrify": "1.0.1", + "micromatch": "2.3.11", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "require-main-filename": "1.0.1" + } + }, + "to-fast-properties": { + "version": "1.0.3", + "bundled": true, + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "uglify-js": { + "version": "2.8.22", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "source-map": "0.5.6", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "yargs": { + "version": "3.10.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "validate-npm-package-license": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "spdx-correct": "1.0.2", + "spdx-expression-parse": "1.0.4" + } + }, + "which": { + "version": "1.2.14", + "bundled": true, + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "window-size": { + "version": "0.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "wordwrap": { + "version": "0.0.3", + "bundled": true, + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "write-file-atomic": { + "version": "1.3.4", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "imurmurhash": "0.1.4", + "slide": "1.1.6" + } + }, + "y18n": { + "version": "3.2.1", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "2.1.2", + "bundled": true, + "dev": true + }, + "yargs": { + "version": "7.1.0", + "bundled": true, + "dev": true, + "requires": { + "camelcase": "3.0.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "1.4.0", + "read-pkg-up": "1.0.1", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "1.0.2", + "which-module": "1.0.0", + "y18n": "3.2.1", + "yargs-parser": "5.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "cliui": { + "version": "3.2.0", + "bundled": true, + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + } + } + } + }, + "yargs-parser": { + "version": "5.0.0", + "bundled": true, + "dev": true, + "requires": { + "camelcase": "3.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "bundled": true, + "dev": true + } + } + } + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, + "observable-to-promise": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/observable-to-promise/-/observable-to-promise-0.4.0.tgz", + "integrity": "sha1-KK/nFkUwjy1B1x9HrT/s4aN35Ss=", + "dev": true, + "requires": { + "is-observable": "0.2.0", + "symbol-observable": "0.2.4" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "option-chain": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/option-chain/-/option-chain-0.1.1.tgz", + "integrity": "sha1-6bgR4AbxwPVIAvKClb/Ilw+Nz70=", + "dev": true, + "requires": { + "object-assign": "4.1.1" + } + }, + "os-browserify": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.2.1.tgz", + "integrity": "sha1-Y/xMzuXS13Y9Jrv4YBB45sLgBE8=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dev": true, + "requires": { + "lcid": "1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", + "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", + "dev": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "package-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-1.2.0.tgz", + "integrity": "sha1-AD5WzVe3NqbtYRTMK4FUJnJ3DkQ=", + "dev": true, + "requires": { + "md5-hex": "1.3.0" + } + }, + "package-json": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-2.4.0.tgz", + "integrity": "sha1-DRW9Z9HLvduyyiIv8u24a8sxqLs=", + "dev": true, + "requires": { + "got": "5.7.1", + "registry-auth-token": "3.3.1", + "registry-url": "3.1.0", + "semver": "5.3.0" + } + }, + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=", + "dev": true + }, + "parse-asn1": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz", + "integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=", + "dev": true, + "requires": { + "asn1.js": "4.9.1", + "browserify-aes": "1.0.6", + "create-hash": "1.1.3", + "evp_bytestokey": "1.0.0", + "pbkdf2": "3.0.12" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, + "parse-ms": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-1.0.1.tgz", + "integrity": "sha1-VjRtR0nXjyNDDKDHE4UK75GqNh0=", + "dev": true + }, + "pascal-case": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-2.0.1.tgz", + "integrity": "sha1-LVeNNFX2YNpl7KGO+VtODekSdh4=", + "dev": true, + "requires": { + "camel-case": "3.0.0", + "upper-case-first": "1.1.2" + } + }, + "path-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", + "dev": true + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "pbkdf2": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.12.tgz", + "integrity": "sha1-vjZ4XFBn6kjYBv+SMojF91C2uKI=", + "dev": true, + "requires": { + "create-hash": "1.1.3", + "create-hmac": "1.1.6", + "ripemd160": "2.0.1", + "safe-buffer": "5.0.1", + "sha.js": "2.4.8" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "pkg-conf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-1.1.3.tgz", + "integrity": "sha1-N45W1v0T6Iv7b0ol33qD+qvduls=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "load-json-file": "1.1.0", + "object-assign": "4.1.1", + "symbol": "0.2.3" + } + }, + "pkg-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "dev": true, + "requires": { + "find-up": "1.1.2" + } + }, + "plur": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/plur/-/plur-2.1.2.tgz", + "integrity": "sha1-dIJFLBoPUI4+NE6uwxLJHCncZVo=", + "dev": true, + "requires": { + "irregular-plurals": "1.2.0" + } + }, + "power-assert-context-formatter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/power-assert-context-formatter/-/power-assert-context-formatter-1.1.1.tgz", + "integrity": "sha1-7bo1LT7YpgMRTWZyZazOYNaJzN8=", + "dev": true, + "requires": { + "core-js": "2.4.1", + "power-assert-context-traversal": "1.1.1" + } + }, + "power-assert-context-traversal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/power-assert-context-traversal/-/power-assert-context-traversal-1.1.1.tgz", + "integrity": "sha1-iMq8oNE7Y1nwfT0+ivppkmRXftk=", + "dev": true, + "requires": { + "core-js": "2.4.1", + "estraverse": "4.2.0" + } + }, + "power-assert-renderer-assertion": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/power-assert-renderer-assertion/-/power-assert-renderer-assertion-1.1.1.tgz", + "integrity": "sha1-y/wOd+AIao+Wrz8djme57n4ozpg=", + "dev": true, + "requires": { + "power-assert-renderer-base": "1.1.1", + "power-assert-util-string-width": "1.1.1" + } + }, + "power-assert-renderer-base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/power-assert-renderer-base/-/power-assert-renderer-base-1.1.1.tgz", + "integrity": "sha1-lqZQxv0F7hvB9mtUrWFELIs/Y+s=", + "dev": true + }, + "power-assert-renderer-diagram": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/power-assert-renderer-diagram/-/power-assert-renderer-diagram-1.1.2.tgz", + "integrity": "sha1-ZV+PcRk1qbbVQbhjJ2VHF8Y3qYY=", + "dev": true, + "requires": { + "core-js": "2.4.1", + "power-assert-renderer-base": "1.1.1", + "power-assert-util-string-width": "1.1.1", + "stringifier": "1.3.0" + } + }, + "power-assert-renderer-succinct": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/power-assert-renderer-succinct/-/power-assert-renderer-succinct-1.1.1.tgz", + "integrity": "sha1-wqRosjgiq9b4Diq6UyI0ewnfR24=", + "dev": true, + "requires": { + "core-js": "2.4.1", + "power-assert-renderer-diagram": "1.1.2" + } + }, + "power-assert-util-string-width": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/power-assert-util-string-width/-/power-assert-util-string-width-1.1.1.tgz", + "integrity": "sha1-vmWet5N/3S5smncmjar2S9W3xZI=", + "dev": true, + "requires": { + "eastasianwidth": "0.1.1" + } + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "pretty-ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-2.1.0.tgz", + "integrity": "sha1-QlfCVt8/sLRR1q/6qwIYhBJpgdw=", + "dev": true, + "requires": { + "is-finite": "1.0.2", + "parse-ms": "1.0.1", + "plur": "1.0.0" + }, + "dependencies": { + "plur": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/plur/-/plur-1.0.0.tgz", + "integrity": "sha1-24XGgU9eXlo7Se/CjWBP7GKXUVY=", + "dev": true + } + } + }, + "private": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.7.tgz", + "integrity": "sha1-aM5eih7woju1cMwoU3tTMqumPvE=", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "prr": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz", + "integrity": "sha1-GoS4WQgyVQFBGFPQCB7j+obikmo=", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "public-encrypt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", + "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=", + "dev": true, + "requires": { + "bn.js": "4.11.6", + "browserify-rsa": "4.0.1", + "create-hash": "1.1.3", + "parse-asn1": "5.1.0", + "randombytes": "2.0.5" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "1.1.5" + } + } + } + }, + "randombytes": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz", + "integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.0.tgz", + "integrity": "sha512-aSLEDudu6OoRr/2rU609gRmnYboRLxgDG1z9o2Q0os7236FwvcqIOO8r8U5JUEwivZOhDaKlFO4SbPTJYyBEyQ==", + "dev": true + } + } + }, + "rc": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.1.tgz", + "integrity": "sha1-LgPo5C7kULjLPc5lvhv4l04d/ZU=", + "dev": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "read-all-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", + "integrity": "sha1-NcPhd/IHjveJ7kv6+kNzB06u9Po=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1", + "readable-stream": "2.2.11" + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.3.8", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "readable-stream": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.11.tgz", + "integrity": "sha512-h+8+r3MKEhkiVrwdKL8aWs1oc1VvBu33ueshOvS26RsZQ3Amhx/oO3TKe4lApSV9ueY6as8EAh7mtuFjdlhg9Q==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.0.1", + "string_decoder": "1.0.2", + "util-deprecate": "1.0.2" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "readable-stream": "2.2.11", + "set-immediate-shim": "1.0.1" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "2.1.0", + "strip-indent": "1.0.1" + } + }, + "regenerate": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.2.tgz", + "integrity": "sha1-0ZQcZ7rUN+G+dkM63Vs4X5WxkmA=", + "dev": true + }, + "regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", + "dev": true + }, + "regenerator-transform": { + "version": "0.9.11", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.9.11.tgz", + "integrity": "sha1-On0GdSDLe3F2dp61/4aGkb7+EoM=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "babel-types": "6.25.0", + "private": "0.1.7" + } + }, + "regex-cache": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.3.tgz", + "integrity": "sha1-mxpsNdTQ3871cRrmUejp09cRQUU=", + "dev": true, + "requires": { + "is-equal-shallow": "0.1.3", + "is-primitive": "2.0.0" + } + }, + "regexpu-core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", + "dev": true, + "requires": { + "regenerate": "1.3.2", + "regjsgen": "0.2.0", + "regjsparser": "0.1.5" + } + }, + "registry-auth-token": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.1.tgz", + "integrity": "sha1-+w0yie4Nmtosu1KvXf5mywcNMAY=", + "dev": true, + "requires": { + "rc": "1.2.1", + "safe-buffer": "5.0.1" + } + }, + "registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "dev": true, + "requires": { + "rc": "1.2.1" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "requires": { + "jsesc": "0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, + "remove-trailing-separator": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz", + "integrity": "sha1-abBi2XhyetFNxrVrpKt3L9jXBRE=", + "dev": true + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "1.0.2" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "require-precompiled": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/require-precompiled/-/require-precompiled-0.1.0.tgz", + "integrity": "sha1-WhtS63Dr7UPrmC6XTIWrWVceVvo=", + "dev": true + }, + "resolve-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-1.0.0.tgz", + "integrity": "sha1-Tq7qQe0EDRcCRX32SkKysH0kb58=", + "dev": true, + "requires": { + "resolve-from": "2.0.0" + } + }, + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "1.1.1", + "onetime": "1.1.0" + } + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", + "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "ripemd160": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=", + "dev": true, + "requires": { + "hash-base": "2.0.2", + "inherits": "2.0.3" + } + }, + "safe-buffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", + "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=", + "dev": true + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + }, + "semver-diff": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "dev": true, + "requires": { + "semver": "5.3.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "sha.js": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.8.tgz", + "integrity": "sha1-NwaMLEdra69ALRSknGf1l5IfY08=", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + }, + "slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", + "dev": true + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "dev": true, + "requires": { + "is-plain-obj": "1.1.0" + } + }, + "source-list-map": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-1.1.2.tgz", + "integrity": "sha1-mIkBnRAkzOVc3AaUmDN+9hhqEaE=", + "dev": true + }, + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", + "dev": true + }, + "source-map-support": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.15.tgz", + "integrity": "sha1-AyAt9lwG0r2MfsI2KhkwVv7407E=", + "dev": true, + "requires": { + "source-map": "0.5.6" + } + }, + "spdx-correct": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "dev": true, + "requires": { + "spdx-license-ids": "1.2.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", + "dev": true + }, + "spdx-license-ids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", + "dev": true + }, + "stack-utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-0.4.0.tgz", + "integrity": "sha1-lAy4L8z6hOj/Lz/fKT/ngBa+zNE=", + "dev": true + }, + "stream-browserify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.2.11" + } + }, + "stream-http": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.1.tgz", + "integrity": "sha1-VGpRdBrVprB+njGwsQRBqRffUoo=", + "dev": true, + "requires": { + "builtin-status-codes": "3.0.0", + "inherits": "2.0.3", + "readable-stream": "2.2.11", + "to-arraybuffer": "1.0.1", + "xtend": "4.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.2.tgz", + "integrity": "sha1-sp4fThEl+pehA4K4pTNze3SR4Xk=", + "dev": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "stringifier": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/stringifier/-/stringifier-1.3.0.tgz", + "integrity": "sha1-3vGDQvaTPbDy2/yaoCF1tEjBeVk=", + "dev": true, + "requires": { + "core-js": "2.4.1", + "traverse": "0.6.6", + "type-name": "2.0.2" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "4.0.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "symbol": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/symbol/-/symbol-0.2.3.tgz", + "integrity": "sha1-O5hzuKkB5Hxu/iFSajrDcu8ou8c=", + "dev": true + }, + "symbol-observable": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-0.2.4.tgz", + "integrity": "sha1-lag9smGG1q9+ehjb2XYKL4bQj0A=", + "dev": true + }, + "tapable": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.6.tgz", + "integrity": "sha1-IGvo4YiGC1FEJTdebxrom/sB/Y0=", + "dev": true + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "the-argv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/the-argv/-/the-argv-1.0.0.tgz", + "integrity": "sha1-AIRwUAVzDdhNt1UlPJMa45jblSI=", + "dev": true + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "dev": true, + "requires": { + "readable-stream": "2.2.11", + "xtend": "4.0.1" + } + }, + "time-require": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/time-require/-/time-require-0.1.2.tgz", + "integrity": "sha1-+eEss3D8JgXhFARYK6VO9corLZg=", + "dev": true, + "requires": { + "chalk": "0.4.0", + "date-time": "0.1.1", + "pretty-ms": "0.2.2", + "text-table": "0.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", + "dev": true + }, + "chalk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", + "dev": true, + "requires": { + "ansi-styles": "1.0.0", + "has-color": "0.1.7", + "strip-ansi": "0.1.1" + } + }, + "parse-ms": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-0.1.2.tgz", + "integrity": "sha1-3T+iXtbC78e93hKtm0bBY6opIk4=", + "dev": true + }, + "pretty-ms": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-0.2.2.tgz", + "integrity": "sha1-2oeaaC/zOjcBEEbxPWJ/Z8c7hPY=", + "dev": true, + "requires": { + "parse-ms": "0.1.2" + } + }, + "strip-ansi": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", + "dev": true + } + } + }, + "timed-out": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-3.1.3.tgz", + "integrity": "sha1-lYYL/MXHbCd/j4Mm/Q9bLiDrohc=", + "dev": true + }, + "timers-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.2.tgz", + "integrity": "sha1-q0iDz1l9zVCvIRNJoA+8pWrIa4Y=", + "dev": true, + "requires": { + "setimmediate": "1.0.5" + } + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "traverse": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", + "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=", + "dev": true + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "type-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/type-name/-/type-name-2.0.2.tgz", + "integrity": "sha1-7+fUEj2KxSr/9/QMfk3sUmYAj7Q=", + "dev": true + }, + "uglify-js": { + "version": "2.8.28", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.28.tgz", + "integrity": "sha512-WqKNbmNJKzIdIEQu/U2ytgGBbhCy2PVks94GoetczOAJ/zCgVu2CuO7gguI5KPFGPtUtI1dmPQl6h0D4cPzypA==", + "dev": true, + "requires": { + "source-map": "0.5.6", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "uid2": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", + "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=", + "dev": true + }, + "unique-temp-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-temp-dir/-/unique-temp-dir-1.0.0.tgz", + "integrity": "sha1-bc6VsmgcoAPuv7MEpBX5y6vMU4U=", + "dev": true, + "requires": { + "mkdirp": "0.5.1", + "os-tmpdir": "1.0.2", + "uid2": "0.0.3" + } + }, + "unzip-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-1.0.2.tgz", + "integrity": "sha1-uYTwh3/AqJwsdzzB73tbIytbBv4=", + "dev": true + }, + "update-notifier": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-1.0.3.tgz", + "integrity": "sha1-j5LFFUgr1oMbfJMBPnD4dVLHz1o=", + "dev": true, + "requires": { + "boxen": "0.6.0", + "chalk": "1.1.3", + "configstore": "2.1.0", + "is-npm": "1.0.0", + "latest-version": "2.0.0", + "lazy-req": "1.1.0", + "semver-diff": "2.1.0", + "xdg-basedir": "2.0.0" + } + }, + "upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", + "dev": true + }, + "upper-case-first": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-1.1.2.tgz", + "integrity": "sha1-XXm+3P8UQZUY/S7bCgUHybaFkRU=", + "dev": true, + "requires": { + "upper-case": "1.1.3" + } + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "dev": true, + "requires": { + "prepend-http": "1.0.4" + } + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "dev": true, + "requires": { + "spdx-correct": "1.0.2", + "spdx-expression-parse": "1.0.4" + } + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "dev": true, + "requires": { + "indexof": "0.0.1" + } + }, + "watchpack": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.3.1.tgz", + "integrity": "sha1-fYaTkHsozmAT5/NhCqKhrPB9rYc=", + "dev": true, + "requires": { + "async": "2.4.1", + "chokidar": "1.7.0", + "graceful-fs": "4.1.11" + } + }, + "webpack": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-2.6.1.tgz", + "integrity": "sha1-LgRX8KuxrF3zqxBsacZy8jZ4Xwc=", + "dev": true, + "requires": { + "acorn": "5.0.3", + "acorn-dynamic-import": "2.0.2", + "ajv": "4.11.8", + "ajv-keywords": "1.5.1", + "async": "2.4.1", + "enhanced-resolve": "3.1.0", + "interpret": "1.0.3", + "json-loader": "0.5.4", + "json5": "0.5.1", + "loader-runner": "2.3.0", + "loader-utils": "0.2.17", + "memory-fs": "0.4.1", + "mkdirp": "0.5.1", + "node-libs-browser": "2.0.0", + "source-map": "0.5.6", + "supports-color": "3.2.3", + "tapable": "0.2.6", + "uglify-js": "2.8.28", + "watchpack": "1.3.1", + "webpack-sources": "0.2.3", + "yargs": "6.6.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + }, + "yargs": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", + "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", + "dev": true, + "requires": { + "camelcase": "3.0.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "1.4.0", + "read-pkg-up": "1.0.1", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "1.0.2", + "which-module": "1.0.0", + "y18n": "3.2.1", + "yargs-parser": "4.2.1" + } + } + } + }, + "webpack-sources": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-0.2.3.tgz", + "integrity": "sha1-F8Yr+vE8cH+dAsR54Nzd6DgGl/s=", + "dev": true, + "requires": { + "source-list-map": "1.1.2", + "source-map": "0.5.6" + } + }, + "which": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", + "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "dev": true + }, + "widest-line": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-1.0.0.tgz", + "integrity": "sha1-DAnIXCqUaD0Nfq+O4JfVZL8OEFw=", + "dev": true, + "requires": { + "string-width": "1.0.2" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write-file-atomic": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", + "integrity": "sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "imurmurhash": "0.1.4", + "slide": "1.1.6" + } + }, + "write-json-file": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/write-json-file/-/write-json-file-1.2.0.tgz", + "integrity": "sha1-LV3+lqvDyIkFfJOXGqQAXvtUgTQ=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "mkdirp": "0.5.1", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "sort-keys": "1.1.2", + "write-file-atomic": "1.3.4" + } + }, + "write-pkg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/write-pkg/-/write-pkg-1.0.0.tgz", + "integrity": "sha1-rriqnU14jh2JPfsIVJaLVDqRn1c=", + "dev": true, + "requires": { + "write-json-file": "1.2.0" + } + }, + "xdg-basedir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-2.0.0.tgz", + "integrity": "sha1-7byQPMOF/ARSPZZqM1UEtVBNG9I=", + "dev": true, + "requires": { + "os-homedir": "1.0.2" + } + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + }, + "dependencies": { + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true + } + } + }, + "yargs-parser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", + "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", + "dev": true, + "requires": { + "camelcase": "3.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + } + } + } + } +} diff --git a/package.json b/package.json index 2cdfbc7..a143584 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,58 @@ { - "name": "javascript-state-machine", - "description": "A simple finite state machine library", - "homepage": "https://github.com/jakesgordon/javascript-state-machine", - "keywords": ["state machine", "server", "client"], - "author": "Jake Gordon ", - "repository": {"type": "git", "url": "git://github.com/jakesgordon/javascript-state-machine.git"}, - "main": "state-machine.js", + "name": "javascript-state-machine", + "description": "A finite state machine library", + "homepage": "https://github.com/jakesgordon/javascript-state-machine", + "repository": { + "type": "git", + "url": "git://github.com/jakesgordon/javascript-state-machine.git" + }, + "keywords": [ + "finite state machine", + "state machine", + "server", + "client" + ], + "author": { + "name": "Jake Gordon", + "email": "jakesgordon@gmail.com" + }, + "maintainers": [ + { + "name": "Jake Gordon", + "email": "jakesgordon@gmail.com" + } + ], + "license": "MIT", + "main": "lib/state-machine.js", + "files": [ + "lib/**/*.js", + "dist/**/*.js" + ], + "directories": {}, "devDependencies": { - "qunit": "~0.6.2" + "ava": "^0.17.0", + "fs-sync": "^1.0.3", + "glob": "^7.1.1", + "nyc": "^10.0.0", + "pascal-case": "^2.0.0", + "uglify-js": "^2.7.5", + "webpack": "^2.2.0-rc.1" + }, + "version": "3.1.0", + "scripts": { + "start": "npm run watch", + "build": "npm run bundle && npm run minify", + "bundle": "webpack", + "minify": "bin/minify", + "watch": "ava --watch", + "test": "nyc ava -v && nyc report --reporter=html" }, - "version": "2.3.5" + "ava": { + "files": [ + "test/**/*.js" + ], + "source": [ + "src/**/*.js" + ] + } } diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..786e7f7 --- /dev/null +++ b/src/app.js @@ -0,0 +1,102 @@ +'use strict' + +//----------------------------------------------------------------------------------------------- + +var mixin = require('./util/mixin'), + camelize = require('./util/camelize'), + plugin = require('./plugin'), + Config = require('./config'), + JSM = require('./jsm'); + +//----------------------------------------------------------------------------------------------- + +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; diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..21601df --- /dev/null +++ b/src/config.js @@ -0,0 +1,161 @@ +'use strict' + +//------------------------------------------------------------------------------------------------- + +var mixin = require('./util/mixin'), + camelize = require('./util/camelize'); + +//------------------------------------------------------------------------------------------------- + +function Config(options, StateMachine) { + + options = options || {}; + + this.options = options; // preserving original options can be useful (e.g visualize plugin) + this.defaults = StateMachine.defaults; + this.states = []; + this.transitions = []; + this.map = {}; + this.lifecycle = this.configureLifecycle(); + this.init = this.configureInitTransition(options.init); + this.data = this.configureData(options.data); + this.methods = this.configureMethods(options.methods); + + this.map[this.defaults.wildcard] = {}; + + this.configureTransitions(options.transitions || []); + + this.plugins = this.configurePlugins(options.plugins, StateMachine.plugin); + +} + +//------------------------------------------------------------------------------------------------- + +mixin(Config.prototype, { + + addState: function(name) { + if (!this.map[name]) { + this.states.push(name); + this.addStateLifecycleNames(name); + this.map[name] = {}; + } + }, + + addStateLifecycleNames: function(name) { + this.lifecycle.onEnter[name] = camelize.prepended('onEnter', name); + this.lifecycle.onLeave[name] = camelize.prepended('onLeave', name); + this.lifecycle.on[name] = camelize.prepended('on', name); + }, + + addTransition: function(name) { + if (this.transitions.indexOf(name) < 0) { + this.transitions.push(name); + this.addTransitionLifecycleNames(name); + } + }, + + addTransitionLifecycleNames: function(name) { + this.lifecycle.onBefore[name] = camelize.prepended('onBefore', name); + this.lifecycle.onAfter[name] = camelize.prepended('onAfter', name); + this.lifecycle.on[name] = camelize.prepended('on', name); + }, + + mapTransition: function(transition) { + var name = transition.name, + from = transition.from, + to = transition.to; + this.addState(from); + if (typeof to !== 'function') + this.addState(to); + this.addTransition(name); + this.map[from][name] = transition; + return transition; + }, + + configureLifecycle: function() { + return { + onBefore: { transition: 'onBeforeTransition' }, + onAfter: { transition: 'onAfterTransition' }, + onEnter: { state: 'onEnterState' }, + onLeave: { state: 'onLeaveState' }, + on: { transition: 'onTransition' } + }; + }, + + configureInitTransition: function(init) { + if (typeof init === 'string') { + return this.mapTransition(mixin({}, this.defaults.init, { to: init, active: true })); + } + else if (typeof init === 'object') { + return this.mapTransition(mixin({}, this.defaults.init, init, { active: true })); + } + else { + this.addState(this.defaults.init.from); + return this.defaults.init; + } + }, + + configureData: function(data) { + if (typeof data === 'function') + return data; + else if (typeof data === 'object') + return function() { return data; } + else + return function() { return {}; } + }, + + configureMethods: function(methods) { + return methods || {}; + }, + + configurePlugins: function(plugins, builtin) { + plugins = plugins || []; + var n, max, plugin; + for(n = 0, max = plugins.length ; n < max ; n++) { + plugin = plugins[n]; + if (typeof plugin === 'function') + plugins[n] = plugin = plugin() + if (plugin.configure) + plugin.configure(this); + } + return plugins + }, + + configureTransitions: function(transitions) { + var i, n, transition, from, to, wildcard = this.defaults.wildcard; + for(n = 0 ; n < transitions.length ; n++) { + transition = transitions[n]; + from = Array.isArray(transition.from) ? transition.from : [transition.from || wildcard] + to = transition.to || wildcard; + for(i = 0 ; i < from.length ; i++) { + this.mapTransition({ name: transition.name, from: from[i], to: to }); + } + } + }, + + transitionFor: function(state, transition) { + var wildcard = this.defaults.wildcard; + return this.map[state][transition] || + this.map[wildcard][transition]; + }, + + transitionsFor: function(state) { + var wildcard = this.defaults.wildcard; + return Object.keys(this.map[state]).concat(Object.keys(this.map[wildcard])); + }, + + allStates: function() { + return this.states; + }, + + allTransitions: function() { + return this.transitions; + } + +}); + +//------------------------------------------------------------------------------------------------- + +module.exports = Config; + +//------------------------------------------------------------------------------------------------- diff --git a/src/jsm.js b/src/jsm.js new file mode 100644 index 0000000..f83c8de --- /dev/null +++ b/src/jsm.js @@ -0,0 +1,182 @@ + +var mixin = require('./util/mixin'), + Exception = require('./util/exception'), + plugin = require('./plugin'), + UNOBSERVED = [ null, [] ]; + +//------------------------------------------------------------------------------------------------- + +function JSM(context, config) { + this.context = context; + this.config = config; + this.state = config.init.from; + this.observers = [context]; +} + +//------------------------------------------------------------------------------------------------- + +mixin(JSM.prototype, { + + init: function(args) { + mixin(this.context, this.config.data.apply(this.context, args)); + plugin.hook(this, 'init'); + if (this.config.init.active) + return this.fire(this.config.init.name, []); + }, + + is: function(state) { + return Array.isArray(state) ? (state.indexOf(this.state) >= 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 new file mode 100644 index 0000000..491e25a --- /dev/null +++ b/src/plugin.js @@ -0,0 +1,40 @@ +'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 new file mode 100644 index 0000000..46799c0 --- /dev/null +++ b/src/plugin/history.js @@ -0,0 +1,83 @@ +'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 new file mode 100644 index 0000000..0bee32b --- /dev/null +++ b/src/plugin/visualize.js @@ -0,0 +1,161 @@ +'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/util/camelize.js b/src/util/camelize.js new file mode 100644 index 0000000..b2645fd --- /dev/null +++ b/src/util/camelize.js @@ -0,0 +1,33 @@ +'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 new file mode 100644 index 0000000..cf62304 --- /dev/null +++ b/src/util/exception.js @@ -0,0 +1,9 @@ +'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 new file mode 100644 index 0000000..330664b --- /dev/null +++ b/src/util/mixin.js @@ -0,0 +1,13 @@ +'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/state-machine.js b/state-machine.js deleted file mode 100755 index aa05d14..0000000 --- a/state-machine.js +++ /dev/null @@ -1,227 +0,0 @@ -/* - - Javascript State Machine Library - https://github.com/jakesgordon/javascript-state-machine - - Copyright (c) 2012, 2013, 2014, 2015, Jake Gordon and contributors - Released under the MIT license - https://github.com/jakesgordon/javascript-state-machine/blob/master/LICENSE - -*/ - -(function () { - - var StateMachine = { - - //--------------------------------------------------------------------------- - - VERSION: "2.3.5", - - //--------------------------------------------------------------------------- - - Result: { - SUCCEEDED: 1, // the event transitioned successfully from one state to another - NOTRANSITION: 2, // the event was successfull but no state transition was necessary - CANCELLED: 3, // the event was cancelled by the caller in a beforeEvent callback - PENDING: 4 // the event is asynchronous and the caller is in control of when the transition occurs - }, - - Error: { - INVALID_TRANSITION: 100, // caller tried to fire an event that was innapropriate in the current state - PENDING_TRANSITION: 200, // caller tried to fire an event while an async transition was still pending - INVALID_CALLBACK: 300 // caller provided callback function threw an exception - }, - - WILDCARD: '*', - ASYNC: 'async', - - //--------------------------------------------------------------------------- - - create: function(cfg, target) { - - var initial = (typeof cfg.initial == 'string') ? { state: cfg.initial } : cfg.initial; // allow for a simple string, or an object with { state: 'foo', event: 'setup', defer: true|false } - var terminal = cfg.terminal || cfg['final']; - var fsm = target || cfg.target || {}; - var events = cfg.events || []; - var callbacks = cfg.callbacks || {}; - var map = {}; // track state transitions allowed for an event { event: { from: [ to ] } } - var transitions = {}; // track events allowed from a state { state: [ event ] } - - var add = function(e) { - var from = (e.from instanceof Array) ? e.from : (e.from ? [e.from] : [StateMachine.WILDCARD]); // allow 'wildcard' transition if 'from' is not specified - map[e.name] = map[e.name] || {}; - for (var n = 0 ; n < from.length ; n++) { - transitions[from[n]] = transitions[from[n]] || []; - transitions[from[n]].push(e.name); - - map[e.name][from[n]] = e.to || from[n]; // allow no-op transition if 'to' is not specified - } - }; - - if (initial) { - initial.event = initial.event || 'startup'; - add({ name: initial.event, from: 'none', to: initial.state }); - } - - for(var n = 0 ; n < events.length ; n++) - add(events[n]); - - for(var name in map) { - if (map.hasOwnProperty(name)) - fsm[name] = StateMachine.buildEvent(name, map[name]); - } - - for(var name in callbacks) { - if (callbacks.hasOwnProperty(name)) - fsm[name] = callbacks[name] - } - - fsm.current = 'none'; - fsm.is = function(state) { return (state instanceof Array) ? (state.indexOf(this.current) >= 0) : (this.current === state); }; - fsm.can = function(event) { return !this.transition && (map[event].hasOwnProperty(this.current) || map[event].hasOwnProperty(StateMachine.WILDCARD)); } - fsm.cannot = function(event) { return !this.can(event); }; - fsm.transitions = function() { return transitions[this.current]; }; - fsm.isFinished = function() { return this.is(terminal); }; - fsm.error = cfg.error || function(name, from, to, args, error, msg, e) { throw e || msg; }; // default behavior when something unexpected happens is to throw an exception, but caller can override this behavior if desired (see github issue #3 and #17) - - if (initial && !initial.defer) - fsm[initial.event](); - - return fsm; - - }, - - //=========================================================================== - - doCallback: function(fsm, func, name, from, to, args) { - if (func) { - try { - return func.apply(fsm, [name, from, to].concat(args)); - } - catch(e) { - return fsm.error(name, from, to, args, StateMachine.Error.INVALID_CALLBACK, "an exception occurred in a caller-provided callback function", e); - } - } - }, - - beforeAnyEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onbeforeevent'], name, from, to, args); }, - afterAnyEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onafterevent'] || fsm['onevent'], name, from, to, args); }, - leaveAnyState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onleavestate'], name, from, to, args); }, - enterAnyState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onenterstate'] || fsm['onstate'], name, from, to, args); }, - changeState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onchangestate'], name, from, to, args); }, - - beforeThisEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onbefore' + name], name, from, to, args); }, - afterThisEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onafter' + name] || fsm['on' + name], name, from, to, args); }, - leaveThisState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onleave' + from], name, from, to, args); }, - enterThisState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onenter' + to] || fsm['on' + to], name, from, to, args); }, - - beforeEvent: function(fsm, name, from, to, args) { - if ((false === StateMachine.beforeThisEvent(fsm, name, from, to, args)) || - (false === StateMachine.beforeAnyEvent( fsm, name, from, to, args))) - return false; - }, - - afterEvent: function(fsm, name, from, to, args) { - StateMachine.afterThisEvent(fsm, name, from, to, args); - StateMachine.afterAnyEvent( fsm, name, from, to, args); - }, - - leaveState: function(fsm, name, from, to, args) { - var specific = StateMachine.leaveThisState(fsm, name, from, to, args), - general = StateMachine.leaveAnyState( fsm, name, from, to, args); - if ((false === specific) || (false === general)) - return false; - else if ((StateMachine.ASYNC === specific) || (StateMachine.ASYNC === general)) - return StateMachine.ASYNC; - }, - - enterState: function(fsm, name, from, to, args) { - StateMachine.enterThisState(fsm, name, from, to, args); - StateMachine.enterAnyState( fsm, name, from, to, args); - }, - - //=========================================================================== - - buildEvent: function(name, map) { - return function() { - - var from = this.current; - var to = map[from] || map[StateMachine.WILDCARD] || from; - var args = Array.prototype.slice.call(arguments); // turn arguments into pure array - - if (this.transition) - return this.error(name, from, to, args, StateMachine.Error.PENDING_TRANSITION, "event " + name + " inappropriate because previous transition did not complete"); - - if (this.cannot(name)) - return this.error(name, from, to, args, StateMachine.Error.INVALID_TRANSITION, "event " + name + " inappropriate in current state " + this.current); - - if (false === StateMachine.beforeEvent(this, name, from, to, args)) - return StateMachine.Result.CANCELLED; - - if (from === to) { - StateMachine.afterEvent(this, name, from, to, args); - return StateMachine.Result.NOTRANSITION; - } - - // prepare a transition method for use EITHER lower down, or by caller if they want an async transition (indicated by an ASYNC return value from leaveState) - var fsm = this; - this.transition = function() { - fsm.transition = null; // this method should only ever be called once - fsm.current = to; - StateMachine.enterState( fsm, name, from, to, args); - StateMachine.changeState(fsm, name, from, to, args); - StateMachine.afterEvent( fsm, name, from, to, args); - return StateMachine.Result.SUCCEEDED; - }; - this.transition.cancel = function() { // provide a way for caller to cancel async transition if desired (issue #22) - fsm.transition = null; - StateMachine.afterEvent(fsm, name, from, to, args); - } - - var leave = StateMachine.leaveState(this, name, from, to, args); - if (false === leave) { - this.transition = null; - return StateMachine.Result.CANCELLED; - } - else if (StateMachine.ASYNC === leave) { - return StateMachine.Result.PENDING; - } - else { - if (this.transition) // need to check in case user manually called transition() but forgot to return StateMachine.ASYNC - return this.transition(); - } - - }; - } - - }; // StateMachine - - //=========================================================================== - - //====== - // NODE - //====== - if (typeof exports !== 'undefined') { - if (typeof module !== 'undefined' && module.exports) { - exports = module.exports = StateMachine; - } - exports.StateMachine = StateMachine; - } - //============ - // AMD/REQUIRE - //============ - else if (typeof define === 'function' && define.amd) { - define(function(require) { return StateMachine; }); - } - //======== - // BROWSER - //======== - else if (typeof window !== 'undefined') { - window.StateMachine = StateMachine; - } - //=========== - // WEB WORKER - //=========== - else if (typeof self !== 'undefined') { - self.StateMachine = StateMachine; - } - -}()); diff --git a/state-machine.min.js b/state-machine.min.js deleted file mode 100644 index 717ca99..0000000 --- a/state-machine.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(){var a={VERSION:"2.3.5",Result:{SUCCEEDED:1,NOTRANSITION:2,CANCELLED:3,PENDING:4},Error:{INVALID_TRANSITION:100,PENDING_TRANSITION:200,INVALID_CALLBACK:300},WILDCARD:"*",ASYNC:"async",create:function(g,h){var j=(typeof g.initial=="string")?{state:g.initial}:g.initial;var f=g.terminal||g["final"];var e=h||g.target||{};var m=g.events||[];var i=g.callbacks||{};var c={};var k={};var l=function(o){var q=(o.from instanceof Array)?o.from:(o.from?[o.from]:[a.WILDCARD]);c[o.name]=c[o.name]||{};for(var p=0;p=0):(this.current===n)};e.can=function(n){return !this.transition&&(c[n].hasOwnProperty(this.current)||c[n].hasOwnProperty(a.WILDCARD))};e.cannot=function(n){return !this.can(n)};e.transitions=function(){return k[this.current]};e.isFinished=function(){return this.is(f)};e.error=g.error||function(p,t,s,o,n,r,q){throw q||r};if(j&&!j.defer){e[j.event]()}return e},doCallback:function(g,d,c,i,h,b){if(d){try{return d.apply(g,[c,i,h].concat(b))}catch(f){return g.error(c,i,h,b,a.Error.INVALID_CALLBACK,"an exception occurred in a caller-provided callback function",f)}}},beforeAnyEvent:function(d,c,f,e,b){return a.doCallback(d,d.onbeforeevent,c,f,e,b)},afterAnyEvent:function(d,c,f,e,b){return a.doCallback(d,d.onafterevent||d.onevent,c,f,e,b)},leaveAnyState:function(d,c,f,e,b){return a.doCallback(d,d.onleavestate,c,f,e,b)},enterAnyState:function(d,c,f,e,b){return a.doCallback(d,d.onenterstate||d.onstate,c,f,e,b)},changeState:function(d,c,f,e,b){return a.doCallback(d,d.onchangestate,c,f,e,b)},beforeThisEvent:function(d,c,f,e,b){return a.doCallback(d,d["onbefore"+c],c,f,e,b)},afterThisEvent:function(d,c,f,e,b){return a.doCallback(d,d["onafter"+c]||d["on"+c],c,f,e,b)},leaveThisState:function(d,c,f,e,b){return a.doCallback(d,d["onleave"+f],c,f,e,b)},enterThisState:function(d,c,f,e,b){return a.doCallback(d,d["onenter"+e]||d["on"+e],c,f,e,b)},beforeEvent:function(d,c,f,e,b){if((false===a.beforeThisEvent(d,c,f,e,b))||(false===a.beforeAnyEvent(d,c,f,e,b))){return false}},afterEvent:function(d,c,f,e,b){a.afterThisEvent(d,c,f,e,b);a.afterAnyEvent(d,c,f,e,b)},leaveState:function(f,e,h,g,d){var c=a.leaveThisState(f,e,h,g,d),b=a.leaveAnyState(f,e,h,g,d);if((false===c)||(false===b)){return false}else{if((a.ASYNC===c)||(a.ASYNC===b)){return a.ASYNC}}},enterState:function(d,c,f,e,b){a.enterThisState(d,c,f,e,b);a.enterAnyState(d,c,f,e,b)},buildEvent:function(b,c){return function(){var h=this.current;var g=c[h]||c[a.WILDCARD]||h;var e=Array.prototype.slice.call(arguments);if(this.transition){return this.error(b,h,g,e,a.Error.PENDING_TRANSITION,"event "+b+" inappropriate because previous transition did not complete")}if(this.cannot(b)){return this.error(b,h,g,e,a.Error.INVALID_TRANSITION,"event "+b+" inappropriate in current state "+this.current)}if(false===a.beforeEvent(this,b,h,g,e)){return a.Result.CANCELLED}if(h===g){a.afterEvent(this,b,h,g,e);return a.Result.NOTRANSITION}var f=this;this.transition=function(){f.transition=null;f.current=g;a.enterState(f,b,h,g,e);a.changeState(f,b,h,g,e);a.afterEvent(f,b,h,g,e);return a.Result.SUCCEEDED};this.transition.cancel=function(){f.transition=null;a.afterEvent(f,b,h,g,e)};var d=a.leaveState(this,b,h,g,e);if(false===d){this.transition=null;return a.Result.CANCELLED}else{if(a.ASYNC===d){return a.Result.PENDING}else{if(this.transition){return this.transition()}}}}}};if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports){exports=module.exports=a}exports.StateMachine=a}else{if(typeof define==="function"&&define.amd){define(function(b){return a})}else{if(typeof window!=="undefined"){window.StateMachine=a}else{if(typeof self!=="undefined"){self.StateMachine=a}}}}}()); \ No newline at end of file diff --git a/test/basics.js b/test/basics.js new file mode 100644 index 0000000..d0f6060 --- /dev/null +++ b/test/basics.js @@ -0,0 +1,130 @@ +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') + +}); + +//----------------------------------------------------------------------------- + +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 + } + + 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 a = new Alarm('A'), + b = new Alarm('B'); + + t.is(a.name, 'A') + t.is(b.name, 'B') + + 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') + +}); + +//----------------------------------------------------------------------------- diff --git a/test/construction.js b/test/construction.js new file mode 100644 index 0000000..911b5a3 --- /dev/null +++ b/test/construction.js @@ -0,0 +1,229 @@ +import test from 'ava' +import StateMachine from '../src/app' + +//------------------------------------------------------------------------------------------------- + +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' } + ] + }); + + 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' ]) + +}) + + +//------------------------------------------------------------------------------------------------- + +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' } + ] + }); + + 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' ]) + +}) + +//------------------------------------------------------------------------------------------------- + +test('singleton construction - with init state and transition', t => { + + var fsm = new StateMachine({ + init: { name: 'boot', to: 'A' }, + transitions: [ + { name: 'step1', from: 'A', to: 'B' }, + { name: 'step2', from: 'B', to: 'C' } + ] + }); + + t.is(fsm.state, 'A') + + t.deepEqual(fsm.allStates(), [ 'none', 'A', 'B', 'C' ]) + t.deepEqual(fsm.allTransitions(), [ 'boot', 'step1', 'step2' ]) + t.deepEqual(fsm.transitions(), [ 'step1' ]) + +}) + +//------------------------------------------------------------------------------------------------- + +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(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) + +}) + +//------------------------------------------------------------------------------------------------- + +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) + +}) + +//------------------------------------------------------------------------------------------------- diff --git a/test/defaults.js b/test/defaults.js new file mode 100644 index 0000000..da1bc08 --- /dev/null +++ b/test/defaults.js @@ -0,0 +1,62 @@ +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 new file mode 100644 index 0000000..ad2ce62 --- /dev/null +++ b/test/empty.js @@ -0,0 +1,79 @@ +import test from 'ava' +import StateMachine from '../src/app' + +//------------------------------------------------------------------------------------------------- + +test('empty state machine', t => { + + var fsm = new StateMachine(); + + t.is(fsm.state, 'none') + + t.deepEqual(fsm.allStates(), [ 'none' ]) + t.deepEqual(fsm.allTransitions(), [ ]) + t.deepEqual(fsm.transitions(), [ ]) + +}) + +//------------------------------------------------------------------------------------------------- + +test('empty state machine - but caller forgot new keyword', t => { + + var fsm = StateMachine() // NOTE: missing 'new' + + t.is(fsm.state, 'none') + + 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(), [ ]) + +}) + +//------------------------------------------------------------------------------------------------- diff --git a/test/errors.js b/test/errors.js new file mode 100644 index 0000000..fb852ca --- /dev/null +++ b/test/errors.js @@ -0,0 +1,160 @@ +import test from 'ava' +import StateMachine from '../src/app' + +//------------------------------------------------------------------------------------------------- + +test('state cannot be modified directly', t => { + + var fsm = new StateMachine({ + transitions: [ + { name: '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') + +}) + +//------------------------------------------------------------------------------------------------- + +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('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') + +}) + +//------------------------------------------------------------------------------------------------- diff --git a/test/goto.js b/test/goto.js new file mode 100644 index 0000000..e8fcbcc --- /dev/null +++ b/test/goto.js @@ -0,0 +1,195 @@ +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 new file mode 100644 index 0000000..fc9eb34 --- /dev/null +++ b/test/helpers/lifecycle_logger.js @@ -0,0 +1,27 @@ + +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/index.html b/test/index.html deleted file mode 100755 index 2cc8628..0000000 --- a/test/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - Finite State Machine Tests - - - - - - - - - - -

QUnit Test Suite

-

-
-

-
    -
    test markup
    - - diff --git a/test/introspection.js b/test/introspection.js new file mode 100644 index 0000000..be9e176 --- /dev/null +++ b/test/introspection.js @@ -0,0 +1,204 @@ +import test from 'ava'; +import StateMachine from '../src/app'; + +//----------------------------------------------------------------------------- + +test('is', 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') + + 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() + + 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 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('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' ]); + +}); + +//----------------------------------------------------------------------------- + +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' + ]); +}) + +//----------------------------------------------------------------------------- + +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(), []) + +}); + +//----------------------------------------------------------------------------- diff --git a/test/issues.js b/test/issues.js new file mode 100644 index 0000000..ff7741d --- /dev/null +++ b/test/issues.js @@ -0,0 +1,103 @@ +import test from 'ava' +import StateMachine from '../src/app' +import LifecycleLogger from './helpers/lifecycle_logger' + +//------------------------------------------------------------------------------------------------- + +test('github issue #12 - transition return values', 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) {}); } + } + }); + + 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'); + +}) + +//------------------------------------------------------------------------------------------------- + +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 */ } + ] + }); + + t.is(fsm.state, 'none') + + fsm.step(); t.is(fsm.state, 'none') + fsm.step(); t.is(fsm.state, 'none') + +}) + +//------------------------------------------------------------------------------------------------- diff --git a/test/lifecycle.js b/test/lifecycle.js new file mode 100644 index 0000000..347ebf4 --- /dev/null +++ b/test/lifecycle.js @@ -0,0 +1,932 @@ +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 new file mode 100644 index 0000000..f20ab6f --- /dev/null +++ b/test/observers.js @@ -0,0 +1,151 @@ +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 new file mode 100644 index 0000000..00f73ef --- /dev/null +++ b/test/plugin/history.js @@ -0,0 +1,493 @@ +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 new file mode 100644 index 0000000..a2b9388 --- /dev/null +++ b/test/plugin/visualize.js @@ -0,0 +1,443 @@ +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 new file mode 100644 index 0000000..486125b --- /dev/null +++ b/test/plugins.js @@ -0,0 +1,151 @@ +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/qunit/qunit.css b/test/qunit/qunit.css deleted file mode 100644 index 93026e3..0000000 --- a/test/qunit/qunit.css +++ /dev/null @@ -1,237 +0,0 @@ -/*! - * QUnit 1.14.0 - * http://qunitjs.com/ - * - * Copyright 2013 jQuery Foundation and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2014-01-31T16:40Z - */ - -/** Font Family and Sizes */ - -#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { - font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; -} - -#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } -#qunit-tests { font-size: smaller; } - - -/** Resets */ - -#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { - margin: 0; - padding: 0; -} - - -/** Header */ - -#qunit-header { - padding: 0.5em 0 0.5em 1em; - - color: #8699A4; - background-color: #0D3349; - - font-size: 1.5em; - line-height: 1em; - font-weight: 400; - - border-radius: 5px 5px 0 0; -} - -#qunit-header a { - text-decoration: none; - color: #C2CCD1; -} - -#qunit-header a:hover, -#qunit-header a:focus { - color: #FFF; -} - -#qunit-testrunner-toolbar label { - display: inline-block; - padding: 0 0.5em 0 0.1em; -} - -#qunit-banner { - height: 5px; -} - -#qunit-testrunner-toolbar { - padding: 0.5em 0 0.5em 2em; - color: #5E740B; - background-color: #EEE; - overflow: hidden; -} - -#qunit-userAgent { - padding: 0.5em 0 0.5em 2.5em; - background-color: #2B81AF; - color: #FFF; - text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; -} - -#qunit-modulefilter-container { - float: right; -} - -/** Tests: Pass/Fail */ - -#qunit-tests { - list-style-position: inside; -} - -#qunit-tests li { - padding: 0.4em 0.5em 0.4em 2.5em; - border-bottom: 1px solid #FFF; - list-style-position: inside; -} - -#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { - display: none; -} - -#qunit-tests li strong { - cursor: pointer; -} - -#qunit-tests li a { - padding: 0.5em; - color: #C2CCD1; - text-decoration: none; -} -#qunit-tests li a:hover, -#qunit-tests li a:focus { - color: #000; -} - -#qunit-tests li .runtime { - float: right; - font-size: smaller; -} - -.qunit-assert-list { - margin-top: 0.5em; - padding: 0.5em; - - background-color: #FFF; - - border-radius: 5px; -} - -.qunit-collapsed { - display: none; -} - -#qunit-tests table { - border-collapse: collapse; - margin-top: 0.2em; -} - -#qunit-tests th { - text-align: right; - vertical-align: top; - padding: 0 0.5em 0 0; -} - -#qunit-tests td { - vertical-align: top; -} - -#qunit-tests pre { - margin: 0; - white-space: pre-wrap; - word-wrap: break-word; -} - -#qunit-tests del { - background-color: #E0F2BE; - color: #374E0C; - text-decoration: none; -} - -#qunit-tests ins { - background-color: #FFCACA; - color: #500; - text-decoration: none; -} - -/*** Test Counts */ - -#qunit-tests b.counts { color: #000; } -#qunit-tests b.passed { color: #5E740B; } -#qunit-tests b.failed { color: #710909; } - -#qunit-tests li li { - padding: 5px; - background-color: #FFF; - border-bottom: none; - list-style-position: inside; -} - -/*** Passing Styles */ - -#qunit-tests li li.pass { - color: #3C510C; - background-color: #FFF; - border-left: 10px solid #C6E746; -} - -#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } -#qunit-tests .pass .test-name { color: #366097; } - -#qunit-tests .pass .test-actual, -#qunit-tests .pass .test-expected { color: #999; } - -#qunit-banner.qunit-pass { background-color: #C6E746; } - -/*** Failing Styles */ - -#qunit-tests li li.fail { - color: #710909; - background-color: #FFF; - border-left: 10px solid #EE5757; - white-space: pre; -} - -#qunit-tests > li:last-child { - border-radius: 0 0 5px 5px; -} - -#qunit-tests .fail { color: #000; background-color: #EE5757; } -#qunit-tests .fail .test-name, -#qunit-tests .fail .module-name { color: #000; } - -#qunit-tests .fail .test-actual { color: #EE5757; } -#qunit-tests .fail .test-expected { color: #008000; } - -#qunit-banner.qunit-fail { background-color: #EE5757; } - - -/** Result */ - -#qunit-testresult { - padding: 0.5em 0.5em 0.5em 2.5em; - - color: #2B81AF; - background-color: #D2E0E6; - - border-bottom: 1px solid #FFF; -} -#qunit-testresult .module-name { - font-weight: 700; -} - -/** Fixture */ - -#qunit-fixture { - position: absolute; - top: -10000px; - left: -10000px; - width: 1000px; - height: 1000px; -} diff --git a/test/qunit/qunit.js b/test/qunit/qunit.js deleted file mode 100644 index 0e279fd..0000000 --- a/test/qunit/qunit.js +++ /dev/null @@ -1,2288 +0,0 @@ -/*! - * QUnit 1.14.0 - * http://qunitjs.com/ - * - * Copyright 2013 jQuery Foundation and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2014-01-31T16:40Z - */ - -(function( window ) { - -var QUnit, - assert, - config, - onErrorFnPrev, - testId = 0, - fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""), - toString = Object.prototype.toString, - hasOwn = Object.prototype.hasOwnProperty, - // Keep a local reference to Date (GH-283) - Date = window.Date, - setTimeout = window.setTimeout, - clearTimeout = window.clearTimeout, - defined = { - document: typeof window.document !== "undefined", - setTimeout: typeof window.setTimeout !== "undefined", - sessionStorage: (function() { - var x = "qunit-test-string"; - try { - sessionStorage.setItem( x, x ); - sessionStorage.removeItem( x ); - return true; - } catch( e ) { - return false; - } - }()) - }, - /** - * Provides a normalized error string, correcting an issue - * with IE 7 (and prior) where Error.prototype.toString is - * not properly implemented - * - * Based on http://es5.github.com/#x15.11.4.4 - * - * @param {String|Error} error - * @return {String} error message - */ - errorString = function( error ) { - var name, message, - errorString = error.toString(); - if ( errorString.substring( 0, 7 ) === "[object" ) { - name = error.name ? error.name.toString() : "Error"; - message = error.message ? error.message.toString() : ""; - if ( name && message ) { - return name + ": " + message; - } else if ( name ) { - return name; - } else if ( message ) { - return message; - } else { - return "Error"; - } - } else { - return errorString; - } - }, - /** - * Makes a clone of an object using only Array or Object as base, - * and copies over the own enumerable properties. - * - * @param {Object} obj - * @return {Object} New object with only the own properties (recursively). - */ - objectValues = function( obj ) { - // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392. - /*jshint newcap: false */ - var key, val, - vals = QUnit.is( "array", obj ) ? [] : {}; - for ( key in obj ) { - if ( hasOwn.call( obj, key ) ) { - val = obj[key]; - vals[key] = val === Object(val) ? objectValues(val) : val; - } - } - return vals; - }; - - -// Root QUnit object. -// `QUnit` initialized at top of scope -QUnit = { - - // call on start of module test to prepend name to all tests - module: function( name, testEnvironment ) { - config.currentModule = name; - config.currentModuleTestEnvironment = testEnvironment; - config.modules[name] = true; - }, - - asyncTest: function( testName, expected, callback ) { - if ( arguments.length === 2 ) { - callback = expected; - expected = null; - } - - QUnit.test( testName, expected, callback, true ); - }, - - test: function( testName, expected, callback, async ) { - var test, - nameHtml = "" + escapeText( testName ) + ""; - - if ( arguments.length === 2 ) { - callback = expected; - expected = null; - } - - if ( config.currentModule ) { - nameHtml = "" + escapeText( config.currentModule ) + ": " + nameHtml; - } - - test = new Test({ - nameHtml: nameHtml, - testName: testName, - expected: expected, - async: async, - callback: callback, - module: config.currentModule, - moduleTestEnvironment: config.currentModuleTestEnvironment, - stack: sourceFromStacktrace( 2 ) - }); - - if ( !validTest( test ) ) { - return; - } - - test.queue(); - }, - - // Specify the number of expected assertions to guarantee that failed test (no assertions are run at all) don't slip through. - expect: function( asserts ) { - if (arguments.length === 1) { - config.current.expected = asserts; - } else { - return config.current.expected; - } - }, - - start: function( count ) { - // QUnit hasn't been initialized yet. - // Note: RequireJS (et al) may delay onLoad - if ( config.semaphore === undefined ) { - QUnit.begin(function() { - // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first - setTimeout(function() { - QUnit.start( count ); - }); - }); - return; - } - - config.semaphore -= count || 1; - // don't start until equal number of stop-calls - if ( config.semaphore > 0 ) { - return; - } - // ignore if start is called more often then stop - if ( config.semaphore < 0 ) { - config.semaphore = 0; - QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) ); - return; - } - // A slight delay, to avoid any current callbacks - if ( defined.setTimeout ) { - setTimeout(function() { - if ( config.semaphore > 0 ) { - return; - } - if ( config.timeout ) { - clearTimeout( config.timeout ); - } - - config.blocking = false; - process( true ); - }, 13); - } else { - config.blocking = false; - process( true ); - } - }, - - stop: function( count ) { - config.semaphore += count || 1; - config.blocking = true; - - if ( config.testTimeout && defined.setTimeout ) { - clearTimeout( config.timeout ); - config.timeout = setTimeout(function() { - QUnit.ok( false, "Test timed out" ); - config.semaphore = 1; - QUnit.start(); - }, config.testTimeout ); - } - } -}; - -// We use the prototype to distinguish between properties that should -// be exposed as globals (and in exports) and those that shouldn't -(function() { - function F() {} - F.prototype = QUnit; - QUnit = new F(); - // Make F QUnit's constructor so that we can add to the prototype later - QUnit.constructor = F; -}()); - -/** - * Config object: Maintain internal state - * Later exposed as QUnit.config - * `config` initialized at top of scope - */ -config = { - // The queue of tests to run - queue: [], - - // block until document ready - blocking: true, - - // when enabled, show only failing tests - // gets persisted through sessionStorage and can be changed in UI via checkbox - hidepassed: false, - - // by default, run previously failed tests first - // very useful in combination with "Hide passed tests" checked - reorder: true, - - // by default, modify document.title when suite is done - altertitle: true, - - // by default, scroll to top of the page when suite is done - scrolltop: true, - - // when enabled, all tests must call expect() - requireExpects: false, - - // add checkboxes that are persisted in the query-string - // when enabled, the id is set to `true` as a `QUnit.config` property - urlConfig: [ - { - id: "noglobals", - label: "Check for Globals", - tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." - }, - { - id: "notrycatch", - label: "No try-catch", - tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." - } - ], - - // Set of all modules. - modules: {}, - - // logging callback queues - begin: [], - done: [], - log: [], - testStart: [], - testDone: [], - moduleStart: [], - moduleDone: [] -}; - -// Initialize more QUnit.config and QUnit.urlParams -(function() { - var i, current, - location = window.location || { search: "", protocol: "file:" }, - params = location.search.slice( 1 ).split( "&" ), - length = params.length, - urlParams = {}; - - if ( params[ 0 ] ) { - for ( i = 0; i < length; i++ ) { - current = params[ i ].split( "=" ); - current[ 0 ] = decodeURIComponent( current[ 0 ] ); - - // allow just a key to turn on a flag, e.g., test.html?noglobals - current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; - if ( urlParams[ current[ 0 ] ] ) { - urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] ); - } else { - urlParams[ current[ 0 ] ] = current[ 1 ]; - } - } - } - - QUnit.urlParams = urlParams; - - // String search anywhere in moduleName+testName - config.filter = urlParams.filter; - - // Exact match of the module name - config.module = urlParams.module; - - config.testNumber = []; - if ( urlParams.testNumber ) { - - // Ensure that urlParams.testNumber is an array - urlParams.testNumber = [].concat( urlParams.testNumber ); - for ( i = 0; i < urlParams.testNumber.length; i++ ) { - current = urlParams.testNumber[ i ]; - config.testNumber.push( parseInt( current, 10 ) ); - } - } - - // Figure out if we're running the tests from a server or not - QUnit.isLocal = location.protocol === "file:"; -}()); - -extend( QUnit, { - - config: config, - - // Initialize the configuration options - init: function() { - extend( config, { - stats: { all: 0, bad: 0 }, - moduleStats: { all: 0, bad: 0 }, - started: +new Date(), - updateRate: 1000, - blocking: false, - autostart: true, - autorun: false, - filter: "", - queue: [], - semaphore: 1 - }); - - var tests, banner, result, - qunit = id( "qunit" ); - - if ( qunit ) { - qunit.innerHTML = - "

    " + escapeText( document.title ) + "

    " + - "

    " + - "
    " + - "

    " + - "
      "; - } - - tests = id( "qunit-tests" ); - banner = id( "qunit-banner" ); - result = id( "qunit-testresult" ); - - if ( tests ) { - tests.innerHTML = ""; - } - - if ( banner ) { - banner.className = ""; - } - - if ( result ) { - result.parentNode.removeChild( result ); - } - - if ( tests ) { - result = document.createElement( "p" ); - result.id = "qunit-testresult"; - result.className = "result"; - tests.parentNode.insertBefore( result, tests ); - result.innerHTML = "Running...
       "; - } - }, - - // Resets the test setup. Useful for tests that modify the DOM. - /* - DEPRECATED: Use multiple tests instead of resetting inside a test. - Use testStart or testDone for custom cleanup. - This method will throw an error in 2.0, and will be removed in 2.1 - */ - reset: function() { - var fixture = id( "qunit-fixture" ); - if ( fixture ) { - fixture.innerHTML = config.fixture; - } - }, - - // Safe object type checking - is: function( type, obj ) { - return QUnit.objectType( obj ) === type; - }, - - objectType: function( obj ) { - if ( typeof obj === "undefined" ) { - return "undefined"; - } - - // Consider: typeof null === object - if ( obj === null ) { - return "null"; - } - - var match = toString.call( obj ).match(/^\[object\s(.*)\]$/), - type = match && match[1] || ""; - - switch ( type ) { - case "Number": - if ( isNaN(obj) ) { - return "nan"; - } - return "number"; - case "String": - case "Boolean": - case "Array": - case "Date": - case "RegExp": - case "Function": - return type.toLowerCase(); - } - if ( typeof obj === "object" ) { - return "object"; - } - return undefined; - }, - - push: function( result, actual, expected, message ) { - if ( !config.current ) { - throw new Error( "assertion outside test context, was " + sourceFromStacktrace() ); - } - - var output, source, - details = { - module: config.current.module, - name: config.current.testName, - result: result, - message: message, - actual: actual, - expected: expected - }; - - message = escapeText( message ) || ( result ? "okay" : "failed" ); - message = "" + message + ""; - output = message; - - if ( !result ) { - expected = escapeText( QUnit.jsDump.parse(expected) ); - actual = escapeText( QUnit.jsDump.parse(actual) ); - output += ""; - - if ( actual !== expected ) { - output += ""; - output += ""; - } - - source = sourceFromStacktrace(); - - if ( source ) { - details.source = source; - output += ""; - } - - output += "
      Expected:
      " + expected + "
      Result:
      " + actual + "
      Diff:
      " + QUnit.diff( expected, actual ) + "
      Source:
      " + escapeText( source ) + "
      "; - } - - runLoggingCallbacks( "log", QUnit, details ); - - config.current.assertions.push({ - result: !!result, - message: output - }); - }, - - pushFailure: function( message, source, actual ) { - if ( !config.current ) { - throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) ); - } - - var output, - details = { - module: config.current.module, - name: config.current.testName, - result: false, - message: message - }; - - message = escapeText( message ) || "error"; - message = "" + message + ""; - output = message; - - output += ""; - - if ( actual ) { - output += ""; - } - - if ( source ) { - details.source = source; - output += ""; - } - - output += "
      Result:
      " + escapeText( actual ) + "
      Source:
      " + escapeText( source ) + "
      "; - - runLoggingCallbacks( "log", QUnit, details ); - - config.current.assertions.push({ - result: false, - message: output - }); - }, - - url: function( params ) { - params = extend( extend( {}, QUnit.urlParams ), params ); - var key, - querystring = "?"; - - for ( key in params ) { - if ( hasOwn.call( params, key ) ) { - querystring += encodeURIComponent( key ) + "=" + - encodeURIComponent( params[ key ] ) + "&"; - } - } - return window.location.protocol + "//" + window.location.host + - window.location.pathname + querystring.slice( 0, -1 ); - }, - - extend: extend, - id: id, - addEvent: addEvent, - addClass: addClass, - hasClass: hasClass, - removeClass: removeClass - // load, equiv, jsDump, diff: Attached later -}); - -/** - * @deprecated: Created for backwards compatibility with test runner that set the hook function - * into QUnit.{hook}, instead of invoking it and passing the hook function. - * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here. - * Doing this allows us to tell if the following methods have been overwritten on the actual - * QUnit object. - */ -extend( QUnit.constructor.prototype, { - - // Logging callbacks; all receive a single argument with the listed properties - // run test/logs.html for any related changes - begin: registerLoggingCallback( "begin" ), - - // done: { failed, passed, total, runtime } - done: registerLoggingCallback( "done" ), - - // log: { result, actual, expected, message } - log: registerLoggingCallback( "log" ), - - // testStart: { name } - testStart: registerLoggingCallback( "testStart" ), - - // testDone: { name, failed, passed, total, runtime } - testDone: registerLoggingCallback( "testDone" ), - - // moduleStart: { name } - moduleStart: registerLoggingCallback( "moduleStart" ), - - // moduleDone: { name, failed, passed, total } - moduleDone: registerLoggingCallback( "moduleDone" ) -}); - -if ( !defined.document || document.readyState === "complete" ) { - config.autorun = true; -} - -QUnit.load = function() { - runLoggingCallbacks( "begin", QUnit, {} ); - - // Initialize the config, saving the execution queue - var banner, filter, i, j, label, len, main, ol, toolbar, val, selection, - urlConfigContainer, moduleFilter, userAgent, - numModules = 0, - moduleNames = [], - moduleFilterHtml = "", - urlConfigHtml = "", - oldconfig = extend( {}, config ); - - QUnit.init(); - extend(config, oldconfig); - - config.blocking = false; - - len = config.urlConfig.length; - - for ( i = 0; i < len; i++ ) { - val = config.urlConfig[i]; - if ( typeof val === "string" ) { - val = { - id: val, - label: val - }; - } - config[ val.id ] = QUnit.urlParams[ val.id ]; - if ( !val.value || typeof val.value === "string" ) { - urlConfigHtml += ""; - } else { - urlConfigHtml += ""; - } - } - for ( i in config.modules ) { - if ( config.modules.hasOwnProperty( i ) ) { - moduleNames.push(i); - } - } - numModules = moduleNames.length; - moduleNames.sort( function( a, b ) { - return a.localeCompare( b ); - }); - moduleFilterHtml += ""; - - // `userAgent` initialized at top of scope - userAgent = id( "qunit-userAgent" ); - if ( userAgent ) { - userAgent.innerHTML = navigator.userAgent; - } - - // `banner` initialized at top of scope - banner = id( "qunit-header" ); - if ( banner ) { - banner.innerHTML = "" + banner.innerHTML + " "; - } - - // `toolbar` initialized at top of scope - toolbar = id( "qunit-testrunner-toolbar" ); - if ( toolbar ) { - // `filter` initialized at top of scope - filter = document.createElement( "input" ); - filter.type = "checkbox"; - filter.id = "qunit-filter-pass"; - - addEvent( filter, "click", function() { - var tmp, - ol = id( "qunit-tests" ); - - if ( filter.checked ) { - ol.className = ol.className + " hidepass"; - } else { - tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; - ol.className = tmp.replace( / hidepass /, " " ); - } - if ( defined.sessionStorage ) { - if (filter.checked) { - sessionStorage.setItem( "qunit-filter-passed-tests", "true" ); - } else { - sessionStorage.removeItem( "qunit-filter-passed-tests" ); - } - } - }); - - if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) { - filter.checked = true; - // `ol` initialized at top of scope - ol = id( "qunit-tests" ); - ol.className = ol.className + " hidepass"; - } - toolbar.appendChild( filter ); - - // `label` initialized at top of scope - label = document.createElement( "label" ); - label.setAttribute( "for", "qunit-filter-pass" ); - label.setAttribute( "title", "Only show tests and assertions that fail. Stored in sessionStorage." ); - label.innerHTML = "Hide passed tests"; - toolbar.appendChild( label ); - - urlConfigContainer = document.createElement("span"); - urlConfigContainer.innerHTML = urlConfigHtml; - // For oldIE support: - // * Add handlers to the individual elements instead of the container - // * Use "click" instead of "change" for checkboxes - // * Fallback from event.target to event.srcElement - addEvents( urlConfigContainer.getElementsByTagName("input"), "click", function( event ) { - var params = {}, - target = event.target || event.srcElement; - params[ target.name ] = target.checked ? - target.defaultValue || true : - undefined; - window.location = QUnit.url( params ); - }); - addEvents( urlConfigContainer.getElementsByTagName("select"), "change", function( event ) { - var params = {}, - target = event.target || event.srcElement; - params[ target.name ] = target.options[ target.selectedIndex ].value || undefined; - window.location = QUnit.url( params ); - }); - toolbar.appendChild( urlConfigContainer ); - - if (numModules > 1) { - moduleFilter = document.createElement( "span" ); - moduleFilter.setAttribute( "id", "qunit-modulefilter-container" ); - moduleFilter.innerHTML = moduleFilterHtml; - addEvent( moduleFilter.lastChild, "change", function() { - var selectBox = moduleFilter.getElementsByTagName("select")[0], - selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value); - - window.location = QUnit.url({ - module: ( selectedModule === "" ) ? undefined : selectedModule, - // Remove any existing filters - filter: undefined, - testNumber: undefined - }); - }); - toolbar.appendChild(moduleFilter); - } - } - - // `main` initialized at top of scope - main = id( "qunit-fixture" ); - if ( main ) { - config.fixture = main.innerHTML; - } - - if ( config.autostart ) { - QUnit.start(); - } -}; - -if ( defined.document ) { - addEvent( window, "load", QUnit.load ); -} - -// `onErrorFnPrev` initialized at top of scope -// Preserve other handlers -onErrorFnPrev = window.onerror; - -// Cover uncaught exceptions -// Returning true will suppress the default browser handler, -// returning false will let it run. -window.onerror = function ( error, filePath, linerNr ) { - var ret = false; - if ( onErrorFnPrev ) { - ret = onErrorFnPrev( error, filePath, linerNr ); - } - - // Treat return value as window.onerror itself does, - // Only do our handling if not suppressed. - if ( ret !== true ) { - if ( QUnit.config.current ) { - if ( QUnit.config.current.ignoreGlobalErrors ) { - return true; - } - QUnit.pushFailure( error, filePath + ":" + linerNr ); - } else { - QUnit.test( "global failure", extend( function() { - QUnit.pushFailure( error, filePath + ":" + linerNr ); - }, { validTest: validTest } ) ); - } - return false; - } - - return ret; -}; - -function done() { - config.autorun = true; - - // Log the last module results - if ( config.previousModule ) { - runLoggingCallbacks( "moduleDone", QUnit, { - name: config.previousModule, - failed: config.moduleStats.bad, - passed: config.moduleStats.all - config.moduleStats.bad, - total: config.moduleStats.all - }); - } - delete config.previousModule; - - var i, key, - banner = id( "qunit-banner" ), - tests = id( "qunit-tests" ), - runtime = +new Date() - config.started, - passed = config.stats.all - config.stats.bad, - html = [ - "Tests completed in ", - runtime, - " milliseconds.
      ", - "", - passed, - " assertions of ", - config.stats.all, - " passed, ", - config.stats.bad, - " failed." - ].join( "" ); - - if ( banner ) { - banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" ); - } - - if ( tests ) { - id( "qunit-testresult" ).innerHTML = html; - } - - if ( config.altertitle && defined.document && document.title ) { - // show ✖ for good, ✔ for bad suite result in title - // use escape sequences in case file gets loaded with non-utf-8-charset - document.title = [ - ( config.stats.bad ? "\u2716" : "\u2714" ), - document.title.replace( /^[\u2714\u2716] /i, "" ) - ].join( " " ); - } - - // clear own sessionStorage items if all tests passed - if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { - // `key` & `i` initialized at top of scope - for ( i = 0; i < sessionStorage.length; i++ ) { - key = sessionStorage.key( i++ ); - if ( key.indexOf( "qunit-test-" ) === 0 ) { - sessionStorage.removeItem( key ); - } - } - } - - // scroll back to top to show results - if ( config.scrolltop && window.scrollTo ) { - window.scrollTo(0, 0); - } - - runLoggingCallbacks( "done", QUnit, { - failed: config.stats.bad, - passed: passed, - total: config.stats.all, - runtime: runtime - }); -} - -/** @return Boolean: true if this test should be ran */ -function validTest( test ) { - var include, - filter = config.filter && config.filter.toLowerCase(), - module = config.module && config.module.toLowerCase(), - fullName = ( test.module + ": " + test.testName ).toLowerCase(); - - // Internally-generated tests are always valid - if ( test.callback && test.callback.validTest === validTest ) { - delete test.callback.validTest; - return true; - } - - if ( config.testNumber.length > 0 ) { - if ( inArray( test.testNumber, config.testNumber ) < 0 ) { - return false; - } - } - - if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { - return false; - } - - if ( !filter ) { - return true; - } - - include = filter.charAt( 0 ) !== "!"; - if ( !include ) { - filter = filter.slice( 1 ); - } - - // If the filter matches, we need to honour include - if ( fullName.indexOf( filter ) !== -1 ) { - return include; - } - - // Otherwise, do the opposite - return !include; -} - -// so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions) -// Later Safari and IE10 are supposed to support error.stack as well -// See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack -function extractStacktrace( e, offset ) { - offset = offset === undefined ? 3 : offset; - - var stack, include, i; - - if ( e.stacktrace ) { - // Opera - return e.stacktrace.split( "\n" )[ offset + 3 ]; - } else if ( e.stack ) { - // Firefox, Chrome - stack = e.stack.split( "\n" ); - if (/^error$/i.test( stack[0] ) ) { - stack.shift(); - } - if ( fileName ) { - include = []; - for ( i = offset; i < stack.length; i++ ) { - if ( stack[ i ].indexOf( fileName ) !== -1 ) { - break; - } - include.push( stack[ i ] ); - } - if ( include.length ) { - return include.join( "\n" ); - } - } - return stack[ offset ]; - } else if ( e.sourceURL ) { - // Safari, PhantomJS - // hopefully one day Safari provides actual stacktraces - // exclude useless self-reference for generated Error objects - if ( /qunit.js$/.test( e.sourceURL ) ) { - return; - } - // for actual exceptions, this is useful - return e.sourceURL + ":" + e.line; - } -} -function sourceFromStacktrace( offset ) { - try { - throw new Error(); - } catch ( e ) { - return extractStacktrace( e, offset ); - } -} - -/** - * Escape text for attribute or text content. - */ -function escapeText( s ) { - if ( !s ) { - return ""; - } - s = s + ""; - // Both single quotes and double quotes (for attributes) - return s.replace( /['"<>&]/g, function( s ) { - switch( s ) { - case "'": - return "'"; - case "\"": - return """; - case "<": - return "<"; - case ">": - return ">"; - case "&": - return "&"; - } - }); -} - -function synchronize( callback, last ) { - config.queue.push( callback ); - - if ( config.autorun && !config.blocking ) { - process( last ); - } -} - -function process( last ) { - function next() { - process( last ); - } - var start = new Date().getTime(); - config.depth = config.depth ? config.depth + 1 : 1; - - while ( config.queue.length && !config.blocking ) { - if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { - config.queue.shift()(); - } else { - setTimeout( next, 13 ); - break; - } - } - config.depth--; - if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { - done(); - } -} - -function saveGlobal() { - config.pollution = []; - - if ( config.noglobals ) { - for ( var key in window ) { - if ( hasOwn.call( window, key ) ) { - // in Opera sometimes DOM element ids show up here, ignore them - if ( /^qunit-test-output/.test( key ) ) { - continue; - } - config.pollution.push( key ); - } - } - } -} - -function checkPollution() { - var newGlobals, - deletedGlobals, - old = config.pollution; - - saveGlobal(); - - newGlobals = diff( config.pollution, old ); - if ( newGlobals.length > 0 ) { - QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); - } - - deletedGlobals = diff( old, config.pollution ); - if ( deletedGlobals.length > 0 ) { - QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); - } -} - -// returns a new Array with the elements that are in a but not in b -function diff( a, b ) { - var i, j, - result = a.slice(); - - for ( i = 0; i < result.length; i++ ) { - for ( j = 0; j < b.length; j++ ) { - if ( result[i] === b[j] ) { - result.splice( i, 1 ); - i--; - break; - } - } - } - return result; -} - -function extend( a, b ) { - for ( var prop in b ) { - if ( hasOwn.call( b, prop ) ) { - // Avoid "Member not found" error in IE8 caused by messing with window.constructor - if ( !( prop === "constructor" && a === window ) ) { - if ( b[ prop ] === undefined ) { - delete a[ prop ]; - } else { - a[ prop ] = b[ prop ]; - } - } - } - } - - return a; -} - -/** - * @param {HTMLElement} elem - * @param {string} type - * @param {Function} fn - */ -function addEvent( elem, type, fn ) { - if ( elem.addEventListener ) { - - // Standards-based browsers - elem.addEventListener( type, fn, false ); - } else if ( elem.attachEvent ) { - - // support: IE <9 - elem.attachEvent( "on" + type, fn ); - } else { - - // Caller must ensure support for event listeners is present - throw new Error( "addEvent() was called in a context without event listener support" ); - } -} - -/** - * @param {Array|NodeList} elems - * @param {string} type - * @param {Function} fn - */ -function addEvents( elems, type, fn ) { - var i = elems.length; - while ( i-- ) { - addEvent( elems[i], type, fn ); - } -} - -function hasClass( elem, name ) { - return (" " + elem.className + " ").indexOf(" " + name + " ") > -1; -} - -function addClass( elem, name ) { - if ( !hasClass( elem, name ) ) { - elem.className += (elem.className ? " " : "") + name; - } -} - -function removeClass( elem, name ) { - var set = " " + elem.className + " "; - // Class name may appear multiple times - while ( set.indexOf(" " + name + " ") > -1 ) { - set = set.replace(" " + name + " " , " "); - } - // If possible, trim it for prettiness, but not necessarily - elem.className = typeof set.trim === "function" ? set.trim() : set.replace(/^\s+|\s+$/g, ""); -} - -function id( name ) { - return defined.document && document.getElementById && document.getElementById( name ); -} - -function registerLoggingCallback( key ) { - return function( callback ) { - config[key].push( callback ); - }; -} - -// Supports deprecated method of completely overwriting logging callbacks -function runLoggingCallbacks( key, scope, args ) { - var i, callbacks; - if ( QUnit.hasOwnProperty( key ) ) { - QUnit[ key ].call(scope, args ); - } else { - callbacks = config[ key ]; - for ( i = 0; i < callbacks.length; i++ ) { - callbacks[ i ].call( scope, args ); - } - } -} - -// from jquery.js -function inArray( elem, array ) { - if ( array.indexOf ) { - return array.indexOf( elem ); - } - - for ( var i = 0, length = array.length; i < length; i++ ) { - if ( array[ i ] === elem ) { - return i; - } - } - - return -1; -} - -function Test( settings ) { - extend( this, settings ); - this.assertions = []; - this.testNumber = ++Test.count; -} - -Test.count = 0; - -Test.prototype = { - init: function() { - var a, b, li, - tests = id( "qunit-tests" ); - - if ( tests ) { - b = document.createElement( "strong" ); - b.innerHTML = this.nameHtml; - - // `a` initialized at top of scope - a = document.createElement( "a" ); - a.innerHTML = "Rerun"; - a.href = QUnit.url({ testNumber: this.testNumber }); - - li = document.createElement( "li" ); - li.appendChild( b ); - li.appendChild( a ); - li.className = "running"; - li.id = this.id = "qunit-test-output" + testId++; - - tests.appendChild( li ); - } - }, - setup: function() { - if ( - // Emit moduleStart when we're switching from one module to another - this.module !== config.previousModule || - // They could be equal (both undefined) but if the previousModule property doesn't - // yet exist it means this is the first test in a suite that isn't wrapped in a - // module, in which case we'll just emit a moduleStart event for 'undefined'. - // Without this, reporters can get testStart before moduleStart which is a problem. - !hasOwn.call( config, "previousModule" ) - ) { - if ( hasOwn.call( config, "previousModule" ) ) { - runLoggingCallbacks( "moduleDone", QUnit, { - name: config.previousModule, - failed: config.moduleStats.bad, - passed: config.moduleStats.all - config.moduleStats.bad, - total: config.moduleStats.all - }); - } - config.previousModule = this.module; - config.moduleStats = { all: 0, bad: 0 }; - runLoggingCallbacks( "moduleStart", QUnit, { - name: this.module - }); - } - - config.current = this; - - this.testEnvironment = extend({ - setup: function() {}, - teardown: function() {} - }, this.moduleTestEnvironment ); - - this.started = +new Date(); - runLoggingCallbacks( "testStart", QUnit, { - name: this.testName, - module: this.module - }); - - /*jshint camelcase:false */ - - - /** - * Expose the current test environment. - * - * @deprecated since 1.12.0: Use QUnit.config.current.testEnvironment instead. - */ - QUnit.current_testEnvironment = this.testEnvironment; - - /*jshint camelcase:true */ - - if ( !config.pollution ) { - saveGlobal(); - } - if ( config.notrycatch ) { - this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert ); - return; - } - try { - this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert ); - } catch( e ) { - QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); - } - }, - run: function() { - config.current = this; - - var running = id( "qunit-testresult" ); - - if ( running ) { - running.innerHTML = "Running:
      " + this.nameHtml; - } - - if ( this.async ) { - QUnit.stop(); - } - - this.callbackStarted = +new Date(); - - if ( config.notrycatch ) { - this.callback.call( this.testEnvironment, QUnit.assert ); - this.callbackRuntime = +new Date() - this.callbackStarted; - return; - } - - try { - this.callback.call( this.testEnvironment, QUnit.assert ); - this.callbackRuntime = +new Date() - this.callbackStarted; - } catch( e ) { - this.callbackRuntime = +new Date() - this.callbackStarted; - - QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); - // else next test will carry the responsibility - saveGlobal(); - - // Restart the tests if they're blocking - if ( config.blocking ) { - QUnit.start(); - } - } - }, - teardown: function() { - config.current = this; - if ( config.notrycatch ) { - if ( typeof this.callbackRuntime === "undefined" ) { - this.callbackRuntime = +new Date() - this.callbackStarted; - } - this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert ); - return; - } else { - try { - this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert ); - } catch( e ) { - QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); - } - } - checkPollution(); - }, - finish: function() { - config.current = this; - if ( config.requireExpects && this.expected === null ) { - QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); - } else if ( this.expected !== null && this.expected !== this.assertions.length ) { - QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); - } else if ( this.expected === null && !this.assertions.length ) { - QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); - } - - var i, assertion, a, b, time, li, ol, - test = this, - good = 0, - bad = 0, - tests = id( "qunit-tests" ); - - this.runtime = +new Date() - this.started; - config.stats.all += this.assertions.length; - config.moduleStats.all += this.assertions.length; - - if ( tests ) { - ol = document.createElement( "ol" ); - ol.className = "qunit-assert-list"; - - for ( i = 0; i < this.assertions.length; i++ ) { - assertion = this.assertions[i]; - - li = document.createElement( "li" ); - li.className = assertion.result ? "pass" : "fail"; - li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" ); - ol.appendChild( li ); - - if ( assertion.result ) { - good++; - } else { - bad++; - config.stats.bad++; - config.moduleStats.bad++; - } - } - - // store result when possible - if ( QUnit.config.reorder && defined.sessionStorage ) { - if ( bad ) { - sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad ); - } else { - sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName ); - } - } - - if ( bad === 0 ) { - addClass( ol, "qunit-collapsed" ); - } - - // `b` initialized at top of scope - b = document.createElement( "strong" ); - b.innerHTML = this.nameHtml + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; - - addEvent(b, "click", function() { - var next = b.parentNode.lastChild, - collapsed = hasClass( next, "qunit-collapsed" ); - ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" ); - }); - - addEvent(b, "dblclick", function( e ) { - var target = e && e.target ? e.target : window.event.srcElement; - if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) { - target = target.parentNode; - } - if ( window.location && target.nodeName.toLowerCase() === "strong" ) { - window.location = QUnit.url({ testNumber: test.testNumber }); - } - }); - - // `time` initialized at top of scope - time = document.createElement( "span" ); - time.className = "runtime"; - time.innerHTML = this.runtime + " ms"; - - // `li` initialized at top of scope - li = id( this.id ); - li.className = bad ? "fail" : "pass"; - li.removeChild( li.firstChild ); - a = li.firstChild; - li.appendChild( b ); - li.appendChild( a ); - li.appendChild( time ); - li.appendChild( ol ); - - } else { - for ( i = 0; i < this.assertions.length; i++ ) { - if ( !this.assertions[i].result ) { - bad++; - config.stats.bad++; - config.moduleStats.bad++; - } - } - } - - runLoggingCallbacks( "testDone", QUnit, { - name: this.testName, - module: this.module, - failed: bad, - passed: this.assertions.length - bad, - total: this.assertions.length, - runtime: this.runtime, - // DEPRECATED: this property will be removed in 2.0.0, use runtime instead - duration: this.runtime - }); - - QUnit.reset(); - - config.current = undefined; - }, - - queue: function() { - var bad, - test = this; - - synchronize(function() { - test.init(); - }); - function run() { - // each of these can by async - synchronize(function() { - test.setup(); - }); - synchronize(function() { - test.run(); - }); - synchronize(function() { - test.teardown(); - }); - synchronize(function() { - test.finish(); - }); - } - - // `bad` initialized at top of scope - // defer when previous test run passed, if storage is available - bad = QUnit.config.reorder && defined.sessionStorage && - +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName ); - - if ( bad ) { - run(); - } else { - synchronize( run, true ); - } - } -}; - -// `assert` initialized at top of scope -// Assert helpers -// All of these must either call QUnit.push() or manually do: -// - runLoggingCallbacks( "log", .. ); -// - config.current.assertions.push({ .. }); -assert = QUnit.assert = { - /** - * Asserts rough true-ish result. - * @name ok - * @function - * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); - */ - ok: function( result, msg ) { - if ( !config.current ) { - throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) ); - } - result = !!result; - msg = msg || ( result ? "okay" : "failed" ); - - var source, - details = { - module: config.current.module, - name: config.current.testName, - result: result, - message: msg - }; - - msg = "" + escapeText( msg ) + ""; - - if ( !result ) { - source = sourceFromStacktrace( 2 ); - if ( source ) { - details.source = source; - msg += "
      Source:
      " +
      -					escapeText( source ) +
      -					"
      "; - } - } - runLoggingCallbacks( "log", QUnit, details ); - config.current.assertions.push({ - result: result, - message: msg - }); - }, - - /** - * Assert that the first two arguments are equal, with an optional message. - * Prints out both actual and expected values. - * @name equal - * @function - * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); - */ - equal: function( actual, expected, message ) { - /*jshint eqeqeq:false */ - QUnit.push( expected == actual, actual, expected, message ); - }, - - /** - * @name notEqual - * @function - */ - notEqual: function( actual, expected, message ) { - /*jshint eqeqeq:false */ - QUnit.push( expected != actual, actual, expected, message ); - }, - - /** - * @name propEqual - * @function - */ - propEqual: function( actual, expected, message ) { - actual = objectValues(actual); - expected = objectValues(expected); - QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); - }, - - /** - * @name notPropEqual - * @function - */ - notPropEqual: function( actual, expected, message ) { - actual = objectValues(actual); - expected = objectValues(expected); - QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); - }, - - /** - * @name deepEqual - * @function - */ - deepEqual: function( actual, expected, message ) { - QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); - }, - - /** - * @name notDeepEqual - * @function - */ - notDeepEqual: function( actual, expected, message ) { - QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); - }, - - /** - * @name strictEqual - * @function - */ - strictEqual: function( actual, expected, message ) { - QUnit.push( expected === actual, actual, expected, message ); - }, - - /** - * @name notStrictEqual - * @function - */ - notStrictEqual: function( actual, expected, message ) { - QUnit.push( expected !== actual, actual, expected, message ); - }, - - "throws": function( block, expected, message ) { - var actual, - expectedOutput = expected, - ok = false; - - // 'expected' is optional - if ( !message && typeof expected === "string" ) { - message = expected; - expected = null; - } - - config.current.ignoreGlobalErrors = true; - try { - block.call( config.current.testEnvironment ); - } catch (e) { - actual = e; - } - config.current.ignoreGlobalErrors = false; - - if ( actual ) { - - // we don't want to validate thrown error - if ( !expected ) { - ok = true; - expectedOutput = null; - - // expected is an Error object - } else if ( expected instanceof Error ) { - ok = actual instanceof Error && - actual.name === expected.name && - actual.message === expected.message; - - // expected is a regexp - } else if ( QUnit.objectType( expected ) === "regexp" ) { - ok = expected.test( errorString( actual ) ); - - // expected is a string - } else if ( QUnit.objectType( expected ) === "string" ) { - ok = expected === errorString( actual ); - - // expected is a constructor - } else if ( actual instanceof expected ) { - ok = true; - - // expected is a validation function which returns true is validation passed - } else if ( expected.call( {}, actual ) === true ) { - expectedOutput = null; - ok = true; - } - - QUnit.push( ok, actual, expectedOutput, message ); - } else { - QUnit.pushFailure( message, null, "No exception was thrown." ); - } - } -}; - -/** - * @deprecated since 1.8.0 - * Kept assertion helpers in root for backwards compatibility. - */ -extend( QUnit.constructor.prototype, assert ); - -/** - * @deprecated since 1.9.0 - * Kept to avoid TypeErrors for undefined methods. - */ -QUnit.constructor.prototype.raises = function() { - QUnit.push( false, false, false, "QUnit.raises has been deprecated since 2012 (fad3c1ea), use QUnit.throws instead" ); -}; - -/** - * @deprecated since 1.0.0, replaced with error pushes since 1.3.0 - * Kept to avoid TypeErrors for undefined methods. - */ -QUnit.constructor.prototype.equals = function() { - QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" ); -}; -QUnit.constructor.prototype.same = function() { - QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" ); -}; - -// Test for equality any JavaScript type. -// Author: Philippe Rathé -QUnit.equiv = (function() { - - // Call the o related callback with the given arguments. - function bindCallbacks( o, callbacks, args ) { - var prop = QUnit.objectType( o ); - if ( prop ) { - if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { - return callbacks[ prop ].apply( callbacks, args ); - } else { - return callbacks[ prop ]; // or undefined - } - } - } - - // the real equiv function - var innerEquiv, - // stack to decide between skip/abort functions - callers = [], - // stack to avoiding loops from circular referencing - parents = [], - parentsB = [], - - getProto = Object.getPrototypeOf || function ( obj ) { - /*jshint camelcase:false */ - return obj.__proto__; - }, - callbacks = (function () { - - // for string, boolean, number and null - function useStrictEquality( b, a ) { - /*jshint eqeqeq:false */ - if ( b instanceof a.constructor || a instanceof b.constructor ) { - // to catch short annotation VS 'new' annotation of a - // declaration - // e.g. var i = 1; - // var j = new Number(1); - return a == b; - } else { - return a === b; - } - } - - return { - "string": useStrictEquality, - "boolean": useStrictEquality, - "number": useStrictEquality, - "null": useStrictEquality, - "undefined": useStrictEquality, - - "nan": function( b ) { - return isNaN( b ); - }, - - "date": function( b, a ) { - return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); - }, - - "regexp": function( b, a ) { - return QUnit.objectType( b ) === "regexp" && - // the regex itself - a.source === b.source && - // and its modifiers - a.global === b.global && - // (gmi) ... - a.ignoreCase === b.ignoreCase && - a.multiline === b.multiline && - a.sticky === b.sticky; - }, - - // - skip when the property is a method of an instance (OOP) - // - abort otherwise, - // initial === would have catch identical references anyway - "function": function() { - var caller = callers[callers.length - 1]; - return caller !== Object && typeof caller !== "undefined"; - }, - - "array": function( b, a ) { - var i, j, len, loop, aCircular, bCircular; - - // b could be an object literal here - if ( QUnit.objectType( b ) !== "array" ) { - return false; - } - - len = a.length; - if ( len !== b.length ) { - // safe and faster - return false; - } - - // track reference to avoid circular references - parents.push( a ); - parentsB.push( b ); - for ( i = 0; i < len; i++ ) { - loop = false; - for ( j = 0; j < parents.length; j++ ) { - aCircular = parents[j] === a[i]; - bCircular = parentsB[j] === b[i]; - if ( aCircular || bCircular ) { - if ( a[i] === b[i] || aCircular && bCircular ) { - loop = true; - } else { - parents.pop(); - parentsB.pop(); - return false; - } - } - } - if ( !loop && !innerEquiv(a[i], b[i]) ) { - parents.pop(); - parentsB.pop(); - return false; - } - } - parents.pop(); - parentsB.pop(); - return true; - }, - - "object": function( b, a ) { - /*jshint forin:false */ - var i, j, loop, aCircular, bCircular, - // Default to true - eq = true, - aProperties = [], - bProperties = []; - - // comparing constructors is more strict than using - // instanceof - if ( a.constructor !== b.constructor ) { - // Allow objects with no prototype to be equivalent to - // objects with Object as their constructor. - if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) || - ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) { - return false; - } - } - - // stack constructor before traversing properties - callers.push( a.constructor ); - - // track reference to avoid circular references - parents.push( a ); - parentsB.push( b ); - - // be strict: don't ensure hasOwnProperty and go deep - for ( i in a ) { - loop = false; - for ( j = 0; j < parents.length; j++ ) { - aCircular = parents[j] === a[i]; - bCircular = parentsB[j] === b[i]; - if ( aCircular || bCircular ) { - if ( a[i] === b[i] || aCircular && bCircular ) { - loop = true; - } else { - eq = false; - break; - } - } - } - aProperties.push(i); - if ( !loop && !innerEquiv(a[i], b[i]) ) { - eq = false; - break; - } - } - - parents.pop(); - parentsB.pop(); - callers.pop(); // unstack, we are done - - for ( i in b ) { - bProperties.push( i ); // collect b's properties - } - - // Ensures identical properties name - return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); - } - }; - }()); - - innerEquiv = function() { // can take multiple arguments - var args = [].slice.apply( arguments ); - if ( args.length < 2 ) { - return true; // end transition - } - - return (function( a, b ) { - if ( a === b ) { - return true; // catch the most you can - } else if ( a === null || b === null || typeof a === "undefined" || - typeof b === "undefined" || - QUnit.objectType(a) !== QUnit.objectType(b) ) { - return false; // don't lose time with error prone cases - } else { - return bindCallbacks(a, callbacks, [ b, a ]); - } - - // apply transition with (1..n) arguments - }( args[0], args[1] ) && innerEquiv.apply( this, args.splice(1, args.length - 1 )) ); - }; - - return innerEquiv; -}()); - -/** - * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | - * http://flesler.blogspot.com Licensed under BSD - * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 - * - * @projectDescription Advanced and extensible data dumping for Javascript. - * @version 1.0.0 - * @author Ariel Flesler - * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} - */ -QUnit.jsDump = (function() { - function quote( str ) { - return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\""; - } - function literal( o ) { - return o + ""; - } - function join( pre, arr, post ) { - var s = jsDump.separator(), - base = jsDump.indent(), - inner = jsDump.indent(1); - if ( arr.join ) { - arr = arr.join( "," + s + inner ); - } - if ( !arr ) { - return pre + post; - } - return [ pre, inner + arr, base + post ].join(s); - } - function array( arr, stack ) { - var i = arr.length, ret = new Array(i); - this.up(); - while ( i-- ) { - ret[i] = this.parse( arr[i] , undefined , stack); - } - this.down(); - return join( "[", ret, "]" ); - } - - var reName = /^function (\w+)/, - jsDump = { - // type is used mostly internally, you can fix a (custom)type in advance - parse: function( obj, type, stack ) { - stack = stack || [ ]; - var inStack, res, - parser = this.parsers[ type || this.typeOf(obj) ]; - - type = typeof parser; - inStack = inArray( obj, stack ); - - if ( inStack !== -1 ) { - return "recursion(" + (inStack - stack.length) + ")"; - } - if ( type === "function" ) { - stack.push( obj ); - res = parser.call( this, obj, stack ); - stack.pop(); - return res; - } - return ( type === "string" ) ? parser : this.parsers.error; - }, - typeOf: function( obj ) { - var type; - if ( obj === null ) { - type = "null"; - } else if ( typeof obj === "undefined" ) { - type = "undefined"; - } else if ( QUnit.is( "regexp", obj) ) { - type = "regexp"; - } else if ( QUnit.is( "date", obj) ) { - type = "date"; - } else if ( QUnit.is( "function", obj) ) { - type = "function"; - } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) { - type = "window"; - } else if ( obj.nodeType === 9 ) { - type = "document"; - } else if ( obj.nodeType ) { - type = "node"; - } else if ( - // native arrays - toString.call( obj ) === "[object Array]" || - // NodeList objects - ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) - ) { - type = "array"; - } else if ( obj.constructor === Error.prototype.constructor ) { - type = "error"; - } else { - type = typeof obj; - } - return type; - }, - separator: function() { - return this.multiline ? this.HTML ? "
      " : "\n" : this.HTML ? " " : " "; - }, - // extra can be a number, shortcut for increasing-calling-decreasing - indent: function( extra ) { - if ( !this.multiline ) { - return ""; - } - var chr = this.indentChar; - if ( this.HTML ) { - chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); - } - return new Array( this.depth + ( extra || 0 ) ).join(chr); - }, - up: function( a ) { - this.depth += a || 1; - }, - down: function( a ) { - this.depth -= a || 1; - }, - setParser: function( name, parser ) { - this.parsers[name] = parser; - }, - // The next 3 are exposed so you can use them - quote: quote, - literal: literal, - join: join, - // - depth: 1, - // This is the list of parsers, to modify them, use jsDump.setParser - parsers: { - window: "[Window]", - document: "[Document]", - error: function(error) { - return "Error(\"" + error.message + "\")"; - }, - unknown: "[Unknown]", - "null": "null", - "undefined": "undefined", - "function": function( fn ) { - var ret = "function", - // functions never have name in IE - name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1]; - - if ( name ) { - ret += " " + name; - } - ret += "( "; - - ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" ); - return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" ); - }, - array: array, - nodelist: array, - "arguments": array, - object: function( map, stack ) { - /*jshint forin:false */ - var ret = [ ], keys, key, val, i; - QUnit.jsDump.up(); - keys = []; - for ( key in map ) { - keys.push( key ); - } - keys.sort(); - for ( i = 0; i < keys.length; i++ ) { - key = keys[ i ]; - val = map[ key ]; - ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) ); - } - QUnit.jsDump.down(); - return join( "{", ret, "}" ); - }, - node: function( node ) { - var len, i, val, - open = QUnit.jsDump.HTML ? "<" : "<", - close = QUnit.jsDump.HTML ? ">" : ">", - tag = node.nodeName.toLowerCase(), - ret = open + tag, - attrs = node.attributes; - - if ( attrs ) { - for ( i = 0, len = attrs.length; i < len; i++ ) { - val = attrs[i].nodeValue; - // IE6 includes all attributes in .attributes, even ones not explicitly set. - // Those have values like undefined, null, 0, false, "" or "inherit". - if ( val && val !== "inherit" ) { - ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" ); - } - } - } - ret += close; - - // Show content of TextNode or CDATASection - if ( node.nodeType === 3 || node.nodeType === 4 ) { - ret += node.nodeValue; - } - - return ret + open + "/" + tag + close; - }, - // function calls it internally, it's the arguments part of the function - functionArgs: function( fn ) { - var args, - l = fn.length; - - if ( !l ) { - return ""; - } - - args = new Array(l); - while ( l-- ) { - // 97 is 'a' - args[l] = String.fromCharCode(97+l); - } - return " " + args.join( ", " ) + " "; - }, - // object calls it internally, the key part of an item in a map - key: quote, - // function calls it internally, it's the content of the function - functionCode: "[code]", - // node calls it internally, it's an html attribute value - attribute: quote, - string: quote, - date: quote, - regexp: literal, - number: literal, - "boolean": literal - }, - // if true, entities are escaped ( <, >, \t, space and \n ) - HTML: false, - // indentation unit - indentChar: " ", - // if true, items in a collection, are separated by a \n, else just a space. - multiline: true - }; - - return jsDump; -}()); - -/* - * Javascript Diff Algorithm - * By John Resig (http://ejohn.org/) - * Modified by Chu Alan "sprite" - * - * Released under the MIT license. - * - * More Info: - * http://ejohn.org/projects/javascript-diff-algorithm/ - * - * Usage: QUnit.diff(expected, actual) - * - * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over" - */ -QUnit.diff = (function() { - /*jshint eqeqeq:false, eqnull:true */ - function diff( o, n ) { - var i, - ns = {}, - os = {}; - - for ( i = 0; i < n.length; i++ ) { - if ( !hasOwn.call( ns, n[i] ) ) { - ns[ n[i] ] = { - rows: [], - o: null - }; - } - ns[ n[i] ].rows.push( i ); - } - - for ( i = 0; i < o.length; i++ ) { - if ( !hasOwn.call( os, o[i] ) ) { - os[ o[i] ] = { - rows: [], - n: null - }; - } - os[ o[i] ].rows.push( i ); - } - - for ( i in ns ) { - if ( hasOwn.call( ns, i ) ) { - if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) { - n[ ns[i].rows[0] ] = { - text: n[ ns[i].rows[0] ], - row: os[i].rows[0] - }; - o[ os[i].rows[0] ] = { - text: o[ os[i].rows[0] ], - row: ns[i].rows[0] - }; - } - } - } - - for ( i = 0; i < n.length - 1; i++ ) { - if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null && - n[ i + 1 ] == o[ n[i].row + 1 ] ) { - - n[ i + 1 ] = { - text: n[ i + 1 ], - row: n[i].row + 1 - }; - o[ n[i].row + 1 ] = { - text: o[ n[i].row + 1 ], - row: i + 1 - }; - } - } - - for ( i = n.length - 1; i > 0; i-- ) { - if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null && - n[ i - 1 ] == o[ n[i].row - 1 ]) { - - n[ i - 1 ] = { - text: n[ i - 1 ], - row: n[i].row - 1 - }; - o[ n[i].row - 1 ] = { - text: o[ n[i].row - 1 ], - row: i - 1 - }; - } - } - - return { - o: o, - n: n - }; - } - - return function( o, n ) { - o = o.replace( /\s+$/, "" ); - n = n.replace( /\s+$/, "" ); - - var i, pre, - str = "", - out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ), - oSpace = o.match(/\s+/g), - nSpace = n.match(/\s+/g); - - if ( oSpace == null ) { - oSpace = [ " " ]; - } - else { - oSpace.push( " " ); - } - - if ( nSpace == null ) { - nSpace = [ " " ]; - } - else { - nSpace.push( " " ); - } - - if ( out.n.length === 0 ) { - for ( i = 0; i < out.o.length; i++ ) { - str += "" + out.o[i] + oSpace[i] + ""; - } - } - else { - if ( out.n[0].text == null ) { - for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) { - str += "" + out.o[n] + oSpace[n] + ""; - } - } - - for ( i = 0; i < out.n.length; i++ ) { - if (out.n[i].text == null) { - str += "" + out.n[i] + nSpace[i] + ""; - } - else { - // `pre` initialized at top of scope - pre = ""; - - for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) { - pre += "" + out.o[n] + oSpace[n] + ""; - } - str += " " + out.n[i].text + nSpace[i] + pre; - } - } - } - - return str; - }; -}()); - -// For browser, export only select globals -if ( typeof window !== "undefined" ) { - extend( window, QUnit.constructor.prototype ); - window.QUnit = QUnit; -} - -// For CommonJS environments, export everything -if ( typeof module !== "undefined" && module.exports ) { - module.exports = QUnit; -} - - -// Get a reference to the global object, like window in browsers -}( (function() { - return this; -})() )); diff --git a/test/requirejs/index.html b/test/requirejs/index.html deleted file mode 100755 index 09908f1..0000000 --- a/test/requirejs/index.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - Finite State Machine Tests - USING REQUIREJS INCLUDE MECHANISM - - - - - - -

      QUnit Test Suite

      -

      -
      -

      -
        -
        test markup
        - - diff --git a/test/requirejs/require.js b/test/requirejs/require.js deleted file mode 100755 index ed535a8..0000000 --- a/test/requirejs/require.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - RequireJS 1.0.6 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. - Available via the MIT or new BSD license. - see: http://github.com/jrburke/requirejs for details -*/ -var requirejs,require,define; -(function(){function J(a){return N.call(a)==="[object Function]"}function F(a){return N.call(a)==="[object Array]"}function Z(a,c,l){for(var j in c)if(!(j in K)&&(!(j in a)||l))a[j]=c[j];return d}function O(a,c,d){a=Error(c+"\nhttp://requirejs.org/docs/errors.html#"+a);if(d)a.originalError=d;return a}function $(a,c,d){var j,k,s;for(j=0;s=c[j];j++){s=typeof s==="string"?{name:s}:s;k=s.location;if(d&&(!k||k.indexOf("/")!==0&&k.indexOf(":")===-1))k=d+"/"+(k||s.name);a[s.name]={name:s.name,location:k|| -s.name,main:(s.main||"main").replace(ea,"").replace(aa,"")}}}function U(a,c){a.holdReady?a.holdReady(c):c?a.readyWait+=1:a.ready(!0)}function fa(a){function c(b,f){var g,m;if(b&&b.charAt(0)===".")if(f){q.pkgs[f]?f=[f]:(f=f.split("/"),f=f.slice(0,f.length-1));g=b=f.concat(b.split("/"));var a;for(m=0;a=g[m];m++)if(a===".")g.splice(m,1),m-=1;else if(a==="..")if(m===1&&(g[2]===".."||g[0]===".."))break;else m>0&&(g.splice(m-1,2),m-=2);m=q.pkgs[g=b[0]];b=b.join("/");m&&b===g+"/"+m.main&&(b=g)}else b.indexOf("./")=== -0&&(b=b.substring(2));return b}function l(b,f){var g=b?b.indexOf("!"):-1,m=null,a=f?f.name:null,h=b,e,d;g!==-1&&(m=b.substring(0,g),b=b.substring(g+1,b.length));m&&(m=c(m,a));b&&(m?e=(g=n[m])&&g.normalize?g.normalize(b,function(b){return c(b,a)}):c(b,a):(e=c(b,a),d=F[e],d||(d=i.nameToUrl(b,null,f),F[e]=d)));return{prefix:m,name:e,parentMap:f,url:d,originalName:h,fullName:m?m+"!"+(e||""):e}}function j(){var b=!0,f=q.priorityWait,g,a;if(f){for(a=0;g=f[a];a++)if(!r[g]){b=!1;break}b&&delete q.priorityWait}return b} -function k(b,f,g){return function(){var a=ga.call(arguments,0),c;if(g&&J(c=a[a.length-1]))c.__requireJsBuild=!0;a.push(f);return b.apply(null,a)}}function s(b,f,g){f=k(g||i.require,b,f);Z(f,{nameToUrl:k(i.nameToUrl,b),toUrl:k(i.toUrl,b),defined:k(i.requireDefined,b),specified:k(i.requireSpecified,b),isBrowser:d.isBrowser});return f}function p(b){var f,g,a,c=b.callback,h=b.map,e=h.fullName,ba=b.deps;a=b.listeners;if(c&&J(c)){if(q.catchError.define)try{g=d.execCb(e,b.callback,ba,n[e])}catch(j){f=j}else g= -d.execCb(e,b.callback,ba,n[e]);if(e)(c=b.cjsModule)&&c.exports!==void 0&&c.exports!==n[e]?g=n[e]=b.cjsModule.exports:g===void 0&&b.usingExports?g=n[e]:(n[e]=g,G[e]&&(S[e]=!0))}else e&&(g=n[e]=c,G[e]&&(S[e]=!0));if(w[b.id])delete w[b.id],b.isDone=!0,i.waitCount-=1,i.waitCount===0&&(I=[]);delete L[e];if(d.onResourceLoad&&!b.placeholder)d.onResourceLoad(i,h,b.depArray);if(f)return g=(e?l(e).url:"")||f.fileName||f.sourceURL,a=f.moduleTree,f=O("defineerror",'Error evaluating module "'+e+'" at location "'+ -g+'":\n'+f+"\nfileName:"+g+"\nlineNumber: "+(f.lineNumber||f.line),f),f.moduleName=e,f.moduleTree=a,d.onError(f);for(f=0;c=a[f];f++)c(g)}function t(b,f){return function(g){b.depDone[f]||(b.depDone[f]=!0,b.deps[f]=g,b.depCount-=1,b.depCount||p(b))}}function o(b,f){var g=f.map,a=g.fullName,c=g.name,h=M[b]||(M[b]=n[b]),e;if(!f.loading)f.loading=!0,e=function(b){f.callback=function(){return b};p(f);r[f.id]=!0;z()},e.fromText=function(b,f){var g=P;r[b]=!1;i.scriptCount+=1;i.fake[b]=!0;g&&(P=!1);d.exec(f); -g&&(P=!0);i.completeLoad(b)},a in n?e(n[a]):h.load(c,s(g.parentMap,!0,function(b,a){var c=[],e,m;for(e=0;m=b[e];e++)m=l(m,g.parentMap),b[e]=m.fullName,m.prefix||c.push(b[e]);f.moduleDeps=(f.moduleDeps||[]).concat(c);return i.require(b,a)}),e,q)}function x(b){w[b.id]||(w[b.id]=b,I.push(b),i.waitCount+=1)}function C(b){this.listeners.push(b)}function u(b,f){var g=b.fullName,a=b.prefix,c=a?M[a]||(M[a]=n[a]):null,h,e;g&&(h=L[g]);if(!h&&(e=!0,h={id:(a&&!c?N++ +"__p@:":"")+(g||"__r@"+N++),map:b,depCount:0, -depDone:[],depCallbacks:[],deps:[],listeners:[],add:C},A[h.id]=!0,g&&(!a||M[a])))L[g]=h;a&&!c?(g=l(a),a in n&&!n[a]&&(delete n[a],delete Q[g.url]),a=u(g,!0),a.add(function(){var f=l(b.originalName,b.parentMap),f=u(f,!0);h.placeholder=!0;f.add(function(b){h.callback=function(){return b};p(h)})})):e&&f&&(r[h.id]=!1,i.paused.push(h),x(h));return h}function B(b,f,a,c){var b=l(b,c),d=b.name,h=b.fullName,e=u(b),j=e.id,k=e.deps,o;if(h){if(h in n||r[j]===!0||h==="jquery"&&q.jQuery&&q.jQuery!==a().fn.jquery)return; -A[j]=!0;r[j]=!0;h==="jquery"&&a&&V(a())}e.depArray=f;e.callback=a;for(a=0;a0)){if(q.priorityWait)if(j())z();else return;for(h in r)if(!(h in K)&&(c=!0,!r[h]))if(b)a+=h+" ";else if(l=!0,h.indexOf("!")===-1){k=[];break}else(e=L[h]&&L[h].moduleDeps)&&k.push.apply(k,e);if(c||i.waitCount){if(b&&a)return b=O("timeout","Load timeout for modules: "+a),b.requireType="timeout",b.requireModules=a,b.contextName=i.contextName,d.onError(b);if(l&&k.length)for(a= -0;h=w[k[a]];a++)if(h=E(h,{})){y(h,{});break}if(!b&&(l||i.scriptCount)){if((H||ca)&&!W)W=setTimeout(function(){W=0;D()},50)}else{if(i.waitCount){for(a=0;h=I[a];a++)y(h,{});i.paused.length&&z();X<5&&(X+=1,D())}X=0;d.checkReadyState()}}}}var i,z,q={waitSeconds:7,baseUrl:"./",paths:{},pkgs:{},catchError:{}},R=[],A={require:!0,exports:!0,module:!0},F={},n={},r={},w={},I=[],Q={},N=0,L={},M={},G={},S={},Y=0;V=function(b){if(!i.jQuery&&(b=b||(typeof jQuery!=="undefined"?jQuery:null))&&!(q.jQuery&&b.fn.jquery!== -q.jQuery)&&("holdReady"in b||"readyWait"in b))if(i.jQuery=b,v(["jquery",[],function(){return jQuery}]),i.scriptCount)U(b,!0),i.jQueryIncremented=!0};z=function(){var b,a,c,l,k,h;i.takeGlobalQueue();Y+=1;if(i.scriptCount<=0)i.scriptCount=0;for(;R.length;)if(b=R.shift(),b[0]===null)return d.onError(O("mismatch","Mismatched anonymous define() module: "+b[b.length-1]));else v(b);if(!q.priorityWait||j())for(;i.paused.length;){k=i.paused;i.pausedCount+=k.length;i.paused=[];for(l=0;b=k[l];l++)a=b.map,c= -a.url,h=a.fullName,a.prefix?o(a.prefix,b):!Q[c]&&!r[h]&&(d.load(i,h,c),c.indexOf("empty:")!==0&&(Q[c]=!0));i.startTime=(new Date).getTime();i.pausedCount-=k.length}Y===1&&D();Y-=1};i={contextName:a,config:q,defQueue:R,waiting:w,waitCount:0,specified:A,loaded:r,urlMap:F,urlFetched:Q,scriptCount:0,defined:n,paused:[],pausedCount:0,plugins:M,needFullExec:G,fake:{},fullExec:S,managerCallbacks:L,makeModuleMap:l,normalize:c,configure:function(b){var a,c,d;b.baseUrl&&b.baseUrl.charAt(b.baseUrl.length-1)!== -"/"&&(b.baseUrl+="/");a=q.paths;d=q.pkgs;Z(q,b,!0);if(b.paths){for(c in b.paths)c in K||(a[c]=b.paths[c]);q.paths=a}if((a=b.packagePaths)||b.packages){if(a)for(c in a)c in K||$(d,a[c],c);b.packages&&$(d,b.packages);q.pkgs=d}if(b.priority)c=i.requireWait,i.requireWait=!1,z(),i.require(b.priority),z(),i.requireWait=c,q.priorityWait=b.priority;if(b.deps||b.callback)i.require(b.deps||[],b.callback)},requireDefined:function(b,a){return l(b,a).fullName in n},requireSpecified:function(b,a){return l(b,a).fullName in -A},require:function(b,c,g){if(typeof b==="string"){if(J(c))return d.onError(O("requireargs","Invalid require call"));if(d.get)return d.get(i,b,c);c=l(b,c);b=c.fullName;return!(b in n)?d.onError(O("notloaded","Module name '"+c.fullName+"' has not been loaded yet for context: "+a)):n[b]}(b&&b.length||c)&&B(null,b,c,g);if(!i.requireWait)for(;!i.scriptCount&&i.paused.length;)z();return i.require},takeGlobalQueue:function(){T.length&&(ia.apply(i.defQueue,[i.defQueue.length-1,0].concat(T)),T=[])},completeLoad:function(b){var a; -for(i.takeGlobalQueue();R.length;)if(a=R.shift(),a[0]===null){a[0]=b;break}else if(a[0]===b)break;else v(a),a=null;a?v(a):v([b,[],b==="jquery"&&typeof jQuery!=="undefined"?function(){return jQuery}:null]);d.isAsync&&(i.scriptCount-=1);z();d.isAsync||(i.scriptCount-=1)},toUrl:function(b,a){var c=b.lastIndexOf("."),d=null;c!==-1&&(d=b.substring(c,b.length),b=b.substring(0,c));return i.nameToUrl(b,d,a)},nameToUrl:function(b,a,g){var l,k,h,e,j=i.config,b=c(b,g&&g.fullName);if(d.jsExtRegExp.test(b))a= -b+(a?a:"");else{l=j.paths;k=j.pkgs;g=b.split("/");for(e=g.length;e>0;e--)if(h=g.slice(0,e).join("/"),l[h]){g.splice(0,e,l[h]);break}else if(h=k[h]){b=b===h.name?h.location+"/"+h.main:h.location;g.splice(0,e,b);break}a=g.join("/")+(a||".js");a=(a.charAt(0)==="/"||a.match(/^\w+:/)?"":j.baseUrl)+a}return j.urlArgs?a+((a.indexOf("?")===-1?"?":"&")+j.urlArgs):a}};i.jQueryCheck=V;i.resume=z;return i}function ja(){var a,c,d;if(B&&B.readyState==="interactive")return B;a=document.getElementsByTagName("script"); -for(c=a.length-1;c>-1&&(d=a[c]);c--)if(d.readyState==="interactive")return B=d;return null}var ka=/(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,la=/require\(\s*["']([^'"\s]+)["']\s*\)/g,ea=/^\.\//,aa=/\.js$/,N=Object.prototype.toString,t=Array.prototype,ga=t.slice,ia=t.splice,H=!!(typeof window!=="undefined"&&navigator&&document),ca=!H&&typeof importScripts!=="undefined",ma=H&&navigator.platform==="PLAYSTATION 3"?/^complete$/:/^(complete|loaded)$/,da=typeof opera!=="undefined"&&opera.toString()==="[object Opera]", -K={},C={},T=[],B=null,X=0,P=!1,ha={require:!0,module:!0,exports:!0},d,t={},I,x,u,D,o,v,E,A,y,V,W;if(typeof define==="undefined"){if(typeof requirejs!=="undefined")if(J(requirejs))return;else t=requirejs,requirejs=void 0;typeof require!=="undefined"&&!J(require)&&(t=require,require=void 0);d=requirejs=function(a,c,d){var j="_",k;!F(a)&&typeof a!=="string"&&(k=a,F(c)?(a=c,c=d):a=[]);if(k&&k.context)j=k.context;d=C[j]||(C[j]=fa(j));k&&d.configure(k);return d.require(a,c)};d.config=function(a){return d(a)}; -require||(require=d);d.toUrl=function(a){return C._.toUrl(a)};d.version="1.0.6";d.jsExtRegExp=/^\/|:|\?|\.js$/;x=d.s={contexts:C,skipAsync:{}};if(d.isAsync=d.isBrowser=H)if(u=x.head=document.getElementsByTagName("head")[0],D=document.getElementsByTagName("base")[0])u=x.head=D.parentNode;d.onError=function(a){throw a;};d.load=function(a,c,l){d.resourcesReady(!1);a.scriptCount+=1;d.attach(l,a,c);if(a.jQuery&&!a.jQueryIncremented)U(a.jQuery,!0),a.jQueryIncremented=!0};define=function(a,c,d){var j,k; -typeof a!=="string"&&(d=c,c=a,a=null);F(c)||(d=c,c=[]);!c.length&&J(d)&&d.length&&(d.toString().replace(ka,"").replace(la,function(a,d){c.push(d)}),c=(d.length===1?["require"]:["require","exports","module"]).concat(c));if(P&&(j=I||ja()))a||(a=j.getAttribute("data-requiremodule")),k=C[j.getAttribute("data-requirecontext")];(k?k.defQueue:T).push([a,c,d])};define.amd={multiversion:!0,plugins:!0,jQuery:!0};d.exec=function(a){return eval(a)};d.execCb=function(a,c,d,j){return c.apply(j,d)};d.addScriptToDom= -function(a){I=a;D?u.insertBefore(a,D):u.appendChild(a);I=null};d.onScriptLoad=function(a){var c=a.currentTarget||a.srcElement,l;if(a.type==="load"||c&&ma.test(c.readyState))B=null,a=c.getAttribute("data-requirecontext"),l=c.getAttribute("data-requiremodule"),C[a].completeLoad(l),c.detachEvent&&!da?c.detachEvent("onreadystatechange",d.onScriptLoad):c.removeEventListener("load",d.onScriptLoad,!1)};d.attach=function(a,c,l,j,k,o){var p;if(H)return j=j||d.onScriptLoad,p=c&&c.config&&c.config.xhtml?document.createElementNS("http://www.w3.org/1999/xhtml", -"html:script"):document.createElement("script"),p.type=k||c&&c.config.scriptType||"text/javascript",p.charset="utf-8",p.async=!x.skipAsync[a],c&&p.setAttribute("data-requirecontext",c.contextName),p.setAttribute("data-requiremodule",l),p.attachEvent&&!da?(P=!0,o?p.onreadystatechange=function(){if(p.readyState==="loaded")p.onreadystatechange=null,p.attachEvent("onreadystatechange",j),o(p)}:p.attachEvent("onreadystatechange",j)):p.addEventListener("load",j,!1),p.src=a,o||d.addScriptToDom(p),p;else ca&& -(importScripts(a),c.completeLoad(l));return null};if(H){o=document.getElementsByTagName("script");for(A=o.length-1;A>-1&&(v=o[A]);A--){if(!u)u=v.parentNode;if(E=v.getAttribute("data-main")){if(!t.baseUrl)o=E.split("/"),v=o.pop(),o=o.length?o.join("/")+"/":"./",t.baseUrl=o,E=v.replace(aa,"");t.deps=t.deps?t.deps.concat(E):[E];break}}}d.checkReadyState=function(){var a=x.contexts,c;for(c in a)if(!(c in K)&&a[c].waitCount)return;d.resourcesReady(!0)};d.resourcesReady=function(a){var c,l;d.resourcesDone= -a;if(d.resourcesDone)for(l in a=x.contexts,a)if(!(l in K)&&(c=a[l],c.jQueryIncremented))U(c.jQuery,!1),c.jQueryIncremented=!1};d.pageLoaded=function(){if(document.readyState!=="complete")document.readyState="complete"};if(H&&document.addEventListener&&!document.readyState)document.readyState="loading",window.addEventListener("load",d.pageLoaded,!1);d(t);if(d.isAsync&&typeof setTimeout!=="undefined")y=x.contexts[t.context||"_"],y.requireWait=!0,setTimeout(function(){y.requireWait=!1;y.scriptCount|| -y.resume();d.checkReadyState()},0)}})(); diff --git a/test/runner.js b/test/runner.js deleted file mode 100644 index 464fa3b..0000000 --- a/test/runner.js +++ /dev/null @@ -1,22 +0,0 @@ -// -// To run tests via nodejs you must have nodejs and npm installed -// -// > npm install # to install node-qunit -// > node test/runner -// - -var runner = require("qunit"); - -runner.run({ - - code: "./state-machine.js", - - tests: [ - "test/test_basics.js", - "test/test_advanced.js", - "test/test_classes.js", - "test/test_async.js", - "test/test_initialize.js" - ] - -}); diff --git a/test/test_advanced.js b/test/test_advanced.js deleted file mode 100755 index bf8ebd5..0000000 --- a/test/test_advanced.js +++ /dev/null @@ -1,280 +0,0 @@ -//----------------------------------------------------------------------------- - -QUnit.module("advanced"); - -//----------------------------------------------------------------------------- - -test("multiple 'from' states for the same event", function() { - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: ['green', 'yellow'], to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: ['yellow', 'red'], to: 'green' }, - ]}); - - equal(fsm.current, 'green', "initial state should be green"); - - ok(fsm.can('warn'), "should be able to warn from green state") - ok(fsm.can('panic'), "should be able to panic from green state") - ok(fsm.cannot('calm'), "should NOT be able to calm from green state") - ok(fsm.cannot('clear'), "should NOT be able to clear from green state") - - fsm.warn(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); - fsm.panic(); equal(fsm.current, 'red', "panic event should transition from yellow to red"); - fsm.calm(); equal(fsm.current, 'yellow', "calm event should transition from red to yellow"); - fsm.clear(); equal(fsm.current, 'green', "clear event should transition from yellow to green"); - - fsm.panic(); equal(fsm.current, 'red', "panic event should transition from green to red"); - fsm.clear(); equal(fsm.current, 'green', "clear event should transition from red to green"); - -}); - -//----------------------------------------------------------------------------- - -test("multiple 'to' states for the same event", function() { - - var fsm = StateMachine.create({ - initial: 'hungry', - events: [ - { name: 'eat', from: 'hungry', to: 'satisfied' }, - { name: 'eat', from: 'satisfied', to: 'full' }, - { name: 'eat', from: 'full', to: 'sick' }, - { name: 'rest', from: ['hungry', 'satisfied', 'full', 'sick'], to: 'hungry' }, - ]}); - - equal(fsm.current, 'hungry'); - - ok(fsm.can('eat')); - ok(fsm.can('rest')); - - fsm.eat(); - equal(fsm.current, 'satisfied'); - - fsm.eat(); - equal(fsm.current, 'full'); - - fsm.eat(); - equal(fsm.current, 'sick'); - - fsm.rest(); - equal(fsm.current, 'hungry'); - -}); - -//----------------------------------------------------------------------------- - -test("no-op transitions (github issue #5) with multiple from states", function() { - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: ['green', 'yellow'], to: 'red' }, - { name: 'noop', from: ['green', 'yellow'] }, // NOTE: 'to' not specified - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: ['yellow', 'red'], to: 'green' }, - ]}); - - equal(fsm.current, 'green', "initial state should be green"); - - ok(fsm.can('warn'), "should be able to warn from green state") - ok(fsm.can('panic'), "should be able to panic from green state") - ok(fsm.can('noop'), "should be able to noop from green state") - ok(fsm.cannot('calm'), "should NOT be able to calm from green state") - ok(fsm.cannot('clear'), "should NOT be able to clear from green state") - - fsm.noop(); equal(fsm.current, 'green', "noop event should not transition"); - fsm.warn(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); - - ok(fsm.cannot('warn'), "should NOT be able to warn from yellow state") - ok(fsm.can('panic'), "should be able to panic from yellow state") - ok(fsm.can('noop'), "should be able to noop from yellow state") - ok(fsm.cannot('calm'), "should NOT be able to calm from yellow state") - ok(fsm.can('clear'), "should be able to clear from yellow state") - - fsm.noop(); equal(fsm.current, 'yellow', "noop event should not transition"); - fsm.panic(); equal(fsm.current, 'red', "panic event should transition from yellow to red"); - - ok(fsm.cannot('warn'), "should NOT be able to warn from red state") - ok(fsm.cannot('panic'), "should NOT be able to panic from red state") - ok(fsm.cannot('noop'), "should NOT be able to noop from red state") - ok(fsm.can('calm'), "should be able to calm from red state") - ok(fsm.can('clear'), "should be able to clear from red state") - -}); - -//----------------------------------------------------------------------------- - -test("callbacks are called when appropriate for multiple 'from' and 'to' transitions", function() { - - var called = []; - - var fsm = StateMachine.create({ - initial: 'hungry', - events: [ - { name: 'eat', from: 'hungry', to: 'satisfied' }, - { name: 'eat', from: 'satisfied', to: 'full' }, - { name: 'eat', from: 'full', to: 'sick' }, - { name: 'rest', from: ['hungry', 'satisfied', 'full', 'sick'], to: 'hungry' }, - ], - callbacks: { - - // generic callbacks - onbeforeevent: function(event,from,to) { called.push('onbefore(' + event + ')'); }, - onafterevent: function(event,from,to) { called.push('onafter(' + event + ')'); }, - onleavestate: function(event,from,to) { called.push('onleave(' + from + ')'); }, - onenterstate: function(event,from,to) { called.push('onenter(' + to + ')'); }, - onchangestate: function(event,from,to) { called.push('onchange(' + from + ',' + to + ')'); }, - - // specific state callbacks - onenterhungry: function() { called.push('onenterhungry'); }, - onleavehungry: function() { called.push('onleavehungry'); }, - onentersatisfied: function() { called.push('onentersatisfied'); }, - onleavesatisfied: function() { called.push('onleavesatisfied'); }, - onenterfull: function() { called.push('onenterfull'); }, - onleavefull: function() { called.push('onleavefull'); }, - onentersick: function() { called.push('onentersick'); }, - onleavesick: function() { called.push('onleavesick'); }, - - // specific event callbacks - onbeforeeat: function() { called.push('onbeforeeat'); }, - onaftereat: function() { called.push('onaftereat'); }, - onbeforerest: function() { called.push('onbeforerest'); }, - onafterrest: function() { called.push('onafterrest'); } - } - }); - - called = []; - fsm.eat(); - deepEqual(called, [ - 'onbeforeeat', - 'onbefore(eat)', - 'onleavehungry', - 'onleave(hungry)', - 'onentersatisfied', - 'onenter(satisfied)', - 'onchange(hungry,satisfied)', - 'onaftereat', - 'onafter(eat)' - ]); - - called = []; - fsm.eat(); - deepEqual(called, [ - 'onbeforeeat', - 'onbefore(eat)', - 'onleavesatisfied', - 'onleave(satisfied)', - 'onenterfull', - 'onenter(full)', - 'onchange(satisfied,full)', - 'onaftereat', - 'onafter(eat)', - ]); - - called = []; - fsm.eat(); - deepEqual(called, [ - 'onbeforeeat', - 'onbefore(eat)', - 'onleavefull', - 'onleave(full)', - 'onentersick', - 'onenter(sick)', - 'onchange(full,sick)', - 'onaftereat', - 'onafter(eat)' - ]); - - called = []; - fsm.rest(); - deepEqual(called, [ - 'onbeforerest', - 'onbefore(rest)', - 'onleavesick', - 'onleave(sick)', - 'onenterhungry', - 'onenter(hungry)', - 'onchange(sick,hungry)', - 'onafterrest', - 'onafter(rest)' - ]); - -}); - -//----------------------------------------------------------------------------- - -test("callbacks are called when appropriate for prototype based state machine", function() { - - var myFSM = function() { - this.called = []; - this.startup(); - }; - - myFSM.prototype = { - - // generic callbacks - onbeforeevent: function(event,from,to) { this.called.push('onbefore(' + event + ')'); }, - onafterevent: function(event,from,to) { this.called.push('onafter(' + event + ')'); }, - onleavestate: function(event,from,to) { this.called.push('onleave(' + from + ')'); }, - onenterstate: function(event,from,to) { this.called.push('onenter(' + to + ')'); }, - onchangestate: function(event,from,to) { this.called.push('onchange(' + from + ',' + to + ')'); }, - - // specific state callbacks - onenternone: function() { this.called.push('onenternone'); }, - onleavenone: function() { this.called.push('onleavenone'); }, - onentergreen: function() { this.called.push('onentergreen'); }, - onleavegreen: function() { this.called.push('onleavegreen'); }, - onenteryellow : function() { this.called.push('onenteryellow'); }, - onleaveyellow: function() { this.called.push('onleaveyellow'); }, - onenterred: function() { this.called.push('onenterred'); }, - onleavered: function() { this.called.push('onleavered'); }, - - // specific event callbacks - onbeforestartup: function() { this.called.push('onbeforestartup'); }, - onafterstartup: function() { this.called.push('onafterstartup'); }, - onbeforewarn: function() { this.called.push('onbeforewarn'); }, - onafterwarn: function() { this.called.push('onafterwarn'); }, - onbeforepanic: function() { this.called.push('onbeforepanic'); }, - onafterpanic: function() { this.called.push('onafterpanic'); }, - onbeforeclear: function() { this.called.push('onbeforeclear'); }, - onafterclear: function() { this.called.push('onafterclear'); } - }; - - StateMachine.create({ - target: myFSM.prototype, - events: [ - { name: 'startup', from: 'none', to: 'green' }, - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'clear', from: 'yellow', to: 'green' } - ] - }); - - var a = new myFSM(); - var b = new myFSM(); - - equal(a.current, 'green', 'start with correct state'); - equal(b.current, 'green', 'start with correct state'); - - deepEqual(a.called, ['onbeforestartup', 'onbefore(startup)', 'onleavenone', 'onleave(none)', 'onentergreen', 'onenter(green)', 'onchange(none,green)', 'onafterstartup', 'onafter(startup)']); - deepEqual(b.called, ['onbeforestartup', 'onbefore(startup)', 'onleavenone', 'onleave(none)', 'onentergreen', 'onenter(green)', 'onchange(none,green)', 'onafterstartup', 'onafter(startup)']); - - a.called = []; - b.called = []; - - a.warn(); - - equal(a.current, 'yellow', 'maintain independent current state'); - equal(b.current, 'green', 'maintain independent current state'); - - deepEqual(a.called, ['onbeforewarn', 'onbefore(warn)', 'onleavegreen', 'onleave(green)', 'onenteryellow', 'onenter(yellow)', 'onchange(green,yellow)', 'onafterwarn', 'onafter(warn)']); - deepEqual(b.called, []); - -}); - - - diff --git a/test/test_async.js b/test/test_async.js deleted file mode 100644 index 17fc6a5..0000000 --- a/test/test_async.js +++ /dev/null @@ -1,408 +0,0 @@ -//----------------------------------------------------------------------------- - -QUnit.module("async"); - -//----------------------------------------------------------------------------- - -test("state transitions", function() { - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ], - callbacks: { - onleavegreen: function() { return StateMachine.ASYNC; }, - onleaveyellow: function() { return StateMachine.ASYNC; }, - onleavered: function() { return StateMachine.ASYNC; } - } - }); - - equal(fsm.current, 'green', "initial state should be green"); - fsm.warn(); equal(fsm.current, 'green', "should still be green because we haven't transitioned yet"); - fsm.transition(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); - fsm.panic(); equal(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet"); - fsm.transition(); equal(fsm.current, 'red', "panic event should transition from yellow to red"); - fsm.calm(); equal(fsm.current, 'red', "should still be red because we haven't transitioned yet"); - fsm.transition(); equal(fsm.current, 'yellow', "calm event should transition from red to yellow"); - fsm.clear(); equal(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet"); - fsm.transition(); equal(fsm.current, 'green', "clear event should transition from yellow to green"); - -}); - -//----------------------------------------------------------------------------- - -test("state transitions with delays", function() { - - stop(); // doing async stuff - dont run next qunit test until I call start() below - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ], - callbacks: { - onleavegreen: function() { return StateMachine.ASYNC; }, - onleaveyellow: function() { return StateMachine.ASYNC; }, - onleavered: function() { return StateMachine.ASYNC; } - } - }); - - equal(fsm.current, 'green', "initial state should be green"); - fsm.warn(); equal(fsm.current, 'green', "should still be green because we haven't transitioned yet"); - setTimeout(function() { - fsm.transition(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); - fsm.panic(); equal(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet"); - setTimeout(function() { - fsm.transition(); equal(fsm.current, 'red', "panic event should transition from yellow to red"); - fsm.calm(); equal(fsm.current, 'red', "should still be red because we haven't transitioned yet"); - setTimeout(function() { - fsm.transition(); equal(fsm.current, 'yellow', "calm event should transition from red to yellow"); - fsm.clear(); equal(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet"); - setTimeout(function() { - fsm.transition(); equal(fsm.current, 'green', "clear event should transition from yellow to green"); - start(); - }, 10); - }, 10); - }, 10); - }, 10); - -}); - -//----------------------------------------------------------------------------- - -test("state transition fired during onleavestate callback - immediate", function() { - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ], - callbacks: { - onleavegreen: function() { this.transition(); return StateMachine.ASYNC; }, - onleaveyellow: function() { this.transition(); return StateMachine.ASYNC; }, - onleavered: function() { this.transition(); return StateMachine.ASYNC; } - } - }); - - equal(fsm.current, 'green', "initial state should be green"); - - fsm.warn(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); - fsm.panic(); equal(fsm.current, 'red', "panic event should transition from yellow to red"); - fsm.calm(); equal(fsm.current, 'yellow', "calm event should transition from red to yellow"); - fsm.clear(); equal(fsm.current, 'green', "clear event should transition from yellow to green"); - -}); - -//----------------------------------------------------------------------------- - -test("state transition fired during onleavestate callback - with delay", function() { - - stop(); // doing async stuff - dont run next qunit test until I call start() below - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'panic', from: 'green', to: 'red' } - ], - callbacks: { - onleavegreen: function() { setTimeout(function() { fsm.transition(); }, 10); return StateMachine.ASYNC; }, - onenterred: function() { - equal(fsm.current, 'red', "panic event should transition from green to red"); - start(); - } - } - }); - - equal(fsm.current, 'green', "initial state should be green"); - fsm.panic(); equal(fsm.current, 'green', "should still be green because we haven't transitioned yet"); - -}); - -//----------------------------------------------------------------------------- - -test("state transition fired during onleavestate callback - but forgot to return ASYNC!", function() { - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ], - callbacks: { - onleavegreen: function() { this.transition(); /* return StateMachine.ASYNC; */ }, - onleaveyellow: function() { this.transition(); /* return StateMachine.ASYNC; */ }, - onleavered: function() { this.transition(); /* return StateMachine.ASYNC; */ } - } - }); - - equal(fsm.current, 'green', "initial state should be green"); - - fsm.warn(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); - fsm.panic(); equal(fsm.current, 'red', "panic event should transition from yellow to red"); - fsm.calm(); equal(fsm.current, 'yellow', "calm event should transition from red to yellow"); - fsm.clear(); equal(fsm.current, 'green', "clear event should transition from yellow to green"); - -}); - -//----------------------------------------------------------------------------- - -test("state transitions sometimes synchronous and sometimes asynchronous", function() { - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ] - }); - - // default behavior is synchronous - - equal(fsm.current, 'green', "initial state should be green"); - fsm.warn(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); - fsm.panic(); equal(fsm.current, 'red', "panic event should transition from yellow to red"); - fsm.calm(); equal(fsm.current, 'yellow', "calm event should transition from red to yellow"); - fsm.clear(); equal(fsm.current, 'green', "clear event should transition from yellow to green"); - - // but add callbacks that return ASYNC and it magically becomes asynchronous - - fsm.onleavegreen = function() { return StateMachine.ASYNC; } - fsm.onleaveyellow = function() { return StateMachine.ASYNC; } - fsm.onleavered = function() { return StateMachine.ASYNC; } - - equal(fsm.current, 'green', "initial state should be green"); - fsm.warn(); equal(fsm.current, 'green', "should still be green because we haven't transitioned yet"); - fsm.transition(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); - fsm.panic(); equal(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet"); - fsm.transition(); equal(fsm.current, 'red', "panic event should transition from yellow to red"); - fsm.calm(); equal(fsm.current, 'red', "should still be red because we haven't transitioned yet"); - fsm.transition(); equal(fsm.current, 'yellow', "calm event should transition from red to yellow"); - fsm.clear(); equal(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet"); - fsm.transition(); equal(fsm.current, 'green', "clear event should transition from yellow to green"); - - // this allows you to make on-the-fly decisions about whether async or not ... - - fsm.onleavegreen = function(event, from, to, async) { - if (async) { - setTimeout(function() { - fsm.transition(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); - start(); // move on to next test - }, 10); - return StateMachine.ASYNC; - } - } - fsm.onleaveyellow = fsm.onleavered = null; - - fsm.warn(false); equal(fsm.current, 'yellow', "expected synchronous transition from green to yellow"); - fsm.clear(); equal(fsm.current, 'green', "clear event should transition from yellow to green"); - fsm.warn(true); equal(fsm.current, 'green', "should still be green because we haven't transitioned yet"); - - stop(); // doing async stuff - dont run next qunit test until I call start() in callback above - -}); - -//----------------------------------------------------------------------------- - - -test("state transition fired without completing previous transition", function() { - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ], - callbacks: { - onleavegreen: function() { return StateMachine.ASYNC; }, - onleaveyellow: function() { return StateMachine.ASYNC; }, - onleavered: function() { return StateMachine.ASYNC; } - } - }); - - equal(fsm.current, 'green', "initial state should be green"); - fsm.warn(); equal(fsm.current, 'green', "should still be green because we haven't transitioned yet"); - fsm.transition(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); - fsm.panic(); equal(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet"); - - throws(fsm.calm.bind(fsm), /event calm inappropriate because previous transition did not complete/); - -}); - -//----------------------------------------------------------------------------- - -test("state transition can be cancelled (github issue #22)", function() { - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ], - callbacks: { - onleavegreen: function() { return StateMachine.ASYNC; }, - onleaveyellow: function() { return StateMachine.ASYNC; }, - onleavered: function() { return StateMachine.ASYNC; } - } - }); - - equal(fsm.current, 'green', "initial state should be green"); - fsm.warn(); equal(fsm.current, 'green', "should still be green because we haven't transitioned yet"); - fsm.transition(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); - fsm.panic(); equal(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet"); - equal(fsm.can('panic'), false, "but cannot panic a 2nd time because a transition is still pending") - - throws(fsm.panic.bind(fsm), /event panic inappropriate because previous transition did not complete/); - - fsm.transition.cancel(); - - equal(fsm.current, 'yellow', "should still be yellow because we cancelled the async transition"); - equal(fsm.can('panic'), true, "can now panic again because we cancelled previous async transition"); - - fsm.panic(); - fsm.transition(); - - equal(fsm.current, 'red', "should finally be red now that we completed the async transition"); - -}); - -//----------------------------------------------------------------------------- - -test("callbacks are ordered correctly", function() { - - var called = []; - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' }, - ], - callbacks: { - - // generic callbacks - onbeforeevent: function(event,from,to) { called.push('onbefore(' + event + ')'); }, - onafterevent: function(event,from,to) { called.push('onafter(' + event + ')'); }, - onleavestate: function(event,from,to) { called.push('onleave(' + from + ')'); }, - onenterstate: function(event,from,to) { called.push('onenter(' + to + ')'); }, - onchangestate: function(event,from,to) { called.push('onchange(' + from + ',' + to + ')'); }, - - // specific state callbacks - onentergreen: function() { called.push('onentergreen'); }, - onenteryellow: function() { called.push('onenteryellow'); }, - onenterred: function() { called.push('onenterred'); }, - onleavegreen: function() { called.push('onleavegreen'); return StateMachine.ASYNC; }, - onleaveyellow: function() { called.push('onleaveyellow'); return StateMachine.ASYNC; }, - onleavered: function() { called.push('onleavered'); return StateMachine.ASYNC; }, - - // specific event callbacks - onbeforewarn: function() { called.push('onbeforewarn'); }, - onbeforepanic: function() { called.push('onbeforepanic'); }, - onbeforecalm: function() { called.push('onbeforecalm'); }, - onbeforeclear: function() { called.push('onbeforeclear'); }, - onafterwarn: function() { called.push('onafterwarn'); }, - onafterpanic: function() { called.push('onafterpanic'); }, - onaftercalm: function() { called.push('onaftercalm'); }, - onafterclear: function() { called.push('onafterclear'); } - } - }); - - called = []; - fsm.warn(); deepEqual(called, ['onbeforewarn', 'onbefore(warn)', 'onleavegreen', 'onleave(green)']); - fsm.transition(); deepEqual(called, ['onbeforewarn', 'onbefore(warn)', 'onleavegreen', 'onleave(green)', 'onenteryellow', 'onenter(yellow)', 'onchange(green,yellow)', 'onafterwarn', 'onafter(warn)']); - - called = []; - fsm.panic(); deepEqual(called, ['onbeforepanic', 'onbefore(panic)', 'onleaveyellow', 'onleave(yellow)']); - fsm.transition(); deepEqual(called, ['onbeforepanic', 'onbefore(panic)', 'onleaveyellow', 'onleave(yellow)', 'onenterred', 'onenter(red)', 'onchange(yellow,red)', 'onafterpanic', 'onafter(panic)']); - - called = []; - fsm.calm(); deepEqual(called, ['onbeforecalm', 'onbefore(calm)', 'onleavered', 'onleave(red)']); - fsm.transition(); deepEqual(called, ['onbeforecalm', 'onbefore(calm)', 'onleavered', 'onleave(red)', 'onenteryellow', 'onenter(yellow)', 'onchange(red,yellow)', 'onaftercalm', 'onafter(calm)']); - - called = []; - fsm.clear(); deepEqual(called, ['onbeforeclear', 'onbefore(clear)', 'onleaveyellow', 'onleave(yellow)']); - fsm.transition(); deepEqual(called, ['onbeforeclear', 'onbefore(clear)', 'onleaveyellow', 'onleave(yellow)', 'onentergreen', 'onenter(green)', 'onchange(yellow,green)', 'onafterclear', 'onafter(clear)']); - -}); - -//----------------------------------------------------------------------------- - -test("cannot fire event during existing transition", function() { - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ], - callbacks: { - onleavegreen: function() { return StateMachine.ASYNC; }, - onleaveyellow: function() { return StateMachine.ASYNC; }, - onleavered: function() { return StateMachine.ASYNC; } - } - }); - - equal(fsm.current, 'green', "initial state should be green"); - equal(fsm.can('warn'), true, "should be able to warn"); - equal(fsm.can('panic'), false, "should NOT be able to panic"); - equal(fsm.can('calm'), false, "should NOT be able to calm"); - equal(fsm.can('clear'), false, "should NOT be able to clear"); - - fsm.warn(); - - equal(fsm.current, 'green', "should still be green because we haven't transitioned yet"); - equal(fsm.can('warn'), false, "should NOT be able to warn - during transition"); - equal(fsm.can('panic'), false, "should NOT be able to panic - during transition"); - equal(fsm.can('calm'), false, "should NOT be able to calm - during transition"); - equal(fsm.can('clear'), false, "should NOT be able to clear - during transition"); - - fsm.transition(); - - equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); - equal(fsm.can('warn'), false, "should NOT be able to warn"); - equal(fsm.can('panic'), true, "should be able to panic"); - equal(fsm.can('calm'), false, "should NOT be able to calm"); - equal(fsm.can('clear'), true, "should be able to clear"); - - fsm.panic(); - - equal(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet"); - equal(fsm.can('warn'), false, "should NOT be able to warn - during transition"); - equal(fsm.can('panic'), false, "should NOT be able to panic - during transition"); - equal(fsm.can('calm'), false, "should NOT be able to calm - during transition"); - equal(fsm.can('clear'), false, "should NOT be able to clear - during transition"); - - fsm.transition(); - - equal(fsm.current, 'red', "panic event should transition from yellow to red"); - equal(fsm.can('warn'), false, "should NOT be able to warn"); - equal(fsm.can('panic'), false, "should NOT be able to panic"); - equal(fsm.can('calm'), true, "should be able to calm"); - equal(fsm.can('clear'), false, "should NOT be able to clear"); - -}); - -//----------------------------------------------------------------------------- - - diff --git a/test/test_basics.js b/test/test_basics.js deleted file mode 100644 index 35ee7b3..0000000 --- a/test/test_basics.js +++ /dev/null @@ -1,701 +0,0 @@ -//----------------------------------------------------------------------------- - -QUnit.module("basic"); - -//----------------------------------------------------------------------------- - -test("standalone state machine", function() { - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ]}); - - equal(fsm.current, 'green', "initial state should be green"); - - fsm.warn(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); - fsm.panic(); equal(fsm.current, 'red', "panic event should transition from yellow to red"); - fsm.calm(); equal(fsm.current, 'yellow', "calm event should transition from red to yellow"); - fsm.clear(); equal(fsm.current, 'green', "clear event should transition from yellow to green"); - -}); - -//----------------------------------------------------------------------------- - -test("targeted state machine", function() { - - StateMachine.create({ - target: this, - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ]}); - - equal(this.current, 'green', "initial state should be green"); - - this.warn(); equal(this.current, 'yellow', "warn event should transition from green to yellow"); - this.panic(); equal(this.current, 'red', "panic event should transition from yellow to red"); - this.calm(); equal(this.current, 'yellow', "calm event should transition from red to yellow"); - this.clear(); equal(this.current, 'green', "clear event should transition from yellow to green"); -}); - -//----------------------------------------------------------------------------- - -test("can & cannot", function() { - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - ]}); - - equal(fsm.current, 'green', "initial state should be green"); - - ok(fsm.can('warn'), "should be able to warn from green state") - ok(fsm.cannot('panic'), "should NOT be able to panic from green state") - ok(fsm.cannot('calm'), "should NOT be able to calm from green state") - - fsm.warn(); - equal(fsm.current, 'yellow', "current state should be yellow"); - ok(fsm.cannot('warn'), "should NOT be able to warn from yellow state") - ok(fsm.can('panic'), "should be able to panic from yellow state") - ok(fsm.cannot('calm'), "should NOT be able to calm from yellow state") - - fsm.panic(); - equal(fsm.current, 'red', "current state should be red"); - ok(fsm.cannot('warn'), "should NOT be able to warn from red state") - ok(fsm.cannot('panic'), "should NOT be able to panic from red state") - ok(fsm.can('calm'), "should be able to calm from red state") - -}); - -//----------------------------------------------------------------------------- - -test("is", function() { - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ]}); - - equal(fsm.current, 'green', "initial state should be green"); - - equal(fsm.is('green'), true, 'current state should match'); - equal(fsm.is('yellow'), false, 'current state should NOT match'); - equal(fsm.is(['green', 'red']), true, 'current state should match when included in array'); - equal(fsm.is(['yellow', 'red']), false, 'current state should NOT match when not included in array'); - - fsm.warn(); - - equal(fsm.current, 'yellow', "current state should be yellow"); - - equal(fsm.is('green'), false, 'current state should NOT match'); - equal(fsm.is('yellow'), true, 'current state should match'); - equal(fsm.is(['green', 'red']), false, 'current state should NOT match when not included in array'); - equal(fsm.is(['yellow', 'red']), true, 'current state should match when included in array'); - -}); - -//----------------------------------------------------------------------------- - -test("transitions", function() { - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ]}); - - equal(fsm.current, 'green', 'current state should be yellow'); - deepEqual(fsm.transitions(), ['warn'], 'current transition(s) should be yellow'); - - fsm.warn(); - equal(fsm.current, 'yellow', 'current state should be yellow'); - deepEqual(fsm.transitions(), ['panic', 'clear'], 'current transition(s) should be panic and clear'); - - fsm.panic(); - equal(fsm.current, 'red', 'current state should be red'); - deepEqual(fsm.transitions(), ['calm'], 'current transition(s) should be calm'); - - fsm.calm(); - equal(fsm.current, 'yellow', 'current state should be yellow'); - deepEqual(fsm.transitions(), ['panic', 'clear'], 'current transion(s) should be panic and clear'); - - fsm.clear(); - equal(fsm.current, 'green', 'current state should be green'); - deepEqual(fsm.transitions(), ['warn'], 'current transion(s) should be warn'); -}); - -//----------------------------------------------------------------------------- - -test("transitions with multiple from states", function() { - - var fsm = StateMachine.create({ - events: [ - { 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' } - ] - }); - - equal(fsm.current, 'none', 'current state should be none'); - deepEqual(fsm.transitions(), ['start'], 'current transition(s) should be start'); - - fsm.start(); - equal(fsm.current, 'green', 'current state should be green'); - deepEqual(fsm.transitions(), ['warn', 'panic'], 'current transition(s) should be warn and panic'); - - fsm.warn(); - equal(fsm.current, 'yellow', 'current state should be yellow'); - deepEqual(fsm.transitions(), ['panic', 'clear'], 'current transition(s) should be panic and clear'); - - fsm.panic(); - equal(fsm.current, 'red', 'current state should be red'); - deepEqual(fsm.transitions(), ['warn', 'clear'], 'current transition(s) should be warn and clear'); - - fsm.clear(); - equal(fsm.current, 'green', 'current state should be green'); - deepEqual(fsm.transitions(), ['warn', 'panic'], 'current transition(s) should be warn and panic'); - -}); - -//----------------------------------------------------------------------------- - -test("isFinished", function() { - - var fsm = StateMachine.create({ - initial: 'green', terminal: 'red', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' } - ]}); - - equal(fsm.current, 'green'); - equal(fsm.isFinished(), false); - - fsm.warn(); - equal(fsm.current, 'yellow'); - equal(fsm.isFinished(), false); - - fsm.panic(); - equal(fsm.current, 'red'); - equal(fsm.isFinished(), true); - -}); - -//----------------------------------------------------------------------------- - -test("isFinished - without specifying terminal state", function() { - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' } - ]}); - - equal(fsm.current, 'green'); - equal(fsm.isFinished(), false); - - fsm.warn(); - equal(fsm.current, 'yellow'); - equal(fsm.isFinished(), false); - - fsm.panic(); - equal(fsm.current, 'red'); - equal(fsm.isFinished(), false); - -}); -//----------------------------------------------------------------------------- - -test("inappropriate events", function() { - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - ]}); - - equal(fsm.current, 'green', "initial state should be green"); - - throws(fsm.panic.bind(fsm), /event panic inappropriate in current state green/); - throws(fsm.calm.bind(fsm), /event calm inappropriate in current state green/); - - fsm.warn(); - equal(fsm.current, 'yellow', "current state should be yellow"); - throws(fsm.warn.bind(fsm), /event warn inappropriate in current state yellow/); - throws(fsm.calm.bind(fsm), /event calm inappropriate in current state yellow/); - - fsm.panic(); - equal(fsm.current, 'red', "current state should be red"); - throws(fsm.warn.bind(fsm), /event warn inappropriate in current state red/); - throws(fsm.panic.bind(fsm), /event panic inappropriate in current state red/); - -}); - -//----------------------------------------------------------------------------- - -test("inappropriate event handling can be customized", function() { - - var fsm = StateMachine.create({ - error: function(name, from, to, args, error, msg) { return msg; }, // return error message instead of throwing an exception - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' } - ]}); - - equal(fsm.current, 'green', "initial state should be green"); - - equal(fsm.panic(), 'event panic inappropriate in current state green'); - equal(fsm.calm(), 'event calm inappropriate in current state green'); - - fsm.warn(); - equal(fsm.current, 'yellow', "current state should be yellow"); - equal(fsm.warn(), 'event warn inappropriate in current state yellow'); - equal(fsm.calm(), 'event calm inappropriate in current state yellow'); - - fsm.panic(); - equal(fsm.current, 'red', "current state should be red"); - equal(fsm.warn(), 'event warn inappropriate in current state red'); - equal(fsm.panic(), 'event panic inappropriate in current state red'); - -}); - -//----------------------------------------------------------------------------- - -test("event is cancelable", function() { - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' } - ]}); - - equal(fsm.current, 'green', 'initial state should be green'); - - fsm.onbeforewarn = function() { return false; } - fsm.warn(); - - equal(fsm.current, 'green', 'state should STAY green when event is cancelled'); - -}); - -//----------------------------------------------------------------------------- - -test("callbacks are ordered correctly", function() { - - var called = []; - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ], - callbacks: { - - // generic callbacks - onbeforeevent: function(event,frmo,to) { called.push('onbefore(' + event + ')'); }, - onafterevent: function(event,frmo,to) { called.push('onafter(' + event + ')'); }, - onleavestate: function(event,from,to) { called.push('onleave(' + from + ')'); }, - onenterstate: function(event,from,to) { called.push('onenter(' + to + ')'); }, - onchangestate: function(event,from,to) { called.push('onchange(' + from + ',' + to + ')'); }, - - // specific state callbacks - onentergreen: function() { called.push('onentergreen'); }, - onenteryellow: function() { called.push('onenteryellow'); }, - onenterred: function() { called.push('onenterred'); }, - onleavegreen: function() { called.push('onleavegreen'); }, - onleaveyellow: function() { called.push('onleaveyellow'); }, - onleavered: function() { called.push('onleavered'); }, - - // specific event callbacks - onbeforewarn: function() { called.push('onbeforewarn'); }, - onbeforepanic: function() { called.push('onbeforepanic'); }, - onbeforecalm: function() { called.push('onbeforecalm'); }, - onbeforeclear: function() { called.push('onbeforeclear'); }, - onafterwarn: function() { called.push('onafterwarn'); }, - onafterpanic: function() { called.push('onafterpanic'); }, - onaftercalm: function() { called.push('onaftercalm'); }, - onafterclear: function() { called.push('onafterclear'); }, - - } - }); - - called = []; - fsm.warn(); - deepEqual(called, [ - 'onbeforewarn', - 'onbefore(warn)', - 'onleavegreen', - 'onleave(green)', - 'onenteryellow', - 'onenter(yellow)', - 'onchange(green,yellow)', - 'onafterwarn', - 'onafter(warn)' - ]); - - called = []; - fsm.panic(); - deepEqual(called, [ - 'onbeforepanic', - 'onbefore(panic)', - 'onleaveyellow', - 'onleave(yellow)', - 'onenterred', - 'onenter(red)', - 'onchange(yellow,red)', - 'onafterpanic', - 'onafter(panic)' - ]); - - called = []; - fsm.calm(); - deepEqual(called, [ - 'onbeforecalm', - 'onbefore(calm)', - 'onleavered', - 'onleave(red)', - 'onenteryellow', - 'onenter(yellow)', - 'onchange(red,yellow)', - 'onaftercalm', - 'onafter(calm)' - ]); - - called = []; - fsm.clear(); - deepEqual(called, [ - 'onbeforeclear', - 'onbefore(clear)', - 'onleaveyellow', - 'onleave(yellow)', - 'onentergreen', - 'onenter(green)', - 'onchange(yellow,green)', - 'onafterclear', - 'onafter(clear)' - ]); - -}); - -//----------------------------------------------------------------------------- - -test("callbacks are ordered correctly - for same state transition", function() { - - var called = []; - - var fsm = StateMachine.create({ - initial: 'waiting', - events: [ - { name: 'data', from: ['waiting', 'receipt'], to: 'receipt' }, - { name: 'nothing', from: ['waiting', 'receipt'], to: 'waiting' }, - { name: 'error', from: ['waiting', 'receipt'], to: 'error' } // bad practice to have event name same as state name - but I'll let it slide just this once - ], - callbacks: { - - // generic callbacks - onbeforeevent: function(event,frmo,to) { called.push('onbefore(' + event + ')'); }, - onafterevent: function(event,frmo,to) { called.push('onafter(' + event + ')'); }, - onleavestate: function(event,from,to) { called.push('onleave(' + from + ')'); }, - onenterstate: function(event,from,to) { called.push('onenter(' + to + ')'); }, - onchangestate: function(event,from,to) { called.push('onchange(' + from + ',' + to + ')'); }, - - // specific state callbacks - onenterwaiting: function() { called.push('onenterwaiting'); }, - onenterreceipt: function() { called.push('onenterreceipt'); }, - onentererror: function() { called.push('onentererror'); }, - onleavewaiting: function() { called.push('onleavewaiting'); }, - onleavereceipt: function() { called.push('onleavereceipt'); }, - onleaveerror: function() { called.push('onleaveerror'); }, - - // specific event callbacks - onbeforedata: function() { called.push('onbeforedata'); }, - onbeforenothing: function() { called.push('onbeforenothing'); }, - onbeforeerror: function() { called.push('onbeforeerror'); }, - onafterdata: function() { called.push('onafterdata'); }, - onafternothing: function() { called.push('onafternothing'); }, - onaftereerror: function() { called.push('onaftererror'); }, - } - }); - - called = []; - fsm.data(); - deepEqual(called, [ - 'onbeforedata', - 'onbefore(data)', - 'onleavewaiting', - 'onleave(waiting)', - 'onenterreceipt', - 'onenter(receipt)', - 'onchange(waiting,receipt)', - 'onafterdata', - 'onafter(data)' - ]); - - called = []; - fsm.data(); // same-state transition - deepEqual(called, [ // so NO enter/leave/change state callbacks are fired - 'onbeforedata', - 'onbefore(data)', - 'onafterdata', - 'onafter(data)' - ]); - - called = []; - fsm.data(); // same-state transition - deepEqual(called, [ // so NO enter/leave/change state callbacks are fired - 'onbeforedata', - 'onbefore(data)', - 'onafterdata', - 'onafter(data)' - ]); - - called = []; - fsm.nothing(); - deepEqual(called, [ - 'onbeforenothing', - 'onbefore(nothing)', - 'onleavereceipt', - 'onleave(receipt)', - 'onenterwaiting', - 'onenter(waiting)', - 'onchange(receipt,waiting)', - 'onafternothing', - 'onafter(nothing)' - ]); - -}); - -//----------------------------------------------------------------------------- - -test("callback arguments are correct", function() { - - var expected = { event: 'startup', from: 'none', to: 'green' }; // first expected callback - - var verify_expected = function(event,from,to,a,b,c) { - equal(event, expected.event) - equal(from, expected.from) - equal(to, expected.to) - equal(a, expected.a) - equal(b, expected.b) - equal(c, expected.c) - }; - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ], - callbacks: { - - // generic callbacks - onbeforeevent: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, - onafterevent: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, - onleavestate: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, - onenterstate: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, - onchangestate: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, - - // specific state callbacks - onentergreen: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, - onenteryellow: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, - onenterred: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, - onleavegreen: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, - onleaveyellow: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, - onleavered: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, - - // specific event callbacks - onbeforewarn: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, - onbeforepanic: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, - onbeforecalm: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, - onbeforeclear: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, - onafterwarn: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, - onafterpanic: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, - onaftercalm: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, - onafterclear: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); } - } - }); - - expected = { event: 'warn', from: 'green', to: 'yellow', a: 1, b: 2, c: 3 }; - fsm.warn(1,2,3); - - expected = { event: 'panic', from: 'yellow', to: 'red', a: 4, b: 5, c: 6 }; - fsm.panic(4,5,6); - - expected = { event: 'calm', from: 'red', to: 'yellow', a: 'foo', b: 'bar', c: null }; - fsm.calm('foo', 'bar'); - - expected = { event: 'clear', from: 'yellow', to: 'green', a: null, b: null, c: null }; - fsm.clear(); - -}); - -//----------------------------------------------------------------------------- - -test("exceptions in caller-provided callbacks are not swallowed (github issue #17)", function() { - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' } - ], - callbacks: { - onenteryellow: function() { throw 'oops'; } - }}); - - equal(fsm.current, 'green', "initial state should be green"); - - throws(fsm.warn.bind(fsm), /oops/); -}); - -//----------------------------------------------------------------------------- - -test("no-op transitions (github issue #5)", function() { - - var fsm = StateMachine.create({ - initial: 'green', - events: [ - { name: 'noop', from: 'green', /* no-op */ }, - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'calm', from: 'red', to: 'yellow' }, - { name: 'clear', from: 'yellow', to: 'green' } - ]}); - - equal(fsm.current, 'green', "initial state should be green"); - - ok(fsm.can('noop'), "should be able to noop from green state") - ok(fsm.can('warn'), "should be able to warn from green state") - - fsm.noop(); equal(fsm.current, 'green', "noop event should not cause a transition (there is no 'to' specified)"); - fsm.warn(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); - - ok(fsm.cannot('noop'), "should NOT be able to noop from yellow state") - ok(fsm.cannot('warn'), "should NOT be able to warn from yellow state") - -}); - -//----------------------------------------------------------------------------- - -test("wildcard 'from' allows event from any state (github issue #11)", function() { - - var fsm = StateMachine.create({ - initial: 'stopped', - events: [ - { 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' } - ]}); - - equal(fsm.current, 'stopped', "initial state should be stopped"); - - fsm.prepare(); equal(fsm.current, 'ready', "prepare event should transition from stopped to ready"); - fsm.stop(); equal(fsm.current, 'stopped', "stop event should transition from ready to stopped"); - - fsm.prepare(); equal(fsm.current, 'ready', "prepare event should transition from stopped to ready"); - fsm.start(); equal(fsm.current, 'running', "start event should transition from ready to running"); - fsm.stop(); equal(fsm.current, 'stopped', "stop event should transition from running to stopped"); - - fsm.prepare(); equal(fsm.current, 'ready', "prepare event should transition from stopped to ready"); - fsm.start(); equal(fsm.current, 'running', "start event should transition from ready to running"); - fsm.pause(); equal(fsm.current, 'paused', "pause event should transition from running to paused"); - fsm.stop(); equal(fsm.current, 'stopped', "stop event should transition from paused to stopped"); - -}); - -//----------------------------------------------------------------------------- - -test("missing 'from' allows event from any state (github issue #11) ", function() { - - var fsm = StateMachine.create({ - initial: 'stopped', - events: [ - { 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' } - ]}); - - equal(fsm.current, 'stopped', "initial state should be stopped"); - - fsm.prepare(); equal(fsm.current, 'ready', "prepare event should transition from stopped to ready"); - fsm.stop(); equal(fsm.current, 'stopped', "stop event should transition from ready to stopped"); - - fsm.prepare(); equal(fsm.current, 'ready', "prepare event should transition from stopped to ready"); - fsm.start(); equal(fsm.current, 'running', "start event should transition from ready to running"); - fsm.stop(); equal(fsm.current, 'stopped', "stop event should transition from running to stopped"); - - fsm.prepare(); equal(fsm.current, 'ready', "prepare event should transition from stopped to ready"); - fsm.start(); equal(fsm.current, 'running', "start event should transition from ready to running"); - fsm.pause(); equal(fsm.current, 'paused', "pause event should transition from running to paused"); - fsm.stop(); equal(fsm.current, 'stopped', "stop event should transition from paused to stopped"); - -}); - -//----------------------------------------------------------------------------- - -test("event return values (github issue #12) ", function() { - - var fsm = StateMachine.create({ - initial: 'stopped', - events: [ - { name: 'prepare', from: 'stopped', to: 'ready' }, - { name: 'fake', from: 'ready', to: 'running' }, - { name: 'start', from: 'ready', to: 'running' } - ], - callbacks: { - onbeforefake: function(event,from,to,a,b,c) { return false; }, // this event will be cancelled - onleaveready: function(event,from,to,a,b,c) { return StateMachine.ASYNC; } // this state transition is ASYNC - } - }); - - equal(fsm.current, 'stopped', "initial state should be stopped"); - - equal(fsm.prepare(), StateMachine.Result.SUCCEEDED, "expected event to have SUCCEEDED"); - equal(fsm.current, 'ready', "prepare event should transition from stopped to ready"); - - equal(fsm.fake(), StateMachine.Result.CANCELLED, "expected event to have been CANCELLED"); - equal(fsm.current, 'ready', "cancelled event should not cause a transition"); - - equal(fsm.start(), StateMachine.Result.PENDING, "expected event to cause a PENDING asynchronous transition"); - equal(fsm.current, 'ready', "async transition hasn't happened yet"); - - equal(fsm.transition(), StateMachine.Result.SUCCEEDED, "expected async transition to have SUCCEEDED"); - equal(fsm.current, 'running', "async transition should now be complete"); - -}); - diff --git a/test/test_classes.js b/test/test_classes.js deleted file mode 100644 index 4c6dc2b..0000000 --- a/test/test_classes.js +++ /dev/null @@ -1,92 +0,0 @@ -//----------------------------------------------------------------------------- - -QUnit.module("classes"); - -//----------------------------------------------------------------------------- - -test("prototype based state machine", function() { - - var myFSM = function() { - this.counter = 42; - this.startup(); - }; - - myFSM.prototype = { - onwarn: function() { this.counter++; } - } - - StateMachine.create({ - target: myFSM.prototype, - events: [ - { name: 'startup', from: 'none', to: 'green' }, - { name: 'warn', from: 'green', to: 'yellow' }, - { name: 'panic', from: 'yellow', to: 'red' }, - { name: 'clear', from: 'yellow', to: 'green' } - ] - }); - - var a = new myFSM(); - var b = new myFSM(); - - equal(a.current, 'green', 'start with correct state'); - equal(b.current, 'green', 'start with correct state'); - - equal(a.counter, 42, 'start with correct counter'); - equal(b.counter, 42, 'start with correct counter'); - - a.warn(); - - equal(a.current, 'yellow', 'maintain independent current state'); - equal(b.current, 'green', 'maintain independent current state'); - - equal(a.counter, 43, 'counter for (a) should have incremented'); - equal(b.counter, 42, 'counter for (b) should remain untouched'); - - ok(a.hasOwnProperty('current'), "each instance should have its own current state"); - ok(b.hasOwnProperty('current'), "each instance should have its own current state"); - ok(!a.hasOwnProperty('warn'), "each instance should NOT have its own event methods"); - ok(!b.hasOwnProperty('warn'), "each instance should NOT have its own event methods"); - ok(a.warn === b.warn, "each instance should share event methods"); - ok(a.warn === a.__proto__.warn, "each instance event methods come from its shared prototype"); - ok(b.warn === b.__proto__.warn, "each instance event methods come from its shared prototype"); - -}); - -//----------------------------------------------------------------------------- - -test("github issue 19", function() { - - var Foo = function() { - this.counter = 7; - this.initFSM(); - }; - - Foo.prototype.onenterready = function() { this.counter++; }; - Foo.prototype.onenterrunning = function() { this.counter++; }; - - StateMachine.create({ - target : Foo.prototype, - initial: { state: 'ready', event: 'initFSM', defer: true }, // unfortunately, trying to apply an IMMEDIATE initial state wont work on prototype based FSM, it MUST be deferred and called in the constructor for each instance - events : [{name: 'execute', from: 'ready', to: 'running'}, - {name: 'abort', from: 'running', to: 'ready'}] - }); - - var foo = new Foo(); - var bar = new Foo(); - - equal(foo.current, 'ready', 'start with correct state'); - equal(bar.current, 'ready', 'start with correct state'); - - equal(foo.counter, 8, 'start with correct counter 7 (from constructor) + 1 (from onenterready)'); - equal(bar.counter, 8, 'start with correct counter 7 (from constructor) + 1 (from onenterready)'); - - foo.execute(); // transition foo, but NOT bar - - equal(foo.current, 'running', 'changed state'); - equal(bar.current, 'ready', 'state remains the same'); - - equal(foo.counter, 9, 'incremented counter during onenterrunning'); - equal(bar.counter, 8, 'counter remains the same'); - -}); - diff --git a/test/test_initialize.js b/test/test_initialize.js deleted file mode 100644 index ae2e2a0..0000000 --- a/test/test_initialize.js +++ /dev/null @@ -1,122 +0,0 @@ -//----------------------------------------------------------------------------- - -QUnit.module("special initialization options", { - - setup: function() { - this.called = []; - this.onbeforeevent = function(event,from,to) { this.called.push('onbefore(' + event + ')'); }, - this.onafterevent = function(event,from,to) { this.called.push('onafter(' + event + ')'); }, - this.onleavestate = function(event,from,to) { this.called.push('onleave(' + from + ')'); }, - this.onenterstate = function(event,from,to) { this.called.push('onenter(' + to + ')'); }, - this.onchangestate = function(event,from,to) { this.called.push('onchange(' + from + ',' + to + ')'); }; - this.onbeforeinit = function() { this.called.push("onbeforeinit"); }; - this.onafterinit = function() { this.called.push("onafterinit"); }; - this.onbeforestartup = function() { this.called.push("onbeforestartup"); }; - this.onafterstartup = function() { this.called.push("onafterstartup"); }; - this.onbeforepanic = function() { this.called.push("onbeforepanic"); }; - this.onafterpanic = function() { this.called.push("onafterpanic"); }; - this.onbeforecalm = function() { this.called.push("onbeforecalm"); }; - this.onaftercalm = function() { this.called.push("onaftercalm"); }; - this.onenternone = function() { this.called.push("onenternone"); }; - this.onentergreen = function() { this.called.push("onentergreen"); }; - this.onenterred = function() { this.called.push("onenterred"); }; - this.onleavenone = function() { this.called.push("onleavenone"); }; - this.onleavegreen = function() { this.called.push("onleavegreen"); }; - this.onleavered = function() { this.called.push("onleavered"); }; - } - -}); - -//----------------------------------------------------------------------------- - -test("initial state defaults to 'none'", function() { - StateMachine.create({ - target: this, - events: [ - { name: 'panic', from: 'green', to: 'red' }, - { name: 'calm', from: 'red', to: 'green' } - ]}); - equal(this.current, 'none'); - deepEqual(this.called, []); -}); - -//----------------------------------------------------------------------------- - -test("initial state can be specified", function() { - StateMachine.create({ - target: this, - initial: 'green', - events: [ - { name: 'panic', from: 'green', to: 'red' }, - { name: 'calm', from: 'red', to: 'green' } - ]}); - equal(this.current, 'green'); - deepEqual(this.called, [ - "onbeforestartup", - "onbefore(startup)", - "onleavenone", - "onleave(none)", - "onentergreen", - "onenter(green)", - "onchange(none,green)", - "onafterstartup", - "onafter(startup)" - ]); -}); - -//----------------------------------------------------------------------------- - -test("startup event name can be specified", function() { - StateMachine.create({ - target: this, - initial: { state: 'green', event: 'init' }, - events: [ - { name: 'panic', from: 'green', to: 'red' }, - { name: 'calm', from: 'red', to: 'green' } - ]}); - equal(this.current, 'green'); - deepEqual(this.called, [ - "onbeforeinit", - "onbefore(init)", - "onleavenone", - "onleave(none)", - "onentergreen", - "onenter(green)", - "onchange(none,green)", - "onafterinit", - "onafter(init)" - ]); -}); - -//----------------------------------------------------------------------------- - -test("startup event can be deferred", function() { - StateMachine.create({ - target: this, - initial: { state: 'green', event: 'init', defer: true }, - events: [ - { name: 'panic', from: 'green', to: 'red' }, - { name: 'calm', from: 'red', to: 'green' } - ]}); - equal(this.current, 'none'); - deepEqual(this.called, []); - - this.init(); - - equal(this.current, 'green'); - deepEqual(this.called, [ - "onbeforeinit", - "onbefore(init)", - "onleavenone", - "onleave(none)", - "onentergreen", - "onenter(green)", - "onchange(none,green)", - "onafterinit", - "onafter(init)" - ]); -}); - -//----------------------------------------------------------------------------- - - diff --git a/test/transitions.js b/test/transitions.js new file mode 100644 index 0000000..942a3b4 --- /dev/null +++ b/test/transitions.js @@ -0,0 +1,311 @@ +import test from 'ava' +import StateMachine from '../src/app' +import LifecycleLogger from './helpers/lifecycle_logger' + +//----------------------------------------------------------------------------- + +test('basic transition from state to state', 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' } + ] + }); + + 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) + +}) + +//----------------------------------------------------------------------------- + +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 ] }, + ]) + +}) + +//------------------------------------------------------------------------------------------------- diff --git a/test/util/camelize.js b/test/util/camelize.js new file mode 100644 index 0000000..84318a2 --- /dev/null +++ b/test/util/camelize.js @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..0fb3561 --- /dev/null +++ b/test/util/mixin.js @@ -0,0 +1,65 @@ +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 new file mode 100644 index 0000000..a97b2af --- /dev/null +++ b/test/wildcards.js @@ -0,0 +1,212 @@ +import test from 'ava' +import StateMachine from '../src/app' + +//----------------------------------------------------------------------------- + +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('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('wildcard :from allows transition to a state that is never declared in any other :from transition ', 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' } + ] + }); + + t.is(fsm.state, 'none') + t.is(fsm.can('step'), true) + t.is(fsm.can('other'), true) + + fsm.step() + + 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 => { + + 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' } + ] + }); + + 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', 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('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') + +}) + +//----------------------------------------------------------------------------- diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..497dba2 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,43 @@ +module.exports = function(env) { + + 'use strict' + + const webpack = require('webpack'), + glob = require('glob'), + path = require('path'), + pascalize = require('pascal-case'), + source = 'src', + output = 'lib', + config = []; + + config.push({ + name: 'state-machine', + library: 'StateMachine', + entry: 'app' + }) + + glob.sync("src/plugin/*.js").forEach(function(plugin) { + const name = path.basename(plugin, '.js'); + config.push({ + library: pascalize('state-machine-' + name), + entry: 'plugin/' + name, + name: name + }) + }); + + return config.map(function(cfg) { + return { + entry: cfg.entry, + resolve: { + modules: [ source ] + }, + output: { + filename: path.join(output, cfg.name + '.js'), + library: cfg.library, + libraryTarget: 'umd', + umdNamedDefine: true + } + } + }); + +}