@@ -13,11 +13,12 @@ import {handleInitialScreenLayout, mfaNavigationRef} from '@components/Multifact
1313import CONST from '@src/CONST' ;
1414import SCREENS from '@src/SCREENS' ;
1515
16- // This file pins the contract between the machine and the rendered modal: it mounts the production
17- // providers and navigator, drives each machine event as a real gesture, and asserts the UI markers of
18- // the reached state at every step of every generated path. The paths come from `getWalkedPaths`, so a
19- // state or event added to the machine gains steps here without edits. The chart-only suites live in
20- // `everyStateReachable.test.ts`, and the guards on the generated coverage in `coverageStaysComplete.test.ts`.
16+ // This file tests that the rendered modal matches the machine: it mounts the production providers and
17+ // navigator, drives each machine event as a real gesture, and asserts the UI markers of the reached
18+ // state at every step of every generated path. A state or event added to the machine appears in the
19+ // walked paths automatically, and the type checks and guard suites then demand the hand-written
20+ // pieces it needs, such as an executor, a payload fixture, or a UI assertion. The machine-only suites
21+ // live in `everyStateReachable.test.ts` and the coverage guards in `coverageStaysComplete.test.ts`.
2122
2223// This mock forces a wide layout so the navigator renders the backdrop used as the mounted marker.
2324jest . mock ( '@hooks/useResponsiveLayout' ) ;
@@ -29,13 +30,11 @@ jest.mock('@libs/XStateInspector', () => ({__esModule: true, default: {inspect:
2930jest . mock ( '@components/MultifactorAuthentication/biometrics/useBiometrics' , ( ) => jest . requireActual < typeof MfaRealUiMocks > ( 'tests/utils/mfa/realUi/mocks' ) . biometricsHookMock ( ) ) ;
3031// Browser and Android history synchronization is outside the contract between the machine and UI.
3132jest . mock ( '@components/MultifactorAuthentication/useSyncMfaModalNavigatorWithHistory' , ( ) => jest . requireActual < typeof MfaRealUiMocks > ( 'tests/utils/mfa/realUi/mocks' ) . syncHistoryMock ( ) ) ;
32- // This mock reuses the shared Navigation implementation and overrides the transition methods used by the MFA flow .
33+ // jsdom runs no real navigation transitions, so the mock controls when the transition callbacks fire .
3334jest . mock ( '@libs/Navigation/Navigation' , ( ) => jest . requireActual < typeof MfaRealUiMocks > ( 'tests/utils/mfa/realUi/mocks' ) . navigationMock ( ) ) ;
3435
35- // These UI markers distinguish the closed, closing, and outcome states. `OutcomeScreenBase` identifies the
36- // outcome screen, while the backdrop exists only when the MFA navigator is mounted. Every outcome screen
37- // renders the same `OutcomeScreenBase`, so the success assertion also checks the route name to pin which
38- // outcome screen is on top.
36+ // These UI markers distinguish the closed, closing, and outcome states. The backdrop exists only while
37+ // the MFA navigator is mounted.
3938const OUTCOME_SCREEN_TEST_ID = 'OutcomeScreenBase' ;
4039const MODAL_BACKDROP_TEST_ID = 'MultifactorAuthenticationModalBackdrop' ;
4140
@@ -45,14 +44,17 @@ const CONFIRM_BUTTON_TEST_ID = 'MultifactorAuthenticationOutcomeConfirmButton';
4544const MFA_STATE = CONST . MULTIFACTOR_AUTHENTICATION . MFA_STATE ;
4645
4746type MfaEventType = MfaEvent [ 'type' ] ;
48- // The event carries only its `type` here because `xstate/graph` erases the payload from the executor's
49- // step type. The INIT executor recovers the payload through `isTestScenarioInitEvent`.
47+
48+ /**
49+ * The event carries only its `type` here because `xstate/graph` erases the payload from the executor's
50+ * step type. The INIT executor recovers the payload through `isTestScenarioInitEvent`.
51+ */
5052type MfaEventExecutor = ( step : { event : { type : MfaEventType } } ) => Promise < void > ;
5153
5254/**
53- * `INIT` enters through the public API with the scenario and payload of the step's own event, so paths
54- * built from different INIT fixtures drive different flows . `MODAL_CLOSED` runs the navigator's teardown
55- * callback. `satisfies Record<MfaEventType, ...>` requires an explicit executor for every machine event.
55+ * `INIT` starts a flow through the public API, using the scenario and payload from the step's own
56+ * event . `MODAL_CLOSED` runs the navigator's teardown callback. `satisfies Record<MfaEventType, ...>`
57+ * requires an explicit executor for every machine event.
5658 */
5759/* eslint-disable @typescript-eslint/naming-convention -- keys mirror the machine's event type union. */
5860const mfaEventExecutors = {
@@ -91,6 +93,7 @@ const testConfig = {
9193 [ `${ MFA_STATE . OPEN } .${ MFA_STATE . OUTCOME } .${ MFA_STATE . SUCCESS } ` ] : ( ) => {
9294 expect ( screen . queryAllByTestId ( MODAL_BACKDROP_TEST_ID ) ) . not . toHaveLength ( 0 ) ;
9395 expect ( screen . queryAllByTestId ( OUTCOME_SCREEN_TEST_ID ) ) . not . toHaveLength ( 0 ) ;
96+ // Every outcome screen renders the same `OutcomeScreenBase`, so the route name identifies which one is on top.
9497 expect ( mfaNavigationRef . getCurrentRoute ( ) ?. name ) . toBe ( SCREENS . MULTIFACTOR_AUTHENTICATION . OUTCOME_SUCCESS ) ;
9598 } ,
9699 [ MFA_STATE . CLOSING ] : ( ) => {
@@ -102,10 +105,13 @@ const testConfig = {
102105
103106const walkedPaths = getWalkedPaths ( ) ;
104107
105- // `path.description` serializes the complete event payload, so test names use the short labels from
106- // `describeTraversalEvent` instead. The synthetic `xstate.init` event is excluded because it is not part
107- // of `MfaEvent`.
108108const INIT_STEP_EVENT_TYPE = 'xstate.init' ;
109+
110+ /**
111+ * Builds the event part of a test name. `path.description` would serialize the complete event payload,
112+ * so the name uses the short labels from `describeTraversalEvent` instead. The synthetic `xstate.init`
113+ * event is excluded because it is not part of `MfaEvent`.
114+ */
109115function describeDrivenEvents ( steps : ReadonlyArray < { event : { type : string } } > ) : string {
110116 const drivenEventLabels = steps
111117 . map ( ( step ) => step . event )
@@ -115,9 +121,9 @@ function describeDrivenEvents(steps: ReadonlyArray<{event: {type: string}}>): st
115121}
116122
117123describe ( 'the real MFA modal matches the machine at every step of every generated path' , ( ) => {
118- // The navigation buffer is deliberately not reset here. The machine owns that cleanup on `closed`
119- // entry , which also runs when each test's fresh actor starts, so a reset here would hide a machine
120- // that stopped performing it .
124+ // The navigation buffer is deliberately not reset here. The machine resets it when it enters
125+ // `closed` , which also runs when each test's fresh actor starts, so a reset here would hide a
126+ // machine that stopped doing that cleanup .
121127 beforeEach ( ( ) => {
122128 resetMfaUiMocks ( ) ;
123129 } ) ;
@@ -138,10 +144,9 @@ describe('the real MFA modal matches the machine at every step of every generate
138144// TestModel runs only the state assertions whose keys match the reached state, so if no key matches a
139145// state, the test passes without checking it. These guards fail in that case.
140146//
141- // Types cannot do this. TypeScript's inferred type does not record whether a state has an `always`
142- // transition, so it cannot tell a real leaf from a pass-through state such as `{open: "preparing"}`. A
143- // `Record<leaf, ...>` would then need an empty assertion for every pass-through state, and that empty
144- // entry would make the check pass on its own once the state later loses its `always` and becomes settleable.
147+ // A type cannot enforce this, because TypeScript does not know which states auto-advance. A
148+ // `Record` over all leaf states would then need empty assertions for the pass-through states, and an
149+ // empty assertion keeps passing silently.
145150describe ( 'testConfig defines a UI assertion for every settleable state and for nothing else' , ( ) => {
146151 const settleableLeafStates = getSettleableLeafStates ( mfaMachine . root ) ;
147152 const configuredStateKeys = Object . keys ( testConfig . states ) ;
0 commit comments