Skip to content

Commit d2e0226

Browse files
committed
refactor(mfa): scope the harness controls to each render
The module-level controlsHolder and getMfaControls are replaced by a ref created inside renderMfaUi, which now returns executeScenario with the render result. The returned function reads the ref at call time, so the walk keeps driving the latest context API after re-renders. The event executors become a factory bound to the current render.
1 parent ecb1b2b commit d2e0226

2 files changed

Lines changed: 51 additions & 42 deletions

File tree

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

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
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 {getMfaControls, renderMfaUi} from 'tests/utils/mfa/realUi/harness';
4+
import renderMfaUi from 'tests/utils/mfa/realUi/harness';
55
import {pendingModalClose, resetMfaUiMocks} from 'tests/utils/mfa/realUi/mocks';
66
import type * as MfaRealUiMocks from 'tests/utils/mfa/realUi/mocks';
77
import getSettleableLeafStates from 'tests/utils/mfa/settleableLeafStates';
@@ -47,37 +47,41 @@ type MfaEventType = MfaEvent['type'];
4747

4848
type MfaEventExecutor = () => Promise<void>;
4949

50+
type ExecuteScenario = ReturnType<typeof renderMfaUi>['executeScenario'];
51+
5052
/**
5153
* Maps every machine event to the action that produces it in the rendered app, such as a button press
5254
* or a navigator callback. The walk drives each path step through this table, and the `satisfies`
53-
* clause makes a machine event without an executor fail compilation.
55+
* clause makes a machine event without an executor fail compilation. The executors act on a concrete
56+
* render, so each test builds them from its own `executeScenario`.
5457
*/
5558
/* eslint-disable @typescript-eslint/naming-convention -- keys mirror the machine's event type union. */
56-
const mfaEventExecutors = {
57-
INIT: async () => {
58-
await act(async () => {
59-
await getMfaControls().executeScenario(MFA_TEST_SCENARIO_NAME);
60-
});
61-
await waitForBatchedUpdatesWithAct();
62-
// The initial screen's `onLayout` does not fire in jsdom, so the test calls the same handler to flush the
63-
// buffered navigation.
64-
act(() => handleInitialScreenLayout());
65-
await waitForBatchedUpdatesWithAct();
66-
},
67-
CLOSE_MODAL: async () => {
68-
fireEvent.press(screen.getByTestId(CONFIRM_BUTTON_TEST_ID));
69-
await waitForBatchedUpdatesWithAct();
70-
},
71-
MODAL_CLOSED: async () => {
72-
act(() => pendingModalClose.run());
73-
await waitForBatchedUpdatesWithAct();
74-
},
75-
} satisfies Record<MfaEventType, MfaEventExecutor>;
59+
function createMfaEventExecutors(executeScenario: ExecuteScenario) {
60+
return {
61+
INIT: async () => {
62+
await act(async () => {
63+
await executeScenario(MFA_TEST_SCENARIO_NAME);
64+
});
65+
await waitForBatchedUpdatesWithAct();
66+
// The initial screen's `onLayout` does not fire in jsdom, so the test calls the same handler to flush the
67+
// buffered navigation.
68+
act(() => handleInitialScreenLayout());
69+
await waitForBatchedUpdatesWithAct();
70+
},
71+
CLOSE_MODAL: async () => {
72+
fireEvent.press(screen.getByTestId(CONFIRM_BUTTON_TEST_ID));
73+
await waitForBatchedUpdatesWithAct();
74+
},
75+
MODAL_CLOSED: async () => {
76+
act(() => pendingModalClose.run());
77+
await waitForBatchedUpdatesWithAct();
78+
},
79+
} satisfies Record<MfaEventType, MfaEventExecutor>;
80+
}
7681
/* eslint-enable @typescript-eslint/naming-convention */
7782

7883
// Dot-path state keys let `matchesState` target nested leaves such as `open.outcome.success`.
7984
const testConfig = {
80-
events: mfaEventExecutors,
8185
states: {
8286
[MFA_STATE.CLOSED]: () => {
8387
expect(screen.queryAllByTestId(MODAL_BACKDROP_TEST_ID)).toHaveLength(0);
@@ -131,9 +135,9 @@ describe('the real MFA modal matches the machine at every step of every generate
131135
});
132136

133137
it.each(walkedPathTestCases)('$title', async ({path}) => {
134-
renderMfaUi();
138+
const {executeScenario} = renderMfaUi();
135139
await waitForBatchedUpdatesWithAct();
136-
await path.test(testConfig);
140+
await path.test({...testConfig, events: createMfaEventExecutors(executeScenario)});
137141
});
138142
});
139143

tests/utils/mfa/realUi/harness.tsx

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,41 +10,46 @@ type MfaUiControls = {
1010
executeScenario: ReturnType<typeof useMultifactorAuthentication>['executeScenario'];
1111
};
1212

13-
const controlsHolder: {current: MfaUiControls | undefined} = {current: undefined};
13+
type MfaControlsRef = {current: MfaUiControls | undefined};
1414

1515
/**
16-
* Renders nothing. It sits inside the providers only to capture the live context API so the event
17-
* executors can start a flow through the public API.
16+
* Renders nothing. It sits inside the providers only to capture the live context API so the
17+
* `executeScenario` returned by `renderMfaUi` can start a flow through the public API.
1818
*/
19-
function MfaControlsCapture() {
19+
function MfaControlsCapture({controlsRef}: {controlsRef: MfaControlsRef}) {
2020
const {executeScenario} = useMultifactorAuthentication();
2121
useEffect(() => {
22-
controlsHolder.current = {executeScenario};
23-
}, [executeScenario]);
22+
// eslint-disable-next-line no-param-reassign -- the ref exists to carry the capture out to `renderMfaUi`.
23+
controlsRef.current = {executeScenario};
24+
}, [controlsRef, executeScenario]);
2425
return null;
2526
}
2627

2728
/**
2829
* Mounts the production MFA providers and modal navigator. The global safe-area mock provides fixed
29-
* values without a `SafeAreaProvider`.
30+
* values without a `SafeAreaProvider`. The returned `executeScenario` reads the captured controls at
31+
* call time because the provider recreates the context API on every render.
3032
*/
3133
function renderMfaUi() {
32-
controlsHolder.current = undefined;
33-
return render(
34+
const controlsRef: MfaControlsRef = {current: undefined};
35+
36+
const renderResult = render(
3437
<ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider]}>
3538
<MultifactorAuthenticationContextProviders>
36-
<MfaControlsCapture />
39+
<MfaControlsCapture controlsRef={controlsRef} />
3740
<MultifactorAuthenticationModalNavigator />
3841
</MultifactorAuthenticationContextProviders>
3942
</ComposeProviders>,
4043
);
41-
}
4244

43-
function getMfaControls(): MfaUiControls {
44-
if (!controlsHolder.current) {
45-
throw new Error('MFA UI controls were not captured. Call renderMfaUi() and await waitForBatchedUpdatesWithAct() first.');
46-
}
47-
return controlsHolder.current;
45+
const executeScenario: MfaUiControls['executeScenario'] = (scenarioName, ...args) => {
46+
if (!controlsRef.current) {
47+
throw new Error('MFA UI controls were not captured. Await waitForBatchedUpdatesWithAct() after renderMfaUi() first.');
48+
}
49+
return controlsRef.current.executeScenario(scenarioName, ...args);
50+
};
51+
52+
return {executeScenario, ...renderResult};
4853
}
4954

50-
export {renderMfaUi, getMfaControls};
55+
export default renderMfaUi;

0 commit comments

Comments
 (0)