Skip to content

Commit

Permalink
Call root's output with the done state event of the outermost compl…
Browse files Browse the repository at this point in the history
…eted state node (#4365)

* Call root's `output` with the done state event of the outermost completed state node

* Add more tests
  • Loading branch information
Andarist authored Oct 18, 2023
1 parent 5348142 commit d239a1e
Show file tree
Hide file tree
Showing 2 changed files with 221 additions and 15 deletions.
34 changes: 19 additions & 15 deletions packages/core/src/stateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1086,15 +1086,20 @@ function getMachineOutput(
event: AnyEventObject,
actorCtx: AnyActorContext,
rootNode: AnyStateNode,
enteredNode: AnyStateNode
rootCompletionNode: AnyStateNode
) {
if (!rootNode.output) {
return;
}
const doneStateEvent = createDoneStateEvent(
enteredNode.id,
enteredNode.output && enteredNode.parent
? resolveOutput(enteredNode.output, state.context, event, actorCtx.self)
rootCompletionNode.id,
rootCompletionNode.output && rootCompletionNode.parent
? resolveOutput(
rootCompletionNode.output,
state.context,
event,
actorCtx.self
)
: undefined
);
return resolveOutput(
Expand Down Expand Up @@ -1158,6 +1163,8 @@ function enterStates(

if (stateNodeToEnter.type === 'final') {
const parent = stateNodeToEnter.parent;
let rootCompletionNode =
parent?.type === 'parallel' ? parent : stateNodeToEnter;
let ancestorMarker: typeof parent | undefined = parent?.parent;

if (ancestorMarker) {
Expand All @@ -1174,16 +1181,13 @@ function enterStates(
: undefined
)
);
while (ancestorMarker) {
if (
ancestorMarker.type === 'parallel' &&
isInFinalState(mutConfiguration, ancestorMarker)
) {
internalQueue.push(createDoneStateEvent(ancestorMarker.id));
ancestorMarker = ancestorMarker.parent;
continue;
}
break;
while (
ancestorMarker?.type === 'parallel' &&
isInFinalState(mutConfiguration, ancestorMarker)
) {
internalQueue.push(createDoneStateEvent(ancestorMarker.id));
rootCompletionNode = ancestorMarker;
ancestorMarker = ancestorMarker.parent;
}
}
if (ancestorMarker) {
Expand All @@ -1197,7 +1201,7 @@ function enterStates(
event,
actorCtx,
currentState.configuration[0].machine.root,
stateNodeToEnter
rootCompletionNode
)
});
continue;
Expand Down
202 changes: 202 additions & 0 deletions packages/core/test/final.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,4 +574,206 @@ describe('final states', () => {

expect(actorRef.getSnapshot().status).toEqual('done');
});
it('root output should be called with a "xstate.done.state.*" event of the parallel root when a direct final child of that parallel root is reached', () => {
const spy = jest.fn();
const machine = createMachine({
type: 'parallel',
states: {
a: {
type: 'final'
}
},
output: ({ event }) => {
spy(event);
}
});

createActor(machine).start();

expect(spy.mock.calls).toMatchInlineSnapshot(`
[
[
{
"output": undefined,
"type": "xstate.done.state.(machine)",
},
],
]
`);
});

it('root output should be called with a "xstate.done.state.*" event of the parallel root when a final child of its compound child is reached', () => {
const spy = jest.fn();
const machine = createMachine({
type: 'parallel',
states: {
a: {
initial: 'b',
states: {
b: {
type: 'final'
}
}
}
},
output: ({ event }) => {
spy(event);
}
});

createActor(machine).start();

expect(spy.mock.calls).toMatchInlineSnapshot(`
[
[
{
"output": undefined,
"type": "xstate.done.state.(machine)",
},
],
]
`);
});

it('root output should be called with a "xstate.done.state.*" event of the parallel root when a final descendant is reached 2 parallel levels deep', () => {
const spy = jest.fn();
const machine = createMachine({
type: 'parallel',
states: {
a: {
type: 'parallel',
states: {
b: {
initial: 'c',
states: {
c: {
type: 'final'
}
}
}
}
}
},
output: ({ event }) => {
spy(event);
}
});

createActor(machine).start();

expect(spy.mock.calls).toMatchInlineSnapshot(`
[
[
{
"output": undefined,
"type": "xstate.done.state.(machine)",
},
],
]
`);
});

it('onDone of an outer parallel state should be called with its own "xstate.done.state.*" event when its direct parallel child completes', () => {
const spy = jest.fn();
const machine = createMachine({
initial: 'a',
states: {
a: {
type: 'parallel',
states: {
b: {
type: 'parallel',
states: {
c: {
initial: 'd',
states: {
d: {
type: 'final'
}
}
}
}
}
},
onDone: {
actions: ({ event }) => {
spy(event);
}
}
}
}
});
createActor(machine).start();

expect(spy.mock.calls).toMatchInlineSnapshot(`
[
[
{
"output": undefined,
"type": "xstate.done.state.(machine).a",
},
],
]
`);
});

it('onDone should not be called when the machine reaches its final state', () => {
const spy = jest.fn();
const machine = createMachine({
type: 'parallel',
states: {
a: {
type: 'parallel',
states: {
b: {
initial: 'c',
states: {
c: {
type: 'final'
}
},
onDone: {
actions: spy
}
}
},
onDone: {
actions: spy
}
}
},
onDone: {
actions: spy
}
});
createActor(machine).start();

expect(spy).not.toHaveBeenCalled();
});

it('machine should not complete when a parallel child of a compound state completes', () => {
const spy = jest.fn();
const machine = createMachine({
initial: 'a',
states: {
a: {
type: 'parallel',
states: {
b: {
initial: 'c',
states: {
c: {
type: 'final'
}
}
}
}
}
}
});

const actorRef = createActor(machine).start();

expect(actorRef.getSnapshot().status).toBe('active');
});
});

0 comments on commit d239a1e

Please sign in to comment.