From 84c46c1ae95cea34355245a12ca2471eca996337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Mon, 16 Oct 2023 20:40:57 +0200 Subject: [PATCH] Fixed an issue with not all actions of initial transitions being executed (#4357) --- .changeset/grumpy-shrimps-draw.md | 5 +++++ packages/core/src/StateMachine.ts | 5 +++-- packages/core/src/stateUtils.ts | 27 ++++++++++++++++-------- packages/core/test/actions.test.ts | 34 ++++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 11 deletions(-) create mode 100644 .changeset/grumpy-shrimps-draw.md diff --git a/.changeset/grumpy-shrimps-draw.md b/.changeset/grumpy-shrimps-draw.md new file mode 100644 index 0000000000..2d5e264fe6 --- /dev/null +++ b/.changeset/grumpy-shrimps-draw.md @@ -0,0 +1,5 @@ +--- +'xstate': patch +--- + +Fixed an issue with not all actions of initial transitions resolving to the initial state of the machine itself being executed. diff --git a/packages/core/src/StateMachine.ts b/packages/core/src/StateMachine.ts index e181a78137..9dc73d7447 100644 --- a/packages/core/src/StateMachine.ts +++ b/packages/core/src/StateMachine.ts @@ -15,7 +15,8 @@ import { resolveActionsAndContext, resolveStateValue, transitionNode, - isAtomicStateNode + isAtomicStateNode, + getInitialStateNodes } from './stateUtils.ts'; import type { AreAllImplementationsAssumedToBeProvided, @@ -455,7 +456,7 @@ export class StateMachine< const nextState = microstep( [ { - target: [...preInitialState.configuration].filter(isAtomicStateNode), + target: [...getInitialStateNodes(this.root)], source: this.root, reenter: true, actions: [], diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index cb9fea5d22..11d25d7991 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -126,7 +126,9 @@ export function getConfiguration( for (const s of configuration) { // if previously active, add existing child nodes if (s.type === 'compound' && (!adjList.get(s) || !adjList.get(s)!.length)) { - getInitialStateNodes(s).forEach((sn) => configurationSet.add(sn)); + getInitialStateNodesWithTheirAncestors(s).forEach((sn) => + configurationSet.add(sn) + ); } else { if (s.type === 'parallel') { for (const child of getChildren(s)) { @@ -135,7 +137,8 @@ export function getConfiguration( } if (!configurationSet.has(child)) { - for (const initialStateNode of getInitialStateNodes(child)) { + const initialStates = getInitialStateNodesWithTheirAncestors(child); + for (const initialStateNode of initialStates) { configurationSet.add(initialStateNode); } } @@ -594,9 +597,19 @@ function isHistoryNode( return stateNode.type === 'history'; } -export function getInitialStateNodes( +export function getInitialStateNodesWithTheirAncestors( stateNode: AnyStateNode -): Array { +) { + const states = getInitialStateNodes(stateNode); + for (const initialState of states) { + for (const ancestor of getProperAncestors(initialState, stateNode)) { + states.add(ancestor); + } + } + return states; +} + +export function getInitialStateNodes(stateNode: AnyStateNode) { const set = new Set(); function iter(descStateNode: AnyStateNode): void { @@ -606,10 +619,6 @@ export function getInitialStateNodes( set.add(descStateNode); if (descStateNode.type === 'compound') { for (const targetStateNode of descStateNode.initial.target) { - for (const a of getProperAncestors(targetStateNode, stateNode)) { - set.add(a); - } - iter(targetStateNode); } } else if (descStateNode.type === 'parallel') { @@ -621,7 +630,7 @@ export function getInitialStateNodes( iter(stateNode); - return [...set]; + return set; } /** * Returns the child state node from its relative `stateKey`, or throws. diff --git a/packages/core/test/actions.test.ts b/packages/core/test/actions.test.ts index 8e8b55e3db..9b9a1a6a25 100644 --- a/packages/core/test/actions.test.ts +++ b/packages/core/test/actions.test.ts @@ -2170,6 +2170,40 @@ describe('initial actions', () => { ] `); }); + + it('should execute actions of all initial transitions resolving to the initial state value', () => { + const spy = jest.fn(); + const machine = createMachine({ + initial: { + target: 'a', + actions: () => spy('root') + }, + states: { + a: { + initial: { + target: 'a1', + actions: () => spy('inner') + }, + states: { + a1: {} + } + } + } + }); + + createActor(machine).start(); + + expect(spy.mock.calls).toMatchInlineSnapshot(` + [ + [ + "root", + ], + [ + "inner", + ], + ] + `); + }); }); describe('actions on invalid transition', () => {