├── .gitignore ├── LICENSE ├── README.md ├── RELEASE_NOTES.md ├── Rakefile ├── bower.json ├── demo ├── demo.css ├── demo.js └── images │ ├── alerts.green.png │ ├── alerts.red.png │ └── alerts.yellow.png ├── index.html ├── minifier ├── LICENSE.TXT ├── minifier.rb └── yuicompressor-2.4.6.jar ├── package.json ├── state-machine.js ├── state-machine.min.js ├── state-machine.ts └── test ├── index.html ├── qunit ├── qunit.css └── qunit.js ├── requirejs ├── index.html └── require.js ├── runner.js ├── test_advanced.js ├── test_async.js ├── test_basics.js ├── test_classes.js └── test_initialize.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, 2013, 2014, 2015, Jake Gordon and contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Javascript Finite State Machine (v2.3.5) 2 | ======================================== 3 | 4 | This standalone javascript micro-framework provides a finite state machine for your pleasure. 5 | 6 | * You can find the [code here](https://github.com/jakesgordon/javascript-state-machine) 7 | * You can find a [description here](http://codeincomplete.com/posts/2013/1/26/javascript_state_machine_v2_2_0/) 8 | * You can find a [working demo here](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/example/) 9 | 10 | This library has also been ported to: 11 | 12 | * [Go](https://github.com/looplab/fsm) by @maxpersson 13 | * [Python](https://github.com/oxplot/fysom) by @oxplot 14 | 15 | Download 16 | ======== 17 | 18 | You can download [state-machine.js](https://github.com/jakesgordon/javascript-state-machine/raw/master/state-machine.js), 19 | or the [minified version](https://github.com/jakesgordon/javascript-state-machine/raw/master/state-machine.min.js) 20 | 21 | Alternatively: 22 | 23 | git clone git@github.com:jakesgordon/javascript-state-machine 24 | 25 | 26 | * All code is in state-machine.js 27 | * Minified version provided in state-machine.min.js 28 | * No 3rd party library is required 29 | * Demo can be found in /index.html 30 | * QUnit (browser) tests can be found in /test/index.html 31 | * QUnit (headless) tests can be run with "node test/runner.js" (after installing node-qunit with "npm install") 32 | 33 | Usage 34 | ===== 35 | 36 | Include `state-machine.js` in your web application, or, for nodejs `require("javascript-state-machine.js")`. 37 | 38 | In its simplest form, create a standalone state machine using: 39 | 40 | var fsm = StateMachine.create({ 41 | initial: 'green', 42 | events: [ 43 | { name: 'warn', from: 'green', to: 'yellow' }, 44 | { name: 'panic', from: 'yellow', to: 'red' }, 45 | { name: 'calm', from: 'red', to: 'yellow' }, 46 | { name: 'clear', from: 'yellow', to: 'green' } 47 | ]}); 48 | 49 | ... will create an object with a method for each event: 50 | 51 | * fsm.warn() - transition from 'green' to 'yellow' 52 | * fsm.panic() - transition from 'yellow' to 'red' 53 | * fsm.calm() - transition from 'red' to 'yellow' 54 | * fsm.clear() - transition from 'yellow' to 'green' 55 | 56 | along with the following members: 57 | 58 | * fsm.current - contains the current state 59 | * fsm.is(s) - return true if state `s` is the current state 60 | * fsm.can(e) - return true if event `e` can be fired in the current state 61 | * fsm.cannot(e) - return true if event `e` cannot be fired in the current state 62 | * fsm.transitions() - return list of events that are allowed from the current state 63 | 64 | Multiple 'from' and 'to' states for a single event 65 | ================================================== 66 | 67 | If an event is allowed **from** multiple states, and always transitions to the same 68 | state, then simply provide an array of states in the `from` attribute of an event. However, 69 | if an event is allowed from multiple states, but should transition **to** a different 70 | state depending on the current state, then provide multiple event entries with 71 | the same name: 72 | 73 | var fsm = StateMachine.create({ 74 | initial: 'hungry', 75 | events: [ 76 | { name: 'eat', from: 'hungry', to: 'satisfied' }, 77 | { name: 'eat', from: 'satisfied', to: 'full' }, 78 | { name: 'eat', from: 'full', to: 'sick' }, 79 | { name: 'rest', from: ['hungry', 'satisfied', 'full', 'sick'], to: 'hungry' }, 80 | ]}); 81 | 82 | This example will create an object with 2 event methods: 83 | 84 | * fsm.eat() 85 | * fsm.rest() 86 | 87 | The `rest` event will always transition to the `hungry` state, while the `eat` event 88 | will transition to a state that is dependent on the current state. 89 | 90 | >> NOTE: The `rest` event could use a wildcard '*' for the 'from' state if it should be 91 | allowed from any current state. 92 | 93 | >> NOTE: The `rest` event in the above example can also be specified as multiple events with 94 | the same name if you prefer the verbose approach. 95 | 96 | Callbacks 97 | ========= 98 | 99 | 4 types of callback are available by attaching methods to your StateMachine using the following naming conventions: 100 | 101 | * `onbeforeEVENT` - fired before the event 102 | * `onleaveSTATE` - fired when leaving the old state 103 | * `onenterSTATE` - fired when entering the new state 104 | * `onafterEVENT` - fired after the event 105 | 106 | >> (using your **specific** EVENT and STATE names) 107 | 108 | For convenience, the 2 most useful callbacks can be shortened: 109 | 110 | * `onEVENT` - convenience shorthand for `onafterEVENT` 111 | * `onSTATE` - convenience shorthand for `onenterSTATE` 112 | 113 | In addition, 4 general-purpose callbacks can be used to capture **all** event and state changes: 114 | 115 | * `onbeforeevent` - fired before *any* event 116 | * `onleavestate` - fired when leaving *any* state 117 | * `onenterstate` - fired when entering *any* state 118 | * `onafterevent` - fired after *any* event 119 | 120 | All callbacks will be passed the same arguments: 121 | 122 | * **event** name 123 | * **from** state 124 | * **to** state 125 | * _(followed by any arguments you passed into the original event method)_ 126 | 127 | Callbacks can be specified when the state machine is first created: 128 | 129 | var fsm = StateMachine.create({ 130 | initial: 'green', 131 | events: [ 132 | { name: 'warn', from: 'green', to: 'yellow' }, 133 | { name: 'panic', from: 'yellow', to: 'red' }, 134 | { name: 'calm', from: 'red', to: 'yellow' }, 135 | { name: 'clear', from: 'yellow', to: 'green' } 136 | ], 137 | callbacks: { 138 | onpanic: function(event, from, to, msg) { alert('panic! ' + msg); }, 139 | onclear: function(event, from, to, msg) { alert('thanks to ' + msg); }, 140 | ongreen: function(event, from, to) { document.body.className = 'green'; }, 141 | onyellow: function(event, from, to) { document.body.className = 'yellow'; }, 142 | onred: function(event, from, to) { document.body.className = 'red'; }, 143 | } 144 | }); 145 | 146 | fsm.panic('killer bees'); 147 | fsm.clear('sedatives in the honey pots'); 148 | ... 149 | 150 | Additionally, they can be added and removed from the state machine at any time: 151 | 152 | fsm.ongreen = null; 153 | fsm.onyellow = null; 154 | fsm.onred = null; 155 | fsm.onenterstate = function(event, from, to) { document.body.className = to; }; 156 | 157 | 158 | The order in which callbacks occur is as follows: 159 | 160 | >> assume event **go** transitions from **red** state to **green** 161 | 162 | * `onbeforego` - specific handler for the **go** event only 163 | * `onbeforeevent` - generic handler for all events 164 | * `onleavered` - specific handler for the **red** state only 165 | * `onleavestate` - generic handler for all states 166 | * `onentergreen` - specific handler for the **green** state only 167 | * `onenterstate` - generic handler for all states 168 | * `onaftergo` - specific handler for the **go** event only 169 | * `onafterevent` - generic handler for all events 170 | 171 | >> NOTE: the legacy `onchangestate` handler has been deprecated and will be removed in a future version 172 | 173 | You can affect the event in 3 ways: 174 | 175 | * return `false` from an `onbeforeEVENT` handler to cancel the event. 176 | * return `false` from an `onleaveSTATE` handler to cancel the event. 177 | * return `ASYNC` from an `onleaveSTATE` handler to perform an asynchronous state transition (see next section) 178 | 179 | Asynchronous State Transitions 180 | ============================== 181 | 182 | Sometimes, you need to execute some asynchronous code during a state transition and ensure the 183 | new state is not entered until your code has completed. 184 | 185 | A good example of this is when you transition out of a `menu` state, perhaps you want to gradually 186 | fade the menu away, or slide it off the screen and don't want to transition to your `game` state 187 | until after that animation has been performed. 188 | 189 | You can now return `StateMachine.ASYNC` from your `onleavestate` handler and the state machine 190 | will be _'put on hold'_ until you are ready to trigger the transition using the new `transition()` 191 | method. 192 | 193 | For example, using jQuery effects: 194 | 195 | var fsm = StateMachine.create({ 196 | 197 | initial: 'menu', 198 | 199 | events: [ 200 | { name: 'play', from: 'menu', to: 'game' }, 201 | { name: 'quit', from: 'game', to: 'menu' } 202 | ], 203 | 204 | callbacks: { 205 | 206 | onentermenu: function() { $('#menu').show(); }, 207 | onentergame: function() { $('#game').show(); }, 208 | 209 | onleavemenu: function() { 210 | $('#menu').fadeOut('fast', function() { 211 | fsm.transition(); 212 | }); 213 | return StateMachine.ASYNC; // tell StateMachine to defer next state until we call transition (in fadeOut callback above) 214 | }, 215 | 216 | onleavegame: function() { 217 | $('#game').slideDown('slow', function() { 218 | fsm.transition(); 219 | }; 220 | return StateMachine.ASYNC; // tell StateMachine to defer next state until we call transition (in slideDown callback above) 221 | } 222 | 223 | } 224 | }); 225 | 226 | >> _NOTE: If you decide to cancel the ASYNC event, you can call `fsm.transition.cancel();` 227 | 228 | State Machine Classes 229 | ===================== 230 | 231 | You can also turn all instances of a _class_ into an FSM by applying 232 | the state machine functionality to the prototype, including your callbacks 233 | in your prototype, and providing a `startup` event for use when constructing 234 | instances: 235 | 236 | MyFSM = function() { // my constructor function 237 | this.startup(); 238 | }; 239 | 240 | MyFSM.prototype = { 241 | 242 | onpanic: function(event, from, to) { alert('panic'); }, 243 | onclear: function(event, from, to) { alert('all is clear'); }, 244 | 245 | // my other prototype methods 246 | 247 | }; 248 | 249 | StateMachine.create({ 250 | target: MyFSM.prototype, 251 | events: [ 252 | { name: 'startup', from: 'none', to: 'green' }, 253 | { name: 'warn', from: 'green', to: 'yellow' }, 254 | { name: 'panic', from: 'yellow', to: 'red' }, 255 | { name: 'calm', from: 'red', to: 'yellow' }, 256 | { name: 'clear', from: 'yellow', to: 'green' } 257 | ]}); 258 | 259 | 260 | This should be easy to adjust to fit your appropriate mechanism for object construction. 261 | 262 | >> _NOTE: the `startup` event can be given any name, but it must be present in some form to 263 | ensure that each instance constructed is initialized with its own unique `current` state._ 264 | 265 | Initialization Options 266 | ====================== 267 | 268 | How the state machine should initialize can depend on your application requirements, so 269 | the library provides a number of simple options. 270 | 271 | By default, if you don't specify any initial state, the state machine will be in the `'none'` 272 | state and you would need to provide an event to take it out of this state: 273 | 274 | var fsm = StateMachine.create({ 275 | events: [ 276 | { name: 'startup', from: 'none', to: 'green' }, 277 | { name: 'panic', from: 'green', to: 'red' }, 278 | { name: 'calm', from: 'red', to: 'green' }, 279 | ]}); 280 | alert(fsm.current); // "none" 281 | fsm.startup(); 282 | alert(fsm.current); // "green" 283 | 284 | If you specify the name of your initial state (as in all the earlier examples), then an 285 | implicit `startup` event will be created for you and fired when the state machine is constructed. 286 | 287 | var fsm = StateMachine.create({ 288 | initial: 'green', 289 | events: [ 290 | { name: 'panic', from: 'green', to: 'red' }, 291 | { name: 'calm', from: 'red', to: 'green' }, 292 | ]}); 293 | alert(fsm.current); // "green" 294 | 295 | If your object already has a `startup` method you can use a different name for the initial event 296 | 297 | var fsm = StateMachine.create({ 298 | initial: { state: 'green', event: 'init' }, 299 | events: [ 300 | { name: 'panic', from: 'green', to: 'red' }, 301 | { name: 'calm', from: 'red', to: 'green' }, 302 | ]}); 303 | alert(fsm.current); // "green" 304 | 305 | Finally, if you want to wait to call the initial state transition event until a later date you 306 | can `defer` it: 307 | 308 | var fsm = StateMachine.create({ 309 | initial: { state: 'green', event: 'init', defer: true }, 310 | events: [ 311 | { name: 'panic', from: 'green', to: 'red' }, 312 | { name: 'calm', from: 'red', to: 'green' }, 313 | ]}); 314 | alert(fsm.current); // "none" 315 | fsm.init(); 316 | alert(fsm.current); // "green" 317 | 318 | Of course, we have now come full circle, this last example is pretty much functionally the 319 | same as the first example in this section where you simply define your own startup event. 320 | 321 | So you have a number of choices available to you when initializing your state machine. 322 | 323 | >> _IMPORTANT NOTE: if you are using the pattern described in the previous section "State Machine 324 | Classes", and wish to declare an `initial` state in this manner, you MUST use the `defer: true` 325 | attribute and manually call the starting event in your constructor function. This will ensure 326 | that each instance gets its own unique `current` state, rather than an (unwanted) shared 327 | `current` state on the prototype object itself._ 328 | 329 | Handling Failures 330 | ====================== 331 | 332 | By default, if you try to call an event method that is not allowed in the current state, the 333 | state machine will throw an exception. If you prefer to handle the problem yourself, you can 334 | define a custom `error` handler: 335 | 336 | var fsm = StateMachine.create({ 337 | initial: 'green', 338 | error: function(eventName, from, to, args, errorCode, errorMessage) { 339 | return 'event ' + eventName + ' was naughty :- ' + errorMessage; 340 | }, 341 | events: [ 342 | { name: 'panic', from: 'green', to: 'red' }, 343 | { name: 'calm', from: 'red', to: 'green' }, 344 | ]}); 345 | alert(fsm.calm()); // "event calm was naughty :- event not allowed in current state green" 346 | 347 | Release Notes 348 | ============= 349 | 350 | See [RELEASE NOTES](https://github.com/jakesgordon/javascript-state-machine/blob/master/RELEASE_NOTES.md) file. 351 | 352 | License 353 | ======= 354 | 355 | See [LICENSE](https://github.com/jakesgordon/javascript-state-machine/blob/master/LICENSE) file. 356 | 357 | Contact 358 | ======= 359 | 360 | If you have any ideas, feedback, requests or bug reports, you can reach me at 361 | [jake@codeincomplete.com](mailto:jake@codeincomplete.com), or via 362 | my website: [Code inComplete](http://codeincomplete.com/) 363 | 364 | 365 | 366 | 367 | 368 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | Version 2.3.5 (January 20 2014) 2 | ------------------------------- 3 | 4 | * fix for broken transitions() method (issue #74) 5 | 6 | Version 2.3.4 (January 17 2014) 7 | ------------------------------- 8 | 9 | * helper method to list which events are allowed from the current state (issue #71 - thanks to @mgoldsborough and @chopj) 10 | 11 | Version 2.3.3 (October 17 2014) 12 | ------------------------------- 13 | 14 | * added web worker compatability (issue #65 - thanks to @offirmo) 15 | 16 | Version 2.3.2 (March 16 2014) 17 | ----------------------------- 18 | 19 | * had to bump the version number after messing up npmjs.org package registration 20 | 21 | Version 2.3.0 (March 15 2014) 22 | ----------------------------- 23 | 24 | * Added support for bower 25 | * Added support for nodejs (finally) 26 | * Added ability to run tests in console via nodejs ("npm install" to get node-qunit, then "node test/runner.js") 27 | 28 | Version 2.2.0 (January 26th 2013) 29 | --------------------------------- 30 | 31 | * Added optional `final` state(s) and `isFinished()` helper method (issue #23) 32 | * extended `fsm.is()` to accept an array of states (in addition to a single state) 33 | * Added generic event callbacks 'onbeforeevent' and 'onafterevent' (issue #28) 34 | * Added generic state callbacks 'onleavestate' and 'onenterstate' (issue #28) 35 | * Fixed 'undefined' event return codes (issue #34) - pull from gentooboontoo (thanks!) 36 | * Allow async event transition to be cancelled (issue #22) 37 | * [read more...](http://codeincomplete.com/posts/2013/1/26/javascript_state_machine_v2_2_0/) 38 | 39 | Version 2.1.0 (January 7th 2012) 40 | -------------------------------- 41 | 42 | * Wrapped in self executing function to be more easily used with loaders like `require.js` or `curl.js` (issue #15) 43 | * Allow event to be cancelled by returning `false` from `onleavestate` handler (issue #13) - WARNING: this breaks backward compatibility for async transitions (you now need to return `StateMachine.ASYNC` instead of `false`) 44 | * Added explicit return values for event methods (issue #12) 45 | * Added support for wildcard events that can be fired 'from' any state (issue #11) 46 | * Added support for no-op events that transition 'to' the same state (issue #5) 47 | * extended custom error callback to handle any exceptions caused by caller provided callbacks 48 | * added custom error callback to override exception when an illegal state transition is attempted (thanks to cboone) 49 | * fixed typos (thanks to cboone) 50 | * fixed issue #4 - ensure before/after event hooks are called even if the event doesn't result in a state change 51 | 52 | Version 2.0.0 (August 19th 2011) 53 | -------------------------------- 54 | 55 | * adding support for asynchronous state transitions (see README) - with lots of qunit tests (see test/async.js). 56 | * consistent arguments for ALL callbacks, first 3 args are ALWAYS event name, from state and to state, followed by whatever arguments the user passed to the original event method. 57 | * added a generic `onchangestate(event,from,to)` callback to detect all state changes with a single function. 58 | * allow callbacks to be declared at creation time (instead of having to attach them afterwards) 59 | * renamed 'hooks' => 'callbacks' 60 | * [read more...](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/) 61 | 62 | Version 1.2.0 (June 21st 2011) 63 | ------------------------------ 64 | * allows the same event to transition to different states, depending on the current state (see 'Multiple...' section in README.md) 65 | * [read more...](http://codeincomplete.com/posts/2011/6/21/javascript_state_machine_v1_2_0/) 66 | 67 | Version 1.0.0 (June 1st 2011) 68 | ----------------------------- 69 | * initial version 70 | * [read more...](http://codeincomplete.com/posts/2011/6/1/javascript_state_machine/) 71 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | 2 | desc "create minified version of state-machine.js" 3 | task :minify do 4 | require File.expand_path(File.join(File.dirname(__FILE__), 'minifier/minifier')) 5 | Minifier.enabled = true 6 | Minifier.minify('state-machine.js') 7 | end 8 | 9 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "javascript-state-machine", 3 | "version": "2.3.5", 4 | "homepage": "https://github.com/jakesgordon/javascript-state-machine", 5 | "authors": [ 6 | "Jake Gordon " 7 | ], 8 | "description": "a simple finite state machine library", 9 | "main": "state-machine.js", 10 | "moduleType": [ 11 | "amd", 12 | "globals", 13 | "node" 14 | ], 15 | "keywords": [ 16 | "state machine", 17 | "server", 18 | "client" 19 | ], 20 | "license": "MIT", 21 | "ignore": [ 22 | "**/.*", 23 | "node_modules", 24 | "bower_components", 25 | "test", 26 | "tests" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | #demo { width: 400px; margin: 0 auto; text-align: center; } 2 | 3 | #controls { text-align: center; } 4 | 5 | #demo #notes { margin-bottom: 1em; } 6 | #demo #diagram { width: 400px; height: 275px; } 7 | #demo #output { width: 100%; height: 30em; } 8 | 9 | #demo.green #diagram { background: url(images/alerts.green.png); } 10 | #demo.yellow #diagram { background: url(images/alerts.yellow.png); } 11 | #demo.red #diagram { background: url(images/alerts.red.png); } 12 | 13 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | Demo = function() { 2 | 3 | var output = document.getElementById('output'), 4 | demo = document.getElementById('demo'), 5 | panic = document.getElementById('panic'), 6 | warn = document.getElementById('warn'), 7 | calm = document.getElementById('calm'), 8 | clear = document.getElementById('clear'), 9 | count = 0; 10 | 11 | var log = function(msg, separate) { 12 | count = count + (separate ? 1 : 0); 13 | output.value = count + ": " + msg + "\n" + (separate ? "\n" : "") + output.value; 14 | demo.className = fsm.current; 15 | panic.disabled = fsm.cannot('panic'); 16 | warn.disabled = fsm.cannot('warn'); 17 | calm.disabled = fsm.cannot('calm'); 18 | clear.disabled = fsm.cannot('clear'); 19 | }; 20 | 21 | var fsm = StateMachine.create({ 22 | 23 | events: [ 24 | { name: 'start', from: 'none', to: 'green' }, 25 | { name: 'warn', from: 'green', to: 'yellow' }, 26 | { name: 'panic', from: 'green', to: 'red' }, 27 | { name: 'panic', from: 'yellow', to: 'red' }, 28 | { name: 'calm', from: 'red', to: 'yellow' }, 29 | { name: 'clear', from: 'red', to: 'green' }, 30 | { name: 'clear', from: 'yellow', to: 'green' }, 31 | ], 32 | 33 | callbacks: { 34 | onbeforestart: function(event, from, to) { log("STARTING UP"); }, 35 | onstart: function(event, from, to) { log("READY"); }, 36 | 37 | onbeforewarn: function(event, from, to) { log("START EVENT: warn!", true); }, 38 | onbeforepanic: function(event, from, to) { log("START EVENT: panic!", true); }, 39 | onbeforecalm: function(event, from, to) { log("START EVENT: calm!", true); }, 40 | onbeforeclear: function(event, from, to) { log("START EVENT: clear!", true); }, 41 | 42 | onwarn: function(event, from, to) { log("FINISH EVENT: warn!"); }, 43 | onpanic: function(event, from, to) { log("FINISH EVENT: panic!"); }, 44 | oncalm: function(event, from, to) { log("FINISH EVENT: calm!"); }, 45 | onclear: function(event, from, to) { log("FINISH EVENT: clear!"); }, 46 | 47 | onleavegreen: function(event, from, to) { log("LEAVE STATE: green"); }, 48 | onleaveyellow: function(event, from, to) { log("LEAVE STATE: yellow"); }, 49 | onleavered: function(event, from, to) { log("LEAVE STATE: red"); async(to); return StateMachine.ASYNC; }, 50 | 51 | ongreen: function(event, from, to) { log("ENTER STATE: green"); }, 52 | onyellow: function(event, from, to) { log("ENTER STATE: yellow"); }, 53 | onred: function(event, from, to) { log("ENTER STATE: red"); }, 54 | 55 | onchangestate: function(event, from, to) { log("CHANGED STATE: " + from + " to " + to); } 56 | } 57 | }); 58 | 59 | var async = function(to) { 60 | pending(to, 3); 61 | setTimeout(function() { 62 | pending(to, 2); 63 | setTimeout(function() { 64 | pending(to, 1); 65 | setTimeout(function() { 66 | fsm.transition(); // trigger deferred state transition 67 | }, 1000); 68 | }, 1000); 69 | }, 1000); 70 | }; 71 | 72 | var pending = function(to, n) { log("PENDING STATE: " + to + " in ..." + n); }; 73 | 74 | fsm.start(); 75 | return fsm; 76 | 77 | }(); 78 | 79 | -------------------------------------------------------------------------------- /demo/images/alerts.green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raphaelfeng/typescript-state-machine/54f54456efae2b3574f363948956a4656aa4f5a0/demo/images/alerts.green.png -------------------------------------------------------------------------------- /demo/images/alerts.red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raphaelfeng/typescript-state-machine/54f54456efae2b3574f363948956a4656aa4f5a0/demo/images/alerts.red.png -------------------------------------------------------------------------------- /demo/images/alerts.yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raphaelfeng/typescript-state-machine/54f54456efae2b3574f363948956a4656aa4f5a0/demo/images/alerts.yellow.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Javascript Finite State Machine 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |

Finite State Machine

14 | 15 |
16 | 17 | 18 | 19 | 20 |
21 | 22 |
23 |
24 | 25 |
26 | dashed lines are asynchronous state transitions (3 seconds) 27 |
28 | 29 | 31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /minifier/LICENSE.TXT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raphaelfeng/typescript-state-machine/54f54456efae2b3574f363948956a4656aa4f5a0/minifier/LICENSE.TXT -------------------------------------------------------------------------------- /minifier/minifier.rb: -------------------------------------------------------------------------------- 1 | module Minifier 2 | 3 | class << self 4 | attr_accessor :enabled 5 | attr_accessor :extensions 6 | end 7 | 8 | self.extensions = ['.js', '.css'] 9 | 10 | def self.available? 11 | @available ||= !`which java`.empty? # warning: linux only way of checking if java is available 12 | end 13 | 14 | def self.enabled?(name = nil) 15 | enabled && available? && (name.nil? || extensions.include?(File.extname(name))) 16 | end 17 | 18 | def self.minified_name(name) 19 | if enabled?(name) 20 | ext = File.extname(name) 21 | name.sub(ext, ".min#{ext}") 22 | else 23 | name 24 | end 25 | end 26 | 27 | def self.minify(name) 28 | if name && enabled?(name) && File.exists?(name) 29 | minified_name = minified_name(name) 30 | `java -jar "#{File.dirname(__FILE__)}/yuicompressor-2.4.6.jar" "#{name}" -o "#{minified_name}"` 31 | end 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /minifier/yuicompressor-2.4.6.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raphaelfeng/typescript-state-machine/54f54456efae2b3574f363948956a4656aa4f5a0/minifier/yuicompressor-2.4.6.jar -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "javascript-state-machine", 3 | "description": "A simple finite state machine library", 4 | "homepage": "https://github.com/jakesgordon/javascript-state-machine", 5 | "keywords": ["state machine", "server", "client"], 6 | "author": "Jake Gordon ", 7 | "repository": {"type": "git", "url": "git://github.com/jakesgordon/javascript-state-machine.git"}, 8 | "main": "state-machine.js", 9 | "devDependencies": { 10 | "qunit": "~0.6.2" 11 | }, 12 | "version": "2.3.5" 13 | } 14 | -------------------------------------------------------------------------------- /state-machine.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Javascript State Machine Library - https://github.com/jakesgordon/javascript-state-machine 4 | 5 | Copyright (c) 2012, 2013, 2014, 2015, Jake Gordon and contributors 6 | Released under the MIT license - https://github.com/jakesgordon/javascript-state-machine/blob/master/LICENSE 7 | 8 | */ 9 | var StateMachine; 10 | (function (StateMachine) { 11 | //--------------------------------------------------------------------------- 12 | StateMachine.VERSION = "2.3.5"; 13 | //--------------------------------------------------------------------------- 14 | StateMachine.Result = { 15 | SUCCEEDED: 1, 16 | NOTRANSITION: 2, 17 | CANCELLED: 3, 18 | PENDING: 4 // the event is asynchronous and the caller is in control of when the transition occurs 19 | }; 20 | StateMachine.Error = { 21 | INVALID_TRANSITION: 100, 22 | PENDING_TRANSITION: 200, 23 | INVALID_CALLBACK: 300 // caller provided callback function threw an exception 24 | }; 25 | StateMachine.WILDCARD = '*'; 26 | StateMachine.ASYNC = 'async'; 27 | //--------------------------------------------------------------------------- 28 | function create(cfg, target) { 29 | 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 } 30 | var terminal = cfg.terminal || cfg['final']; 31 | var fsm = target || cfg.target || {}; 32 | var events = cfg.events || []; 33 | var callbacks = cfg.callbacks || {}; 34 | var map = {}; // track state transitions allowed for an event { event: { from: [ to ] } } 35 | var transitions = {}; // track events allowed from a state { state: [ event ] } 36 | var add = function (e) { 37 | var from = (e.from instanceof Array) ? e.from : (e.from ? [e.from] : [StateMachine.WILDCARD]); // allow 'wildcard' transition if 'from' is not specified 38 | map[e.name] = map[e.name] || {}; 39 | for (var n = 0; n < from.length; n++) { 40 | transitions[from[n]] = transitions[from[n]] || []; 41 | transitions[from[n]].push(e.name); 42 | map[e.name][from[n]] = e.to || from[n]; // allow no-op transition if 'to' is not specified 43 | } 44 | }; 45 | if (initial) { 46 | initial.event = initial.event || 'startup'; 47 | add({ name: initial.event, from: 'none', to: initial.state }); 48 | } 49 | for (var n = 0; n < events.length; n++) 50 | add(events[n]); 51 | for (var name in map) { 52 | if (map.hasOwnProperty(name)) 53 | fsm[name] = buildEvent(name, map[name]); 54 | } 55 | for (var name in callbacks) { 56 | if (callbacks.hasOwnProperty(name)) 57 | fsm[name] = callbacks[name]; 58 | } 59 | fsm.current = 'none'; 60 | fsm.is = function (state) { 61 | return (state instanceof Array) ? (state.indexOf(this.current) >= 0) : (this.current === state); 62 | }; 63 | fsm.can = function (event) { 64 | return !this.transition && (map[event].hasOwnProperty(this.current) || map[event].hasOwnProperty(StateMachine.WILDCARD)); 65 | }; 66 | fsm.cannot = function (event) { 67 | return !this.can(event); 68 | }; 69 | fsm.transitions = function () { 70 | return transitions[this.current]; 71 | }; 72 | fsm.isFinished = function () { 73 | return this.is(terminal); 74 | }; 75 | fsm.error = cfg.error || function (name, from, to, args, error, msg, e) { 76 | throw e || msg; 77 | }; // 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) 78 | if (initial && !initial.defer) 79 | fsm[initial.event](); 80 | return fsm; 81 | } 82 | StateMachine.create = create; 83 | ; 84 | //=========================================================================== 85 | function doCallback(fsm, func, name, from, to, args) { 86 | if (func) { 87 | try { 88 | return func.apply(fsm, [name, from, to].concat(args)); 89 | } 90 | catch (e) { 91 | return fsm.error(name, from, to, args, StateMachine.Error.INVALID_CALLBACK, "an exception occurred in a caller-provided callback function", e); 92 | } 93 | } 94 | } 95 | function beforeAnyEvent(fsm, name, from, to, args) { 96 | return doCallback(fsm, fsm['onbeforeevent'], name, from, to, args); 97 | } 98 | function afterAnyEvent(fsm, name, from, to, args) { 99 | return doCallback(fsm, fsm['onafterevent'] || fsm['onevent'], name, from, to, args); 100 | } 101 | function leaveAnyState(fsm, name, from, to, args) { 102 | return doCallback(fsm, fsm['onleavestate'], name, from, to, args); 103 | } 104 | function enterAnyState(fsm, name, from, to, args) { 105 | return doCallback(fsm, fsm['onenterstate'] || fsm['onstate'], name, from, to, args); 106 | } 107 | function changeState(fsm, name, from, to, args) { 108 | return doCallback(fsm, fsm['onchangestate'], name, from, to, args); 109 | } 110 | function beforeThisEvent(fsm, name, from, to, args) { 111 | return doCallback(fsm, fsm['onbefore' + name], name, from, to, args); 112 | } 113 | function afterThisEvent(fsm, name, from, to, args) { 114 | return doCallback(fsm, fsm['onafter' + name] || fsm['on' + name], name, from, to, args); 115 | } 116 | function leaveThisState(fsm, name, from, to, args) { 117 | return doCallback(fsm, fsm['onleave' + from], name, from, to, args); 118 | } 119 | function enterThisState(fsm, name, from, to, args) { 120 | return doCallback(fsm, fsm['onenter' + to] || fsm['on' + to], name, from, to, args); 121 | } 122 | function beforeEvent(fsm, name, from, to, args) { 123 | if ((false === beforeThisEvent(fsm, name, from, to, args)) || (false === beforeAnyEvent(fsm, name, from, to, args))) 124 | return false; 125 | } 126 | function afterEvent(fsm, name, from, to, args) { 127 | afterThisEvent(fsm, name, from, to, args); 128 | afterAnyEvent(fsm, name, from, to, args); 129 | } 130 | function leaveState(fsm, name, from, to, args) { 131 | var specific = leaveThisState(fsm, name, from, to, args), general = leaveAnyState(fsm, name, from, to, args); 132 | if ((false === specific) || (false === general)) 133 | return false; 134 | else if ((StateMachine.ASYNC === specific) || (StateMachine.ASYNC === general)) 135 | return StateMachine.ASYNC; 136 | } 137 | function enterState(fsm, name, from, to, args) { 138 | enterThisState(fsm, name, from, to, args); 139 | enterAnyState(fsm, name, from, to, args); 140 | } 141 | //=========================================================================== 142 | function buildEvent(name, map) { 143 | return function () { 144 | var from = this.current; 145 | var to = map[from] || map[StateMachine.WILDCARD] || from; 146 | var args = Array.prototype.slice.call(arguments); // turn arguments into pure array 147 | if (this.transition) 148 | return this.error(name, from, to, args, StateMachine.Error.PENDING_TRANSITION, "event " + name + " inappropriate because previous transition did not complete"); 149 | if (this.cannot(name)) 150 | return this.error(name, from, to, args, StateMachine.Error.INVALID_TRANSITION, "event " + name + " inappropriate in current state " + this.current); 151 | if (false === beforeEvent(this, name, from, to, args)) 152 | return StateMachine.Result.CANCELLED; 153 | if (from === to) { 154 | afterEvent(this, name, from, to, args); 155 | return StateMachine.Result.NOTRANSITION; 156 | } 157 | // 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) 158 | var fsm = this; 159 | this.transition = function () { 160 | fsm.transition = null; // this method should only ever be called once 161 | fsm.current = to; 162 | enterState(fsm, name, from, to, args); 163 | changeState(fsm, name, from, to, args); 164 | afterEvent(fsm, name, from, to, args); 165 | return StateMachine.Result.SUCCEEDED; 166 | }; 167 | this.transition.cancel = function () { 168 | fsm.transition = null; 169 | afterEvent(fsm, name, from, to, args); 170 | }; 171 | var leave = leaveState(this, name, from, to, args); 172 | if (false === leave) { 173 | this.transition = null; 174 | return StateMachine.Result.CANCELLED; 175 | } 176 | else if (StateMachine.ASYNC === leave) { 177 | return StateMachine.Result.PENDING; 178 | } 179 | else { 180 | if (this.transition) 181 | return this.transition(); 182 | } 183 | }; 184 | } 185 | })(StateMachine || (StateMachine = {})); // StateMachine 186 | -------------------------------------------------------------------------------- /state-machine.min.js: -------------------------------------------------------------------------------- 1 | (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}}}}}()); -------------------------------------------------------------------------------- /state-machine.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Javascript State Machine Library - https://github.com/jakesgordon/javascript-state-machine 4 | 5 | Copyright (c) 2012, 2013, 2014, 2015, Jake Gordon and contributors 6 | Released under the MIT license - https://github.com/jakesgordon/javascript-state-machine/blob/master/LICENSE 7 | 8 | */ 9 | 10 | module StateMachine { 11 | 12 | //--------------------------------------------------------------------------- 13 | 14 | export var VERSION = "2.3.5"; 15 | 16 | //--------------------------------------------------------------------------- 17 | 18 | export var Result = { 19 | SUCCEEDED: 1, // the event transitioned successfully from one state to another 20 | NOTRANSITION: 2, // the event was successfull but no state transition was necessary 21 | CANCELLED: 3, // the event was cancelled by the caller in a beforeEvent callback 22 | PENDING: 4 // the event is asynchronous and the caller is in control of when the transition occurs 23 | }; 24 | 25 | export var Error = { 26 | INVALID_TRANSITION: 100, // caller tried to fire an event that was innapropriate in the current state 27 | PENDING_TRANSITION: 200, // caller tried to fire an event while an async transition was still pending 28 | INVALID_CALLBACK: 300 // caller provided callback function threw an exception 29 | }; 30 | 31 | export var WILDCARD = '*'; 32 | export var ASYNC = 'async'; 33 | 34 | //--------------------------------------------------------------------------- 35 | 36 | export function create(cfg: any, target) { 37 | 38 | var initial: any = (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 } 39 | var terminal = cfg.terminal || cfg['final']; 40 | var fsm = target || cfg.target || {}; 41 | var events = cfg.events || []; 42 | var callbacks = cfg.callbacks || {}; 43 | var map: any = {}; // track state transitions allowed for an event { event: { from: [ to ] } } 44 | var transitions = {}; // track events allowed from a state { state: [ event ] } 45 | 46 | var add = function(e) { 47 | var from = (e.from instanceof Array) ? e.from : (e.from ? [e.from] : [StateMachine.WILDCARD]); // allow 'wildcard' transition if 'from' is not specified 48 | map[e.name] = map[e.name] || {}; 49 | for (var n = 0 ; n < from.length ; n++) { 50 | transitions[from[n]] = transitions[from[n]] || []; 51 | transitions[from[n]].push(e.name); 52 | 53 | map[e.name][from[n]] = e.to || from[n]; // allow no-op transition if 'to' is not specified 54 | } 55 | }; 56 | 57 | if (initial) { 58 | initial.event = initial.event || 'startup'; 59 | add({ name: initial.event, from: 'none', to: initial.state }); 60 | } 61 | 62 | for(var n = 0 ; n < events.length ; n++) 63 | add(events[n]); 64 | 65 | for(var name in map) { 66 | if (map.hasOwnProperty(name)) 67 | fsm[name] = buildEvent(name, map[name]); 68 | } 69 | 70 | for(var name in callbacks) { 71 | if (callbacks.hasOwnProperty(name)) 72 | fsm[name] = callbacks[name] 73 | } 74 | 75 | fsm.current = 'none'; 76 | fsm.is = function(state) { return (state instanceof Array) ? (state.indexOf(this.current) >= 0) : (this.current === state); }; 77 | fsm.can = function(event) { return !this.transition && (map[event].hasOwnProperty(this.current) || map[event].hasOwnProperty(StateMachine.WILDCARD)); } 78 | fsm.cannot = function(event) { return !this.can(event); }; 79 | fsm.transitions = function() { return transitions[this.current]; }; 80 | fsm.isFinished = function() { return this.is(terminal); }; 81 | 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) 82 | 83 | if (initial && !initial.defer) 84 | fsm[initial.event](); 85 | 86 | return fsm; 87 | 88 | }; 89 | 90 | //=========================================================================== 91 | 92 | function doCallback(fsm, func, name, from, to, args) { 93 | if (func) { 94 | try { 95 | return func.apply(fsm, [name, from, to].concat(args)); 96 | } 97 | catch(e) { 98 | return fsm.error(name, from, to, args, StateMachine.Error.INVALID_CALLBACK, "an exception occurred in a caller-provided callback function", e); 99 | } 100 | } 101 | } 102 | 103 | function beforeAnyEvent(fsm, name, from, to, args) { return doCallback(fsm, fsm['onbeforeevent'], name, from, to, args); } 104 | function afterAnyEvent(fsm, name, from, to, args) { return doCallback(fsm, fsm['onafterevent'] || fsm['onevent'], name, from, to, args); } 105 | function leaveAnyState(fsm, name, from, to, args) { return doCallback(fsm, fsm['onleavestate'], name, from, to, args); } 106 | function enterAnyState(fsm, name, from, to, args) { return doCallback(fsm, fsm['onenterstate'] || fsm['onstate'], name, from, to, args); } 107 | function changeState(fsm, name, from, to, args) { return doCallback(fsm, fsm['onchangestate'], name, from, to, args); } 108 | 109 | function beforeThisEvent(fsm, name, from, to, args) { return doCallback(fsm, fsm['onbefore' + name], name, from, to, args); } 110 | function afterThisEvent(fsm, name, from, to, args) { return doCallback(fsm, fsm['onafter' + name] || fsm['on' + name], name, from, to, args); } 111 | function leaveThisState(fsm, name, from, to, args) { return doCallback(fsm, fsm['onleave' + from], name, from, to, args); } 112 | function enterThisState(fsm, name, from, to, args) { return doCallback(fsm, fsm['onenter' + to] || fsm['on' + to], name, from, to, args); } 113 | 114 | function beforeEvent(fsm, name, from, to, args) { 115 | if ((false === beforeThisEvent(fsm, name, from, to, args)) || 116 | (false === beforeAnyEvent( fsm, name, from, to, args))) 117 | return false; 118 | } 119 | 120 | function afterEvent(fsm, name, from, to, args) { 121 | afterThisEvent(fsm, name, from, to, args); 122 | afterAnyEvent( fsm, name, from, to, args); 123 | } 124 | 125 | function leaveState(fsm, name, from, to, args): boolean | string { 126 | var specific = leaveThisState(fsm, name, from, to, args), 127 | general = leaveAnyState( fsm, name, from, to, args); 128 | if ((false === specific) || (false === general)) 129 | return false; 130 | else if ((ASYNC === specific) || (ASYNC === general)) 131 | return ASYNC; 132 | } 133 | 134 | function enterState(fsm, name, from, to, args) { 135 | enterThisState(fsm, name, from, to, args); 136 | enterAnyState( fsm, name, from, to, args); 137 | } 138 | 139 | //=========================================================================== 140 | 141 | function buildEvent(name, map) { 142 | return function() { 143 | 144 | var from = this.current; 145 | var to = map[from] || map[StateMachine.WILDCARD] || from; 146 | var args = Array.prototype.slice.call(arguments); // turn arguments into pure array 147 | 148 | if (this.transition) 149 | return this.error(name, from, to, args, Error.PENDING_TRANSITION, "event " + name + " inappropriate because previous transition did not complete"); 150 | 151 | if (this.cannot(name)) 152 | return this.error(name, from, to, args, Error.INVALID_TRANSITION, "event " + name + " inappropriate in current state " + this.current); 153 | 154 | if (false === beforeEvent(this, name, from, to, args)) 155 | return Result.CANCELLED; 156 | 157 | if (from === to) { 158 | afterEvent(this, name, from, to, args); 159 | return Result.NOTRANSITION; 160 | } 161 | 162 | // 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) 163 | var fsm = this; 164 | this.transition = function() { 165 | fsm.transition = null; // this method should only ever be called once 166 | fsm.current = to; 167 | enterState( fsm, name, from, to, args); 168 | changeState(fsm, name, from, to, args); 169 | afterEvent( fsm, name, from, to, args); 170 | return Result.SUCCEEDED; 171 | }; 172 | this.transition.cancel = function() { // provide a way for caller to cancel async transition if desired (issue #22) 173 | fsm.transition = null; 174 | afterEvent(fsm, name, from, to, args); 175 | } 176 | 177 | var leave = leaveState(this, name, from, to, args); 178 | if (false === leave) { 179 | this.transition = null; 180 | return Result.CANCELLED; 181 | } 182 | else if (ASYNC === leave) { 183 | return Result.PENDING; 184 | } 185 | else { 186 | if (this.transition) // need to check in case user manually called transition() but forgot to return StateMachine.ASYNC 187 | return this.transition(); 188 | } 189 | 190 | }; 191 | } 192 | } // StateMachine 193 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Finite State Machine Tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

QUnit Test Suite

16 |

17 |
18 |

19 |
    20 |
    test markup
    21 | 22 | 23 | -------------------------------------------------------------------------------- /test/qunit/qunit.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * QUnit 1.14.0 3 | * http://qunitjs.com/ 4 | * 5 | * Copyright 2013 jQuery Foundation and other contributors 6 | * Released under the MIT license 7 | * http://jquery.org/license 8 | * 9 | * Date: 2014-01-31T16:40Z 10 | */ 11 | 12 | /** Font Family and Sizes */ 13 | 14 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 15 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 16 | } 17 | 18 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 19 | #qunit-tests { font-size: smaller; } 20 | 21 | 22 | /** Resets */ 23 | 24 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | 30 | /** Header */ 31 | 32 | #qunit-header { 33 | padding: 0.5em 0 0.5em 1em; 34 | 35 | color: #8699A4; 36 | background-color: #0D3349; 37 | 38 | font-size: 1.5em; 39 | line-height: 1em; 40 | font-weight: 400; 41 | 42 | border-radius: 5px 5px 0 0; 43 | } 44 | 45 | #qunit-header a { 46 | text-decoration: none; 47 | color: #C2CCD1; 48 | } 49 | 50 | #qunit-header a:hover, 51 | #qunit-header a:focus { 52 | color: #FFF; 53 | } 54 | 55 | #qunit-testrunner-toolbar label { 56 | display: inline-block; 57 | padding: 0 0.5em 0 0.1em; 58 | } 59 | 60 | #qunit-banner { 61 | height: 5px; 62 | } 63 | 64 | #qunit-testrunner-toolbar { 65 | padding: 0.5em 0 0.5em 2em; 66 | color: #5E740B; 67 | background-color: #EEE; 68 | overflow: hidden; 69 | } 70 | 71 | #qunit-userAgent { 72 | padding: 0.5em 0 0.5em 2.5em; 73 | background-color: #2B81AF; 74 | color: #FFF; 75 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 76 | } 77 | 78 | #qunit-modulefilter-container { 79 | float: right; 80 | } 81 | 82 | /** Tests: Pass/Fail */ 83 | 84 | #qunit-tests { 85 | list-style-position: inside; 86 | } 87 | 88 | #qunit-tests li { 89 | padding: 0.4em 0.5em 0.4em 2.5em; 90 | border-bottom: 1px solid #FFF; 91 | list-style-position: inside; 92 | } 93 | 94 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 95 | display: none; 96 | } 97 | 98 | #qunit-tests li strong { 99 | cursor: pointer; 100 | } 101 | 102 | #qunit-tests li a { 103 | padding: 0.5em; 104 | color: #C2CCD1; 105 | text-decoration: none; 106 | } 107 | #qunit-tests li a:hover, 108 | #qunit-tests li a:focus { 109 | color: #000; 110 | } 111 | 112 | #qunit-tests li .runtime { 113 | float: right; 114 | font-size: smaller; 115 | } 116 | 117 | .qunit-assert-list { 118 | margin-top: 0.5em; 119 | padding: 0.5em; 120 | 121 | background-color: #FFF; 122 | 123 | border-radius: 5px; 124 | } 125 | 126 | .qunit-collapsed { 127 | display: none; 128 | } 129 | 130 | #qunit-tests table { 131 | border-collapse: collapse; 132 | margin-top: 0.2em; 133 | } 134 | 135 | #qunit-tests th { 136 | text-align: right; 137 | vertical-align: top; 138 | padding: 0 0.5em 0 0; 139 | } 140 | 141 | #qunit-tests td { 142 | vertical-align: top; 143 | } 144 | 145 | #qunit-tests pre { 146 | margin: 0; 147 | white-space: pre-wrap; 148 | word-wrap: break-word; 149 | } 150 | 151 | #qunit-tests del { 152 | background-color: #E0F2BE; 153 | color: #374E0C; 154 | text-decoration: none; 155 | } 156 | 157 | #qunit-tests ins { 158 | background-color: #FFCACA; 159 | color: #500; 160 | text-decoration: none; 161 | } 162 | 163 | /*** Test Counts */ 164 | 165 | #qunit-tests b.counts { color: #000; } 166 | #qunit-tests b.passed { color: #5E740B; } 167 | #qunit-tests b.failed { color: #710909; } 168 | 169 | #qunit-tests li li { 170 | padding: 5px; 171 | background-color: #FFF; 172 | border-bottom: none; 173 | list-style-position: inside; 174 | } 175 | 176 | /*** Passing Styles */ 177 | 178 | #qunit-tests li li.pass { 179 | color: #3C510C; 180 | background-color: #FFF; 181 | border-left: 10px solid #C6E746; 182 | } 183 | 184 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 185 | #qunit-tests .pass .test-name { color: #366097; } 186 | 187 | #qunit-tests .pass .test-actual, 188 | #qunit-tests .pass .test-expected { color: #999; } 189 | 190 | #qunit-banner.qunit-pass { background-color: #C6E746; } 191 | 192 | /*** Failing Styles */ 193 | 194 | #qunit-tests li li.fail { 195 | color: #710909; 196 | background-color: #FFF; 197 | border-left: 10px solid #EE5757; 198 | white-space: pre; 199 | } 200 | 201 | #qunit-tests > li:last-child { 202 | border-radius: 0 0 5px 5px; 203 | } 204 | 205 | #qunit-tests .fail { color: #000; background-color: #EE5757; } 206 | #qunit-tests .fail .test-name, 207 | #qunit-tests .fail .module-name { color: #000; } 208 | 209 | #qunit-tests .fail .test-actual { color: #EE5757; } 210 | #qunit-tests .fail .test-expected { color: #008000; } 211 | 212 | #qunit-banner.qunit-fail { background-color: #EE5757; } 213 | 214 | 215 | /** Result */ 216 | 217 | #qunit-testresult { 218 | padding: 0.5em 0.5em 0.5em 2.5em; 219 | 220 | color: #2B81AF; 221 | background-color: #D2E0E6; 222 | 223 | border-bottom: 1px solid #FFF; 224 | } 225 | #qunit-testresult .module-name { 226 | font-weight: 700; 227 | } 228 | 229 | /** Fixture */ 230 | 231 | #qunit-fixture { 232 | position: absolute; 233 | top: -10000px; 234 | left: -10000px; 235 | width: 1000px; 236 | height: 1000px; 237 | } 238 | -------------------------------------------------------------------------------- /test/qunit/qunit.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * QUnit 1.14.0 3 | * http://qunitjs.com/ 4 | * 5 | * Copyright 2013 jQuery Foundation and other contributors 6 | * Released under the MIT license 7 | * http://jquery.org/license 8 | * 9 | * Date: 2014-01-31T16:40Z 10 | */ 11 | 12 | (function( window ) { 13 | 14 | var QUnit, 15 | assert, 16 | config, 17 | onErrorFnPrev, 18 | testId = 0, 19 | fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""), 20 | toString = Object.prototype.toString, 21 | hasOwn = Object.prototype.hasOwnProperty, 22 | // Keep a local reference to Date (GH-283) 23 | Date = window.Date, 24 | setTimeout = window.setTimeout, 25 | clearTimeout = window.clearTimeout, 26 | defined = { 27 | document: typeof window.document !== "undefined", 28 | setTimeout: typeof window.setTimeout !== "undefined", 29 | sessionStorage: (function() { 30 | var x = "qunit-test-string"; 31 | try { 32 | sessionStorage.setItem( x, x ); 33 | sessionStorage.removeItem( x ); 34 | return true; 35 | } catch( e ) { 36 | return false; 37 | } 38 | }()) 39 | }, 40 | /** 41 | * Provides a normalized error string, correcting an issue 42 | * with IE 7 (and prior) where Error.prototype.toString is 43 | * not properly implemented 44 | * 45 | * Based on http://es5.github.com/#x15.11.4.4 46 | * 47 | * @param {String|Error} error 48 | * @return {String} error message 49 | */ 50 | errorString = function( error ) { 51 | var name, message, 52 | errorString = error.toString(); 53 | if ( errorString.substring( 0, 7 ) === "[object" ) { 54 | name = error.name ? error.name.toString() : "Error"; 55 | message = error.message ? error.message.toString() : ""; 56 | if ( name && message ) { 57 | return name + ": " + message; 58 | } else if ( name ) { 59 | return name; 60 | } else if ( message ) { 61 | return message; 62 | } else { 63 | return "Error"; 64 | } 65 | } else { 66 | return errorString; 67 | } 68 | }, 69 | /** 70 | * Makes a clone of an object using only Array or Object as base, 71 | * and copies over the own enumerable properties. 72 | * 73 | * @param {Object} obj 74 | * @return {Object} New object with only the own properties (recursively). 75 | */ 76 | objectValues = function( obj ) { 77 | // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392. 78 | /*jshint newcap: false */ 79 | var key, val, 80 | vals = QUnit.is( "array", obj ) ? [] : {}; 81 | for ( key in obj ) { 82 | if ( hasOwn.call( obj, key ) ) { 83 | val = obj[key]; 84 | vals[key] = val === Object(val) ? objectValues(val) : val; 85 | } 86 | } 87 | return vals; 88 | }; 89 | 90 | 91 | // Root QUnit object. 92 | // `QUnit` initialized at top of scope 93 | QUnit = { 94 | 95 | // call on start of module test to prepend name to all tests 96 | module: function( name, testEnvironment ) { 97 | config.currentModule = name; 98 | config.currentModuleTestEnvironment = testEnvironment; 99 | config.modules[name] = true; 100 | }, 101 | 102 | asyncTest: function( testName, expected, callback ) { 103 | if ( arguments.length === 2 ) { 104 | callback = expected; 105 | expected = null; 106 | } 107 | 108 | QUnit.test( testName, expected, callback, true ); 109 | }, 110 | 111 | test: function( testName, expected, callback, async ) { 112 | var test, 113 | nameHtml = "" + escapeText( testName ) + ""; 114 | 115 | if ( arguments.length === 2 ) { 116 | callback = expected; 117 | expected = null; 118 | } 119 | 120 | if ( config.currentModule ) { 121 | nameHtml = "" + escapeText( config.currentModule ) + ": " + nameHtml; 122 | } 123 | 124 | test = new Test({ 125 | nameHtml: nameHtml, 126 | testName: testName, 127 | expected: expected, 128 | async: async, 129 | callback: callback, 130 | module: config.currentModule, 131 | moduleTestEnvironment: config.currentModuleTestEnvironment, 132 | stack: sourceFromStacktrace( 2 ) 133 | }); 134 | 135 | if ( !validTest( test ) ) { 136 | return; 137 | } 138 | 139 | test.queue(); 140 | }, 141 | 142 | // Specify the number of expected assertions to guarantee that failed test (no assertions are run at all) don't slip through. 143 | expect: function( asserts ) { 144 | if (arguments.length === 1) { 145 | config.current.expected = asserts; 146 | } else { 147 | return config.current.expected; 148 | } 149 | }, 150 | 151 | start: function( count ) { 152 | // QUnit hasn't been initialized yet. 153 | // Note: RequireJS (et al) may delay onLoad 154 | if ( config.semaphore === undefined ) { 155 | QUnit.begin(function() { 156 | // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first 157 | setTimeout(function() { 158 | QUnit.start( count ); 159 | }); 160 | }); 161 | return; 162 | } 163 | 164 | config.semaphore -= count || 1; 165 | // don't start until equal number of stop-calls 166 | if ( config.semaphore > 0 ) { 167 | return; 168 | } 169 | // ignore if start is called more often then stop 170 | if ( config.semaphore < 0 ) { 171 | config.semaphore = 0; 172 | QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) ); 173 | return; 174 | } 175 | // A slight delay, to avoid any current callbacks 176 | if ( defined.setTimeout ) { 177 | setTimeout(function() { 178 | if ( config.semaphore > 0 ) { 179 | return; 180 | } 181 | if ( config.timeout ) { 182 | clearTimeout( config.timeout ); 183 | } 184 | 185 | config.blocking = false; 186 | process( true ); 187 | }, 13); 188 | } else { 189 | config.blocking = false; 190 | process( true ); 191 | } 192 | }, 193 | 194 | stop: function( count ) { 195 | config.semaphore += count || 1; 196 | config.blocking = true; 197 | 198 | if ( config.testTimeout && defined.setTimeout ) { 199 | clearTimeout( config.timeout ); 200 | config.timeout = setTimeout(function() { 201 | QUnit.ok( false, "Test timed out" ); 202 | config.semaphore = 1; 203 | QUnit.start(); 204 | }, config.testTimeout ); 205 | } 206 | } 207 | }; 208 | 209 | // We use the prototype to distinguish between properties that should 210 | // be exposed as globals (and in exports) and those that shouldn't 211 | (function() { 212 | function F() {} 213 | F.prototype = QUnit; 214 | QUnit = new F(); 215 | // Make F QUnit's constructor so that we can add to the prototype later 216 | QUnit.constructor = F; 217 | }()); 218 | 219 | /** 220 | * Config object: Maintain internal state 221 | * Later exposed as QUnit.config 222 | * `config` initialized at top of scope 223 | */ 224 | config = { 225 | // The queue of tests to run 226 | queue: [], 227 | 228 | // block until document ready 229 | blocking: true, 230 | 231 | // when enabled, show only failing tests 232 | // gets persisted through sessionStorage and can be changed in UI via checkbox 233 | hidepassed: false, 234 | 235 | // by default, run previously failed tests first 236 | // very useful in combination with "Hide passed tests" checked 237 | reorder: true, 238 | 239 | // by default, modify document.title when suite is done 240 | altertitle: true, 241 | 242 | // by default, scroll to top of the page when suite is done 243 | scrolltop: true, 244 | 245 | // when enabled, all tests must call expect() 246 | requireExpects: false, 247 | 248 | // add checkboxes that are persisted in the query-string 249 | // when enabled, the id is set to `true` as a `QUnit.config` property 250 | urlConfig: [ 251 | { 252 | id: "noglobals", 253 | label: "Check for Globals", 254 | tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." 255 | }, 256 | { 257 | id: "notrycatch", 258 | label: "No try-catch", 259 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." 260 | } 261 | ], 262 | 263 | // Set of all modules. 264 | modules: {}, 265 | 266 | // logging callback queues 267 | begin: [], 268 | done: [], 269 | log: [], 270 | testStart: [], 271 | testDone: [], 272 | moduleStart: [], 273 | moduleDone: [] 274 | }; 275 | 276 | // Initialize more QUnit.config and QUnit.urlParams 277 | (function() { 278 | var i, current, 279 | location = window.location || { search: "", protocol: "file:" }, 280 | params = location.search.slice( 1 ).split( "&" ), 281 | length = params.length, 282 | urlParams = {}; 283 | 284 | if ( params[ 0 ] ) { 285 | for ( i = 0; i < length; i++ ) { 286 | current = params[ i ].split( "=" ); 287 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 288 | 289 | // allow just a key to turn on a flag, e.g., test.html?noglobals 290 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 291 | if ( urlParams[ current[ 0 ] ] ) { 292 | urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] ); 293 | } else { 294 | urlParams[ current[ 0 ] ] = current[ 1 ]; 295 | } 296 | } 297 | } 298 | 299 | QUnit.urlParams = urlParams; 300 | 301 | // String search anywhere in moduleName+testName 302 | config.filter = urlParams.filter; 303 | 304 | // Exact match of the module name 305 | config.module = urlParams.module; 306 | 307 | config.testNumber = []; 308 | if ( urlParams.testNumber ) { 309 | 310 | // Ensure that urlParams.testNumber is an array 311 | urlParams.testNumber = [].concat( urlParams.testNumber ); 312 | for ( i = 0; i < urlParams.testNumber.length; i++ ) { 313 | current = urlParams.testNumber[ i ]; 314 | config.testNumber.push( parseInt( current, 10 ) ); 315 | } 316 | } 317 | 318 | // Figure out if we're running the tests from a server or not 319 | QUnit.isLocal = location.protocol === "file:"; 320 | }()); 321 | 322 | extend( QUnit, { 323 | 324 | config: config, 325 | 326 | // Initialize the configuration options 327 | init: function() { 328 | extend( config, { 329 | stats: { all: 0, bad: 0 }, 330 | moduleStats: { all: 0, bad: 0 }, 331 | started: +new Date(), 332 | updateRate: 1000, 333 | blocking: false, 334 | autostart: true, 335 | autorun: false, 336 | filter: "", 337 | queue: [], 338 | semaphore: 1 339 | }); 340 | 341 | var tests, banner, result, 342 | qunit = id( "qunit" ); 343 | 344 | if ( qunit ) { 345 | qunit.innerHTML = 346 | "

    " + escapeText( document.title ) + "

    " + 347 | "

    " + 348 | "
    " + 349 | "

    " + 350 | "
      "; 351 | } 352 | 353 | tests = id( "qunit-tests" ); 354 | banner = id( "qunit-banner" ); 355 | result = id( "qunit-testresult" ); 356 | 357 | if ( tests ) { 358 | tests.innerHTML = ""; 359 | } 360 | 361 | if ( banner ) { 362 | banner.className = ""; 363 | } 364 | 365 | if ( result ) { 366 | result.parentNode.removeChild( result ); 367 | } 368 | 369 | if ( tests ) { 370 | result = document.createElement( "p" ); 371 | result.id = "qunit-testresult"; 372 | result.className = "result"; 373 | tests.parentNode.insertBefore( result, tests ); 374 | result.innerHTML = "Running...
       "; 375 | } 376 | }, 377 | 378 | // Resets the test setup. Useful for tests that modify the DOM. 379 | /* 380 | DEPRECATED: Use multiple tests instead of resetting inside a test. 381 | Use testStart or testDone for custom cleanup. 382 | This method will throw an error in 2.0, and will be removed in 2.1 383 | */ 384 | reset: function() { 385 | var fixture = id( "qunit-fixture" ); 386 | if ( fixture ) { 387 | fixture.innerHTML = config.fixture; 388 | } 389 | }, 390 | 391 | // Safe object type checking 392 | is: function( type, obj ) { 393 | return QUnit.objectType( obj ) === type; 394 | }, 395 | 396 | objectType: function( obj ) { 397 | if ( typeof obj === "undefined" ) { 398 | return "undefined"; 399 | } 400 | 401 | // Consider: typeof null === object 402 | if ( obj === null ) { 403 | return "null"; 404 | } 405 | 406 | var match = toString.call( obj ).match(/^\[object\s(.*)\]$/), 407 | type = match && match[1] || ""; 408 | 409 | switch ( type ) { 410 | case "Number": 411 | if ( isNaN(obj) ) { 412 | return "nan"; 413 | } 414 | return "number"; 415 | case "String": 416 | case "Boolean": 417 | case "Array": 418 | case "Date": 419 | case "RegExp": 420 | case "Function": 421 | return type.toLowerCase(); 422 | } 423 | if ( typeof obj === "object" ) { 424 | return "object"; 425 | } 426 | return undefined; 427 | }, 428 | 429 | push: function( result, actual, expected, message ) { 430 | if ( !config.current ) { 431 | throw new Error( "assertion outside test context, was " + sourceFromStacktrace() ); 432 | } 433 | 434 | var output, source, 435 | details = { 436 | module: config.current.module, 437 | name: config.current.testName, 438 | result: result, 439 | message: message, 440 | actual: actual, 441 | expected: expected 442 | }; 443 | 444 | message = escapeText( message ) || ( result ? "okay" : "failed" ); 445 | message = "" + message + ""; 446 | output = message; 447 | 448 | if ( !result ) { 449 | expected = escapeText( QUnit.jsDump.parse(expected) ); 450 | actual = escapeText( QUnit.jsDump.parse(actual) ); 451 | output += ""; 452 | 453 | if ( actual !== expected ) { 454 | output += ""; 455 | output += ""; 456 | } 457 | 458 | source = sourceFromStacktrace(); 459 | 460 | if ( source ) { 461 | details.source = source; 462 | output += ""; 463 | } 464 | 465 | output += "
      Expected:
      " + expected + "
      Result:
      " + actual + "
      Diff:
      " + QUnit.diff( expected, actual ) + "
      Source:
      " + escapeText( source ) + "
      "; 466 | } 467 | 468 | runLoggingCallbacks( "log", QUnit, details ); 469 | 470 | config.current.assertions.push({ 471 | result: !!result, 472 | message: output 473 | }); 474 | }, 475 | 476 | pushFailure: function( message, source, actual ) { 477 | if ( !config.current ) { 478 | throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) ); 479 | } 480 | 481 | var output, 482 | details = { 483 | module: config.current.module, 484 | name: config.current.testName, 485 | result: false, 486 | message: message 487 | }; 488 | 489 | message = escapeText( message ) || "error"; 490 | message = "" + message + ""; 491 | output = message; 492 | 493 | output += ""; 494 | 495 | if ( actual ) { 496 | output += ""; 497 | } 498 | 499 | if ( source ) { 500 | details.source = source; 501 | output += ""; 502 | } 503 | 504 | output += "
      Result:
      " + escapeText( actual ) + "
      Source:
      " + escapeText( source ) + "
      "; 505 | 506 | runLoggingCallbacks( "log", QUnit, details ); 507 | 508 | config.current.assertions.push({ 509 | result: false, 510 | message: output 511 | }); 512 | }, 513 | 514 | url: function( params ) { 515 | params = extend( extend( {}, QUnit.urlParams ), params ); 516 | var key, 517 | querystring = "?"; 518 | 519 | for ( key in params ) { 520 | if ( hasOwn.call( params, key ) ) { 521 | querystring += encodeURIComponent( key ) + "=" + 522 | encodeURIComponent( params[ key ] ) + "&"; 523 | } 524 | } 525 | return window.location.protocol + "//" + window.location.host + 526 | window.location.pathname + querystring.slice( 0, -1 ); 527 | }, 528 | 529 | extend: extend, 530 | id: id, 531 | addEvent: addEvent, 532 | addClass: addClass, 533 | hasClass: hasClass, 534 | removeClass: removeClass 535 | // load, equiv, jsDump, diff: Attached later 536 | }); 537 | 538 | /** 539 | * @deprecated: Created for backwards compatibility with test runner that set the hook function 540 | * into QUnit.{hook}, instead of invoking it and passing the hook function. 541 | * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here. 542 | * Doing this allows us to tell if the following methods have been overwritten on the actual 543 | * QUnit object. 544 | */ 545 | extend( QUnit.constructor.prototype, { 546 | 547 | // Logging callbacks; all receive a single argument with the listed properties 548 | // run test/logs.html for any related changes 549 | begin: registerLoggingCallback( "begin" ), 550 | 551 | // done: { failed, passed, total, runtime } 552 | done: registerLoggingCallback( "done" ), 553 | 554 | // log: { result, actual, expected, message } 555 | log: registerLoggingCallback( "log" ), 556 | 557 | // testStart: { name } 558 | testStart: registerLoggingCallback( "testStart" ), 559 | 560 | // testDone: { name, failed, passed, total, runtime } 561 | testDone: registerLoggingCallback( "testDone" ), 562 | 563 | // moduleStart: { name } 564 | moduleStart: registerLoggingCallback( "moduleStart" ), 565 | 566 | // moduleDone: { name, failed, passed, total } 567 | moduleDone: registerLoggingCallback( "moduleDone" ) 568 | }); 569 | 570 | if ( !defined.document || document.readyState === "complete" ) { 571 | config.autorun = true; 572 | } 573 | 574 | QUnit.load = function() { 575 | runLoggingCallbacks( "begin", QUnit, {} ); 576 | 577 | // Initialize the config, saving the execution queue 578 | var banner, filter, i, j, label, len, main, ol, toolbar, val, selection, 579 | urlConfigContainer, moduleFilter, userAgent, 580 | numModules = 0, 581 | moduleNames = [], 582 | moduleFilterHtml = "", 583 | urlConfigHtml = "", 584 | oldconfig = extend( {}, config ); 585 | 586 | QUnit.init(); 587 | extend(config, oldconfig); 588 | 589 | config.blocking = false; 590 | 591 | len = config.urlConfig.length; 592 | 593 | for ( i = 0; i < len; i++ ) { 594 | val = config.urlConfig[i]; 595 | if ( typeof val === "string" ) { 596 | val = { 597 | id: val, 598 | label: val 599 | }; 600 | } 601 | config[ val.id ] = QUnit.urlParams[ val.id ]; 602 | if ( !val.value || typeof val.value === "string" ) { 603 | urlConfigHtml += ""; 611 | } else { 612 | urlConfigHtml += ""; 646 | } 647 | } 648 | for ( i in config.modules ) { 649 | if ( config.modules.hasOwnProperty( i ) ) { 650 | moduleNames.push(i); 651 | } 652 | } 653 | numModules = moduleNames.length; 654 | moduleNames.sort( function( a, b ) { 655 | return a.localeCompare( b ); 656 | }); 657 | moduleFilterHtml += ""; 668 | 669 | // `userAgent` initialized at top of scope 670 | userAgent = id( "qunit-userAgent" ); 671 | if ( userAgent ) { 672 | userAgent.innerHTML = navigator.userAgent; 673 | } 674 | 675 | // `banner` initialized at top of scope 676 | banner = id( "qunit-header" ); 677 | if ( banner ) { 678 | banner.innerHTML = "" + banner.innerHTML + " "; 679 | } 680 | 681 | // `toolbar` initialized at top of scope 682 | toolbar = id( "qunit-testrunner-toolbar" ); 683 | if ( toolbar ) { 684 | // `filter` initialized at top of scope 685 | filter = document.createElement( "input" ); 686 | filter.type = "checkbox"; 687 | filter.id = "qunit-filter-pass"; 688 | 689 | addEvent( filter, "click", function() { 690 | var tmp, 691 | ol = id( "qunit-tests" ); 692 | 693 | if ( filter.checked ) { 694 | ol.className = ol.className + " hidepass"; 695 | } else { 696 | tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 697 | ol.className = tmp.replace( / hidepass /, " " ); 698 | } 699 | if ( defined.sessionStorage ) { 700 | if (filter.checked) { 701 | sessionStorage.setItem( "qunit-filter-passed-tests", "true" ); 702 | } else { 703 | sessionStorage.removeItem( "qunit-filter-passed-tests" ); 704 | } 705 | } 706 | }); 707 | 708 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) { 709 | filter.checked = true; 710 | // `ol` initialized at top of scope 711 | ol = id( "qunit-tests" ); 712 | ol.className = ol.className + " hidepass"; 713 | } 714 | toolbar.appendChild( filter ); 715 | 716 | // `label` initialized at top of scope 717 | label = document.createElement( "label" ); 718 | label.setAttribute( "for", "qunit-filter-pass" ); 719 | label.setAttribute( "title", "Only show tests and assertions that fail. Stored in sessionStorage." ); 720 | label.innerHTML = "Hide passed tests"; 721 | toolbar.appendChild( label ); 722 | 723 | urlConfigContainer = document.createElement("span"); 724 | urlConfigContainer.innerHTML = urlConfigHtml; 725 | // For oldIE support: 726 | // * Add handlers to the individual elements instead of the container 727 | // * Use "click" instead of "change" for checkboxes 728 | // * Fallback from event.target to event.srcElement 729 | addEvents( urlConfigContainer.getElementsByTagName("input"), "click", function( event ) { 730 | var params = {}, 731 | target = event.target || event.srcElement; 732 | params[ target.name ] = target.checked ? 733 | target.defaultValue || true : 734 | undefined; 735 | window.location = QUnit.url( params ); 736 | }); 737 | addEvents( urlConfigContainer.getElementsByTagName("select"), "change", function( event ) { 738 | var params = {}, 739 | target = event.target || event.srcElement; 740 | params[ target.name ] = target.options[ target.selectedIndex ].value || undefined; 741 | window.location = QUnit.url( params ); 742 | }); 743 | toolbar.appendChild( urlConfigContainer ); 744 | 745 | if (numModules > 1) { 746 | moduleFilter = document.createElement( "span" ); 747 | moduleFilter.setAttribute( "id", "qunit-modulefilter-container" ); 748 | moduleFilter.innerHTML = moduleFilterHtml; 749 | addEvent( moduleFilter.lastChild, "change", function() { 750 | var selectBox = moduleFilter.getElementsByTagName("select")[0], 751 | selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value); 752 | 753 | window.location = QUnit.url({ 754 | module: ( selectedModule === "" ) ? undefined : selectedModule, 755 | // Remove any existing filters 756 | filter: undefined, 757 | testNumber: undefined 758 | }); 759 | }); 760 | toolbar.appendChild(moduleFilter); 761 | } 762 | } 763 | 764 | // `main` initialized at top of scope 765 | main = id( "qunit-fixture" ); 766 | if ( main ) { 767 | config.fixture = main.innerHTML; 768 | } 769 | 770 | if ( config.autostart ) { 771 | QUnit.start(); 772 | } 773 | }; 774 | 775 | if ( defined.document ) { 776 | addEvent( window, "load", QUnit.load ); 777 | } 778 | 779 | // `onErrorFnPrev` initialized at top of scope 780 | // Preserve other handlers 781 | onErrorFnPrev = window.onerror; 782 | 783 | // Cover uncaught exceptions 784 | // Returning true will suppress the default browser handler, 785 | // returning false will let it run. 786 | window.onerror = function ( error, filePath, linerNr ) { 787 | var ret = false; 788 | if ( onErrorFnPrev ) { 789 | ret = onErrorFnPrev( error, filePath, linerNr ); 790 | } 791 | 792 | // Treat return value as window.onerror itself does, 793 | // Only do our handling if not suppressed. 794 | if ( ret !== true ) { 795 | if ( QUnit.config.current ) { 796 | if ( QUnit.config.current.ignoreGlobalErrors ) { 797 | return true; 798 | } 799 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 800 | } else { 801 | QUnit.test( "global failure", extend( function() { 802 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 803 | }, { validTest: validTest } ) ); 804 | } 805 | return false; 806 | } 807 | 808 | return ret; 809 | }; 810 | 811 | function done() { 812 | config.autorun = true; 813 | 814 | // Log the last module results 815 | if ( config.previousModule ) { 816 | runLoggingCallbacks( "moduleDone", QUnit, { 817 | name: config.previousModule, 818 | failed: config.moduleStats.bad, 819 | passed: config.moduleStats.all - config.moduleStats.bad, 820 | total: config.moduleStats.all 821 | }); 822 | } 823 | delete config.previousModule; 824 | 825 | var i, key, 826 | banner = id( "qunit-banner" ), 827 | tests = id( "qunit-tests" ), 828 | runtime = +new Date() - config.started, 829 | passed = config.stats.all - config.stats.bad, 830 | html = [ 831 | "Tests completed in ", 832 | runtime, 833 | " milliseconds.
      ", 834 | "", 835 | passed, 836 | " assertions of ", 837 | config.stats.all, 838 | " passed, ", 839 | config.stats.bad, 840 | " failed." 841 | ].join( "" ); 842 | 843 | if ( banner ) { 844 | banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" ); 845 | } 846 | 847 | if ( tests ) { 848 | id( "qunit-testresult" ).innerHTML = html; 849 | } 850 | 851 | if ( config.altertitle && defined.document && document.title ) { 852 | // show ✖ for good, ✔ for bad suite result in title 853 | // use escape sequences in case file gets loaded with non-utf-8-charset 854 | document.title = [ 855 | ( config.stats.bad ? "\u2716" : "\u2714" ), 856 | document.title.replace( /^[\u2714\u2716] /i, "" ) 857 | ].join( " " ); 858 | } 859 | 860 | // clear own sessionStorage items if all tests passed 861 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { 862 | // `key` & `i` initialized at top of scope 863 | for ( i = 0; i < sessionStorage.length; i++ ) { 864 | key = sessionStorage.key( i++ ); 865 | if ( key.indexOf( "qunit-test-" ) === 0 ) { 866 | sessionStorage.removeItem( key ); 867 | } 868 | } 869 | } 870 | 871 | // scroll back to top to show results 872 | if ( config.scrolltop && window.scrollTo ) { 873 | window.scrollTo(0, 0); 874 | } 875 | 876 | runLoggingCallbacks( "done", QUnit, { 877 | failed: config.stats.bad, 878 | passed: passed, 879 | total: config.stats.all, 880 | runtime: runtime 881 | }); 882 | } 883 | 884 | /** @return Boolean: true if this test should be ran */ 885 | function validTest( test ) { 886 | var include, 887 | filter = config.filter && config.filter.toLowerCase(), 888 | module = config.module && config.module.toLowerCase(), 889 | fullName = ( test.module + ": " + test.testName ).toLowerCase(); 890 | 891 | // Internally-generated tests are always valid 892 | if ( test.callback && test.callback.validTest === validTest ) { 893 | delete test.callback.validTest; 894 | return true; 895 | } 896 | 897 | if ( config.testNumber.length > 0 ) { 898 | if ( inArray( test.testNumber, config.testNumber ) < 0 ) { 899 | return false; 900 | } 901 | } 902 | 903 | if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { 904 | return false; 905 | } 906 | 907 | if ( !filter ) { 908 | return true; 909 | } 910 | 911 | include = filter.charAt( 0 ) !== "!"; 912 | if ( !include ) { 913 | filter = filter.slice( 1 ); 914 | } 915 | 916 | // If the filter matches, we need to honour include 917 | if ( fullName.indexOf( filter ) !== -1 ) { 918 | return include; 919 | } 920 | 921 | // Otherwise, do the opposite 922 | return !include; 923 | } 924 | 925 | // so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions) 926 | // Later Safari and IE10 are supposed to support error.stack as well 927 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack 928 | function extractStacktrace( e, offset ) { 929 | offset = offset === undefined ? 3 : offset; 930 | 931 | var stack, include, i; 932 | 933 | if ( e.stacktrace ) { 934 | // Opera 935 | return e.stacktrace.split( "\n" )[ offset + 3 ]; 936 | } else if ( e.stack ) { 937 | // Firefox, Chrome 938 | stack = e.stack.split( "\n" ); 939 | if (/^error$/i.test( stack[0] ) ) { 940 | stack.shift(); 941 | } 942 | if ( fileName ) { 943 | include = []; 944 | for ( i = offset; i < stack.length; i++ ) { 945 | if ( stack[ i ].indexOf( fileName ) !== -1 ) { 946 | break; 947 | } 948 | include.push( stack[ i ] ); 949 | } 950 | if ( include.length ) { 951 | return include.join( "\n" ); 952 | } 953 | } 954 | return stack[ offset ]; 955 | } else if ( e.sourceURL ) { 956 | // Safari, PhantomJS 957 | // hopefully one day Safari provides actual stacktraces 958 | // exclude useless self-reference for generated Error objects 959 | if ( /qunit.js$/.test( e.sourceURL ) ) { 960 | return; 961 | } 962 | // for actual exceptions, this is useful 963 | return e.sourceURL + ":" + e.line; 964 | } 965 | } 966 | function sourceFromStacktrace( offset ) { 967 | try { 968 | throw new Error(); 969 | } catch ( e ) { 970 | return extractStacktrace( e, offset ); 971 | } 972 | } 973 | 974 | /** 975 | * Escape text for attribute or text content. 976 | */ 977 | function escapeText( s ) { 978 | if ( !s ) { 979 | return ""; 980 | } 981 | s = s + ""; 982 | // Both single quotes and double quotes (for attributes) 983 | return s.replace( /['"<>&]/g, function( s ) { 984 | switch( s ) { 985 | case "'": 986 | return "'"; 987 | case "\"": 988 | return """; 989 | case "<": 990 | return "<"; 991 | case ">": 992 | return ">"; 993 | case "&": 994 | return "&"; 995 | } 996 | }); 997 | } 998 | 999 | function synchronize( callback, last ) { 1000 | config.queue.push( callback ); 1001 | 1002 | if ( config.autorun && !config.blocking ) { 1003 | process( last ); 1004 | } 1005 | } 1006 | 1007 | function process( last ) { 1008 | function next() { 1009 | process( last ); 1010 | } 1011 | var start = new Date().getTime(); 1012 | config.depth = config.depth ? config.depth + 1 : 1; 1013 | 1014 | while ( config.queue.length && !config.blocking ) { 1015 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { 1016 | config.queue.shift()(); 1017 | } else { 1018 | setTimeout( next, 13 ); 1019 | break; 1020 | } 1021 | } 1022 | config.depth--; 1023 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 1024 | done(); 1025 | } 1026 | } 1027 | 1028 | function saveGlobal() { 1029 | config.pollution = []; 1030 | 1031 | if ( config.noglobals ) { 1032 | for ( var key in window ) { 1033 | if ( hasOwn.call( window, key ) ) { 1034 | // in Opera sometimes DOM element ids show up here, ignore them 1035 | if ( /^qunit-test-output/.test( key ) ) { 1036 | continue; 1037 | } 1038 | config.pollution.push( key ); 1039 | } 1040 | } 1041 | } 1042 | } 1043 | 1044 | function checkPollution() { 1045 | var newGlobals, 1046 | deletedGlobals, 1047 | old = config.pollution; 1048 | 1049 | saveGlobal(); 1050 | 1051 | newGlobals = diff( config.pollution, old ); 1052 | if ( newGlobals.length > 0 ) { 1053 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); 1054 | } 1055 | 1056 | deletedGlobals = diff( old, config.pollution ); 1057 | if ( deletedGlobals.length > 0 ) { 1058 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); 1059 | } 1060 | } 1061 | 1062 | // returns a new Array with the elements that are in a but not in b 1063 | function diff( a, b ) { 1064 | var i, j, 1065 | result = a.slice(); 1066 | 1067 | for ( i = 0; i < result.length; i++ ) { 1068 | for ( j = 0; j < b.length; j++ ) { 1069 | if ( result[i] === b[j] ) { 1070 | result.splice( i, 1 ); 1071 | i--; 1072 | break; 1073 | } 1074 | } 1075 | } 1076 | return result; 1077 | } 1078 | 1079 | function extend( a, b ) { 1080 | for ( var prop in b ) { 1081 | if ( hasOwn.call( b, prop ) ) { 1082 | // Avoid "Member not found" error in IE8 caused by messing with window.constructor 1083 | if ( !( prop === "constructor" && a === window ) ) { 1084 | if ( b[ prop ] === undefined ) { 1085 | delete a[ prop ]; 1086 | } else { 1087 | a[ prop ] = b[ prop ]; 1088 | } 1089 | } 1090 | } 1091 | } 1092 | 1093 | return a; 1094 | } 1095 | 1096 | /** 1097 | * @param {HTMLElement} elem 1098 | * @param {string} type 1099 | * @param {Function} fn 1100 | */ 1101 | function addEvent( elem, type, fn ) { 1102 | if ( elem.addEventListener ) { 1103 | 1104 | // Standards-based browsers 1105 | elem.addEventListener( type, fn, false ); 1106 | } else if ( elem.attachEvent ) { 1107 | 1108 | // support: IE <9 1109 | elem.attachEvent( "on" + type, fn ); 1110 | } else { 1111 | 1112 | // Caller must ensure support for event listeners is present 1113 | throw new Error( "addEvent() was called in a context without event listener support" ); 1114 | } 1115 | } 1116 | 1117 | /** 1118 | * @param {Array|NodeList} elems 1119 | * @param {string} type 1120 | * @param {Function} fn 1121 | */ 1122 | function addEvents( elems, type, fn ) { 1123 | var i = elems.length; 1124 | while ( i-- ) { 1125 | addEvent( elems[i], type, fn ); 1126 | } 1127 | } 1128 | 1129 | function hasClass( elem, name ) { 1130 | return (" " + elem.className + " ").indexOf(" " + name + " ") > -1; 1131 | } 1132 | 1133 | function addClass( elem, name ) { 1134 | if ( !hasClass( elem, name ) ) { 1135 | elem.className += (elem.className ? " " : "") + name; 1136 | } 1137 | } 1138 | 1139 | function removeClass( elem, name ) { 1140 | var set = " " + elem.className + " "; 1141 | // Class name may appear multiple times 1142 | while ( set.indexOf(" " + name + " ") > -1 ) { 1143 | set = set.replace(" " + name + " " , " "); 1144 | } 1145 | // If possible, trim it for prettiness, but not necessarily 1146 | elem.className = typeof set.trim === "function" ? set.trim() : set.replace(/^\s+|\s+$/g, ""); 1147 | } 1148 | 1149 | function id( name ) { 1150 | return defined.document && document.getElementById && document.getElementById( name ); 1151 | } 1152 | 1153 | function registerLoggingCallback( key ) { 1154 | return function( callback ) { 1155 | config[key].push( callback ); 1156 | }; 1157 | } 1158 | 1159 | // Supports deprecated method of completely overwriting logging callbacks 1160 | function runLoggingCallbacks( key, scope, args ) { 1161 | var i, callbacks; 1162 | if ( QUnit.hasOwnProperty( key ) ) { 1163 | QUnit[ key ].call(scope, args ); 1164 | } else { 1165 | callbacks = config[ key ]; 1166 | for ( i = 0; i < callbacks.length; i++ ) { 1167 | callbacks[ i ].call( scope, args ); 1168 | } 1169 | } 1170 | } 1171 | 1172 | // from jquery.js 1173 | function inArray( elem, array ) { 1174 | if ( array.indexOf ) { 1175 | return array.indexOf( elem ); 1176 | } 1177 | 1178 | for ( var i = 0, length = array.length; i < length; i++ ) { 1179 | if ( array[ i ] === elem ) { 1180 | return i; 1181 | } 1182 | } 1183 | 1184 | return -1; 1185 | } 1186 | 1187 | function Test( settings ) { 1188 | extend( this, settings ); 1189 | this.assertions = []; 1190 | this.testNumber = ++Test.count; 1191 | } 1192 | 1193 | Test.count = 0; 1194 | 1195 | Test.prototype = { 1196 | init: function() { 1197 | var a, b, li, 1198 | tests = id( "qunit-tests" ); 1199 | 1200 | if ( tests ) { 1201 | b = document.createElement( "strong" ); 1202 | b.innerHTML = this.nameHtml; 1203 | 1204 | // `a` initialized at top of scope 1205 | a = document.createElement( "a" ); 1206 | a.innerHTML = "Rerun"; 1207 | a.href = QUnit.url({ testNumber: this.testNumber }); 1208 | 1209 | li = document.createElement( "li" ); 1210 | li.appendChild( b ); 1211 | li.appendChild( a ); 1212 | li.className = "running"; 1213 | li.id = this.id = "qunit-test-output" + testId++; 1214 | 1215 | tests.appendChild( li ); 1216 | } 1217 | }, 1218 | setup: function() { 1219 | if ( 1220 | // Emit moduleStart when we're switching from one module to another 1221 | this.module !== config.previousModule || 1222 | // They could be equal (both undefined) but if the previousModule property doesn't 1223 | // yet exist it means this is the first test in a suite that isn't wrapped in a 1224 | // module, in which case we'll just emit a moduleStart event for 'undefined'. 1225 | // Without this, reporters can get testStart before moduleStart which is a problem. 1226 | !hasOwn.call( config, "previousModule" ) 1227 | ) { 1228 | if ( hasOwn.call( config, "previousModule" ) ) { 1229 | runLoggingCallbacks( "moduleDone", QUnit, { 1230 | name: config.previousModule, 1231 | failed: config.moduleStats.bad, 1232 | passed: config.moduleStats.all - config.moduleStats.bad, 1233 | total: config.moduleStats.all 1234 | }); 1235 | } 1236 | config.previousModule = this.module; 1237 | config.moduleStats = { all: 0, bad: 0 }; 1238 | runLoggingCallbacks( "moduleStart", QUnit, { 1239 | name: this.module 1240 | }); 1241 | } 1242 | 1243 | config.current = this; 1244 | 1245 | this.testEnvironment = extend({ 1246 | setup: function() {}, 1247 | teardown: function() {} 1248 | }, this.moduleTestEnvironment ); 1249 | 1250 | this.started = +new Date(); 1251 | runLoggingCallbacks( "testStart", QUnit, { 1252 | name: this.testName, 1253 | module: this.module 1254 | }); 1255 | 1256 | /*jshint camelcase:false */ 1257 | 1258 | 1259 | /** 1260 | * Expose the current test environment. 1261 | * 1262 | * @deprecated since 1.12.0: Use QUnit.config.current.testEnvironment instead. 1263 | */ 1264 | QUnit.current_testEnvironment = this.testEnvironment; 1265 | 1266 | /*jshint camelcase:true */ 1267 | 1268 | if ( !config.pollution ) { 1269 | saveGlobal(); 1270 | } 1271 | if ( config.notrycatch ) { 1272 | this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert ); 1273 | return; 1274 | } 1275 | try { 1276 | this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert ); 1277 | } catch( e ) { 1278 | QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); 1279 | } 1280 | }, 1281 | run: function() { 1282 | config.current = this; 1283 | 1284 | var running = id( "qunit-testresult" ); 1285 | 1286 | if ( running ) { 1287 | running.innerHTML = "Running:
      " + this.nameHtml; 1288 | } 1289 | 1290 | if ( this.async ) { 1291 | QUnit.stop(); 1292 | } 1293 | 1294 | this.callbackStarted = +new Date(); 1295 | 1296 | if ( config.notrycatch ) { 1297 | this.callback.call( this.testEnvironment, QUnit.assert ); 1298 | this.callbackRuntime = +new Date() - this.callbackStarted; 1299 | return; 1300 | } 1301 | 1302 | try { 1303 | this.callback.call( this.testEnvironment, QUnit.assert ); 1304 | this.callbackRuntime = +new Date() - this.callbackStarted; 1305 | } catch( e ) { 1306 | this.callbackRuntime = +new Date() - this.callbackStarted; 1307 | 1308 | QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); 1309 | // else next test will carry the responsibility 1310 | saveGlobal(); 1311 | 1312 | // Restart the tests if they're blocking 1313 | if ( config.blocking ) { 1314 | QUnit.start(); 1315 | } 1316 | } 1317 | }, 1318 | teardown: function() { 1319 | config.current = this; 1320 | if ( config.notrycatch ) { 1321 | if ( typeof this.callbackRuntime === "undefined" ) { 1322 | this.callbackRuntime = +new Date() - this.callbackStarted; 1323 | } 1324 | this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert ); 1325 | return; 1326 | } else { 1327 | try { 1328 | this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert ); 1329 | } catch( e ) { 1330 | QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); 1331 | } 1332 | } 1333 | checkPollution(); 1334 | }, 1335 | finish: function() { 1336 | config.current = this; 1337 | if ( config.requireExpects && this.expected === null ) { 1338 | QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); 1339 | } else if ( this.expected !== null && this.expected !== this.assertions.length ) { 1340 | QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); 1341 | } else if ( this.expected === null && !this.assertions.length ) { 1342 | QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); 1343 | } 1344 | 1345 | var i, assertion, a, b, time, li, ol, 1346 | test = this, 1347 | good = 0, 1348 | bad = 0, 1349 | tests = id( "qunit-tests" ); 1350 | 1351 | this.runtime = +new Date() - this.started; 1352 | config.stats.all += this.assertions.length; 1353 | config.moduleStats.all += this.assertions.length; 1354 | 1355 | if ( tests ) { 1356 | ol = document.createElement( "ol" ); 1357 | ol.className = "qunit-assert-list"; 1358 | 1359 | for ( i = 0; i < this.assertions.length; i++ ) { 1360 | assertion = this.assertions[i]; 1361 | 1362 | li = document.createElement( "li" ); 1363 | li.className = assertion.result ? "pass" : "fail"; 1364 | li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" ); 1365 | ol.appendChild( li ); 1366 | 1367 | if ( assertion.result ) { 1368 | good++; 1369 | } else { 1370 | bad++; 1371 | config.stats.bad++; 1372 | config.moduleStats.bad++; 1373 | } 1374 | } 1375 | 1376 | // store result when possible 1377 | if ( QUnit.config.reorder && defined.sessionStorage ) { 1378 | if ( bad ) { 1379 | sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad ); 1380 | } else { 1381 | sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName ); 1382 | } 1383 | } 1384 | 1385 | if ( bad === 0 ) { 1386 | addClass( ol, "qunit-collapsed" ); 1387 | } 1388 | 1389 | // `b` initialized at top of scope 1390 | b = document.createElement( "strong" ); 1391 | b.innerHTML = this.nameHtml + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 1392 | 1393 | addEvent(b, "click", function() { 1394 | var next = b.parentNode.lastChild, 1395 | collapsed = hasClass( next, "qunit-collapsed" ); 1396 | ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" ); 1397 | }); 1398 | 1399 | addEvent(b, "dblclick", function( e ) { 1400 | var target = e && e.target ? e.target : window.event.srcElement; 1401 | if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) { 1402 | target = target.parentNode; 1403 | } 1404 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 1405 | window.location = QUnit.url({ testNumber: test.testNumber }); 1406 | } 1407 | }); 1408 | 1409 | // `time` initialized at top of scope 1410 | time = document.createElement( "span" ); 1411 | time.className = "runtime"; 1412 | time.innerHTML = this.runtime + " ms"; 1413 | 1414 | // `li` initialized at top of scope 1415 | li = id( this.id ); 1416 | li.className = bad ? "fail" : "pass"; 1417 | li.removeChild( li.firstChild ); 1418 | a = li.firstChild; 1419 | li.appendChild( b ); 1420 | li.appendChild( a ); 1421 | li.appendChild( time ); 1422 | li.appendChild( ol ); 1423 | 1424 | } else { 1425 | for ( i = 0; i < this.assertions.length; i++ ) { 1426 | if ( !this.assertions[i].result ) { 1427 | bad++; 1428 | config.stats.bad++; 1429 | config.moduleStats.bad++; 1430 | } 1431 | } 1432 | } 1433 | 1434 | runLoggingCallbacks( "testDone", QUnit, { 1435 | name: this.testName, 1436 | module: this.module, 1437 | failed: bad, 1438 | passed: this.assertions.length - bad, 1439 | total: this.assertions.length, 1440 | runtime: this.runtime, 1441 | // DEPRECATED: this property will be removed in 2.0.0, use runtime instead 1442 | duration: this.runtime 1443 | }); 1444 | 1445 | QUnit.reset(); 1446 | 1447 | config.current = undefined; 1448 | }, 1449 | 1450 | queue: function() { 1451 | var bad, 1452 | test = this; 1453 | 1454 | synchronize(function() { 1455 | test.init(); 1456 | }); 1457 | function run() { 1458 | // each of these can by async 1459 | synchronize(function() { 1460 | test.setup(); 1461 | }); 1462 | synchronize(function() { 1463 | test.run(); 1464 | }); 1465 | synchronize(function() { 1466 | test.teardown(); 1467 | }); 1468 | synchronize(function() { 1469 | test.finish(); 1470 | }); 1471 | } 1472 | 1473 | // `bad` initialized at top of scope 1474 | // defer when previous test run passed, if storage is available 1475 | bad = QUnit.config.reorder && defined.sessionStorage && 1476 | +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName ); 1477 | 1478 | if ( bad ) { 1479 | run(); 1480 | } else { 1481 | synchronize( run, true ); 1482 | } 1483 | } 1484 | }; 1485 | 1486 | // `assert` initialized at top of scope 1487 | // Assert helpers 1488 | // All of these must either call QUnit.push() or manually do: 1489 | // - runLoggingCallbacks( "log", .. ); 1490 | // - config.current.assertions.push({ .. }); 1491 | assert = QUnit.assert = { 1492 | /** 1493 | * Asserts rough true-ish result. 1494 | * @name ok 1495 | * @function 1496 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 1497 | */ 1498 | ok: function( result, msg ) { 1499 | if ( !config.current ) { 1500 | throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) ); 1501 | } 1502 | result = !!result; 1503 | msg = msg || ( result ? "okay" : "failed" ); 1504 | 1505 | var source, 1506 | details = { 1507 | module: config.current.module, 1508 | name: config.current.testName, 1509 | result: result, 1510 | message: msg 1511 | }; 1512 | 1513 | msg = "" + escapeText( msg ) + ""; 1514 | 1515 | if ( !result ) { 1516 | source = sourceFromStacktrace( 2 ); 1517 | if ( source ) { 1518 | details.source = source; 1519 | msg += "
      Source:
      " +
      1520 | 					escapeText( source ) +
      1521 | 					"
      "; 1522 | } 1523 | } 1524 | runLoggingCallbacks( "log", QUnit, details ); 1525 | config.current.assertions.push({ 1526 | result: result, 1527 | message: msg 1528 | }); 1529 | }, 1530 | 1531 | /** 1532 | * Assert that the first two arguments are equal, with an optional message. 1533 | * Prints out both actual and expected values. 1534 | * @name equal 1535 | * @function 1536 | * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); 1537 | */ 1538 | equal: function( actual, expected, message ) { 1539 | /*jshint eqeqeq:false */ 1540 | QUnit.push( expected == actual, actual, expected, message ); 1541 | }, 1542 | 1543 | /** 1544 | * @name notEqual 1545 | * @function 1546 | */ 1547 | notEqual: function( actual, expected, message ) { 1548 | /*jshint eqeqeq:false */ 1549 | QUnit.push( expected != actual, actual, expected, message ); 1550 | }, 1551 | 1552 | /** 1553 | * @name propEqual 1554 | * @function 1555 | */ 1556 | propEqual: function( actual, expected, message ) { 1557 | actual = objectValues(actual); 1558 | expected = objectValues(expected); 1559 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); 1560 | }, 1561 | 1562 | /** 1563 | * @name notPropEqual 1564 | * @function 1565 | */ 1566 | notPropEqual: function( actual, expected, message ) { 1567 | actual = objectValues(actual); 1568 | expected = objectValues(expected); 1569 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); 1570 | }, 1571 | 1572 | /** 1573 | * @name deepEqual 1574 | * @function 1575 | */ 1576 | deepEqual: function( actual, expected, message ) { 1577 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); 1578 | }, 1579 | 1580 | /** 1581 | * @name notDeepEqual 1582 | * @function 1583 | */ 1584 | notDeepEqual: function( actual, expected, message ) { 1585 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); 1586 | }, 1587 | 1588 | /** 1589 | * @name strictEqual 1590 | * @function 1591 | */ 1592 | strictEqual: function( actual, expected, message ) { 1593 | QUnit.push( expected === actual, actual, expected, message ); 1594 | }, 1595 | 1596 | /** 1597 | * @name notStrictEqual 1598 | * @function 1599 | */ 1600 | notStrictEqual: function( actual, expected, message ) { 1601 | QUnit.push( expected !== actual, actual, expected, message ); 1602 | }, 1603 | 1604 | "throws": function( block, expected, message ) { 1605 | var actual, 1606 | expectedOutput = expected, 1607 | ok = false; 1608 | 1609 | // 'expected' is optional 1610 | if ( !message && typeof expected === "string" ) { 1611 | message = expected; 1612 | expected = null; 1613 | } 1614 | 1615 | config.current.ignoreGlobalErrors = true; 1616 | try { 1617 | block.call( config.current.testEnvironment ); 1618 | } catch (e) { 1619 | actual = e; 1620 | } 1621 | config.current.ignoreGlobalErrors = false; 1622 | 1623 | if ( actual ) { 1624 | 1625 | // we don't want to validate thrown error 1626 | if ( !expected ) { 1627 | ok = true; 1628 | expectedOutput = null; 1629 | 1630 | // expected is an Error object 1631 | } else if ( expected instanceof Error ) { 1632 | ok = actual instanceof Error && 1633 | actual.name === expected.name && 1634 | actual.message === expected.message; 1635 | 1636 | // expected is a regexp 1637 | } else if ( QUnit.objectType( expected ) === "regexp" ) { 1638 | ok = expected.test( errorString( actual ) ); 1639 | 1640 | // expected is a string 1641 | } else if ( QUnit.objectType( expected ) === "string" ) { 1642 | ok = expected === errorString( actual ); 1643 | 1644 | // expected is a constructor 1645 | } else if ( actual instanceof expected ) { 1646 | ok = true; 1647 | 1648 | // expected is a validation function which returns true is validation passed 1649 | } else if ( expected.call( {}, actual ) === true ) { 1650 | expectedOutput = null; 1651 | ok = true; 1652 | } 1653 | 1654 | QUnit.push( ok, actual, expectedOutput, message ); 1655 | } else { 1656 | QUnit.pushFailure( message, null, "No exception was thrown." ); 1657 | } 1658 | } 1659 | }; 1660 | 1661 | /** 1662 | * @deprecated since 1.8.0 1663 | * Kept assertion helpers in root for backwards compatibility. 1664 | */ 1665 | extend( QUnit.constructor.prototype, assert ); 1666 | 1667 | /** 1668 | * @deprecated since 1.9.0 1669 | * Kept to avoid TypeErrors for undefined methods. 1670 | */ 1671 | QUnit.constructor.prototype.raises = function() { 1672 | QUnit.push( false, false, false, "QUnit.raises has been deprecated since 2012 (fad3c1ea), use QUnit.throws instead" ); 1673 | }; 1674 | 1675 | /** 1676 | * @deprecated since 1.0.0, replaced with error pushes since 1.3.0 1677 | * Kept to avoid TypeErrors for undefined methods. 1678 | */ 1679 | QUnit.constructor.prototype.equals = function() { 1680 | QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" ); 1681 | }; 1682 | QUnit.constructor.prototype.same = function() { 1683 | QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" ); 1684 | }; 1685 | 1686 | // Test for equality any JavaScript type. 1687 | // Author: Philippe Rathé 1688 | QUnit.equiv = (function() { 1689 | 1690 | // Call the o related callback with the given arguments. 1691 | function bindCallbacks( o, callbacks, args ) { 1692 | var prop = QUnit.objectType( o ); 1693 | if ( prop ) { 1694 | if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { 1695 | return callbacks[ prop ].apply( callbacks, args ); 1696 | } else { 1697 | return callbacks[ prop ]; // or undefined 1698 | } 1699 | } 1700 | } 1701 | 1702 | // the real equiv function 1703 | var innerEquiv, 1704 | // stack to decide between skip/abort functions 1705 | callers = [], 1706 | // stack to avoiding loops from circular referencing 1707 | parents = [], 1708 | parentsB = [], 1709 | 1710 | getProto = Object.getPrototypeOf || function ( obj ) { 1711 | /*jshint camelcase:false */ 1712 | return obj.__proto__; 1713 | }, 1714 | callbacks = (function () { 1715 | 1716 | // for string, boolean, number and null 1717 | function useStrictEquality( b, a ) { 1718 | /*jshint eqeqeq:false */ 1719 | if ( b instanceof a.constructor || a instanceof b.constructor ) { 1720 | // to catch short annotation VS 'new' annotation of a 1721 | // declaration 1722 | // e.g. var i = 1; 1723 | // var j = new Number(1); 1724 | return a == b; 1725 | } else { 1726 | return a === b; 1727 | } 1728 | } 1729 | 1730 | return { 1731 | "string": useStrictEquality, 1732 | "boolean": useStrictEquality, 1733 | "number": useStrictEquality, 1734 | "null": useStrictEquality, 1735 | "undefined": useStrictEquality, 1736 | 1737 | "nan": function( b ) { 1738 | return isNaN( b ); 1739 | }, 1740 | 1741 | "date": function( b, a ) { 1742 | return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); 1743 | }, 1744 | 1745 | "regexp": function( b, a ) { 1746 | return QUnit.objectType( b ) === "regexp" && 1747 | // the regex itself 1748 | a.source === b.source && 1749 | // and its modifiers 1750 | a.global === b.global && 1751 | // (gmi) ... 1752 | a.ignoreCase === b.ignoreCase && 1753 | a.multiline === b.multiline && 1754 | a.sticky === b.sticky; 1755 | }, 1756 | 1757 | // - skip when the property is a method of an instance (OOP) 1758 | // - abort otherwise, 1759 | // initial === would have catch identical references anyway 1760 | "function": function() { 1761 | var caller = callers[callers.length - 1]; 1762 | return caller !== Object && typeof caller !== "undefined"; 1763 | }, 1764 | 1765 | "array": function( b, a ) { 1766 | var i, j, len, loop, aCircular, bCircular; 1767 | 1768 | // b could be an object literal here 1769 | if ( QUnit.objectType( b ) !== "array" ) { 1770 | return false; 1771 | } 1772 | 1773 | len = a.length; 1774 | if ( len !== b.length ) { 1775 | // safe and faster 1776 | return false; 1777 | } 1778 | 1779 | // track reference to avoid circular references 1780 | parents.push( a ); 1781 | parentsB.push( b ); 1782 | for ( i = 0; i < len; i++ ) { 1783 | loop = false; 1784 | for ( j = 0; j < parents.length; j++ ) { 1785 | aCircular = parents[j] === a[i]; 1786 | bCircular = parentsB[j] === b[i]; 1787 | if ( aCircular || bCircular ) { 1788 | if ( a[i] === b[i] || aCircular && bCircular ) { 1789 | loop = true; 1790 | } else { 1791 | parents.pop(); 1792 | parentsB.pop(); 1793 | return false; 1794 | } 1795 | } 1796 | } 1797 | if ( !loop && !innerEquiv(a[i], b[i]) ) { 1798 | parents.pop(); 1799 | parentsB.pop(); 1800 | return false; 1801 | } 1802 | } 1803 | parents.pop(); 1804 | parentsB.pop(); 1805 | return true; 1806 | }, 1807 | 1808 | "object": function( b, a ) { 1809 | /*jshint forin:false */ 1810 | var i, j, loop, aCircular, bCircular, 1811 | // Default to true 1812 | eq = true, 1813 | aProperties = [], 1814 | bProperties = []; 1815 | 1816 | // comparing constructors is more strict than using 1817 | // instanceof 1818 | if ( a.constructor !== b.constructor ) { 1819 | // Allow objects with no prototype to be equivalent to 1820 | // objects with Object as their constructor. 1821 | if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) || 1822 | ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) { 1823 | return false; 1824 | } 1825 | } 1826 | 1827 | // stack constructor before traversing properties 1828 | callers.push( a.constructor ); 1829 | 1830 | // track reference to avoid circular references 1831 | parents.push( a ); 1832 | parentsB.push( b ); 1833 | 1834 | // be strict: don't ensure hasOwnProperty and go deep 1835 | for ( i in a ) { 1836 | loop = false; 1837 | for ( j = 0; j < parents.length; j++ ) { 1838 | aCircular = parents[j] === a[i]; 1839 | bCircular = parentsB[j] === b[i]; 1840 | if ( aCircular || bCircular ) { 1841 | if ( a[i] === b[i] || aCircular && bCircular ) { 1842 | loop = true; 1843 | } else { 1844 | eq = false; 1845 | break; 1846 | } 1847 | } 1848 | } 1849 | aProperties.push(i); 1850 | if ( !loop && !innerEquiv(a[i], b[i]) ) { 1851 | eq = false; 1852 | break; 1853 | } 1854 | } 1855 | 1856 | parents.pop(); 1857 | parentsB.pop(); 1858 | callers.pop(); // unstack, we are done 1859 | 1860 | for ( i in b ) { 1861 | bProperties.push( i ); // collect b's properties 1862 | } 1863 | 1864 | // Ensures identical properties name 1865 | return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); 1866 | } 1867 | }; 1868 | }()); 1869 | 1870 | innerEquiv = function() { // can take multiple arguments 1871 | var args = [].slice.apply( arguments ); 1872 | if ( args.length < 2 ) { 1873 | return true; // end transition 1874 | } 1875 | 1876 | return (function( a, b ) { 1877 | if ( a === b ) { 1878 | return true; // catch the most you can 1879 | } else if ( a === null || b === null || typeof a === "undefined" || 1880 | typeof b === "undefined" || 1881 | QUnit.objectType(a) !== QUnit.objectType(b) ) { 1882 | return false; // don't lose time with error prone cases 1883 | } else { 1884 | return bindCallbacks(a, callbacks, [ b, a ]); 1885 | } 1886 | 1887 | // apply transition with (1..n) arguments 1888 | }( args[0], args[1] ) && innerEquiv.apply( this, args.splice(1, args.length - 1 )) ); 1889 | }; 1890 | 1891 | return innerEquiv; 1892 | }()); 1893 | 1894 | /** 1895 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1896 | * http://flesler.blogspot.com Licensed under BSD 1897 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1898 | * 1899 | * @projectDescription Advanced and extensible data dumping for Javascript. 1900 | * @version 1.0.0 1901 | * @author Ariel Flesler 1902 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1903 | */ 1904 | QUnit.jsDump = (function() { 1905 | function quote( str ) { 1906 | return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\""; 1907 | } 1908 | function literal( o ) { 1909 | return o + ""; 1910 | } 1911 | function join( pre, arr, post ) { 1912 | var s = jsDump.separator(), 1913 | base = jsDump.indent(), 1914 | inner = jsDump.indent(1); 1915 | if ( arr.join ) { 1916 | arr = arr.join( "," + s + inner ); 1917 | } 1918 | if ( !arr ) { 1919 | return pre + post; 1920 | } 1921 | return [ pre, inner + arr, base + post ].join(s); 1922 | } 1923 | function array( arr, stack ) { 1924 | var i = arr.length, ret = new Array(i); 1925 | this.up(); 1926 | while ( i-- ) { 1927 | ret[i] = this.parse( arr[i] , undefined , stack); 1928 | } 1929 | this.down(); 1930 | return join( "[", ret, "]" ); 1931 | } 1932 | 1933 | var reName = /^function (\w+)/, 1934 | jsDump = { 1935 | // type is used mostly internally, you can fix a (custom)type in advance 1936 | parse: function( obj, type, stack ) { 1937 | stack = stack || [ ]; 1938 | var inStack, res, 1939 | parser = this.parsers[ type || this.typeOf(obj) ]; 1940 | 1941 | type = typeof parser; 1942 | inStack = inArray( obj, stack ); 1943 | 1944 | if ( inStack !== -1 ) { 1945 | return "recursion(" + (inStack - stack.length) + ")"; 1946 | } 1947 | if ( type === "function" ) { 1948 | stack.push( obj ); 1949 | res = parser.call( this, obj, stack ); 1950 | stack.pop(); 1951 | return res; 1952 | } 1953 | return ( type === "string" ) ? parser : this.parsers.error; 1954 | }, 1955 | typeOf: function( obj ) { 1956 | var type; 1957 | if ( obj === null ) { 1958 | type = "null"; 1959 | } else if ( typeof obj === "undefined" ) { 1960 | type = "undefined"; 1961 | } else if ( QUnit.is( "regexp", obj) ) { 1962 | type = "regexp"; 1963 | } else if ( QUnit.is( "date", obj) ) { 1964 | type = "date"; 1965 | } else if ( QUnit.is( "function", obj) ) { 1966 | type = "function"; 1967 | } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) { 1968 | type = "window"; 1969 | } else if ( obj.nodeType === 9 ) { 1970 | type = "document"; 1971 | } else if ( obj.nodeType ) { 1972 | type = "node"; 1973 | } else if ( 1974 | // native arrays 1975 | toString.call( obj ) === "[object Array]" || 1976 | // NodeList objects 1977 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) 1978 | ) { 1979 | type = "array"; 1980 | } else if ( obj.constructor === Error.prototype.constructor ) { 1981 | type = "error"; 1982 | } else { 1983 | type = typeof obj; 1984 | } 1985 | return type; 1986 | }, 1987 | separator: function() { 1988 | return this.multiline ? this.HTML ? "
      " : "\n" : this.HTML ? " " : " "; 1989 | }, 1990 | // extra can be a number, shortcut for increasing-calling-decreasing 1991 | indent: function( extra ) { 1992 | if ( !this.multiline ) { 1993 | return ""; 1994 | } 1995 | var chr = this.indentChar; 1996 | if ( this.HTML ) { 1997 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); 1998 | } 1999 | return new Array( this.depth + ( extra || 0 ) ).join(chr); 2000 | }, 2001 | up: function( a ) { 2002 | this.depth += a || 1; 2003 | }, 2004 | down: function( a ) { 2005 | this.depth -= a || 1; 2006 | }, 2007 | setParser: function( name, parser ) { 2008 | this.parsers[name] = parser; 2009 | }, 2010 | // The next 3 are exposed so you can use them 2011 | quote: quote, 2012 | literal: literal, 2013 | join: join, 2014 | // 2015 | depth: 1, 2016 | // This is the list of parsers, to modify them, use jsDump.setParser 2017 | parsers: { 2018 | window: "[Window]", 2019 | document: "[Document]", 2020 | error: function(error) { 2021 | return "Error(\"" + error.message + "\")"; 2022 | }, 2023 | unknown: "[Unknown]", 2024 | "null": "null", 2025 | "undefined": "undefined", 2026 | "function": function( fn ) { 2027 | var ret = "function", 2028 | // functions never have name in IE 2029 | name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1]; 2030 | 2031 | if ( name ) { 2032 | ret += " " + name; 2033 | } 2034 | ret += "( "; 2035 | 2036 | ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" ); 2037 | return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" ); 2038 | }, 2039 | array: array, 2040 | nodelist: array, 2041 | "arguments": array, 2042 | object: function( map, stack ) { 2043 | /*jshint forin:false */ 2044 | var ret = [ ], keys, key, val, i; 2045 | QUnit.jsDump.up(); 2046 | keys = []; 2047 | for ( key in map ) { 2048 | keys.push( key ); 2049 | } 2050 | keys.sort(); 2051 | for ( i = 0; i < keys.length; i++ ) { 2052 | key = keys[ i ]; 2053 | val = map[ key ]; 2054 | ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) ); 2055 | } 2056 | QUnit.jsDump.down(); 2057 | return join( "{", ret, "}" ); 2058 | }, 2059 | node: function( node ) { 2060 | var len, i, val, 2061 | open = QUnit.jsDump.HTML ? "<" : "<", 2062 | close = QUnit.jsDump.HTML ? ">" : ">", 2063 | tag = node.nodeName.toLowerCase(), 2064 | ret = open + tag, 2065 | attrs = node.attributes; 2066 | 2067 | if ( attrs ) { 2068 | for ( i = 0, len = attrs.length; i < len; i++ ) { 2069 | val = attrs[i].nodeValue; 2070 | // IE6 includes all attributes in .attributes, even ones not explicitly set. 2071 | // Those have values like undefined, null, 0, false, "" or "inherit". 2072 | if ( val && val !== "inherit" ) { 2073 | ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" ); 2074 | } 2075 | } 2076 | } 2077 | ret += close; 2078 | 2079 | // Show content of TextNode or CDATASection 2080 | if ( node.nodeType === 3 || node.nodeType === 4 ) { 2081 | ret += node.nodeValue; 2082 | } 2083 | 2084 | return ret + open + "/" + tag + close; 2085 | }, 2086 | // function calls it internally, it's the arguments part of the function 2087 | functionArgs: function( fn ) { 2088 | var args, 2089 | l = fn.length; 2090 | 2091 | if ( !l ) { 2092 | return ""; 2093 | } 2094 | 2095 | args = new Array(l); 2096 | while ( l-- ) { 2097 | // 97 is 'a' 2098 | args[l] = String.fromCharCode(97+l); 2099 | } 2100 | return " " + args.join( ", " ) + " "; 2101 | }, 2102 | // object calls it internally, the key part of an item in a map 2103 | key: quote, 2104 | // function calls it internally, it's the content of the function 2105 | functionCode: "[code]", 2106 | // node calls it internally, it's an html attribute value 2107 | attribute: quote, 2108 | string: quote, 2109 | date: quote, 2110 | regexp: literal, 2111 | number: literal, 2112 | "boolean": literal 2113 | }, 2114 | // if true, entities are escaped ( <, >, \t, space and \n ) 2115 | HTML: false, 2116 | // indentation unit 2117 | indentChar: " ", 2118 | // if true, items in a collection, are separated by a \n, else just a space. 2119 | multiline: true 2120 | }; 2121 | 2122 | return jsDump; 2123 | }()); 2124 | 2125 | /* 2126 | * Javascript Diff Algorithm 2127 | * By John Resig (http://ejohn.org/) 2128 | * Modified by Chu Alan "sprite" 2129 | * 2130 | * Released under the MIT license. 2131 | * 2132 | * More Info: 2133 | * http://ejohn.org/projects/javascript-diff-algorithm/ 2134 | * 2135 | * Usage: QUnit.diff(expected, actual) 2136 | * 2137 | * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over" 2138 | */ 2139 | QUnit.diff = (function() { 2140 | /*jshint eqeqeq:false, eqnull:true */ 2141 | function diff( o, n ) { 2142 | var i, 2143 | ns = {}, 2144 | os = {}; 2145 | 2146 | for ( i = 0; i < n.length; i++ ) { 2147 | if ( !hasOwn.call( ns, n[i] ) ) { 2148 | ns[ n[i] ] = { 2149 | rows: [], 2150 | o: null 2151 | }; 2152 | } 2153 | ns[ n[i] ].rows.push( i ); 2154 | } 2155 | 2156 | for ( i = 0; i < o.length; i++ ) { 2157 | if ( !hasOwn.call( os, o[i] ) ) { 2158 | os[ o[i] ] = { 2159 | rows: [], 2160 | n: null 2161 | }; 2162 | } 2163 | os[ o[i] ].rows.push( i ); 2164 | } 2165 | 2166 | for ( i in ns ) { 2167 | if ( hasOwn.call( ns, i ) ) { 2168 | if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) { 2169 | n[ ns[i].rows[0] ] = { 2170 | text: n[ ns[i].rows[0] ], 2171 | row: os[i].rows[0] 2172 | }; 2173 | o[ os[i].rows[0] ] = { 2174 | text: o[ os[i].rows[0] ], 2175 | row: ns[i].rows[0] 2176 | }; 2177 | } 2178 | } 2179 | } 2180 | 2181 | for ( i = 0; i < n.length - 1; i++ ) { 2182 | if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null && 2183 | n[ i + 1 ] == o[ n[i].row + 1 ] ) { 2184 | 2185 | n[ i + 1 ] = { 2186 | text: n[ i + 1 ], 2187 | row: n[i].row + 1 2188 | }; 2189 | o[ n[i].row + 1 ] = { 2190 | text: o[ n[i].row + 1 ], 2191 | row: i + 1 2192 | }; 2193 | } 2194 | } 2195 | 2196 | for ( i = n.length - 1; i > 0; i-- ) { 2197 | if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null && 2198 | n[ i - 1 ] == o[ n[i].row - 1 ]) { 2199 | 2200 | n[ i - 1 ] = { 2201 | text: n[ i - 1 ], 2202 | row: n[i].row - 1 2203 | }; 2204 | o[ n[i].row - 1 ] = { 2205 | text: o[ n[i].row - 1 ], 2206 | row: i - 1 2207 | }; 2208 | } 2209 | } 2210 | 2211 | return { 2212 | o: o, 2213 | n: n 2214 | }; 2215 | } 2216 | 2217 | return function( o, n ) { 2218 | o = o.replace( /\s+$/, "" ); 2219 | n = n.replace( /\s+$/, "" ); 2220 | 2221 | var i, pre, 2222 | str = "", 2223 | out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ), 2224 | oSpace = o.match(/\s+/g), 2225 | nSpace = n.match(/\s+/g); 2226 | 2227 | if ( oSpace == null ) { 2228 | oSpace = [ " " ]; 2229 | } 2230 | else { 2231 | oSpace.push( " " ); 2232 | } 2233 | 2234 | if ( nSpace == null ) { 2235 | nSpace = [ " " ]; 2236 | } 2237 | else { 2238 | nSpace.push( " " ); 2239 | } 2240 | 2241 | if ( out.n.length === 0 ) { 2242 | for ( i = 0; i < out.o.length; i++ ) { 2243 | str += "" + out.o[i] + oSpace[i] + ""; 2244 | } 2245 | } 2246 | else { 2247 | if ( out.n[0].text == null ) { 2248 | for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) { 2249 | str += "" + out.o[n] + oSpace[n] + ""; 2250 | } 2251 | } 2252 | 2253 | for ( i = 0; i < out.n.length; i++ ) { 2254 | if (out.n[i].text == null) { 2255 | str += "" + out.n[i] + nSpace[i] + ""; 2256 | } 2257 | else { 2258 | // `pre` initialized at top of scope 2259 | pre = ""; 2260 | 2261 | for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) { 2262 | pre += "" + out.o[n] + oSpace[n] + ""; 2263 | } 2264 | str += " " + out.n[i].text + nSpace[i] + pre; 2265 | } 2266 | } 2267 | } 2268 | 2269 | return str; 2270 | }; 2271 | }()); 2272 | 2273 | // For browser, export only select globals 2274 | if ( typeof window !== "undefined" ) { 2275 | extend( window, QUnit.constructor.prototype ); 2276 | window.QUnit = QUnit; 2277 | } 2278 | 2279 | // For CommonJS environments, export everything 2280 | if ( typeof module !== "undefined" && module.exports ) { 2281 | module.exports = QUnit; 2282 | } 2283 | 2284 | 2285 | // Get a reference to the global object, like window in browsers 2286 | }( (function() { 2287 | return this; 2288 | })() )); 2289 | -------------------------------------------------------------------------------- /test/requirejs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Finite State Machine Tests - USING REQUIREJS INCLUDE MECHANISM 5 | 6 | 7 | 8 | 33 | 34 | 35 |

      QUnit Test Suite

      36 |

      37 |
      38 |

      39 |
        40 |
        test markup
        41 | 42 | 43 | -------------------------------------------------------------------------------- /test/requirejs/require.js: -------------------------------------------------------------------------------- 1 | /* 2 | RequireJS 1.0.6 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. 3 | Available via the MIT or new BSD license. 4 | see: http://github.com/jrburke/requirejs for details 5 | */ 6 | var requirejs,require,define; 7 | (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|| 8 | 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("./")=== 9 | 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} 10 | 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= 11 | 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 "'+ 12 | 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); 13 | 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, 14 | 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; 15 | 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= 18 | 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!== 19 | 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= 20 | 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)!== 21 | "/"&&(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 22 | 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; 23 | 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= 24 | 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"); 25 | 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]", 26 | 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)}; 27 | 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; 28 | 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= 29 | 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", 30 | "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&& 31 | (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= 32 | 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|| 33 | y.resume();d.checkReadyState()},0)}})(); 34 | -------------------------------------------------------------------------------- /test/runner.js: -------------------------------------------------------------------------------- 1 | // 2 | // To run tests via nodejs you must have nodejs and npm installed 3 | // 4 | // > npm install # to install node-qunit 5 | // > node test/runner 6 | // 7 | 8 | var runner = require("qunit"); 9 | 10 | runner.run({ 11 | 12 | code: "./state-machine.js", 13 | 14 | tests: [ 15 | "test/test_basics.js", 16 | "test/test_advanced.js", 17 | "test/test_classes.js", 18 | "test/test_async.js", 19 | "test/test_initialize.js" 20 | ] 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /test/test_advanced.js: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | 3 | QUnit.module("advanced"); 4 | 5 | //----------------------------------------------------------------------------- 6 | 7 | test("multiple 'from' states for the same event", function() { 8 | 9 | var fsm = StateMachine.create({ 10 | initial: 'green', 11 | events: [ 12 | { name: 'warn', from: 'green', to: 'yellow' }, 13 | { name: 'panic', from: ['green', 'yellow'], to: 'red' }, 14 | { name: 'calm', from: 'red', to: 'yellow' }, 15 | { name: 'clear', from: ['yellow', 'red'], to: 'green' }, 16 | ]}); 17 | 18 | equal(fsm.current, 'green', "initial state should be green"); 19 | 20 | ok(fsm.can('warn'), "should be able to warn from green state") 21 | ok(fsm.can('panic'), "should be able to panic from green state") 22 | ok(fsm.cannot('calm'), "should NOT be able to calm from green state") 23 | ok(fsm.cannot('clear'), "should NOT be able to clear from green state") 24 | 25 | fsm.warn(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); 26 | fsm.panic(); equal(fsm.current, 'red', "panic event should transition from yellow to red"); 27 | fsm.calm(); equal(fsm.current, 'yellow', "calm event should transition from red to yellow"); 28 | fsm.clear(); equal(fsm.current, 'green', "clear event should transition from yellow to green"); 29 | 30 | fsm.panic(); equal(fsm.current, 'red', "panic event should transition from green to red"); 31 | fsm.clear(); equal(fsm.current, 'green', "clear event should transition from red to green"); 32 | 33 | }); 34 | 35 | //----------------------------------------------------------------------------- 36 | 37 | test("multiple 'to' states for the same event", function() { 38 | 39 | var fsm = StateMachine.create({ 40 | initial: 'hungry', 41 | events: [ 42 | { name: 'eat', from: 'hungry', to: 'satisfied' }, 43 | { name: 'eat', from: 'satisfied', to: 'full' }, 44 | { name: 'eat', from: 'full', to: 'sick' }, 45 | { name: 'rest', from: ['hungry', 'satisfied', 'full', 'sick'], to: 'hungry' }, 46 | ]}); 47 | 48 | equal(fsm.current, 'hungry'); 49 | 50 | ok(fsm.can('eat')); 51 | ok(fsm.can('rest')); 52 | 53 | fsm.eat(); 54 | equal(fsm.current, 'satisfied'); 55 | 56 | fsm.eat(); 57 | equal(fsm.current, 'full'); 58 | 59 | fsm.eat(); 60 | equal(fsm.current, 'sick'); 61 | 62 | fsm.rest(); 63 | equal(fsm.current, 'hungry'); 64 | 65 | }); 66 | 67 | //----------------------------------------------------------------------------- 68 | 69 | test("no-op transitions (github issue #5) with multiple from states", function() { 70 | 71 | var fsm = StateMachine.create({ 72 | initial: 'green', 73 | events: [ 74 | { name: 'warn', from: 'green', to: 'yellow' }, 75 | { name: 'panic', from: ['green', 'yellow'], to: 'red' }, 76 | { name: 'noop', from: ['green', 'yellow'] }, // NOTE: 'to' not specified 77 | { name: 'calm', from: 'red', to: 'yellow' }, 78 | { name: 'clear', from: ['yellow', 'red'], to: 'green' }, 79 | ]}); 80 | 81 | equal(fsm.current, 'green', "initial state should be green"); 82 | 83 | ok(fsm.can('warn'), "should be able to warn from green state") 84 | ok(fsm.can('panic'), "should be able to panic from green state") 85 | ok(fsm.can('noop'), "should be able to noop from green state") 86 | ok(fsm.cannot('calm'), "should NOT be able to calm from green state") 87 | ok(fsm.cannot('clear'), "should NOT be able to clear from green state") 88 | 89 | fsm.noop(); equal(fsm.current, 'green', "noop event should not transition"); 90 | fsm.warn(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); 91 | 92 | ok(fsm.cannot('warn'), "should NOT be able to warn from yellow state") 93 | ok(fsm.can('panic'), "should be able to panic from yellow state") 94 | ok(fsm.can('noop'), "should be able to noop from yellow state") 95 | ok(fsm.cannot('calm'), "should NOT be able to calm from yellow state") 96 | ok(fsm.can('clear'), "should be able to clear from yellow state") 97 | 98 | fsm.noop(); equal(fsm.current, 'yellow', "noop event should not transition"); 99 | fsm.panic(); equal(fsm.current, 'red', "panic event should transition from yellow to red"); 100 | 101 | ok(fsm.cannot('warn'), "should NOT be able to warn from red state") 102 | ok(fsm.cannot('panic'), "should NOT be able to panic from red state") 103 | ok(fsm.cannot('noop'), "should NOT be able to noop from red state") 104 | ok(fsm.can('calm'), "should be able to calm from red state") 105 | ok(fsm.can('clear'), "should be able to clear from red state") 106 | 107 | }); 108 | 109 | //----------------------------------------------------------------------------- 110 | 111 | test("callbacks are called when appropriate for multiple 'from' and 'to' transitions", function() { 112 | 113 | var called = []; 114 | 115 | var fsm = StateMachine.create({ 116 | initial: 'hungry', 117 | events: [ 118 | { name: 'eat', from: 'hungry', to: 'satisfied' }, 119 | { name: 'eat', from: 'satisfied', to: 'full' }, 120 | { name: 'eat', from: 'full', to: 'sick' }, 121 | { name: 'rest', from: ['hungry', 'satisfied', 'full', 'sick'], to: 'hungry' }, 122 | ], 123 | callbacks: { 124 | 125 | // generic callbacks 126 | onbeforeevent: function(event,from,to) { called.push('onbefore(' + event + ')'); }, 127 | onafterevent: function(event,from,to) { called.push('onafter(' + event + ')'); }, 128 | onleavestate: function(event,from,to) { called.push('onleave(' + from + ')'); }, 129 | onenterstate: function(event,from,to) { called.push('onenter(' + to + ')'); }, 130 | onchangestate: function(event,from,to) { called.push('onchange(' + from + ',' + to + ')'); }, 131 | 132 | // specific state callbacks 133 | onenterhungry: function() { called.push('onenterhungry'); }, 134 | onleavehungry: function() { called.push('onleavehungry'); }, 135 | onentersatisfied: function() { called.push('onentersatisfied'); }, 136 | onleavesatisfied: function() { called.push('onleavesatisfied'); }, 137 | onenterfull: function() { called.push('onenterfull'); }, 138 | onleavefull: function() { called.push('onleavefull'); }, 139 | onentersick: function() { called.push('onentersick'); }, 140 | onleavesick: function() { called.push('onleavesick'); }, 141 | 142 | // specific event callbacks 143 | onbeforeeat: function() { called.push('onbeforeeat'); }, 144 | onaftereat: function() { called.push('onaftereat'); }, 145 | onbeforerest: function() { called.push('onbeforerest'); }, 146 | onafterrest: function() { called.push('onafterrest'); } 147 | } 148 | }); 149 | 150 | called = []; 151 | fsm.eat(); 152 | deepEqual(called, [ 153 | 'onbeforeeat', 154 | 'onbefore(eat)', 155 | 'onleavehungry', 156 | 'onleave(hungry)', 157 | 'onentersatisfied', 158 | 'onenter(satisfied)', 159 | 'onchange(hungry,satisfied)', 160 | 'onaftereat', 161 | 'onafter(eat)' 162 | ]); 163 | 164 | called = []; 165 | fsm.eat(); 166 | deepEqual(called, [ 167 | 'onbeforeeat', 168 | 'onbefore(eat)', 169 | 'onleavesatisfied', 170 | 'onleave(satisfied)', 171 | 'onenterfull', 172 | 'onenter(full)', 173 | 'onchange(satisfied,full)', 174 | 'onaftereat', 175 | 'onafter(eat)', 176 | ]); 177 | 178 | called = []; 179 | fsm.eat(); 180 | deepEqual(called, [ 181 | 'onbeforeeat', 182 | 'onbefore(eat)', 183 | 'onleavefull', 184 | 'onleave(full)', 185 | 'onentersick', 186 | 'onenter(sick)', 187 | 'onchange(full,sick)', 188 | 'onaftereat', 189 | 'onafter(eat)' 190 | ]); 191 | 192 | called = []; 193 | fsm.rest(); 194 | deepEqual(called, [ 195 | 'onbeforerest', 196 | 'onbefore(rest)', 197 | 'onleavesick', 198 | 'onleave(sick)', 199 | 'onenterhungry', 200 | 'onenter(hungry)', 201 | 'onchange(sick,hungry)', 202 | 'onafterrest', 203 | 'onafter(rest)' 204 | ]); 205 | 206 | }); 207 | 208 | //----------------------------------------------------------------------------- 209 | 210 | test("callbacks are called when appropriate for prototype based state machine", function() { 211 | 212 | var myFSM = function() { 213 | this.called = []; 214 | this.startup(); 215 | }; 216 | 217 | myFSM.prototype = { 218 | 219 | // generic callbacks 220 | onbeforeevent: function(event,from,to) { this.called.push('onbefore(' + event + ')'); }, 221 | onafterevent: function(event,from,to) { this.called.push('onafter(' + event + ')'); }, 222 | onleavestate: function(event,from,to) { this.called.push('onleave(' + from + ')'); }, 223 | onenterstate: function(event,from,to) { this.called.push('onenter(' + to + ')'); }, 224 | onchangestate: function(event,from,to) { this.called.push('onchange(' + from + ',' + to + ')'); }, 225 | 226 | // specific state callbacks 227 | onenternone: function() { this.called.push('onenternone'); }, 228 | onleavenone: function() { this.called.push('onleavenone'); }, 229 | onentergreen: function() { this.called.push('onentergreen'); }, 230 | onleavegreen: function() { this.called.push('onleavegreen'); }, 231 | onenteryellow : function() { this.called.push('onenteryellow'); }, 232 | onleaveyellow: function() { this.called.push('onleaveyellow'); }, 233 | onenterred: function() { this.called.push('onenterred'); }, 234 | onleavered: function() { this.called.push('onleavered'); }, 235 | 236 | // specific event callbacks 237 | onbeforestartup: function() { this.called.push('onbeforestartup'); }, 238 | onafterstartup: function() { this.called.push('onafterstartup'); }, 239 | onbeforewarn: function() { this.called.push('onbeforewarn'); }, 240 | onafterwarn: function() { this.called.push('onafterwarn'); }, 241 | onbeforepanic: function() { this.called.push('onbeforepanic'); }, 242 | onafterpanic: function() { this.called.push('onafterpanic'); }, 243 | onbeforeclear: function() { this.called.push('onbeforeclear'); }, 244 | onafterclear: function() { this.called.push('onafterclear'); } 245 | }; 246 | 247 | StateMachine.create({ 248 | target: myFSM.prototype, 249 | events: [ 250 | { name: 'startup', from: 'none', to: 'green' }, 251 | { name: 'warn', from: 'green', to: 'yellow' }, 252 | { name: 'panic', from: 'yellow', to: 'red' }, 253 | { name: 'clear', from: 'yellow', to: 'green' } 254 | ] 255 | }); 256 | 257 | var a = new myFSM(); 258 | var b = new myFSM(); 259 | 260 | equal(a.current, 'green', 'start with correct state'); 261 | equal(b.current, 'green', 'start with correct state'); 262 | 263 | deepEqual(a.called, ['onbeforestartup', 'onbefore(startup)', 'onleavenone', 'onleave(none)', 'onentergreen', 'onenter(green)', 'onchange(none,green)', 'onafterstartup', 'onafter(startup)']); 264 | deepEqual(b.called, ['onbeforestartup', 'onbefore(startup)', 'onleavenone', 'onleave(none)', 'onentergreen', 'onenter(green)', 'onchange(none,green)', 'onafterstartup', 'onafter(startup)']); 265 | 266 | a.called = []; 267 | b.called = []; 268 | 269 | a.warn(); 270 | 271 | equal(a.current, 'yellow', 'maintain independent current state'); 272 | equal(b.current, 'green', 'maintain independent current state'); 273 | 274 | deepEqual(a.called, ['onbeforewarn', 'onbefore(warn)', 'onleavegreen', 'onleave(green)', 'onenteryellow', 'onenter(yellow)', 'onchange(green,yellow)', 'onafterwarn', 'onafter(warn)']); 275 | deepEqual(b.called, []); 276 | 277 | }); 278 | 279 | 280 | 281 | -------------------------------------------------------------------------------- /test/test_async.js: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | 3 | QUnit.module("async"); 4 | 5 | //----------------------------------------------------------------------------- 6 | 7 | test("state transitions", function() { 8 | 9 | var fsm = StateMachine.create({ 10 | initial: 'green', 11 | events: [ 12 | { name: 'warn', from: 'green', to: 'yellow' }, 13 | { name: 'panic', from: 'yellow', to: 'red' }, 14 | { name: 'calm', from: 'red', to: 'yellow' }, 15 | { name: 'clear', from: 'yellow', to: 'green' } 16 | ], 17 | callbacks: { 18 | onleavegreen: function() { return StateMachine.ASYNC; }, 19 | onleaveyellow: function() { return StateMachine.ASYNC; }, 20 | onleavered: function() { return StateMachine.ASYNC; } 21 | } 22 | }); 23 | 24 | equal(fsm.current, 'green', "initial state should be green"); 25 | fsm.warn(); equal(fsm.current, 'green', "should still be green because we haven't transitioned yet"); 26 | fsm.transition(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); 27 | fsm.panic(); equal(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet"); 28 | fsm.transition(); equal(fsm.current, 'red', "panic event should transition from yellow to red"); 29 | fsm.calm(); equal(fsm.current, 'red', "should still be red because we haven't transitioned yet"); 30 | fsm.transition(); equal(fsm.current, 'yellow', "calm event should transition from red to yellow"); 31 | fsm.clear(); equal(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet"); 32 | fsm.transition(); equal(fsm.current, 'green', "clear event should transition from yellow to green"); 33 | 34 | }); 35 | 36 | //----------------------------------------------------------------------------- 37 | 38 | test("state transitions with delays", function() { 39 | 40 | stop(); // doing async stuff - dont run next qunit test until I call start() below 41 | 42 | var fsm = StateMachine.create({ 43 | initial: 'green', 44 | events: [ 45 | { name: 'warn', from: 'green', to: 'yellow' }, 46 | { name: 'panic', from: 'yellow', to: 'red' }, 47 | { name: 'calm', from: 'red', to: 'yellow' }, 48 | { name: 'clear', from: 'yellow', to: 'green' } 49 | ], 50 | callbacks: { 51 | onleavegreen: function() { return StateMachine.ASYNC; }, 52 | onleaveyellow: function() { return StateMachine.ASYNC; }, 53 | onleavered: function() { return StateMachine.ASYNC; } 54 | } 55 | }); 56 | 57 | equal(fsm.current, 'green', "initial state should be green"); 58 | fsm.warn(); equal(fsm.current, 'green', "should still be green because we haven't transitioned yet"); 59 | setTimeout(function() { 60 | fsm.transition(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); 61 | fsm.panic(); equal(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet"); 62 | setTimeout(function() { 63 | fsm.transition(); equal(fsm.current, 'red', "panic event should transition from yellow to red"); 64 | fsm.calm(); equal(fsm.current, 'red', "should still be red because we haven't transitioned yet"); 65 | setTimeout(function() { 66 | fsm.transition(); equal(fsm.current, 'yellow', "calm event should transition from red to yellow"); 67 | fsm.clear(); equal(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet"); 68 | setTimeout(function() { 69 | fsm.transition(); equal(fsm.current, 'green', "clear event should transition from yellow to green"); 70 | start(); 71 | }, 10); 72 | }, 10); 73 | }, 10); 74 | }, 10); 75 | 76 | }); 77 | 78 | //----------------------------------------------------------------------------- 79 | 80 | test("state transition fired during onleavestate callback - immediate", function() { 81 | 82 | var fsm = StateMachine.create({ 83 | initial: 'green', 84 | events: [ 85 | { name: 'warn', from: 'green', to: 'yellow' }, 86 | { name: 'panic', from: 'yellow', to: 'red' }, 87 | { name: 'calm', from: 'red', to: 'yellow' }, 88 | { name: 'clear', from: 'yellow', to: 'green' } 89 | ], 90 | callbacks: { 91 | onleavegreen: function() { this.transition(); return StateMachine.ASYNC; }, 92 | onleaveyellow: function() { this.transition(); return StateMachine.ASYNC; }, 93 | onleavered: function() { this.transition(); return StateMachine.ASYNC; } 94 | } 95 | }); 96 | 97 | equal(fsm.current, 'green', "initial state should be green"); 98 | 99 | fsm.warn(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); 100 | fsm.panic(); equal(fsm.current, 'red', "panic event should transition from yellow to red"); 101 | fsm.calm(); equal(fsm.current, 'yellow', "calm event should transition from red to yellow"); 102 | fsm.clear(); equal(fsm.current, 'green', "clear event should transition from yellow to green"); 103 | 104 | }); 105 | 106 | //----------------------------------------------------------------------------- 107 | 108 | test("state transition fired during onleavestate callback - with delay", function() { 109 | 110 | stop(); // doing async stuff - dont run next qunit test until I call start() below 111 | 112 | var fsm = StateMachine.create({ 113 | initial: 'green', 114 | events: [ 115 | { name: 'panic', from: 'green', to: 'red' } 116 | ], 117 | callbacks: { 118 | onleavegreen: function() { setTimeout(function() { fsm.transition(); }, 10); return StateMachine.ASYNC; }, 119 | onenterred: function() { 120 | equal(fsm.current, 'red', "panic event should transition from green to red"); 121 | start(); 122 | } 123 | } 124 | }); 125 | 126 | equal(fsm.current, 'green', "initial state should be green"); 127 | fsm.panic(); equal(fsm.current, 'green', "should still be green because we haven't transitioned yet"); 128 | 129 | }); 130 | 131 | //----------------------------------------------------------------------------- 132 | 133 | test("state transition fired during onleavestate callback - but forgot to return ASYNC!", function() { 134 | 135 | var fsm = StateMachine.create({ 136 | initial: 'green', 137 | events: [ 138 | { name: 'warn', from: 'green', to: 'yellow' }, 139 | { name: 'panic', from: 'yellow', to: 'red' }, 140 | { name: 'calm', from: 'red', to: 'yellow' }, 141 | { name: 'clear', from: 'yellow', to: 'green' } 142 | ], 143 | callbacks: { 144 | onleavegreen: function() { this.transition(); /* return StateMachine.ASYNC; */ }, 145 | onleaveyellow: function() { this.transition(); /* return StateMachine.ASYNC; */ }, 146 | onleavered: function() { this.transition(); /* return StateMachine.ASYNC; */ } 147 | } 148 | }); 149 | 150 | equal(fsm.current, 'green', "initial state should be green"); 151 | 152 | fsm.warn(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); 153 | fsm.panic(); equal(fsm.current, 'red', "panic event should transition from yellow to red"); 154 | fsm.calm(); equal(fsm.current, 'yellow', "calm event should transition from red to yellow"); 155 | fsm.clear(); equal(fsm.current, 'green', "clear event should transition from yellow to green"); 156 | 157 | }); 158 | 159 | //----------------------------------------------------------------------------- 160 | 161 | test("state transitions sometimes synchronous and sometimes asynchronous", function() { 162 | 163 | var fsm = StateMachine.create({ 164 | initial: 'green', 165 | events: [ 166 | { name: 'warn', from: 'green', to: 'yellow' }, 167 | { name: 'panic', from: 'yellow', to: 'red' }, 168 | { name: 'calm', from: 'red', to: 'yellow' }, 169 | { name: 'clear', from: 'yellow', to: 'green' } 170 | ] 171 | }); 172 | 173 | // default behavior is synchronous 174 | 175 | equal(fsm.current, 'green', "initial state should be green"); 176 | fsm.warn(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); 177 | fsm.panic(); equal(fsm.current, 'red', "panic event should transition from yellow to red"); 178 | fsm.calm(); equal(fsm.current, 'yellow', "calm event should transition from red to yellow"); 179 | fsm.clear(); equal(fsm.current, 'green', "clear event should transition from yellow to green"); 180 | 181 | // but add callbacks that return ASYNC and it magically becomes asynchronous 182 | 183 | fsm.onleavegreen = function() { return StateMachine.ASYNC; } 184 | fsm.onleaveyellow = function() { return StateMachine.ASYNC; } 185 | fsm.onleavered = function() { return StateMachine.ASYNC; } 186 | 187 | equal(fsm.current, 'green', "initial state should be green"); 188 | fsm.warn(); equal(fsm.current, 'green', "should still be green because we haven't transitioned yet"); 189 | fsm.transition(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); 190 | fsm.panic(); equal(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet"); 191 | fsm.transition(); equal(fsm.current, 'red', "panic event should transition from yellow to red"); 192 | fsm.calm(); equal(fsm.current, 'red', "should still be red because we haven't transitioned yet"); 193 | fsm.transition(); equal(fsm.current, 'yellow', "calm event should transition from red to yellow"); 194 | fsm.clear(); equal(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet"); 195 | fsm.transition(); equal(fsm.current, 'green', "clear event should transition from yellow to green"); 196 | 197 | // this allows you to make on-the-fly decisions about whether async or not ... 198 | 199 | fsm.onleavegreen = function(event, from, to, async) { 200 | if (async) { 201 | setTimeout(function() { 202 | fsm.transition(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); 203 | start(); // move on to next test 204 | }, 10); 205 | return StateMachine.ASYNC; 206 | } 207 | } 208 | fsm.onleaveyellow = fsm.onleavered = null; 209 | 210 | fsm.warn(false); equal(fsm.current, 'yellow', "expected synchronous transition from green to yellow"); 211 | fsm.clear(); equal(fsm.current, 'green', "clear event should transition from yellow to green"); 212 | fsm.warn(true); equal(fsm.current, 'green', "should still be green because we haven't transitioned yet"); 213 | 214 | stop(); // doing async stuff - dont run next qunit test until I call start() in callback above 215 | 216 | }); 217 | 218 | //----------------------------------------------------------------------------- 219 | 220 | 221 | test("state transition fired without completing previous transition", function() { 222 | 223 | var fsm = StateMachine.create({ 224 | initial: 'green', 225 | events: [ 226 | { name: 'warn', from: 'green', to: 'yellow' }, 227 | { name: 'panic', from: 'yellow', to: 'red' }, 228 | { name: 'calm', from: 'red', to: 'yellow' }, 229 | { name: 'clear', from: 'yellow', to: 'green' } 230 | ], 231 | callbacks: { 232 | onleavegreen: function() { return StateMachine.ASYNC; }, 233 | onleaveyellow: function() { return StateMachine.ASYNC; }, 234 | onleavered: function() { return StateMachine.ASYNC; } 235 | } 236 | }); 237 | 238 | equal(fsm.current, 'green', "initial state should be green"); 239 | fsm.warn(); equal(fsm.current, 'green', "should still be green because we haven't transitioned yet"); 240 | fsm.transition(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); 241 | fsm.panic(); equal(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet"); 242 | 243 | throws(fsm.calm.bind(fsm), /event calm inappropriate because previous transition did not complete/); 244 | 245 | }); 246 | 247 | //----------------------------------------------------------------------------- 248 | 249 | test("state transition can be cancelled (github issue #22)", function() { 250 | 251 | var fsm = StateMachine.create({ 252 | initial: 'green', 253 | events: [ 254 | { name: 'warn', from: 'green', to: 'yellow' }, 255 | { name: 'panic', from: 'yellow', to: 'red' }, 256 | { name: 'calm', from: 'red', to: 'yellow' }, 257 | { name: 'clear', from: 'yellow', to: 'green' } 258 | ], 259 | callbacks: { 260 | onleavegreen: function() { return StateMachine.ASYNC; }, 261 | onleaveyellow: function() { return StateMachine.ASYNC; }, 262 | onleavered: function() { return StateMachine.ASYNC; } 263 | } 264 | }); 265 | 266 | equal(fsm.current, 'green', "initial state should be green"); 267 | fsm.warn(); equal(fsm.current, 'green', "should still be green because we haven't transitioned yet"); 268 | fsm.transition(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); 269 | fsm.panic(); equal(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet"); 270 | equal(fsm.can('panic'), false, "but cannot panic a 2nd time because a transition is still pending") 271 | 272 | throws(fsm.panic.bind(fsm), /event panic inappropriate because previous transition did not complete/); 273 | 274 | fsm.transition.cancel(); 275 | 276 | equal(fsm.current, 'yellow', "should still be yellow because we cancelled the async transition"); 277 | equal(fsm.can('panic'), true, "can now panic again because we cancelled previous async transition"); 278 | 279 | fsm.panic(); 280 | fsm.transition(); 281 | 282 | equal(fsm.current, 'red', "should finally be red now that we completed the async transition"); 283 | 284 | }); 285 | 286 | //----------------------------------------------------------------------------- 287 | 288 | test("callbacks are ordered correctly", function() { 289 | 290 | var called = []; 291 | 292 | var fsm = StateMachine.create({ 293 | initial: 'green', 294 | events: [ 295 | { name: 'warn', from: 'green', to: 'yellow' }, 296 | { name: 'panic', from: 'yellow', to: 'red' }, 297 | { name: 'calm', from: 'red', to: 'yellow' }, 298 | { name: 'clear', from: 'yellow', to: 'green' }, 299 | ], 300 | callbacks: { 301 | 302 | // generic callbacks 303 | onbeforeevent: function(event,from,to) { called.push('onbefore(' + event + ')'); }, 304 | onafterevent: function(event,from,to) { called.push('onafter(' + event + ')'); }, 305 | onleavestate: function(event,from,to) { called.push('onleave(' + from + ')'); }, 306 | onenterstate: function(event,from,to) { called.push('onenter(' + to + ')'); }, 307 | onchangestate: function(event,from,to) { called.push('onchange(' + from + ',' + to + ')'); }, 308 | 309 | // specific state callbacks 310 | onentergreen: function() { called.push('onentergreen'); }, 311 | onenteryellow: function() { called.push('onenteryellow'); }, 312 | onenterred: function() { called.push('onenterred'); }, 313 | onleavegreen: function() { called.push('onleavegreen'); return StateMachine.ASYNC; }, 314 | onleaveyellow: function() { called.push('onleaveyellow'); return StateMachine.ASYNC; }, 315 | onleavered: function() { called.push('onleavered'); return StateMachine.ASYNC; }, 316 | 317 | // specific event callbacks 318 | onbeforewarn: function() { called.push('onbeforewarn'); }, 319 | onbeforepanic: function() { called.push('onbeforepanic'); }, 320 | onbeforecalm: function() { called.push('onbeforecalm'); }, 321 | onbeforeclear: function() { called.push('onbeforeclear'); }, 322 | onafterwarn: function() { called.push('onafterwarn'); }, 323 | onafterpanic: function() { called.push('onafterpanic'); }, 324 | onaftercalm: function() { called.push('onaftercalm'); }, 325 | onafterclear: function() { called.push('onafterclear'); } 326 | } 327 | }); 328 | 329 | called = []; 330 | fsm.warn(); deepEqual(called, ['onbeforewarn', 'onbefore(warn)', 'onleavegreen', 'onleave(green)']); 331 | fsm.transition(); deepEqual(called, ['onbeforewarn', 'onbefore(warn)', 'onleavegreen', 'onleave(green)', 'onenteryellow', 'onenter(yellow)', 'onchange(green,yellow)', 'onafterwarn', 'onafter(warn)']); 332 | 333 | called = []; 334 | fsm.panic(); deepEqual(called, ['onbeforepanic', 'onbefore(panic)', 'onleaveyellow', 'onleave(yellow)']); 335 | fsm.transition(); deepEqual(called, ['onbeforepanic', 'onbefore(panic)', 'onleaveyellow', 'onleave(yellow)', 'onenterred', 'onenter(red)', 'onchange(yellow,red)', 'onafterpanic', 'onafter(panic)']); 336 | 337 | called = []; 338 | fsm.calm(); deepEqual(called, ['onbeforecalm', 'onbefore(calm)', 'onleavered', 'onleave(red)']); 339 | fsm.transition(); deepEqual(called, ['onbeforecalm', 'onbefore(calm)', 'onleavered', 'onleave(red)', 'onenteryellow', 'onenter(yellow)', 'onchange(red,yellow)', 'onaftercalm', 'onafter(calm)']); 340 | 341 | called = []; 342 | fsm.clear(); deepEqual(called, ['onbeforeclear', 'onbefore(clear)', 'onleaveyellow', 'onleave(yellow)']); 343 | fsm.transition(); deepEqual(called, ['onbeforeclear', 'onbefore(clear)', 'onleaveyellow', 'onleave(yellow)', 'onentergreen', 'onenter(green)', 'onchange(yellow,green)', 'onafterclear', 'onafter(clear)']); 344 | 345 | }); 346 | 347 | //----------------------------------------------------------------------------- 348 | 349 | test("cannot fire event during existing transition", function() { 350 | 351 | var fsm = StateMachine.create({ 352 | initial: 'green', 353 | events: [ 354 | { name: 'warn', from: 'green', to: 'yellow' }, 355 | { name: 'panic', from: 'yellow', to: 'red' }, 356 | { name: 'calm', from: 'red', to: 'yellow' }, 357 | { name: 'clear', from: 'yellow', to: 'green' } 358 | ], 359 | callbacks: { 360 | onleavegreen: function() { return StateMachine.ASYNC; }, 361 | onleaveyellow: function() { return StateMachine.ASYNC; }, 362 | onleavered: function() { return StateMachine.ASYNC; } 363 | } 364 | }); 365 | 366 | equal(fsm.current, 'green', "initial state should be green"); 367 | equal(fsm.can('warn'), true, "should be able to warn"); 368 | equal(fsm.can('panic'), false, "should NOT be able to panic"); 369 | equal(fsm.can('calm'), false, "should NOT be able to calm"); 370 | equal(fsm.can('clear'), false, "should NOT be able to clear"); 371 | 372 | fsm.warn(); 373 | 374 | equal(fsm.current, 'green', "should still be green because we haven't transitioned yet"); 375 | equal(fsm.can('warn'), false, "should NOT be able to warn - during transition"); 376 | equal(fsm.can('panic'), false, "should NOT be able to panic - during transition"); 377 | equal(fsm.can('calm'), false, "should NOT be able to calm - during transition"); 378 | equal(fsm.can('clear'), false, "should NOT be able to clear - during transition"); 379 | 380 | fsm.transition(); 381 | 382 | equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); 383 | equal(fsm.can('warn'), false, "should NOT be able to warn"); 384 | equal(fsm.can('panic'), true, "should be able to panic"); 385 | equal(fsm.can('calm'), false, "should NOT be able to calm"); 386 | equal(fsm.can('clear'), true, "should be able to clear"); 387 | 388 | fsm.panic(); 389 | 390 | equal(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet"); 391 | equal(fsm.can('warn'), false, "should NOT be able to warn - during transition"); 392 | equal(fsm.can('panic'), false, "should NOT be able to panic - during transition"); 393 | equal(fsm.can('calm'), false, "should NOT be able to calm - during transition"); 394 | equal(fsm.can('clear'), false, "should NOT be able to clear - during transition"); 395 | 396 | fsm.transition(); 397 | 398 | equal(fsm.current, 'red', "panic event should transition from yellow to red"); 399 | equal(fsm.can('warn'), false, "should NOT be able to warn"); 400 | equal(fsm.can('panic'), false, "should NOT be able to panic"); 401 | equal(fsm.can('calm'), true, "should be able to calm"); 402 | equal(fsm.can('clear'), false, "should NOT be able to clear"); 403 | 404 | }); 405 | 406 | //----------------------------------------------------------------------------- 407 | 408 | 409 | -------------------------------------------------------------------------------- /test/test_basics.js: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | 3 | QUnit.module("basic"); 4 | 5 | //----------------------------------------------------------------------------- 6 | 7 | test("standalone state machine", function() { 8 | 9 | var fsm = StateMachine.create({ 10 | initial: 'green', 11 | events: [ 12 | { name: 'warn', from: 'green', to: 'yellow' }, 13 | { name: 'panic', from: 'yellow', to: 'red' }, 14 | { name: 'calm', from: 'red', to: 'yellow' }, 15 | { name: 'clear', from: 'yellow', to: 'green' } 16 | ]}); 17 | 18 | equal(fsm.current, 'green', "initial state should be green"); 19 | 20 | fsm.warn(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); 21 | fsm.panic(); equal(fsm.current, 'red', "panic event should transition from yellow to red"); 22 | fsm.calm(); equal(fsm.current, 'yellow', "calm event should transition from red to yellow"); 23 | fsm.clear(); equal(fsm.current, 'green', "clear event should transition from yellow to green"); 24 | 25 | }); 26 | 27 | //----------------------------------------------------------------------------- 28 | 29 | test("targeted state machine", function() { 30 | 31 | StateMachine.create({ 32 | target: this, 33 | initial: 'green', 34 | events: [ 35 | { name: 'warn', from: 'green', to: 'yellow' }, 36 | { name: 'panic', from: 'yellow', to: 'red' }, 37 | { name: 'calm', from: 'red', to: 'yellow' }, 38 | { name: 'clear', from: 'yellow', to: 'green' } 39 | ]}); 40 | 41 | equal(this.current, 'green', "initial state should be green"); 42 | 43 | this.warn(); equal(this.current, 'yellow', "warn event should transition from green to yellow"); 44 | this.panic(); equal(this.current, 'red', "panic event should transition from yellow to red"); 45 | this.calm(); equal(this.current, 'yellow', "calm event should transition from red to yellow"); 46 | this.clear(); equal(this.current, 'green', "clear event should transition from yellow to green"); 47 | }); 48 | 49 | //----------------------------------------------------------------------------- 50 | 51 | test("can & cannot", function() { 52 | 53 | var fsm = StateMachine.create({ 54 | initial: 'green', 55 | events: [ 56 | { name: 'warn', from: 'green', to: 'yellow' }, 57 | { name: 'panic', from: 'yellow', to: 'red' }, 58 | { name: 'calm', from: 'red', to: 'yellow' }, 59 | ]}); 60 | 61 | equal(fsm.current, 'green', "initial state should be green"); 62 | 63 | ok(fsm.can('warn'), "should be able to warn from green state") 64 | ok(fsm.cannot('panic'), "should NOT be able to panic from green state") 65 | ok(fsm.cannot('calm'), "should NOT be able to calm from green state") 66 | 67 | fsm.warn(); 68 | equal(fsm.current, 'yellow', "current state should be yellow"); 69 | ok(fsm.cannot('warn'), "should NOT be able to warn from yellow state") 70 | ok(fsm.can('panic'), "should be able to panic from yellow state") 71 | ok(fsm.cannot('calm'), "should NOT be able to calm from yellow state") 72 | 73 | fsm.panic(); 74 | equal(fsm.current, 'red', "current state should be red"); 75 | ok(fsm.cannot('warn'), "should NOT be able to warn from red state") 76 | ok(fsm.cannot('panic'), "should NOT be able to panic from red state") 77 | ok(fsm.can('calm'), "should be able to calm from red state") 78 | 79 | }); 80 | 81 | //----------------------------------------------------------------------------- 82 | 83 | test("is", function() { 84 | 85 | var fsm = StateMachine.create({ 86 | initial: 'green', 87 | events: [ 88 | { name: 'warn', from: 'green', to: 'yellow' }, 89 | { name: 'panic', from: 'yellow', to: 'red' }, 90 | { name: 'calm', from: 'red', to: 'yellow' }, 91 | { name: 'clear', from: 'yellow', to: 'green' } 92 | ]}); 93 | 94 | equal(fsm.current, 'green', "initial state should be green"); 95 | 96 | equal(fsm.is('green'), true, 'current state should match'); 97 | equal(fsm.is('yellow'), false, 'current state should NOT match'); 98 | equal(fsm.is(['green', 'red']), true, 'current state should match when included in array'); 99 | equal(fsm.is(['yellow', 'red']), false, 'current state should NOT match when not included in array'); 100 | 101 | fsm.warn(); 102 | 103 | equal(fsm.current, 'yellow', "current state should be yellow"); 104 | 105 | equal(fsm.is('green'), false, 'current state should NOT match'); 106 | equal(fsm.is('yellow'), true, 'current state should match'); 107 | equal(fsm.is(['green', 'red']), false, 'current state should NOT match when not included in array'); 108 | equal(fsm.is(['yellow', 'red']), true, 'current state should match when included in array'); 109 | 110 | }); 111 | 112 | //----------------------------------------------------------------------------- 113 | 114 | test("transitions", function() { 115 | 116 | var fsm = StateMachine.create({ 117 | initial: 'green', 118 | events: [ 119 | { name: 'warn', from: 'green', to: 'yellow' }, 120 | { name: 'panic', from: 'yellow', to: 'red' }, 121 | { name: 'calm', from: 'red', to: 'yellow' }, 122 | { name: 'clear', from: 'yellow', to: 'green' } 123 | ]}); 124 | 125 | equal(fsm.current, 'green', 'current state should be yellow'); 126 | deepEqual(fsm.transitions(), ['warn'], 'current transition(s) should be yellow'); 127 | 128 | fsm.warn(); 129 | equal(fsm.current, 'yellow', 'current state should be yellow'); 130 | deepEqual(fsm.transitions(), ['panic', 'clear'], 'current transition(s) should be panic and clear'); 131 | 132 | fsm.panic(); 133 | equal(fsm.current, 'red', 'current state should be red'); 134 | deepEqual(fsm.transitions(), ['calm'], 'current transition(s) should be calm'); 135 | 136 | fsm.calm(); 137 | equal(fsm.current, 'yellow', 'current state should be yellow'); 138 | deepEqual(fsm.transitions(), ['panic', 'clear'], 'current transion(s) should be panic and clear'); 139 | 140 | fsm.clear(); 141 | equal(fsm.current, 'green', 'current state should be green'); 142 | deepEqual(fsm.transitions(), ['warn'], 'current transion(s) should be warn'); 143 | }); 144 | 145 | //----------------------------------------------------------------------------- 146 | 147 | test("transitions with multiple from states", function() { 148 | 149 | var fsm = StateMachine.create({ 150 | events: [ 151 | { name: 'start', from: 'none', to: 'green' }, 152 | { name: 'warn', from: ['green', 'red'], to: 'yellow' }, 153 | { name: 'panic', from: ['green', 'yellow'], to: 'red' }, 154 | { name: 'clear', from: ['red', 'yellow'], to: 'green' } 155 | ] 156 | }); 157 | 158 | equal(fsm.current, 'none', 'current state should be none'); 159 | deepEqual(fsm.transitions(), ['start'], 'current transition(s) should be start'); 160 | 161 | fsm.start(); 162 | equal(fsm.current, 'green', 'current state should be green'); 163 | deepEqual(fsm.transitions(), ['warn', 'panic'], 'current transition(s) should be warn and panic'); 164 | 165 | fsm.warn(); 166 | equal(fsm.current, 'yellow', 'current state should be yellow'); 167 | deepEqual(fsm.transitions(), ['panic', 'clear'], 'current transition(s) should be panic and clear'); 168 | 169 | fsm.panic(); 170 | equal(fsm.current, 'red', 'current state should be red'); 171 | deepEqual(fsm.transitions(), ['warn', 'clear'], 'current transition(s) should be warn and clear'); 172 | 173 | fsm.clear(); 174 | equal(fsm.current, 'green', 'current state should be green'); 175 | deepEqual(fsm.transitions(), ['warn', 'panic'], 'current transition(s) should be warn and panic'); 176 | 177 | }); 178 | 179 | //----------------------------------------------------------------------------- 180 | 181 | test("isFinished", function() { 182 | 183 | var fsm = StateMachine.create({ 184 | initial: 'green', terminal: 'red', 185 | events: [ 186 | { name: 'warn', from: 'green', to: 'yellow' }, 187 | { name: 'panic', from: 'yellow', to: 'red' } 188 | ]}); 189 | 190 | equal(fsm.current, 'green'); 191 | equal(fsm.isFinished(), false); 192 | 193 | fsm.warn(); 194 | equal(fsm.current, 'yellow'); 195 | equal(fsm.isFinished(), false); 196 | 197 | fsm.panic(); 198 | equal(fsm.current, 'red'); 199 | equal(fsm.isFinished(), true); 200 | 201 | }); 202 | 203 | //----------------------------------------------------------------------------- 204 | 205 | test("isFinished - without specifying terminal state", function() { 206 | 207 | var fsm = StateMachine.create({ 208 | initial: 'green', 209 | events: [ 210 | { name: 'warn', from: 'green', to: 'yellow' }, 211 | { name: 'panic', from: 'yellow', to: 'red' } 212 | ]}); 213 | 214 | equal(fsm.current, 'green'); 215 | equal(fsm.isFinished(), false); 216 | 217 | fsm.warn(); 218 | equal(fsm.current, 'yellow'); 219 | equal(fsm.isFinished(), false); 220 | 221 | fsm.panic(); 222 | equal(fsm.current, 'red'); 223 | equal(fsm.isFinished(), false); 224 | 225 | }); 226 | //----------------------------------------------------------------------------- 227 | 228 | test("inappropriate events", function() { 229 | 230 | var fsm = StateMachine.create({ 231 | initial: 'green', 232 | events: [ 233 | { name: 'warn', from: 'green', to: 'yellow' }, 234 | { name: 'panic', from: 'yellow', to: 'red' }, 235 | { name: 'calm', from: 'red', to: 'yellow' }, 236 | ]}); 237 | 238 | equal(fsm.current, 'green', "initial state should be green"); 239 | 240 | throws(fsm.panic.bind(fsm), /event panic inappropriate in current state green/); 241 | throws(fsm.calm.bind(fsm), /event calm inappropriate in current state green/); 242 | 243 | fsm.warn(); 244 | equal(fsm.current, 'yellow', "current state should be yellow"); 245 | throws(fsm.warn.bind(fsm), /event warn inappropriate in current state yellow/); 246 | throws(fsm.calm.bind(fsm), /event calm inappropriate in current state yellow/); 247 | 248 | fsm.panic(); 249 | equal(fsm.current, 'red', "current state should be red"); 250 | throws(fsm.warn.bind(fsm), /event warn inappropriate in current state red/); 251 | throws(fsm.panic.bind(fsm), /event panic inappropriate in current state red/); 252 | 253 | }); 254 | 255 | //----------------------------------------------------------------------------- 256 | 257 | test("inappropriate event handling can be customized", function() { 258 | 259 | var fsm = StateMachine.create({ 260 | error: function(name, from, to, args, error, msg) { return msg; }, // return error message instead of throwing an exception 261 | initial: 'green', 262 | events: [ 263 | { name: 'warn', from: 'green', to: 'yellow' }, 264 | { name: 'panic', from: 'yellow', to: 'red' }, 265 | { name: 'calm', from: 'red', to: 'yellow' } 266 | ]}); 267 | 268 | equal(fsm.current, 'green', "initial state should be green"); 269 | 270 | equal(fsm.panic(), 'event panic inappropriate in current state green'); 271 | equal(fsm.calm(), 'event calm inappropriate in current state green'); 272 | 273 | fsm.warn(); 274 | equal(fsm.current, 'yellow', "current state should be yellow"); 275 | equal(fsm.warn(), 'event warn inappropriate in current state yellow'); 276 | equal(fsm.calm(), 'event calm inappropriate in current state yellow'); 277 | 278 | fsm.panic(); 279 | equal(fsm.current, 'red', "current state should be red"); 280 | equal(fsm.warn(), 'event warn inappropriate in current state red'); 281 | equal(fsm.panic(), 'event panic inappropriate in current state red'); 282 | 283 | }); 284 | 285 | //----------------------------------------------------------------------------- 286 | 287 | test("event is cancelable", function() { 288 | 289 | var fsm = StateMachine.create({ 290 | initial: 'green', 291 | events: [ 292 | { name: 'warn', from: 'green', to: 'yellow' }, 293 | { name: 'panic', from: 'yellow', to: 'red' }, 294 | { name: 'calm', from: 'red', to: 'yellow' } 295 | ]}); 296 | 297 | equal(fsm.current, 'green', 'initial state should be green'); 298 | 299 | fsm.onbeforewarn = function() { return false; } 300 | fsm.warn(); 301 | 302 | equal(fsm.current, 'green', 'state should STAY green when event is cancelled'); 303 | 304 | }); 305 | 306 | //----------------------------------------------------------------------------- 307 | 308 | test("callbacks are ordered correctly", function() { 309 | 310 | var called = []; 311 | 312 | var fsm = StateMachine.create({ 313 | initial: 'green', 314 | events: [ 315 | { name: 'warn', from: 'green', to: 'yellow' }, 316 | { name: 'panic', from: 'yellow', to: 'red' }, 317 | { name: 'calm', from: 'red', to: 'yellow' }, 318 | { name: 'clear', from: 'yellow', to: 'green' } 319 | ], 320 | callbacks: { 321 | 322 | // generic callbacks 323 | onbeforeevent: function(event,frmo,to) { called.push('onbefore(' + event + ')'); }, 324 | onafterevent: function(event,frmo,to) { called.push('onafter(' + event + ')'); }, 325 | onleavestate: function(event,from,to) { called.push('onleave(' + from + ')'); }, 326 | onenterstate: function(event,from,to) { called.push('onenter(' + to + ')'); }, 327 | onchangestate: function(event,from,to) { called.push('onchange(' + from + ',' + to + ')'); }, 328 | 329 | // specific state callbacks 330 | onentergreen: function() { called.push('onentergreen'); }, 331 | onenteryellow: function() { called.push('onenteryellow'); }, 332 | onenterred: function() { called.push('onenterred'); }, 333 | onleavegreen: function() { called.push('onleavegreen'); }, 334 | onleaveyellow: function() { called.push('onleaveyellow'); }, 335 | onleavered: function() { called.push('onleavered'); }, 336 | 337 | // specific event callbacks 338 | onbeforewarn: function() { called.push('onbeforewarn'); }, 339 | onbeforepanic: function() { called.push('onbeforepanic'); }, 340 | onbeforecalm: function() { called.push('onbeforecalm'); }, 341 | onbeforeclear: function() { called.push('onbeforeclear'); }, 342 | onafterwarn: function() { called.push('onafterwarn'); }, 343 | onafterpanic: function() { called.push('onafterpanic'); }, 344 | onaftercalm: function() { called.push('onaftercalm'); }, 345 | onafterclear: function() { called.push('onafterclear'); }, 346 | 347 | } 348 | }); 349 | 350 | called = []; 351 | fsm.warn(); 352 | deepEqual(called, [ 353 | 'onbeforewarn', 354 | 'onbefore(warn)', 355 | 'onleavegreen', 356 | 'onleave(green)', 357 | 'onenteryellow', 358 | 'onenter(yellow)', 359 | 'onchange(green,yellow)', 360 | 'onafterwarn', 361 | 'onafter(warn)' 362 | ]); 363 | 364 | called = []; 365 | fsm.panic(); 366 | deepEqual(called, [ 367 | 'onbeforepanic', 368 | 'onbefore(panic)', 369 | 'onleaveyellow', 370 | 'onleave(yellow)', 371 | 'onenterred', 372 | 'onenter(red)', 373 | 'onchange(yellow,red)', 374 | 'onafterpanic', 375 | 'onafter(panic)' 376 | ]); 377 | 378 | called = []; 379 | fsm.calm(); 380 | deepEqual(called, [ 381 | 'onbeforecalm', 382 | 'onbefore(calm)', 383 | 'onleavered', 384 | 'onleave(red)', 385 | 'onenteryellow', 386 | 'onenter(yellow)', 387 | 'onchange(red,yellow)', 388 | 'onaftercalm', 389 | 'onafter(calm)' 390 | ]); 391 | 392 | called = []; 393 | fsm.clear(); 394 | deepEqual(called, [ 395 | 'onbeforeclear', 396 | 'onbefore(clear)', 397 | 'onleaveyellow', 398 | 'onleave(yellow)', 399 | 'onentergreen', 400 | 'onenter(green)', 401 | 'onchange(yellow,green)', 402 | 'onafterclear', 403 | 'onafter(clear)' 404 | ]); 405 | 406 | }); 407 | 408 | //----------------------------------------------------------------------------- 409 | 410 | test("callbacks are ordered correctly - for same state transition", function() { 411 | 412 | var called = []; 413 | 414 | var fsm = StateMachine.create({ 415 | initial: 'waiting', 416 | events: [ 417 | { name: 'data', from: ['waiting', 'receipt'], to: 'receipt' }, 418 | { name: 'nothing', from: ['waiting', 'receipt'], to: 'waiting' }, 419 | { 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 420 | ], 421 | callbacks: { 422 | 423 | // generic callbacks 424 | onbeforeevent: function(event,frmo,to) { called.push('onbefore(' + event + ')'); }, 425 | onafterevent: function(event,frmo,to) { called.push('onafter(' + event + ')'); }, 426 | onleavestate: function(event,from,to) { called.push('onleave(' + from + ')'); }, 427 | onenterstate: function(event,from,to) { called.push('onenter(' + to + ')'); }, 428 | onchangestate: function(event,from,to) { called.push('onchange(' + from + ',' + to + ')'); }, 429 | 430 | // specific state callbacks 431 | onenterwaiting: function() { called.push('onenterwaiting'); }, 432 | onenterreceipt: function() { called.push('onenterreceipt'); }, 433 | onentererror: function() { called.push('onentererror'); }, 434 | onleavewaiting: function() { called.push('onleavewaiting'); }, 435 | onleavereceipt: function() { called.push('onleavereceipt'); }, 436 | onleaveerror: function() { called.push('onleaveerror'); }, 437 | 438 | // specific event callbacks 439 | onbeforedata: function() { called.push('onbeforedata'); }, 440 | onbeforenothing: function() { called.push('onbeforenothing'); }, 441 | onbeforeerror: function() { called.push('onbeforeerror'); }, 442 | onafterdata: function() { called.push('onafterdata'); }, 443 | onafternothing: function() { called.push('onafternothing'); }, 444 | onaftereerror: function() { called.push('onaftererror'); }, 445 | } 446 | }); 447 | 448 | called = []; 449 | fsm.data(); 450 | deepEqual(called, [ 451 | 'onbeforedata', 452 | 'onbefore(data)', 453 | 'onleavewaiting', 454 | 'onleave(waiting)', 455 | 'onenterreceipt', 456 | 'onenter(receipt)', 457 | 'onchange(waiting,receipt)', 458 | 'onafterdata', 459 | 'onafter(data)' 460 | ]); 461 | 462 | called = []; 463 | fsm.data(); // same-state transition 464 | deepEqual(called, [ // so NO enter/leave/change state callbacks are fired 465 | 'onbeforedata', 466 | 'onbefore(data)', 467 | 'onafterdata', 468 | 'onafter(data)' 469 | ]); 470 | 471 | called = []; 472 | fsm.data(); // same-state transition 473 | deepEqual(called, [ // so NO enter/leave/change state callbacks are fired 474 | 'onbeforedata', 475 | 'onbefore(data)', 476 | 'onafterdata', 477 | 'onafter(data)' 478 | ]); 479 | 480 | called = []; 481 | fsm.nothing(); 482 | deepEqual(called, [ 483 | 'onbeforenothing', 484 | 'onbefore(nothing)', 485 | 'onleavereceipt', 486 | 'onleave(receipt)', 487 | 'onenterwaiting', 488 | 'onenter(waiting)', 489 | 'onchange(receipt,waiting)', 490 | 'onafternothing', 491 | 'onafter(nothing)' 492 | ]); 493 | 494 | }); 495 | 496 | //----------------------------------------------------------------------------- 497 | 498 | test("callback arguments are correct", function() { 499 | 500 | var expected = { event: 'startup', from: 'none', to: 'green' }; // first expected callback 501 | 502 | var verify_expected = function(event,from,to,a,b,c) { 503 | equal(event, expected.event) 504 | equal(from, expected.from) 505 | equal(to, expected.to) 506 | equal(a, expected.a) 507 | equal(b, expected.b) 508 | equal(c, expected.c) 509 | }; 510 | 511 | var fsm = StateMachine.create({ 512 | initial: 'green', 513 | events: [ 514 | { name: 'warn', from: 'green', to: 'yellow' }, 515 | { name: 'panic', from: 'yellow', to: 'red' }, 516 | { name: 'calm', from: 'red', to: 'yellow' }, 517 | { name: 'clear', from: 'yellow', to: 'green' } 518 | ], 519 | callbacks: { 520 | 521 | // generic callbacks 522 | onbeforeevent: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, 523 | onafterevent: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, 524 | onleavestate: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, 525 | onenterstate: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, 526 | onchangestate: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, 527 | 528 | // specific state callbacks 529 | onentergreen: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, 530 | onenteryellow: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, 531 | onenterred: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, 532 | onleavegreen: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, 533 | onleaveyellow: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, 534 | onleavered: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, 535 | 536 | // specific event callbacks 537 | onbeforewarn: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, 538 | onbeforepanic: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, 539 | onbeforecalm: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, 540 | onbeforeclear: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, 541 | onafterwarn: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, 542 | onafterpanic: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, 543 | onaftercalm: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); }, 544 | onafterclear: function(event,from,to,a,b,c) { verify_expected(event,from,to,a,b,c); } 545 | } 546 | }); 547 | 548 | expected = { event: 'warn', from: 'green', to: 'yellow', a: 1, b: 2, c: 3 }; 549 | fsm.warn(1,2,3); 550 | 551 | expected = { event: 'panic', from: 'yellow', to: 'red', a: 4, b: 5, c: 6 }; 552 | fsm.panic(4,5,6); 553 | 554 | expected = { event: 'calm', from: 'red', to: 'yellow', a: 'foo', b: 'bar', c: null }; 555 | fsm.calm('foo', 'bar'); 556 | 557 | expected = { event: 'clear', from: 'yellow', to: 'green', a: null, b: null, c: null }; 558 | fsm.clear(); 559 | 560 | }); 561 | 562 | //----------------------------------------------------------------------------- 563 | 564 | test("exceptions in caller-provided callbacks are not swallowed (github issue #17)", function() { 565 | 566 | var fsm = StateMachine.create({ 567 | initial: 'green', 568 | events: [ 569 | { name: 'warn', from: 'green', to: 'yellow' }, 570 | { name: 'panic', from: 'yellow', to: 'red' }, 571 | { name: 'calm', from: 'red', to: 'yellow' } 572 | ], 573 | callbacks: { 574 | onenteryellow: function() { throw 'oops'; } 575 | }}); 576 | 577 | equal(fsm.current, 'green', "initial state should be green"); 578 | 579 | throws(fsm.warn.bind(fsm), /oops/); 580 | }); 581 | 582 | //----------------------------------------------------------------------------- 583 | 584 | test("no-op transitions (github issue #5)", function() { 585 | 586 | var fsm = StateMachine.create({ 587 | initial: 'green', 588 | events: [ 589 | { name: 'noop', from: 'green', /* no-op */ }, 590 | { name: 'warn', from: 'green', to: 'yellow' }, 591 | { name: 'panic', from: 'yellow', to: 'red' }, 592 | { name: 'calm', from: 'red', to: 'yellow' }, 593 | { name: 'clear', from: 'yellow', to: 'green' } 594 | ]}); 595 | 596 | equal(fsm.current, 'green', "initial state should be green"); 597 | 598 | ok(fsm.can('noop'), "should be able to noop from green state") 599 | ok(fsm.can('warn'), "should be able to warn from green state") 600 | 601 | fsm.noop(); equal(fsm.current, 'green', "noop event should not cause a transition (there is no 'to' specified)"); 602 | fsm.warn(); equal(fsm.current, 'yellow', "warn event should transition from green to yellow"); 603 | 604 | ok(fsm.cannot('noop'), "should NOT be able to noop from yellow state") 605 | ok(fsm.cannot('warn'), "should NOT be able to warn from yellow state") 606 | 607 | }); 608 | 609 | //----------------------------------------------------------------------------- 610 | 611 | test("wildcard 'from' allows event from any state (github issue #11)", function() { 612 | 613 | var fsm = StateMachine.create({ 614 | initial: 'stopped', 615 | events: [ 616 | { name: 'prepare', from: 'stopped', to: 'ready' }, 617 | { name: 'start', from: 'ready', to: 'running' }, 618 | { name: 'resume', from: 'paused', to: 'running' }, 619 | { name: 'pause', from: 'running', to: 'paused' }, 620 | { name: 'stop', from: '*', to: 'stopped' } 621 | ]}); 622 | 623 | equal(fsm.current, 'stopped', "initial state should be stopped"); 624 | 625 | fsm.prepare(); equal(fsm.current, 'ready', "prepare event should transition from stopped to ready"); 626 | fsm.stop(); equal(fsm.current, 'stopped', "stop event should transition from ready to stopped"); 627 | 628 | fsm.prepare(); equal(fsm.current, 'ready', "prepare event should transition from stopped to ready"); 629 | fsm.start(); equal(fsm.current, 'running', "start event should transition from ready to running"); 630 | fsm.stop(); equal(fsm.current, 'stopped', "stop event should transition from running to stopped"); 631 | 632 | fsm.prepare(); equal(fsm.current, 'ready', "prepare event should transition from stopped to ready"); 633 | fsm.start(); equal(fsm.current, 'running', "start event should transition from ready to running"); 634 | fsm.pause(); equal(fsm.current, 'paused', "pause event should transition from running to paused"); 635 | fsm.stop(); equal(fsm.current, 'stopped', "stop event should transition from paused to stopped"); 636 | 637 | }); 638 | 639 | //----------------------------------------------------------------------------- 640 | 641 | test("missing 'from' allows event from any state (github issue #11) ", function() { 642 | 643 | var fsm = StateMachine.create({ 644 | initial: 'stopped', 645 | events: [ 646 | { name: 'prepare', from: 'stopped', to: 'ready' }, 647 | { name: 'start', from: 'ready', to: 'running' }, 648 | { name: 'resume', from: 'paused', to: 'running' }, 649 | { name: 'pause', from: 'running', to: 'paused' }, 650 | { name: 'stop', /* any from state */ to: 'stopped' } 651 | ]}); 652 | 653 | equal(fsm.current, 'stopped', "initial state should be stopped"); 654 | 655 | fsm.prepare(); equal(fsm.current, 'ready', "prepare event should transition from stopped to ready"); 656 | fsm.stop(); equal(fsm.current, 'stopped', "stop event should transition from ready to stopped"); 657 | 658 | fsm.prepare(); equal(fsm.current, 'ready', "prepare event should transition from stopped to ready"); 659 | fsm.start(); equal(fsm.current, 'running', "start event should transition from ready to running"); 660 | fsm.stop(); equal(fsm.current, 'stopped', "stop event should transition from running to stopped"); 661 | 662 | fsm.prepare(); equal(fsm.current, 'ready', "prepare event should transition from stopped to ready"); 663 | fsm.start(); equal(fsm.current, 'running', "start event should transition from ready to running"); 664 | fsm.pause(); equal(fsm.current, 'paused', "pause event should transition from running to paused"); 665 | fsm.stop(); equal(fsm.current, 'stopped', "stop event should transition from paused to stopped"); 666 | 667 | }); 668 | 669 | //----------------------------------------------------------------------------- 670 | 671 | test("event return values (github issue #12) ", function() { 672 | 673 | var fsm = StateMachine.create({ 674 | initial: 'stopped', 675 | events: [ 676 | { name: 'prepare', from: 'stopped', to: 'ready' }, 677 | { name: 'fake', from: 'ready', to: 'running' }, 678 | { name: 'start', from: 'ready', to: 'running' } 679 | ], 680 | callbacks: { 681 | onbeforefake: function(event,from,to,a,b,c) { return false; }, // this event will be cancelled 682 | onleaveready: function(event,from,to,a,b,c) { return StateMachine.ASYNC; } // this state transition is ASYNC 683 | } 684 | }); 685 | 686 | equal(fsm.current, 'stopped', "initial state should be stopped"); 687 | 688 | equal(fsm.prepare(), StateMachine.Result.SUCCEEDED, "expected event to have SUCCEEDED"); 689 | equal(fsm.current, 'ready', "prepare event should transition from stopped to ready"); 690 | 691 | equal(fsm.fake(), StateMachine.Result.CANCELLED, "expected event to have been CANCELLED"); 692 | equal(fsm.current, 'ready', "cancelled event should not cause a transition"); 693 | 694 | equal(fsm.start(), StateMachine.Result.PENDING, "expected event to cause a PENDING asynchronous transition"); 695 | equal(fsm.current, 'ready', "async transition hasn't happened yet"); 696 | 697 | equal(fsm.transition(), StateMachine.Result.SUCCEEDED, "expected async transition to have SUCCEEDED"); 698 | equal(fsm.current, 'running', "async transition should now be complete"); 699 | 700 | }); 701 | 702 | -------------------------------------------------------------------------------- /test/test_classes.js: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | 3 | QUnit.module("classes"); 4 | 5 | //----------------------------------------------------------------------------- 6 | 7 | test("prototype based state machine", function() { 8 | 9 | var myFSM = function() { 10 | this.counter = 42; 11 | this.startup(); 12 | }; 13 | 14 | myFSM.prototype = { 15 | onwarn: function() { this.counter++; } 16 | } 17 | 18 | StateMachine.create({ 19 | target: myFSM.prototype, 20 | events: [ 21 | { name: 'startup', from: 'none', to: 'green' }, 22 | { name: 'warn', from: 'green', to: 'yellow' }, 23 | { name: 'panic', from: 'yellow', to: 'red' }, 24 | { name: 'clear', from: 'yellow', to: 'green' } 25 | ] 26 | }); 27 | 28 | var a = new myFSM(); 29 | var b = new myFSM(); 30 | 31 | equal(a.current, 'green', 'start with correct state'); 32 | equal(b.current, 'green', 'start with correct state'); 33 | 34 | equal(a.counter, 42, 'start with correct counter'); 35 | equal(b.counter, 42, 'start with correct counter'); 36 | 37 | a.warn(); 38 | 39 | equal(a.current, 'yellow', 'maintain independent current state'); 40 | equal(b.current, 'green', 'maintain independent current state'); 41 | 42 | equal(a.counter, 43, 'counter for (a) should have incremented'); 43 | equal(b.counter, 42, 'counter for (b) should remain untouched'); 44 | 45 | ok(a.hasOwnProperty('current'), "each instance should have its own current state"); 46 | ok(b.hasOwnProperty('current'), "each instance should have its own current state"); 47 | ok(!a.hasOwnProperty('warn'), "each instance should NOT have its own event methods"); 48 | ok(!b.hasOwnProperty('warn'), "each instance should NOT have its own event methods"); 49 | ok(a.warn === b.warn, "each instance should share event methods"); 50 | ok(a.warn === a.__proto__.warn, "each instance event methods come from its shared prototype"); 51 | ok(b.warn === b.__proto__.warn, "each instance event methods come from its shared prototype"); 52 | 53 | }); 54 | 55 | //----------------------------------------------------------------------------- 56 | 57 | test("github issue 19", function() { 58 | 59 | var Foo = function() { 60 | this.counter = 7; 61 | this.initFSM(); 62 | }; 63 | 64 | Foo.prototype.onenterready = function() { this.counter++; }; 65 | Foo.prototype.onenterrunning = function() { this.counter++; }; 66 | 67 | StateMachine.create({ 68 | target : Foo.prototype, 69 | 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 70 | events : [{name: 'execute', from: 'ready', to: 'running'}, 71 | {name: 'abort', from: 'running', to: 'ready'}] 72 | }); 73 | 74 | var foo = new Foo(); 75 | var bar = new Foo(); 76 | 77 | equal(foo.current, 'ready', 'start with correct state'); 78 | equal(bar.current, 'ready', 'start with correct state'); 79 | 80 | equal(foo.counter, 8, 'start with correct counter 7 (from constructor) + 1 (from onenterready)'); 81 | equal(bar.counter, 8, 'start with correct counter 7 (from constructor) + 1 (from onenterready)'); 82 | 83 | foo.execute(); // transition foo, but NOT bar 84 | 85 | equal(foo.current, 'running', 'changed state'); 86 | equal(bar.current, 'ready', 'state remains the same'); 87 | 88 | equal(foo.counter, 9, 'incremented counter during onenterrunning'); 89 | equal(bar.counter, 8, 'counter remains the same'); 90 | 91 | }); 92 | 93 | -------------------------------------------------------------------------------- /test/test_initialize.js: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | 3 | QUnit.module("special initialization options", { 4 | 5 | setup: function() { 6 | this.called = []; 7 | this.onbeforeevent = function(event,from,to) { this.called.push('onbefore(' + event + ')'); }, 8 | this.onafterevent = function(event,from,to) { this.called.push('onafter(' + event + ')'); }, 9 | this.onleavestate = function(event,from,to) { this.called.push('onleave(' + from + ')'); }, 10 | this.onenterstate = function(event,from,to) { this.called.push('onenter(' + to + ')'); }, 11 | this.onchangestate = function(event,from,to) { this.called.push('onchange(' + from + ',' + to + ')'); }; 12 | this.onbeforeinit = function() { this.called.push("onbeforeinit"); }; 13 | this.onafterinit = function() { this.called.push("onafterinit"); }; 14 | this.onbeforestartup = function() { this.called.push("onbeforestartup"); }; 15 | this.onafterstartup = function() { this.called.push("onafterstartup"); }; 16 | this.onbeforepanic = function() { this.called.push("onbeforepanic"); }; 17 | this.onafterpanic = function() { this.called.push("onafterpanic"); }; 18 | this.onbeforecalm = function() { this.called.push("onbeforecalm"); }; 19 | this.onaftercalm = function() { this.called.push("onaftercalm"); }; 20 | this.onenternone = function() { this.called.push("onenternone"); }; 21 | this.onentergreen = function() { this.called.push("onentergreen"); }; 22 | this.onenterred = function() { this.called.push("onenterred"); }; 23 | this.onleavenone = function() { this.called.push("onleavenone"); }; 24 | this.onleavegreen = function() { this.called.push("onleavegreen"); }; 25 | this.onleavered = function() { this.called.push("onleavered"); }; 26 | } 27 | 28 | }); 29 | 30 | //----------------------------------------------------------------------------- 31 | 32 | test("initial state defaults to 'none'", function() { 33 | StateMachine.create({ 34 | target: this, 35 | events: [ 36 | { name: 'panic', from: 'green', to: 'red' }, 37 | { name: 'calm', from: 'red', to: 'green' } 38 | ]}); 39 | equal(this.current, 'none'); 40 | deepEqual(this.called, []); 41 | }); 42 | 43 | //----------------------------------------------------------------------------- 44 | 45 | test("initial state can be specified", function() { 46 | StateMachine.create({ 47 | target: this, 48 | initial: 'green', 49 | events: [ 50 | { name: 'panic', from: 'green', to: 'red' }, 51 | { name: 'calm', from: 'red', to: 'green' } 52 | ]}); 53 | equal(this.current, 'green'); 54 | deepEqual(this.called, [ 55 | "onbeforestartup", 56 | "onbefore(startup)", 57 | "onleavenone", 58 | "onleave(none)", 59 | "onentergreen", 60 | "onenter(green)", 61 | "onchange(none,green)", 62 | "onafterstartup", 63 | "onafter(startup)" 64 | ]); 65 | }); 66 | 67 | //----------------------------------------------------------------------------- 68 | 69 | test("startup event name can be specified", function() { 70 | StateMachine.create({ 71 | target: this, 72 | initial: { state: 'green', event: 'init' }, 73 | events: [ 74 | { name: 'panic', from: 'green', to: 'red' }, 75 | { name: 'calm', from: 'red', to: 'green' } 76 | ]}); 77 | equal(this.current, 'green'); 78 | deepEqual(this.called, [ 79 | "onbeforeinit", 80 | "onbefore(init)", 81 | "onleavenone", 82 | "onleave(none)", 83 | "onentergreen", 84 | "onenter(green)", 85 | "onchange(none,green)", 86 | "onafterinit", 87 | "onafter(init)" 88 | ]); 89 | }); 90 | 91 | //----------------------------------------------------------------------------- 92 | 93 | test("startup event can be deferred", function() { 94 | StateMachine.create({ 95 | target: this, 96 | initial: { state: 'green', event: 'init', defer: true }, 97 | events: [ 98 | { name: 'panic', from: 'green', to: 'red' }, 99 | { name: 'calm', from: 'red', to: 'green' } 100 | ]}); 101 | equal(this.current, 'none'); 102 | deepEqual(this.called, []); 103 | 104 | this.init(); 105 | 106 | equal(this.current, 'green'); 107 | deepEqual(this.called, [ 108 | "onbeforeinit", 109 | "onbefore(init)", 110 | "onleavenone", 111 | "onleave(none)", 112 | "onentergreen", 113 | "onenter(green)", 114 | "onchange(none,green)", 115 | "onafterinit", 116 | "onafter(init)" 117 | ]); 118 | }); 119 | 120 | //----------------------------------------------------------------------------- 121 | 122 | 123 | --------------------------------------------------------------------------------