From d36a5c66941bbf78ef4ca0e3bb1285021e4e20b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Tue, 17 Mar 2020 00:01:53 +0100 Subject: [PATCH 1/4] Implement conditional actions --- packages/core/src/StateNode.ts | 118 +++--------------- packages/core/src/actionTypes.ts | 1 + packages/core/src/actions.ts | 110 +++++++++++++++- packages/core/src/scxml.ts | 207 ++++++++++++++++++++----------- packages/core/src/types.ts | 18 ++- packages/core/src/utils.ts | 40 +++++- packages/core/test/scxml.test.ts | 10 +- 7 files changed, 315 insertions(+), 189 deletions(-) diff --git a/packages/core/src/StateNode.ts b/packages/core/src/StateNode.ts index 4673841c16..968515b3af 100644 --- a/packages/core/src/StateNode.ts +++ b/packages/core/src/StateNode.ts @@ -14,7 +14,6 @@ import { isBuiltInEvent, partition, updateHistoryValue, - updateContext, warn, isArray, isFunction, @@ -24,7 +23,8 @@ import { toSCXMLEvent, mapContext, toTransitionConfigArray, - normalizeTarget + normalizeTarget, + evaluateGuard } from './utils'; import { Event, @@ -38,7 +38,6 @@ import { HistoryValue, StateNodeDefinition, TransitionDefinition, - AssignAction, DelayedTransitionDefinition, ActivityDefinition, StateNodeConfig, @@ -50,20 +49,13 @@ import { ActionObject, Mapper, PropertyMapper, - SendAction, NullEvent, - Guard, - GuardPredicate, - GuardMeta, MachineConfig, - PureAction, InvokeCreator, DoneEventObject, SingleOrArray, - LogAction, SendActionObject, SpecialTargets, - RaiseAction, SCXML, RaiseActionObject, ActivityActionObject, @@ -87,14 +79,12 @@ import { doneInvoke, error, toActionObject, - resolveSend, initEvent, toActionObjects, - resolveLog, - resolveRaise + resolveActions } from './actions'; import { IS_PRODUCTION } from './environment'; -import { DEFAULT_GUARD_TYPE, STATE_DELIMITER } from './constants'; +import { STATE_DELIMITER } from './constants'; import { getValue, getConfiguration, @@ -824,7 +814,8 @@ class StateNode< try { guardPassed = - !cond || this.evaluateGuard(cond, resolvedContext, _event, state); + !cond || + evaluateGuard(this.machine, cond, resolvedContext, _event, state); } catch (err) { throw new Error( `Unable to evaluate guard '${cond!.name || @@ -920,38 +911,6 @@ class StateNode< return true; } - private evaluateGuard( - guard: Guard, - context: TContext, - _event: SCXML.Event, - state: State - ): boolean { - const { guards } = this.machine.options; - const guardMeta: GuardMeta = { - state, - cond: guard, - _event - }; - - // TODO: do not hardcode! - if (guard.type === DEFAULT_GUARD_TYPE) { - return (guard as GuardPredicate).predicate( - context, - _event.data, - guardMeta - ); - } - - const condFn = guards[guard.type]; - - if (!condFn) { - throw new Error( - `Guard '${guard.type}' is not implemented on machine '${this.machine.id}'.` - ); - } - - return condFn(context, _event.data, guardMeta); - } private getActions( transition: StateTransition, @@ -1177,57 +1136,12 @@ class StateNode< } } - const [assignActions, otherActions] = partition( - actions, - (action): action is AssignAction => - action.type === actionTypes.assign - ); - - const updatedContext = assignActions.length - ? updateContext(currentContext, _event, assignActions, currentState) - : currentContext; - - const resolvedActions = flatten( - otherActions.map(actionObject => { - switch (actionObject.type) { - case actionTypes.raise: - return resolveRaise(actionObject as RaiseAction); - case actionTypes.send: - const sendAction = resolveSend( - actionObject as SendAction, - updatedContext, - _event, - this.machine.options.delays - ) as ActionObject; // TODO: fix ActionTypes.Init - - if (!IS_PRODUCTION) { - // warn after resolving as we can create better contextual message here - warn( - !isString(actionObject.delay) || - typeof sendAction.delay === 'number', - // tslint:disable-next-line:max-line-length - `No delay reference for delay expression '${actionObject.delay}' was found on machine '${this.machine.id}'` - ); - } - - return sendAction; - case actionTypes.log: - return resolveLog( - actionObject as LogAction, - updatedContext, - _event - ); - case actionTypes.pure: - return ( - (actionObject as PureAction).get( - updatedContext, - _event.data - ) || [] - ); - default: - return toActionObject(actionObject, this.options.actions); - } - }) + const [resolvedActions, updatedContext] = resolveActions( + this, + currentState, + currentContext, + _event, + actions ); const [raisedEvents, nonRaisedActions] = partition( @@ -1297,7 +1211,7 @@ class StateNode< !resolvedStateValue || stateTransition.source ? currentState : undefined, - actions: resolvedStateValue ? nonRaisedActions : [], + actions: resolvedStateValue ? nonRaisedActions : ([] as any[]), activities: resolvedStateValue ? activities : currentState @@ -1315,8 +1229,9 @@ class StateNode< done: isDone }); - nextState.changed = - _event.name === actionTypes.update || !!assignActions.length; + const didUpdateContext = currentContext !== updatedContext; + + nextState.changed = _event.name === actionTypes.update || didUpdateContext; // Dispose of penultimate histories to prevent memory leaks const { history } = nextState; @@ -1360,7 +1275,6 @@ class StateNode< maybeNextState.changed || (history ? !!maybeNextState.actions.length || - !!assignActions.length || typeof history.value !== typeof maybeNextState.value || !stateValuesEqual(maybeNextState.value, history.value) : undefined); diff --git a/packages/core/src/actionTypes.ts b/packages/core/src/actionTypes.ts index 96a1736ef2..56804fc85e 100644 --- a/packages/core/src/actionTypes.ts +++ b/packages/core/src/actionTypes.ts @@ -17,4 +17,5 @@ export const errorExecution = ActionTypes.ErrorExecution; export const errorPlatform = ActionTypes.ErrorPlatform; export const error = ActionTypes.ErrorCustom; export const update = ActionTypes.Update; +export const decide = ActionTypes.Decide; export const pure = ActionTypes.Pure; diff --git a/packages/core/src/actions.ts b/packages/core/src/actions.ts index 54f31271e8..a9d86b635b 100644 --- a/packages/core/src/actions.ts +++ b/packages/core/src/actions.ts @@ -30,7 +30,9 @@ import { LogActionObject, DelayFunctionMap, SCXML, - ExprWithMeta + ExprWithMeta, + DecideConditon, + DecideAction } from './types'; import * as actionTypes from './actionTypes'; import { @@ -38,9 +40,19 @@ import { isFunction, isString, toEventObject, - toSCXMLEvent + toSCXMLEvent, + partition, + flatten, + updateContext, + warn, + toGuard, + evaluateGuard, + toArray } from './utils'; import { isArray } from './utils'; +import { State } from './State'; +import { StateNode } from './StateNode'; +import { IS_PRODUCTION } from './environment'; export { actionTypes }; @@ -505,3 +517,97 @@ export function escalate< } ); } + +export function decide( + conds: DecideConditon[] +): DecideAction { + return { + type: ActionTypes.Decide, + conds + }; +} + +export function resolveActions( + machine: StateNode, + currentState: State | undefined, + currentContext: TContext, + _event: SCXML.Event, + actions: ActionObject[] +): [ActionObject[], TContext] { + const [assignActions, otherActions] = partition( + actions, + (action): action is AssignAction => + action.type === actionTypes.assign + ); + + let updatedContext = assignActions.length + ? updateContext(currentContext, _event, assignActions, currentState) + : currentContext; + + const resolvedActions = flatten( + otherActions.map(actionObject => { + switch (actionObject.type) { + case actionTypes.raise: + return resolveRaise(actionObject as RaiseAction); + case actionTypes.send: + const sendAction = resolveSend( + actionObject as SendAction, + updatedContext, + _event, + machine.options.delays + ) as ActionObject; // TODO: fix ActionTypes.Init + + if (!IS_PRODUCTION) { + // warn after resolving as we can create better contextual message here + warn( + !isString(actionObject.delay) || + typeof sendAction.delay === 'number', + // tslint:disable-next-line:max-line-length + `No delay reference for delay expression '${actionObject.delay}' was found on machine '${machine.id}'` + ); + } + + return sendAction; + case actionTypes.log: + return resolveLog( + actionObject as LogAction, + updatedContext, + _event + ); + case actionTypes.decide:{ + const decideAction = actionObject as DecideAction + const matchedActions = decideAction.conds.find( + condition => { + const guard = toGuard(condition.cond, machine.options.guards) + return !guard || evaluateGuard(machine, guard, updatedContext, _event, currentState as any) + } + )?.actions; + + if (!matchedActions) { + return [] + } + + const resolved = resolveActions(machine, currentState, updatedContext, _event, toActionObjects(toArray(matchedActions))) + updatedContext = resolved[1] + return resolved[0] + } + case actionTypes.pure:{ + const matchedActions = ( + (actionObject as PureAction).get( + updatedContext, + _event.data + )) + if (!matchedActions) { + return [] + } + const resolved = resolveActions(machine, currentState, updatedContext, _event, toActionObjects(toArray(matchedActions))) + updatedContext = resolved[1] + return resolved[0] + } + default: + return toActionObject(actionObject, machine.options.actions); + } + }) + ); + return [resolvedActions, updatedContext]; +} diff --git a/packages/core/src/scxml.ts b/packages/core/src/scxml.ts index c2e3589d63..e499fae28e 100644 --- a/packages/core/src/scxml.ts +++ b/packages/core/src/scxml.ts @@ -4,7 +4,8 @@ import { ActionObject, SCXMLEventMeta, SendExpr, - DelayExpr + DelayExpr, + DecideConditon } from './types'; import { StateNode, Machine } from './index'; import { mapValues, keys, isString } from './utils'; @@ -121,91 +122,153 @@ const evaluateExecutableContent = < return fn(context, meta._event); }; -function mapActions< +function createCond< TContext extends object, TEvent extends EventObject = EventObject ->(elements: XMLElement[]): Array> { - return elements.map(element => { - switch (element.name) { - case 'raise': - return actions.raise(element.attributes! - .event! as string); - case 'assign': - return actions.assign((context, e, meta) => { - const fnBody = ` +>(cond: string) { + return (context: TContext, _event: TEvent, meta) => { + return evaluateExecutableContent(context, _event, meta, `return ${cond};`); + }; +} + +function mapAction< + TContext extends object, + TEvent extends EventObject = EventObject +>(element: XMLElement): ActionObject { + switch (element.name) { + case 'raise': { + return actions.raise(element.attributes! + .event! as string); + } + case 'assign': { + return actions.assign((context, e, meta) => { + const fnBody = ` return {'${element.attributes!.location}': ${ - element.attributes!.expr - }}; + element.attributes!.expr + }}; `; - return evaluateExecutableContent(context, e, meta, fnBody); - }); - case 'send': - const { event, eventexpr, target } = element.attributes!; - - let convertedEvent: TEvent['type'] | SendExpr; - let convertedDelay: number | DelayExpr | undefined; - - const params = - element.elements && - element.elements.reduce((acc, child) => { - if (child.name === 'content') { - throw new Error( - 'Conversion of inside not implemented.' - ); - } - return `${acc}${child.attributes!.name}:${ - child.attributes!.expr - },\n`; - }, ''); - - if (event && !params) { - convertedEvent = event as TEvent['type']; - } else { - convertedEvent = (context, _ev, meta) => { - const fnBody = ` + return evaluateExecutableContent(context, e, meta, fnBody); + }); + } + case 'send': { + const { event, eventexpr, target } = element.attributes!; + + let convertedEvent: TEvent['type'] | SendExpr; + let convertedDelay: number | DelayExpr | undefined; + + const params = + element.elements && + element.elements.reduce((acc, child) => { + if (child.name === 'content') { + throw new Error( + 'Conversion of inside not implemented.' + ); + } + return `${acc}${child.attributes!.name}:${child.attributes!.expr},\n`; + }, ''); + + if (event && !params) { + convertedEvent = event as TEvent['type']; + } else { + convertedEvent = (context, _ev, meta) => { + const fnBody = ` return { type: ${event ? `"${event}"` : eventexpr}, ${ - params ? params : '' - } } + params ? params : '' + } } `; - return evaluateExecutableContent(context, _ev, meta, fnBody); - }; - } + return evaluateExecutableContent(context, _ev, meta, fnBody); + }; + } - if ('delay' in element.attributes!) { - convertedDelay = delayToMs(element.attributes!.delay); - } else if (element.attributes!.delayexpr) { - convertedDelay = (context, _ev, meta) => { - const fnBody = ` + if ('delay' in element.attributes!) { + convertedDelay = delayToMs(element.attributes!.delay); + } else if (element.attributes!.delayexpr) { + convertedDelay = (context, _ev, meta) => { + const fnBody = ` return (${delayToMs})(${element.attributes!.delayexpr}); `; - return evaluateExecutableContent(context, _ev, meta, fnBody); - }; - } + return evaluateExecutableContent(context, _ev, meta, fnBody); + }; + } - return actions.send(convertedEvent, { - delay: convertedDelay, - to: target as string | undefined - }); - case 'log': - const label = element.attributes!.label; + return actions.send(convertedEvent, { + delay: convertedDelay, + to: target as string | undefined + }); + } + case 'log': { + const label = element.attributes!.label; - return actions.log( - (context, e, meta) => { - const fnBody = ` + return actions.log( + (context, e, meta) => { + const fnBody = ` return ${element.attributes!.expr}; `; - return evaluateExecutableContent(context, e, meta, fnBody); - }, - label !== undefined ? String(label) : undefined - ); - default: - return { type: 'not-implemented' }; + return evaluateExecutableContent(context, e, meta, fnBody); + }, + label !== undefined ? String(label) : undefined + ); } - }); + case 'if': { + const conds: DecideConditon[] = []; + + let current: DecideConditon = { + cond: createCond(element.attributes!.cond as string), + actions: [] + }; + + for (const el of element.elements!) { + if (el.type === 'comment') { + continue; + } + + switch (el.name) { + case 'elseif': + conds.push(current); + current = { + cond: createCond(el.attributes!.cond as string), + actions: [] + }; + break; + case 'else': + conds.push(current); + current = { actions: [] }; + break; + default: + (current.actions as any[]).push(mapAction(el)); + break; + } + } + + conds.push(current); + return actions.decide(conds); + } + default: + throw new Error( + `Conversion of "${element.name}" elements is not implemented yet.` + ); + } +} + +function mapActions< + TContext extends object, + TEvent extends EventObject = EventObject +>(elements: XMLElement[]): Array> { + const mapped: Array> = []; + + for (const element of elements) { + if (element.type === 'comment') { + continue; + } + + mapped.push(mapAction(element)); + } + + return mapped; } function toConfig( @@ -302,13 +365,7 @@ function toConfig( ...(value.elements ? executableContent(value.elements) : undefined), ...(value.attributes && value.attributes.cond ? { - cond: (context, _event, meta) => { - const fnBody = ` - return ${value.attributes!.cond as string}; - `; - - return evaluateExecutableContent(context, _event, meta, fnBody); - } + cond: createCond(value.attributes!.cond as string) } : undefined), internal diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 6012d9630b..e969348ec0 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -64,8 +64,13 @@ export type ActionFunction = ( meta: ActionMeta ) => any | void; -// export type InternalAction = SendAction | AssignAction; -export type Action = +export interface DecideConditon { + cond?: Condition; + actions: Actions; +} + +export // export type InternalAction = SendAction | AssignAction; +type Action = | ActionType | ActionObject | ActionFunction @@ -717,7 +722,8 @@ export enum ActionTypes { ErrorPlatform = 'error.platform', ErrorCustom = 'xstate.error', Update = 'xstate.update', - Pure = 'xstate.pure' + Pure = 'xstate.pure', + Decide = 'xstate.decide' } export interface RaiseAction { @@ -895,6 +901,12 @@ export interface PureAction ) => SingleOrArray> | undefined; } +export interface DecideAction + extends ActionObject { + type: ActionTypes.Decide; + conds: DecideConditon[]; +} + export interface TransitionDefinition extends TransitionConfig { target: Array> | undefined; diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 22b8f2809b..9664619373 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -10,7 +10,6 @@ import { HistoryValue, AssignAction, Condition, - Guard, Subscribable, StateMachine, ConditionPredicate, @@ -20,7 +19,10 @@ import { TransitionConfig, TransitionConfigTargetShortcut, NullEvent, - SingleOrArray + SingleOrArray, + Guard, + GuardPredicate, + GuardMeta } from './types'; import { STATE_DELIMITER, @@ -644,3 +646,37 @@ export function reportUnhandledExceptionOnInvocation( } } } + +export function evaluateGuard( + machine: StateNode, + guard: Guard, + context: TContext, + _event: SCXML.Event, + state: State +): boolean { + const { guards } = machine.options; + const guardMeta: GuardMeta = { + state, + cond: guard, + _event + }; + + // TODO: do not hardcode! + if (guard.type === DEFAULT_GUARD_TYPE) { + return (guard as GuardPredicate).predicate( + context, + _event.data, + guardMeta + ); + } + + const condFn = guards[guard.type]; + + if (!condFn) { + throw new Error( + `Guard '${guard.type}' is not implemented on machine '${machine.id}'.` + ); + } + + return condFn(context, _event.data, guardMeta); +} diff --git a/packages/core/test/scxml.test.ts b/packages/core/test/scxml.test.ts index 98f06dbaf7..4047a43636 100644 --- a/packages/core/test/scxml.test.ts +++ b/packages/core/test/scxml.test.ts @@ -63,7 +63,7 @@ const testGroups = { 'history6' ], 'if-else': [ - // 'test0', // not implemented + // 'test0', // microstep not implemented correctly ], in: [ // 'TestInPredicate', // In() conversion not implemented yet @@ -140,12 +140,12 @@ const testGroups = { 'targetless-transition': ['test0', 'test1', 'test2', 'test3'], 'w3c-ecma': [ 'test144.txml', - // 'test147.txml', - // 'test148.txml', + 'test147.txml', + 'test148.txml', 'test149.txml', // 'test150.txml', // 'test151.txml', - // 'test152.txml', + // 'test152.txml', // not implemented yet // 'test153.txml', // 'test155.txml', // 'test156.txml', @@ -272,7 +272,7 @@ const testGroups = { 'test405.txml', 'test406.txml', 'test407.txml', - 'test409.txml', + // 'test409.txml', // conversion of In() predicate not implemented yet // 'test411.txml', // 'test412.txml', // 'test413.txml', From 94c029a5fa894f546a4e64ef77d1b5ba90a33a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Mon, 23 Mar 2020 00:06:08 +0100 Subject: [PATCH 2/4] Add tests for conditional actions --- packages/core/src/actions.ts | 68 ++++++---- packages/core/src/types.ts | 7 +- packages/core/test/actions.test.ts | 211 ++++++++++++++++++++++++++++- 3 files changed, 256 insertions(+), 30 deletions(-) diff --git a/packages/core/src/actions.ts b/packages/core/src/actions.ts index a9d86b635b..dab1856670 100644 --- a/packages/core/src/actions.ts +++ b/packages/core/src/actions.ts @@ -456,7 +456,7 @@ export function error(id: string, data?: any): ErrorPlatformEvent & string { eventObject.toString = () => type; - return eventObject as (ErrorPlatformEvent & string); + return eventObject as ErrorPlatformEvent & string; } export function pure( @@ -574,35 +574,53 @@ export function resolveActions( updatedContext, _event ); - case actionTypes.decide:{ - const decideAction = actionObject as DecideAction - const matchedActions = decideAction.conds.find( - condition => { - const guard = toGuard(condition.cond, machine.options.guards) - return !guard || evaluateGuard(machine, guard, updatedContext, _event, currentState as any) - } - )?.actions; + case actionTypes.decide: { + const decideAction = actionObject as DecideAction; + const matchedActions = decideAction.conds.find(condition => { + const guard = toGuard(condition.cond, machine.options.guards); + return ( + !guard || + evaluateGuard( + machine, + guard, + updatedContext, + _event, + currentState as any + ) + ); + })?.actions; if (!matchedActions) { - return [] + return []; } - const resolved = resolveActions(machine, currentState, updatedContext, _event, toActionObjects(toArray(matchedActions))) - updatedContext = resolved[1] - return resolved[0] + const resolved = resolveActions( + machine, + currentState, + updatedContext, + _event, + toActionObjects(toArray(matchedActions)) + ); + updatedContext = resolved[1]; + return resolved[0]; } - case actionTypes.pure:{ - const matchedActions = ( - (actionObject as PureAction).get( - updatedContext, - _event.data - )) - if (!matchedActions) { - return [] - } - const resolved = resolveActions(machine, currentState, updatedContext, _event, toActionObjects(toArray(matchedActions))) - updatedContext = resolved[1] - return resolved[0] + case actionTypes.pure: { + const matchedActions = (actionObject as PureAction< + TContext, + TEvent + >).get(updatedContext, _event.data); + if (!matchedActions) { + return []; + } + const resolved = resolveActions( + machine, + currentState, + updatedContext, + _event, + toActionObjects(toArray(matchedActions)) + ); + updatedContext = resolved[1]; + return resolved[0]; } default: return toActionObject(actionObject, machine.options.actions); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index e969348ec0..eea6a3d6a6 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -76,7 +76,8 @@ type Action = | ActionFunction | AssignAction, TEvent> | SendAction - | RaiseAction; + | RaiseAction + | DecideAction; export type Actions = SingleOrArray< Action @@ -128,9 +129,9 @@ export interface GuardPredicate { export type Guard = | GuardPredicate - | Record & { + | (Record & { type: string; - }; + }); export interface GuardMeta extends StateMeta { diff --git a/packages/core/test/actions.test.ts b/packages/core/test/actions.test.ts index bcf69a8640..ef42e3544f 100644 --- a/packages/core/test/actions.test.ts +++ b/packages/core/test/actions.test.ts @@ -1,5 +1,12 @@ -import { Machine, assign, forwardTo, interpret, spawn } from '../src/index'; -import { pure, sendParent, log } from '../src/actions'; +import { + Machine, + createMachine, + assign, + forwardTo, + interpret, + spawn +} from '../src/index'; +import { pure, sendParent, log, decide } from '../src/actions'; describe('onEntry/onExit actions', () => { const pedestrianStates = { @@ -1029,3 +1036,203 @@ describe('log()', () => { `); }); }); + +describe('decide', () => { + it('should execute a single conditional action', () => { + type Ctx = { answer?: number }; + const machine = createMachine({ + context: {}, + initial: 'foo', + states: { + foo: { + entry: decide([ + { cond: () => true, actions: assign({ answer: 42 }) } + ]) + } + } + }); + + const service = interpret(machine).start(); + + expect(service.state.context).toEqual({ answer: 42 }); + }); + + it('should execute a multiple conditional actions', () => { + let executed = false; + + type Ctx = { answer?: number }; + + const machine = createMachine({ + context: {}, + initial: 'foo', + states: { + foo: { + entry: decide([ + { + cond: () => true, + actions: [() => (executed = true), assign({ answer: 42 })] + } + ]) + } + } + }); + + const service = interpret(machine).start(); + + expect(service.state.context).toEqual({ answer: 42 }); + expect(executed).toBeTruthy(); + }); + + it('should only execute matched actions', () => { + type Ctx = { answer?: number; shouldNotAppear?: boolean }; + + const machine = createMachine({ + context: {}, + initial: 'foo', + states: { + foo: { + entry: decide([ + { + cond: () => false, + actions: assign({ shouldNotAppear: true }) + }, + { cond: () => true, actions: assign({ answer: 42 }) } + ]) + } + } + }); + + const service = interpret(machine).start(); + + expect(service.state.context).toEqual({ answer: 42 }); + }); + + it('should allow for fallback unguarded actions', () => { + type Ctx = { answer?: number; shouldNotAppear?: boolean }; + + const machine = createMachine({ + context: {}, + initial: 'foo', + states: { + foo: { + entry: decide([ + { + cond: () => false, + actions: assign({ shouldNotAppear: true }) + }, + { actions: assign({ answer: 42 }) } + ]) + } + } + }); + + const service = interpret(machine).start(); + + expect(service.state.context).toEqual({ answer: 42 }); + }); + + it('should allow for nested conditional actions', () => { + type Ctx = { + firstLevel: boolean; + secondLevel: boolean; + thirdLevel: boolean; + }; + + const machine = createMachine({ + context: { + firstLevel: false, + secondLevel: false, + thirdLevel: false + }, + initial: 'foo', + states: { + foo: { + entry: decide([ + { + cond: () => true, + actions: [ + assign({ firstLevel: true }), + decide([ + { + cond: () => true, + actions: [ + assign({ secondLevel: true }), + decide([ + { + cond: () => true, + actions: [assign({ thirdLevel: true })] + } + ]) + ] + } + ]) + ] + } + ]) + } + } + }); + + const service = interpret(machine).start(); + + expect(service.state.context).toEqual({ + firstLevel: true, + secondLevel: true, + thirdLevel: true + }); + }); + + it('should provide context to a condition expression', () => { + type Ctx = { counter: number; answer?: number }; + const machine = createMachine({ + context: { + counter: 101 + }, + initial: 'foo', + states: { + foo: { + entry: decide([ + { + cond: ctx => ctx.counter > 100, + actions: assign({ answer: 42 }) + } + ]) + } + } + }); + + const service = interpret(machine).start(); + + expect(service.state.context).toEqual({ counter: 101, answer: 42 }); + }); + + it('should provide event to a condition expression', () => { + type Ctx = { answer?: number }; + type Events = { type: 'NEXT'; counter: number }; + + const machine = createMachine({ + context: {}, + initial: 'foo', + states: { + foo: { + on: { + NEXT: { + target: 'bar', + actions: decide([ + { + cond: (_, event) => event.counter > 100, + actions: assign({ answer: 42 }) + } + ]) + } + } + }, + bar: {} + } + }); + + const service = interpret(machine).start(); + service.send({ type: 'NEXT', counter: 101 }); + expect(service.state.context).toEqual({ answer: 42 }); + }); +}); From c39077170a100bd017b0ea9e5f38f3cff66a6d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Mon, 23 Mar 2020 00:27:52 +0100 Subject: [PATCH 3/4] Fixed tests --- packages/core/src/StateNode.ts | 86 +++++++++++------------- packages/core/test/scxml.test.ts | 18 +++-- packages/xstate-scxml/test/scxml.test.ts | 16 ++--- 3 files changed, 62 insertions(+), 58 deletions(-) diff --git a/packages/core/src/StateNode.ts b/packages/core/src/StateNode.ts index 968515b3af..ab34c8610c 100644 --- a/packages/core/src/StateNode.ts +++ b/packages/core/src/StateNode.ts @@ -363,9 +363,9 @@ class StateNode< this.strict = !!this.config.strict; // TODO: deprecate (entry) - this.onEntry = toArray(this.config.entry || this.config.onEntry).map( - action => toActionObject(action) - ); + this.onEntry = toArray( + this.config.entry || this.config.onEntry + ).map(action => toActionObject(action)); // TODO: deprecate (exit) this.onExit = toArray(this.config.exit || this.config.onExit).map(action => toActionObject(action) @@ -500,14 +500,11 @@ class StateNode< const transitions = this.transitions; - return (this.__cache.on = transitions.reduce( - (map, transition) => { - map[transition.eventType] = map[transition.eventType] || []; - map[transition.eventType].push(transition as any); - return map; - }, - {} as TransitionDefinitionMap - )); + return (this.__cache.on = transitions.reduce((map, transition) => { + map[transition.eventType] = map[transition.eventType] || []; + map[transition.eventType].push(transition as any); + return map; + }, {} as TransitionDefinitionMap)); } public get after(): Array> { @@ -632,21 +629,20 @@ class StateNode< } const subStateKeys = keys(stateValue); - const subStateNodes: Array< - StateNode - > = subStateKeys.map(subStateKey => this.getStateNode(subStateKey)); + const subStateNodes: Array> = subStateKeys.map(subStateKey => this.getStateNode(subStateKey)); return subStateNodes.concat( - subStateKeys.reduce( - (allSubStateNodes, subStateKey) => { - const subStateNode = this.getStateNode(subStateKey).getStateNodes( - stateValue[subStateKey] - ); + subStateKeys.reduce((allSubStateNodes, subStateKey) => { + const subStateNode = this.getStateNode(subStateKey).getStateNodes( + stateValue[subStateKey] + ); - return allSubStateNodes.concat(subStateNode); - }, - [] as Array> - ) + return allSubStateNodes.concat(subStateNode); + }, [] as Array>) ); } @@ -1182,15 +1178,12 @@ class StateNode< ? currentState.configuration : []; - const meta = resolvedConfiguration.reduce( - (acc, stateNode) => { - if (stateNode.meta !== undefined) { - acc[stateNode.id] = stateNode.meta; - } - return acc; - }, - {} as Record - ); + const meta = resolvedConfiguration.reduce((acc, stateNode) => { + if (stateNode.meta !== undefined) { + acc[stateNode.id] = stateNode.meta; + } + return acc; + }, {} as Record); const isDone = isInFinalState(resolvedConfiguration, this); @@ -1275,6 +1268,7 @@ class StateNode< maybeNextState.changed || (history ? !!maybeNextState.actions.length || + didUpdateContext || typeof history.value !== typeof maybeNextState.value || !stateValuesEqual(maybeNextState.value, history.value) : undefined); @@ -1663,9 +1657,10 @@ class StateNode< : parent.initialStateNodes; } - const subHistoryValue = nestedPath(parent.path, 'states')( - historyValue - ).current; + const subHistoryValue = nestedPath( + parent.path, + 'states' + )(historyValue).current; if (isString(subHistoryValue)) { return [parent.getStateNode(subHistoryValue)]; @@ -1817,11 +1812,9 @@ class StateNode< return transition; } private formatTransitions(): Array> { - let onConfig: Array< - TransitionConfig & { - event: string; - } - >; + let onConfig: Array & { + event: string; + }>; if (!this.config.on) { onConfig = []; @@ -1846,11 +1839,14 @@ class StateNode< return arrayified; }) .concat( - toTransitionConfigArray(WILDCARD, wildcardConfigs as SingleOrArray< - TransitionConfig & { - event: '*'; - } - >) + toTransitionConfigArray( + WILDCARD, + wildcardConfigs as SingleOrArray< + TransitionConfig & { + event: '*'; + } + > + ) ) ); } diff --git a/packages/core/test/scxml.test.ts b/packages/core/test/scxml.test.ts index 4047a43636..bcdd538a03 100644 --- a/packages/core/test/scxml.test.ts +++ b/packages/core/test/scxml.test.ts @@ -13,9 +13,11 @@ import { pathsToStateValue } from '../src/utils'; // import { Event, StateValue, ActionObject } from '../src/types'; // import { actionTypes } from '../src/actions'; -const TEST_FRAMEWORK = path.dirname(pkgUp.sync({ - cwd: require.resolve('@scion-scxml/test-framework') -}) as string); +const TEST_FRAMEWORK = path.dirname( + pkgUp.sync({ + cwd: require.resolve('@scion-scxml/test-framework') + }) as string +); const testGroups = { actionSend: [ @@ -32,9 +34,15 @@ const testGroups = { ], assign: [ // 'assign_invalid', // TODO: handle error.execution event - 'assign_obj_literal' + // 'assign_obj_literal' //