jssm
Version:
A Javascript state machine with a simple API. Well tested, and typed with Flowtype. MIT License.
676 lines (391 loc) • 19.3 kB
JavaScript
/* eslint-disable max-len */
import {test, describe} from 'ava-spec';
const jssm = require('../../../build/jssm.es5.js');
test('build-set version number is present', t => t.is(typeof jssm.version, 'string'));
describe('Stochastic weather', async _it => {
new jssm.Machine({
start_states: ['breezy'],
transitions: [
{ from: 'breezy', to: 'breezy', probability: 0.4 },
{ from: 'breezy', to: 'sunny', probability: 0.3 },
{ from: 'breezy', to: 'cloudy', probability: 0.15 },
{ from: 'breezy', to: 'windy', probability: 0.1 },
{ from: 'breezy', to: 'rain', probability: 0.05 },
{ from: 'sunny', to: 'sunny', probability: 0.5 },
{ from: 'sunny', to: 'hot', probability: 0.15 },
{ from: 'sunny', to: 'breezy', probability: 0.15 },
{ from: 'sunny', to: 'cloudy', probability: 0.15 },
{ from: 'sunny', to: 'rain', probability: 0.05 },
{ from: 'hot', to: 'hot', probability: 0.75 },
{ from: 'hot', to: 'breezy', probability: 0.05 },
{ from: 'hot', to: 'sunny', probability: 0.2 },
{ from: 'cloudy', to: 'cloudy', probability: 0.6 },
{ from: 'cloudy', to: 'sunny', probability: 0.2 },
{ from: 'cloudy', to: 'rain', probability: 0.15 },
{ from: 'cloudy', to: 'breezy', probability: 0.05 },
{ from: 'windy', to: 'windy', probability: 0.3 },
{ from: 'windy', to: 'gale', probability: 0.1 },
{ from: 'windy', to: 'breezy', probability: 0.4 },
{ from: 'windy', to: 'rain', probability: 0.15 },
{ from: 'windy', to: 'sunny', probability: 0.05 },
{ from: 'gale', to: 'gale', probability: 0.65 },
{ from: 'gale', to: 'windy', probability: 0.25 },
{ from: 'gale', to: 'torrent', probability: 0.05 },
{ from: 'gale', to: 'hot', probability: 0.05 },
{ from: 'rain', to: 'rain', probability: 0.3 },
{ from: 'rain', to: 'torrent', probability: 0.05 },
{ from: 'rain', to: 'windy', probability: 0.1 },
{ from: 'rain', to: 'breezy', probability: 0.15 },
{ from: 'rain', to: 'sunny', probability: 0.1 },
{ from: 'rain', to: 'cloudy', probability: 0.3 },
{ from: 'torrent', to: 'torrent', probability: 0.65 },
{ from: 'torrent', to: 'rain', probability: 0.25 },
{ from: 'torrent', to: 'cloudy', probability: 0.05 },
{ from: 'torrent', to: 'gale', probability: 0.05 }
]
});
});
describe('list exit actions', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[ { from:'off', to:'red', action:'on' }, { from:'red', to:'off',action:'off' } ]
});
it('shows "on" from off as default', t => t.is('on', machine.list_exit_actions()[0] ) );
it('shows "on" from off', t => t.is('on', machine.list_exit_actions('off')[0] ) );
it('shows "off" from red', t => t.is('off', machine.list_exit_actions('red')[0] ) );
});
describe('probable exits for', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[ { from:'off', to:'red' } ]
});
it('probable exits are an array', t => t.is(true, Array.isArray(machine.probable_exits_for('off')) ) );
it('one probable exit in example', t => t.is(1, machine.probable_exits_for('off').length ) );
it('exit is an object', t => t.is('object', typeof machine.probable_exits_for('off')[0] ) );
it('exit 0 has a string from property', t => t.is('string', typeof machine.probable_exits_for('off')[0].from ) );
});
describe('probable action exits', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[ { from:'off', to:'red', action:'on' }, { from:'red', to:'off',action:'off' } ]
});
it('probable action exits are an array', t => t.is(true, Array.isArray(machine.probable_action_exits()) ) );
it('probable action exit 1 is on', t => t.is('on', machine.probable_action_exits()[0].action ) );
it('probable action exits are an array', t => t.is(true, Array.isArray(machine.probable_action_exits('off')) ) );
it('probable action exit 1 is on', t => t.is('on', machine.probable_action_exits('off')[0].action ) );
it('probable action exits are an array', t => t.is(true, Array.isArray(machine.probable_action_exits('red')) ) );
it('probable action exit 1 is on', t => t.is('off', machine.probable_action_exits('red')[0].action ) );
});
describe('probabilistic_transition', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[ { from:'off', to:'red' } ]
});
machine.probabilistic_transition();
it('solo after probabilistic is red', t => t.is('red', machine.state() ) );
});
describe('probabilistic_walk', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[ { from:'off', to:'red' }, { from:'red', to:'off' } ]
});
machine.probabilistic_walk(3);
it('solo after probabilistic walk 3 is red', t => t.is('red', machine.state() ) );
});
describe('probabilistic_histo_walk', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[ { from:'off', to:'red' }, { from:'red', to:'off' } ]
});
const histo = machine.probabilistic_histo_walk(3);
it('histo is a Map', t => t.is(true, histo instanceof Map) );
it('histo red is 2', t => t.is(2, histo.get('red')) );
it('histo off is 2', t => t.is(2, histo.get('off')) );
});
describe('reports state_is_final', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[
{ from:'off', to:'red' },
{ from:'off', to:'mid' },
{ from:'mid', to:'fin' }
],
complete:['red', 'mid']
});
it('final false for neither', t => t.is(false, machine.state_is_final('off') ) );
it('final false for just terminal', t => t.is(false, machine.state_is_final('mid') ) );
it('final false for just complete', t => t.is(false, machine.state_is_final('fin') ) );
it('final true', t => t.is(true, machine.state_is_final('red') ) );
});
describe('reports is_final', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[
{ from:'off', to:'red' }
],
complete:['red']
});
const init_final = machine.is_final();
machine.transition('red');
const fin_final = machine.is_final();
it('final false', t => t.is(false, init_final ) );
it('final true', t => t.is(true, fin_final ) );
/* todo whargarbl needs another two tests for is_changing once reintroduced */
});
describe('reports state_is_terminal', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[ { name:'turn_on', action:'power_on', from:'off', to:'red'} ]
});
it('terminal false', t => t.is(false, machine.state_is_terminal('off') ) );
it('terminal true', t => t.is(true, machine.state_is_terminal('red') ) );
});
describe('reports is_terminal', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[ { name:'turn_on', action:'power_on', from:'off', to:'red'} ]
});
const first = machine.is_terminal();
machine.transition('red');
const second = machine.is_terminal();
const terms = machine.has_terminals();
it('terminal false', t => t.is( false, first ) );
it('terminal true', t => t.is( true, second ) );
it('has_terminals', t => t.is( true, terms ) );
});
describe('reports state_is_complete', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[ { name:'turn_on', action:'power_on', from:'off', to:'red'} ],
complete:['off'] // huhu
});
it('state_is_complete false', t => t.is( true, machine.state_is_complete('off') ) );
it('state_is_complete true', t => t.is( false, machine.state_is_complete('red') ) );
});
describe('reports is_complete', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[ { name:'turn_on', action:'power_on', from:'off', to:'red'} ],
complete:['off'] // huhu
});
const first = machine.is_complete();
machine.transition('red');
const second = machine.is_complete();
const terms = machine.has_completes();
it('is_complete false', t => t.is( true, first ) );
it('is_complete true', t => t.is( false, second ) );
it('has_completes', t => t.is( true, terms ) );
});
describe('reports on actions', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[ { name:'turn_on', action:'power_on', from:'off', to:'red'} ]
});
const a = machine.list_actions(); // todo comeback
it('that it has', t => t.is('number', typeof machine.current_action_for('power_on') ) );
it('that it doesn\'t have', t => t.is('undefined', typeof machine.current_action_for('power_left') ) );
it('correct list type', t => t.is(true, Array.isArray(a) ) );
it('correct list size', t => t.is(1, a.length ) );
});
describe('actions', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[ { from:'off', to:'red', action:'on' }, { from:'red', to:'off',action:'off' } ]
});
it('red has actions().length 1', t => t.is(1, machine.actions().length ) );
it('red has actions()[0] "on"', t => t.is('on', machine.actions()[0] ) );
it('red has actions().length 1', t => t.is(1, machine.actions('off').length ) );
it('red has actions()[0] "on"', t => t.is('on', machine.actions('off')[0] ) );
it('red has actions().length 1', t => t.is(1, machine.actions('red').length ) );
it('red has actions()[0] "off"', t => t.is('off', machine.actions('red')[0] ) );
});
describe('states having action', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[ { from:'off', to:'red', action:'on' }, { from:'red', to:'off',action:'off' } ]
});
it('one action has on', t => t.is(1, machine.list_states_having_action('on').length ) );
it('on is had by off', t => t.is('off', machine.list_states_having_action('on')[0] ) );
});
describe('unenterables', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[ { name:'turn_on', action:'power_on', from:'off', to:'red'} ]
});
it('off isn\'t enterable', t => t.is(true, machine.is_unenterable('off') ) );
it('red is enterable', t => t.is(false, machine.is_unenterable('red') ) );
it('machine has unenterables', t => t.is(true, machine.has_unenterables() ) );
});
describe('reports on action edges', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[ { name:'turn_on', action:'power_on', from:'off', to:'red'} ]
});
it('that it has', t => t.is('object', typeof machine.current_action_edge_for('power_on')) );
it('that it doesn\'t have', t => t.throws(() => { machine.current_action_edge_for('power_west'); }) );
});
describe('reports on states', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[ { name:'turn_on', action:'power_on', from:'off', to:'red'} ]
});
it('that it has', t => t.is('object', typeof machine.state_for('off') ) );
it('that it doesn\'t have', t => t.throws(() => { machine.state_for('no such state'); }) );
});
describe('returns states', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[ { name:'turn_on', action:'power_on', from:'off', to:'red'} ]
});
it('that it has', t => t.is('object', typeof machine.machine_state() ) );
});
describe('reports on transitions', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[ { name:'turn_on', action:'power_on', from:'off', to:'red'} ]
});
it('unspecified transition return type', t => t.is('object', typeof machine.list_transitions() ) );
it('unspecified transition correct entrance count', t => t.is(0, machine.list_transitions().entrances.length ) );
it('unspecified transition correct exit count', t => t.is(1, machine.list_transitions().exits.length ) );
it('specified transition return type', t => t.is('object', typeof machine.list_transitions('off') ) );
it('specified transition correct entrance count', t => t.is(0, machine.list_transitions('off').entrances.length ) );
it('specified transition correct exit count', t => t.is(1, machine.list_transitions('off').exits.length ) );
it('no such spec trans return type', t => t.is('object', typeof machine.list_transitions('moot') ) );
it('no such spec trans correct entrance count', t => t.is(0, machine.list_transitions('moot').entrances.length ) );
it('no such spec trans correct exit count', t => t.is(0, machine.list_transitions('moot').exits.length ) );
it('unspecified entrance return type', t => t.is(true, Array.isArray( machine.list_entrances() ) ) );
it('unspecified entrance correct count', t => t.is(0, machine.list_entrances().length ) );
it('specified entrance return type', t => t.is(true, Array.isArray( machine.list_entrances('off') ) ) );
it('specified entrance correct count', t => t.is(0, machine.list_entrances('off').length ) );
it('no such specified entrance return type', t => t.is(true, Array.isArray( machine.list_entrances('moot') ) ) ); // todo whargarbl should these throw?
it('no such specified entrance correct count', t => t.is(0, machine.list_entrances('moot').length ) );
it('unspecified exit return type', t => t.is(true, Array.isArray( machine.list_exits() ) ) );
it('unspecified exit correct count', t => t.is(1, machine.list_exits().length ) );
it('specified exit return type', t => t.is(true, Array.isArray( machine.list_exits('off') ) ) );
it('specified exit correct count', t => t.is(1, machine.list_exits('off').length ) );
it('no such specified exit return type', t => t.is(true, Array.isArray( machine.list_exits('moot') ) ) );
it('no such specified exit correct count', t => t.is(0, machine.list_exits('moot').length ) );
it('edge list return type', t => t.is(true, Array.isArray( machine.list_edges() ) ) );
it('edge list correct count', t => t.is(1, machine.list_edges().length ) );
});
describe('transition by state names', async it => {
const machine = new jssm.Machine({
start_states: ['off'],
transitions:[ { name:'turn_on', action:'power_on', from:'off', to:'red'} ]
});
it('finds off -> red', t => t.is(0, machine.get_transition_by_state_names('off', 'red') ) );
it('does not find off -> blue', t => t.is(undefined, machine.get_transition_by_state_names('off', 'blue') ) );
it('does not find blue -> red', t => t.is(undefined, machine.get_transition_by_state_names('blue', 'red') ) );
});
describe('Illegal machines', async it => {
it('catch repeated names', t => t.throws(() => {
new jssm.Machine({
start_states: ['moot'],
transitions:[
{ name:'identical', from:'1', to:'2' },
{ name:'identical', from:'2', to:'3' }
]
});
}, Error));
it('must define from', t => t.throws(() => {
new jssm.Machine({
start_states: ['moot'],
transitions:[
{ name:'identical', to:'2' }
]
});
}, Error));
it('must define to', t => t.throws(() => {
new jssm.Machine({
start_states: ['moot'],
transitions:[
{ name:'identical', from:'1' }
]
});
}, Error));
it('must not have two identical edges', t => t.throws(() => {
new jssm.Machine({
start_states: ['moot'],
transitions:[
{ name:'id1', from:'1', to:'2' },
{ name:'id2', from:'1', to:'2' }
]
});
}, Error));
it('must not have two of the same action from the same source', t => t.throws(() => {
new jssm.Machine({
start_states: ['moot'],
transitions:[
{ name:'id1', from:'1', to:'2', action:'identical' },
{ name:'id2', from:'1', to:'3', action:'identical' }
]
});
}, Error));
it('must not have completion of non-state', t => t.throws(() => {
const machine = new jssm.Machine({
start_states: ['moot'],
transitions:[
{ name:'id1', from:'1', to:'2', action:'identical' }
]
});
machine.is_complete('no such state');
}, Error));
it('internal state helper must not accept double states', t => t.throws(() => {
const machine = new jssm.Machine({
start_states: ['moot'],
transitions:[
{ name:'id1', from:'1', to:'2', action:'identical' }
]
});
machine._new_state({from: '1', name:'id1', to:'2', complete:false});
machine._new_state({from: '1', name:'id1', to:'2', complete:false});
}, Error));
it('can\'t get actions of non-state', t => t.throws(() => {
const machine = new jssm.Machine({
start_states: ['1'],
transitions:[
{ name:'id1', from:'1', to:'2', action:'identical' }
]
});
machine.actions('three');
}, Error));
it('can\'t get states having non-action', t => t.throws(() => {
const machine = new jssm.Machine({
start_states: ['1'],
transitions:[
{ name:'id1', from:'1', to:'2', action:'identical' }
]
});
machine.list_states_having_action('no such action');
}, Error));
it('can\'t list exit states of non-action', t => t.throws(() => {
const machine = new jssm.Machine({
start_states: ['1'],
transitions:[
{ name:'id1', from:'1', to:'2', action:'identical' }
]
});
machine.list_exit_actions('no such action');
}, Error));
it('probable exits for throws on non-state', t => t.throws(() => {
const machine = new jssm.Machine({
start_states: ['1'],
transitions:[
{ name:'id1', from:'1', to:'2', action:'identical' }
]
});
machine.probable_exits_for('3');
}, Error));
test(t => {
t.pass();
});
it('no probable action exits of non-action', t => t.throws(() => {
const machine = new jssm.Machine({
start_states: ['1'],
transitions:[
{ name:'id1', from:'1', to:'2', action:'identical' }
]
});
machine.probable_action_exits('no such action');
}, Error));
});