Skip to main content
Module

x/xstate/test/model.test.ts

State machines and statecharts for the modern web.
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
import { ContextFrom, createMachine, EventFrom } from '../src';import { assign, cancel, choose, log, send, sendParent, sendUpdate, stop} from '../src/actions';import { createModel } from '../src/model';
describe('createModel', () => { it('model.assign updates context and is typed correctly', () => { type UserEvent = | { type: 'updateName'; value: string; } | { type: 'updateAge'; value: number } | { type: 'anotherEvent' };
interface UserContext { name: string; age: number; }
const userModel = createModel<UserContext, UserEvent>({ name: 'David', age: 30 });
// Example of an externally-defined assign action const assignName = userModel.assign( { name: (_, event) => { return event.value; } }, 'updateName' );
const machine = createMachine<UserContext, UserEvent>({ context: userModel.initialContext, initial: 'active', states: { active: { on: { updateName: { // pre-defined assign action actions: assignName }, updateAge: { // inline assign action actions: userModel.assign((_, e) => { return { age: e.value }; }) } } } } });
const updatedState = machine.transition(undefined, { type: 'updateName', value: 'Anyone' });
expect(updatedState.context.name).toEqual('Anyone'); });
it('model.reset resets the context to its initial value', () => { type UserEvent = | { type: 'updateName'; value: string; } | { type: 'updateAge'; value: number } | { type: 'reset' };
interface UserContext { name: string; age: number; }
const userModel = createModel<UserContext, UserEvent>({ name: 'David', age: 30 });
// Example of an externally-defined assign action const assignName = userModel.assign( { name: (_, event) => { return event.value; } }, 'updateName' );
const machine = createMachine<UserContext, UserEvent>({ context: userModel.initialContext, initial: 'active', states: { active: { on: { updateName: { // pre-defined assign action actions: assignName }, updateAge: { // inline assign action actions: userModel.assign((_, e) => { return { age: e.value }; }) }, reset: { actions: userModel.reset() } } } } });
const updatedState = machine.transition(undefined, { type: 'updateName', value: 'Anyone' });
expect(updatedState.context.name).toEqual('Anyone');
const resetState = machine.transition(undefined, { type: 'reset' });
expect(resetState.context).toEqual(userModel.initialContext); });
it('can model events', () => { const userModel = createModel( { name: 'David', age: 30 }, { events: { updateName: (value: string) => ({ value }), updateAge: (value: number) => { const payload = { value }; (payload as any).type = 'this should be overwritten'; return payload; }, anotherEvent: () => ({}) } } );
// Example of an externally-defined assign action const assignName = userModel.assign( { name: (_, event) => { return event.value; } }, 'updateName' );
const machine = userModel.createMachine({ initial: 'active', states: { active: { on: { updateName: { // pre-defined assign action actions: [assignName] }, updateAge: { // inline assign action actions: userModel.assign((_, e) => { return { age: e.value }; }) } } } } });
let updatedState = machine.transition( undefined, userModel.events.updateName('Anyone') );
expect(updatedState.context.name).toEqual('Anyone');
updatedState = machine.transition( updatedState, userModel.events.updateAge(42) );
expect(updatedState.context.age).toEqual(42); });
it('can model actions', () => { const userModel = createModel( { name: 'David', age: 30 }, { actions: { greet: (message: string) => ({ message }) } } );
userModel.createMachine({ context: userModel.initialContext, initial: 'active', entry: { type: 'greet', message: 'hello' }, exit: { type: 'greet', message: 'goodbye' }, states: { active: { entry: [userModel.actions.greet('hello')] } } });
userModel.createMachine({ context: userModel.initialContext, // @ts-expect-error entry: { type: 'greet' } // missing message });
userModel.createMachine({ context: userModel.initialContext, // @ts-expect-error entry: { type: 'fake' } // wrong message }); });
it('works with built-in actions', () => { const model = createModel( {}, { events: { SAMPLE: () => ({}) }, actions: { custom: () => ({}) } } );
model.createMachine({ context: model.initialContext, entry: [ model.actions.custom(), // raise('SAMPLE'), send('SAMPLE'), sendParent('SOMETHING'), sendUpdate(), // respond('SOMETHING'), log('something'), cancel('something'), stop('something'), model.assign({}), choose([]) ], exit: [ model.actions.custom(), // raise('SAMPLE'), send('SAMPLE'), sendParent('SOMETHING'), sendUpdate(), // respond('SOMETHING'), log('something'), cancel('something'), stop('something'), model.assign({}), choose([]) ], on: { SAMPLE: { actions: [ model.actions.custom(), // raise('SAMPLE'), send('SAMPLE'), sendParent('SOMETHING'), sendUpdate(), // respond('SOMETHING'), log('something'), cancel('something'), stop('something'), model.assign({}), choose([]) ] } }, initial: 'someState', states: { someState: { entry: [ model.actions.custom(), // raise('SAMPLE'), send('SAMPLE'), sendParent('SOMETHING'), sendUpdate(), // respond('SOMETHING'), log('something'), cancel('something'), stop('something'), model.assign({}), choose([]) ], exit: [ model.actions.custom(), // raise('SAMPLE'), send('SAMPLE'), sendParent('SOMETHING'), sendUpdate(), // respond('SOMETHING'), log('something'), cancel('something'), stop('something'), model.assign({}), choose([]) ] } } }); });
it('should strongly type action implementations', () => { const model = createModel( {}, { events: { SAMPLE: () => ({}) }, actions: { custom: (param: string) => ({ param }) } } );
model.createMachine( { context: {} }, { actions: { custom: (_ctx, _e, { action }) => { action.param.toUpperCase();
// @ts-expect-error action.param.whatever();
// @ts-expect-error action.unknown; } } } ); });
it('should strongly type action implementations with model.createMachine(...)', () => { const model = createModel( {}, { events: { SAMPLE: () => ({}) }, actions: { custom: (param: string) => ({ param }) } } );
model.createMachine( { context: {} }, { actions: { custom: (_ctx, _e, { action }) => { action.param.toUpperCase();
// @ts-expect-error action.param.whatever();
// @ts-expect-error action.unknown; } } } ); });
it('should disallow string actions for non-simple actions', () => { const model = createModel( {}, { events: { SAMPLE: () => ({}) }, actions: { simple: () => ({}), custom: (param: string) => ({ param }) } } );
model.createMachine({ entry: ['simple', { type: 'custom', param: 'something' }],
// @ts-expect-error exit: ['custom'], initial: 'test', states: { test: { entry: ['simple', { type: 'custom', param: 'something' }],
// @ts-expect-error exit: ['custom'] } } }); });
it('should typecheck `createMachine` for model without creators', () => { const toggleModel = createModel( { count: 0 }, { events: { TOGGLE: () => ({}) } } );
toggleModel.createMachine({ id: 'machine', initial: 'inactive', states: { inactive: { on: { TOGGLE: 'active' } }, active: { on: { TOGGLE: 'inactive' } } } }); });
it('model.createMachine(...) should provide the initial context', () => { const toggleModel = createModel({ count: 0 });
const machine = toggleModel.createMachine({});
expect(machine.initialState.context.count).toBe(0); });
it('should not allow using events if creators have not been configured', () => { const model = createModel({ count: 0 });
// this is a type test for something that is not available at runtime so we suppress runtime error with try/catch try { // this should not end up being `any` // @ts-expect-error model.events.test(); } catch (err) {} });
it('should not allow using actions if creators have not been configured', () => { const model = createModel({ count: 0 });
// this is a type test for something that is not available at runtime so we suppress runtime error with try/catch try { // this should not end up being `any` // @ts-expect-error model.actions.test(); } catch (err) {} });
it('should allow for the action type to be explicitly given when creators have not been configured', () => { const model = createModel< { count: number }, { type: 'EV' }, { type: 'fooAction' } >({ count: 0 });
model.createMachine({ context: model.initialContext, initial: 'a', states: { a: { entry: 'fooAction' }, b: { // @ts-expect-error entry: 'barAction' } } }); });
it('should allow any action if actions are not specified', () => { const model = createModel( {}, { events: {} } );
model.createMachine({ entry: 'someAction', exit: { type: 'someObjectAction' }, on: { // @ts-expect-error UNEXPECTED_EVENT: {} } }); });
it('should infer context correctly when actions are not specified', () => { const model = createModel( { foo: 100 }, { events: { BAR: () => ({}) } } );
model.createMachine({ entry: (ctx) => { // @ts-expect-error assert indirectly that `ctx` is not `any` or `unknown` ctx.other; }, exit: assign({ foo: (ctx) => { // @ts-expect-error assert indirectly that `ctx` is not `any` or `unknown` ctx.other; return ctx.foo; } }) }); });
it('should keep the context type on the state after using `state.matches`', () => { const model = createModel<{ count: number }, { type: 'INC' }>({ count: 0 });
const machine = model.createMachine({ context: model.initialContext, states: { a: {} } });
if (machine.initialState.matches('a')) { machine.initialState.context.count; // @ts-expect-error machine.initialState.context.unknown; } });
it('ContextFrom accepts a model type', () => { const model = createModel( { count: 3 }, { events: {} } );
const val = ({} as unknown) as ContextFrom<typeof model>;
// expect no type error here // with previous ContextFrom behavior, this will not compile val.count;
// @ts-expect-error (sanity check) val.unknown; });
it('EventFrom accepts a model type', () => { const model = createModel( { count: 3 }, { events: { INC: () => ({}) } } );
const val = ({} as unknown) as EventFrom<typeof model>;
// expect no type error here // with previous EventFrom behavior, this will not compile val.type;
// @ts-expect-error (sanity check) val.count; });});