Skip to content

Commit fc62112

Browse files
committed
refactor(mfa): split stable from settleable leaves in one leafStates module
The reachability suite now checks the stable set, which keeps a leaf whose routing invoke resolves through a later event, while the UI walk keeps asserting only the settleable subset it can stop on. Both getters share one traversal and one set of predicates instead of two near-identical files.
1 parent a4d81db commit fc62112

5 files changed

Lines changed: 57 additions & 37 deletions

File tree

tests/unit/components/MultifactorAuthentication/machine/graphTraversal/everyStateReachable.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import {getDrivingJourneyPaths, getMfaShortestPaths} from 'tests/utils/mfa/flowPaths';
2-
import getSettleableLeafStates from 'tests/utils/mfa/settleableLeafStates';
2+
import {getStableLeafStates} from 'tests/utils/mfa/leafStates';
33
import {matchesState} from 'xstate';
44
import mfaMachine from '@components/MultifactorAuthentication/machine/mfaMachine';
55

66
// Both suites in this file are generated by traversing the mfaMachine graph, so a state added to the
7-
// machine gets a test here automatically. Nothing renders in this file. `settleableLeafStates.ts`
8-
// defines which states count as settleable.
7+
// machine gets a test here automatically. Nothing renders in this file. `leafStates.ts` excludes
8+
// only transient leaves that XState resolves before emitting a snapshot.
99

1010
// Reachability is asserted on the unfiltered shortest paths, so it also covers routes the UI walk in
1111
// `viewMatchesMachine.test.tsx` cannot drive (a delayed transition, for example). A failure here means
1212
// the machine itself lost the state, not that the UI harness broke.
13-
describe('every settleable MFA state is reachable in the machine chart', () => {
13+
describe('every stable MFA state is reachable in the machine chart', () => {
1414
const reachableSnapshots = getMfaShortestPaths().map((path) => path.state);
1515

16-
it.each(getSettleableLeafStates(mfaMachine.root))('$description can be reached from the initial state', ({description}) => {
16+
it.each(getStableLeafStates(mfaMachine.root))('$description can be reached from the initial state', ({description}) => {
1717
expect(reachableSnapshots.some((snapshot) => matchesState(description, snapshot.value))).toBe(true);
1818
});
1919
});

tests/unit/components/MultifactorAuthentication/machine/graphTraversal/viewMatchesMachine.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {act, fireEvent, screen} from '@testing-library/react-native';
22
import {MFA_TEST_SCENARIO_NAME} from 'tests/utils/mfa/flowFixtures';
33
import getWalkedPaths from 'tests/utils/mfa/flowPaths';
4+
import {getSettleableLeafStates} from 'tests/utils/mfa/leafStates';
45
import renderMfaUi from 'tests/utils/mfa/realUi/harness';
56
import {pendingModalClose, resetMfaUiMocks} from 'tests/utils/mfa/realUi/mocks';
67
import type * as MfaRealUiMocks from 'tests/utils/mfa/realUi/mocks';
7-
import getSettleableLeafStates from 'tests/utils/mfa/settleableLeafStates';
88
import waitForBatchedUpdatesWithAct from 'tests/utils/waitForBatchedUpdatesWithAct';
99
import {matchesState} from 'xstate';
1010
import mfaMachine from '@components/MultifactorAuthentication/machine/mfaMachine';

tests/utils/mfa/flowPaths.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ function getDrivingJourneyPaths() {
117117
* Returns the generated coverage paths plus the explicit driving journeys. The journeys are needed
118118
* because a shortest path can be empty, such as the path to the initial `closed` state, so the
119119
* generated paths alone would never drive the teardown. Paths with a delayed step are filtered out
120-
* because the UI walk cannot drive a timer. `everyStateReachable.test.ts` checks settleable-state
120+
* because the UI walk cannot drive a timer. `everyStateReachable.test.ts` checks stable-state
121121
* reachability over the unfiltered graph, while the walk-coverage guard in
122122
* `viewMatchesMachine.test.tsx` catches a state that loses every UI-drivable route.
123123
*

tests/utils/mfa/leafStates.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type {AnyStateNode} from 'xstate';
2+
3+
type LeafState = {description: string};
4+
5+
function getLeafNodes(node: AnyStateNode): AnyStateNode[] {
6+
const children = Object.values(node.states);
7+
if (children.length > 0) {
8+
return children.flatMap(getLeafNodes);
9+
}
10+
return [node];
11+
}
12+
13+
function hasUnguardedAlways(node: AnyStateNode): boolean {
14+
return node.always?.some((transition) => transition.guard === undefined) ?? false;
15+
}
16+
17+
function hasRoutingInvoke(node: AnyStateNode): boolean {
18+
return node.invoke.some((definition) => definition.onDone !== undefined || definition.onError !== undefined);
19+
}
20+
21+
// The dot-path description doubles as a state-value key: `matchesState` splits it on `.` before comparing.
22+
function toLeafState(node: AnyStateNode): LeafState {
23+
return {description: node.path.join('.')};
24+
}
25+
26+
/**
27+
* Returns leaf states that can exist as snapshots after an XState macrostep. An unguarded `always`
28+
* transition is resolved in the same macrostep that enters the state, so graph paths cannot observe
29+
* that leaf. A guarded `always` may keep the machine in place, and an invoked actor completes through
30+
* a later event, so both remain part of machine reachability.
31+
*/
32+
function getStableLeafStates(node: AnyStateNode): LeafState[] {
33+
return getLeafNodes(node)
34+
.filter((leaf) => !hasUnguardedAlways(leaf))
35+
.map(toLeafState);
36+
}
37+
38+
/**
39+
* Returns the stable leaf states that the UI walk can also stop on and assert. An invoked actor
40+
* counts as auto-advancing once it registers `onDone` or `onError`, which is true for an actor that
41+
* completes immediately (the mocked promise actors this harness expects) but not for one that stays
42+
* pending. Revisit this predicate when the first real invoke state is added to the machine.
43+
*/
44+
function getSettleableLeafStates(node: AnyStateNode): LeafState[] {
45+
return getLeafNodes(node)
46+
.filter((leaf) => !hasUnguardedAlways(leaf) && !hasRoutingInvoke(leaf))
47+
.map(toLeafState);
48+
}
49+
50+
export {getStableLeafStates, getSettleableLeafStates};

tests/utils/mfa/settleableLeafStates.ts

Lines changed: 0 additions & 30 deletions
This file was deleted.

0 commit comments

Comments
 (0)