Bug
I'm on xstate 3.3.2, given this code:
const {
Machine,
actions: { raise }
} = require("xstate");
const t = (state, event) => {
state = testMachine.transition(state, event);
console.log(`[${event}]: ${JSON.stringify(state.value)}`);
return state;
};
const testMachine = Machine({
strict: true,
parallel: true,
states: {
OUTER1: {
initial: "C",
states: {
A: {
onEntry: [raise("TURN_OFF")],
on: {
EVENT_OUTER1_B: "B",
EVENT_OUTER1_C: "C"
}
},
B: {
onEntry: [raise("TURN_ON")],
on: {
EVENT_OUTER1_A: "A",
EVENT_OUTER1_C: "C"
}
},
C: {
onEntry: [raise("CLEAR")],
on: {
EVENT_OUTER1_A: "A",
EVENT_OUTER1_B: "B"
}
}
}
},
OUTER2: {
parallel: true,
states: {
INNER1: {
initial: "ON",
states: {
OFF: {
on: {
TURN_ON: "ON"
}
},
ON: {
on: {
CLEAR: "OFF"
}
}
}
},
INNER2: {
initial: "OFF",
states: {
OFF: {
on: {
TURN_ON: "ON"
}
},
ON: {
on: {
TURN_OFF: "OFF"
}
}
}
}
}
}
}
});
let state = testMachine.initialState;
console.log(`INITIAL: ${JSON.stringify(state.value)}`);
state = t(state, "EVENT_OUTER1_B");
state = t(state, "EVENT_OUTER1_A");
state = t(state, "EVENT_OUTER1_C");
INITIAL: {"OUTER1":"C","OUTER2":{"INNER1":"ON","INNER2":"OFF"}}
[EVENT_OUTER1_B]: {"OUTER1":"B","OUTER2":{"INNER1":"ON","INNER2":"ON"}}
[EVENT_OUTER1_A]: {"OUTER1":"A","OUTER2":{"INNER1":"ON","INNER2":"OFF"}}
[EVENT_OUTER1_C]: {"OUTER1":"C","OUTER2":{"INNER1":"OFF","INNER2":"OFF"}}
The OUTER1 state sticks on C. However the events raised by the onEntry are fired off, as shown by the OUTER2 state.
After EVENT_OUTER1_C, the OUTER2 state is mismatched from the expected result due to the C state not having the EVENT_OUTER1_C event.
INITIAL: {"OUTER1":"C","OUTER2":{"INNER1":"ON","INNER2":"OFF"}}
[EVENT_OUTER1_B]: {"OUTER1":"C","OUTER2":{"INNER1":"ON","INNER2":"ON"}}
[EVENT_OUTER1_A]: {"OUTER1":"C","OUTER2":{"INNER1":"ON","INNER2":"OFF"}}
[EVENT_OUTER1_C]: {"OUTER1":"C","OUTER2":{"INNER1":"ON","INNER2":"OFF"}}
Commenting out either the INNER1 or INNER2 state stops the OUTER1 state from sticking on C.
Alternatively, commenting out B's onEntry also has the same effect for the OUTER1 state.
Great catch!
Just to explain the statechart quickly to @davidkpiano:
So controlling the machine using OUTER_A; _B and _C the bug seems to be observing some _side effects_ of _entering B_ (Entering B raises TURN_ON which should cause both OFF states to go to ON) but without actually entering B itself.
Removing the raised events causes the normal state transitions to happen.
Shouldn't the initial state be OUTER1: C, INNER1: OFF, INNER2: OFF? The initial state of OUTER1.C should be raising the CLEAR event, which should cause INNER1.ON to go to INNER1.OFF. Maybe a separate bug.
There is some strange things going on. It seems to be stuck in the initial state of the substate somehow. If I change OUTER1's initial state to A, then it 'sticks' to A instead of C.
Good point on 1, I was too focussed on the sticking to see that!
I found another interesting case with this particular machine.
So with that definition above, but with OUTER1.B's onEntry commented out (to prevent the OUTER1 state from sticking)
const {
Machine,
actions: { raise }
} = require("xstate");
const t = (state, event) => {
state = testMachine.transition(state, event);
console.log(`[${event}]: ${JSON.stringify(state.value)}`);
return state;
};
const testMachine = Machine({
strict: true,
parallel: true,
states: {
OUTER1: {
initial: "C",
states: {
A: {
onEntry: [raise("TURN_OFF")],
on: {
EVENT_OUTER1_B: "B",
EVENT_OUTER1_C: "C"
}
},
B: {
// onEntry: [raise("TURN_ON")],
on: {
EVENT_OUTER1_A: "A",
EVENT_OUTER1_C: "C"
}
},
C: {
onEntry: [raise("CLEAR")],
on: {
EVENT_OUTER1_A: "A",
EVENT_OUTER1_B: "B"
}
}
}
},
OUTER2: {
parallel: true,
states: {
INNER1: {
initial: "ON",
states: {
OFF: {
on: {
TURN_ON: "ON"
}
},
ON: {
on: {
CLEAR: "OFF"
}
}
}
},
INNER2: {
initial: "OFF",
states: {
OFF: {
on: {
TURN_ON: "ON"
}
},
ON: {
on: {
TURN_OFF: "OFF"
}
}
}
}
}
}
}
});
If we were to perform these transitions:
let state = testMachine.initialState;
console.log(`INITIAL: ${JSON.stringify(state.value)}`);
state = t(state, "EVENT_OUTER1_B");
state = t(state, "TURN_ON");
state = t(state, "EVENT_OUTER1_A");
state = t(state, "EVENT_OUTER1_C");
What happens is the OUTER1 state transitions to B on EVENT_OUTER1_B, but goes back to C on the TURN_ON event. I would expect OUTER1 to stay on B.
// INNER1: OFF from point 1.
INITIAL: {"OUTER1":"C","OUTER2":{"INNER1":"OFF","INNER2":"OFF"}}
[EVENT_OUTER1_B]: {"OUTER1":"B","OUTER2":{"INNER1":"OFF","INNER2":"OFF"}}
[TURN_ON]: {"OUTER1":"B","OUTER2":{"INNER1":"ON","INNER2":"ON"}}
[EVENT_OUTER1_A]: {"OUTER1":"A","OUTER2":{"INNER1":"ON","INNER2":"OFF"}}
[EVENT_OUTER1_C]: {"OUTER1":"C","OUTER2":{"INNER1":"OFF","INNER2":"OFF"}}
INITIAL: {"OUTER1":"C","OUTER2":{"INNER1":"ON","INNER2":"OFF"}}
[EVENT_OUTER1_B]: {"OUTER1":"B","OUTER2":{"INNER1":"ON","INNER2":"OFF"}}
[TURN_ON]: {"OUTER1":"C","OUTER2":{"INNER1":"ON","INNER2":"ON"}}
[EVENT_OUTER1_A]: {"OUTER1":"C","OUTER2":{"INNER1":"ON","INNER2":"OFF"}}
[EVENT_OUTER1_C]: {"OUTER1":"C","OUTER2":{"INNER1":"ON","INNER2":"OFF"}}
I'm not sure if this would be classed as a separate bug..
Can you do me a favor and turn this into a PR containing the failing test case? (Should be just a matter of copy-pasting this into the correct test file, probably parallel.test.ts)
Most helpful comment
Great catch!
Just to explain the statechart quickly to @davidkpiano:
So controlling the machine using OUTER_A; _B and _C the bug seems to be observing some _side effects_ of _entering B_ (Entering B raises
TURN_ONwhich should cause both OFF states to go to ON) but without actually entering B itself.Removing the raised events causes the normal state transitions to happen.
Shouldn't the initial state be OUTER1: C, INNER1: OFF, INNER2: OFF? The initial state of OUTER1.C should be raising the CLEAR event, which should cause INNER1.ON to go to INNER1.OFF. Maybe a separate bug.
There is some strange things going on. It seems to be stuck in the initial state of the substate somehow. If I change OUTER1's initial state to A, then it 'sticks' to A instead of C.