Skip to content

Commit e97ba23

Browse files
committed
refactor(mfa): drop trivial test helpers, target confirm button by testID
Remove single-line pass-through helpers that added indirection without logic: - flushMfaUi: call waitForBatchedUpdatesWithAct directly. - getSettleableLeafStates: merge the machine->root wrapper into the recursive collector so one function remains; callers pass mfaMachine.root. - isModalOverlayMounted / isOutcomeScreenVisible: inline the queryAll assertions in the view-config test and move the marker constants next to them. Drop the redundant MfaControlsCapture.displayName; a named function component already exposes that name. Add a stable testID to the outcome screen confirm Button and press it via getByTestId instead of the translated "Got it" label, so the test no longer depends on visible copy.
1 parent 81a1220 commit e97ba23

6 files changed

Lines changed: 34 additions & 50 deletions

File tree

src/components/MultifactorAuthentication/components/OutcomeScreen/OutcomeScreenBase.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ function OutcomeScreenBase({headerTitle, illustration, iconWidth, iconHeight, ti
101101
style={styles.flex1}
102102
onPress={onClose}
103103
text={translate('common.buttonConfirm')}
104+
testID="MultifactorAuthenticationOutcomeConfirmButton"
104105
/>
105106
</View>
106107
</View>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import mfaMachine from '@components/MultifactorAuthentication/machine/mfaMachine
88
describe('every MFA modal state is reachable', () => {
99
const reachableSnapshots = getShortestPaths(mfaMachine).map((path) => path.state);
1010

11-
it.each(getSettleableLeafStates(mfaMachine))('reaches the $description state', ({value}) => {
11+
it.each(getSettleableLeafStates(mfaMachine.root))('reaches the $description state', ({value}) => {
1212
expect(reachableSnapshots.some((snapshot) => matchesState(value, snapshot.value))).toBe(true);
1313
});
1414
});

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

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return -- jest.mock factories delegate to require()'d helpers, which resolve as `any`. */
2+
import {screen} from '@testing-library/react-native';
23
import createMfaTestModel from 'tests/utils/mfa/flowPaths';
34
import getSettleableLeafStates, {toStateValue} from 'tests/utils/mfa/reachableStates';
45
import {resetMfaUiMocks} from 'tests/utils/mfa/realUi/mocks';
5-
import {flushMfaUi, isModalOverlayMounted, isOutcomeScreenVisible, renderMfaUi} from 'tests/utils/mfa/realUi/renderModal';
6+
import {renderMfaUi} from 'tests/utils/mfa/realUi/renderModal';
67
import mfaEventExecutors from 'tests/utils/mfa/realUi/userGestures';
8+
import waitForBatchedUpdatesWithAct from 'tests/utils/waitForBatchedUpdatesWithAct';
79
import {matchesState} from 'xstate';
810
import mfaMachine from '@components/MultifactorAuthentication/machine/mfaMachine';
911
import {resetMfaNavigation} from '@components/MultifactorAuthentication/mfaNavigation';
@@ -20,6 +22,11 @@ jest.mock('@components/MultifactorAuthentication/useSyncMfaModalNavigatorWithHis
2022
// Supplies the Navigation methods the flow drives with real behavior; the Proxy no-ops the rest.
2123
jest.mock('@libs/Navigation/Navigation', () => require('tests/utils/mfa/realUi/jestMocks').navigationMock());
2224

25+
// The queryable markers each state asserts against. `OutcomeScreenBase` is the success or failure screen's
26+
// testID; the backdrop's `Close` label only renders while the MFA navigator is mounted.
27+
const OUTCOME_SCREEN_TEST_ID = 'OutcomeScreenBase';
28+
const MODAL_OVERLAY_LABEL = 'Close';
29+
2330
const MFA_STATE = CONST.MULTIFACTOR_AUTHENTICATION.MFA_STATE;
2431

2532
const testModel = createMfaTestModel();
@@ -31,16 +38,16 @@ const testConfig = {
3138
events: mfaEventExecutors,
3239
states: {
3340
[MFA_STATE.CLOSED]: () => {
34-
expect(isModalOverlayMounted()).toBe(false);
35-
expect(isOutcomeScreenVisible()).toBe(false);
41+
expect(screen.queryAllByLabelText(MODAL_OVERLAY_LABEL)).toHaveLength(0);
42+
expect(screen.queryAllByTestId(OUTCOME_SCREEN_TEST_ID)).toHaveLength(0);
3643
},
3744
[`${MFA_STATE.OPEN}.${MFA_STATE.OUTCOME}.${MFA_STATE.SUCCESS}`]: () => {
38-
expect(isModalOverlayMounted()).toBe(true);
39-
expect(isOutcomeScreenVisible()).toBe(true);
45+
expect(screen.queryAllByLabelText(MODAL_OVERLAY_LABEL)).not.toHaveLength(0);
46+
expect(screen.queryAllByTestId(OUTCOME_SCREEN_TEST_ID)).not.toHaveLength(0);
4047
},
4148
[MFA_STATE.CLOSING]: () => {
42-
expect(isModalOverlayMounted()).toBe(true);
43-
expect(isOutcomeScreenVisible()).toBe(false);
49+
expect(screen.queryAllByLabelText(MODAL_OVERLAY_LABEL)).not.toHaveLength(0);
50+
expect(screen.queryAllByTestId(OUTCOME_SCREEN_TEST_ID)).toHaveLength(0);
4451
},
4552
},
4653
};
@@ -61,7 +68,7 @@ describe('the real MFA modal follows the machine', () => {
6168
for (const path of walkedPaths) {
6269
it(`reaches ${JSON.stringify(path.state.value)} via [${path.description}]`, async () => {
6370
renderMfaUi();
64-
await flushMfaUi();
71+
await waitForBatchedUpdatesWithAct();
6572
await path.test(testConfig);
6673
});
6774
}
@@ -74,7 +81,7 @@ describe('the real MFA modal follows the machine', () => {
7481
describe('the UI walk reaches every settleable leaf', () => {
7582
const walkedLeafValues = walkedPaths.map((path) => path.state.value);
7683

77-
it.each(getSettleableLeafStates(mfaMachine))('walks the $description state', ({value}) => {
84+
it.each(getSettleableLeafStates(mfaMachine.root))('walks the $description state', ({value}) => {
7885
expect(walkedLeafValues.some((reached) => matchesState(value, reached))).toBe(true);
7986
});
8087
});
@@ -88,7 +95,7 @@ describe('the UI walk reaches every settleable leaf', () => {
8895
// `Record<leaf, ...>` would then need an empty assertion for every pass-through state, and that empty
8996
// entry would make the check pass on its own once the state later loses its `always` and becomes settleable.
9097
describe('the view config stays in sync with the machine state by state', () => {
91-
const settleableLeafStates = getSettleableLeafStates(mfaMachine);
98+
const settleableLeafStates = getSettleableLeafStates(mfaMachine.root);
9299
const configuredStates = Object.keys(testConfig.states).map((stateValue) => ({description: stateValue, value: toStateValue(stateValue.split('.'))}));
93100

94101
it.each(settleableLeafStates)('asserts the reachable $description state', ({value}) => {

tests/utils/mfa/reachableStates.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {AnyStateMachine, AnyStateNode, StateValue} from 'xstate';
1+
import type {AnyStateNode, StateValue} from 'xstate';
22

33
type SettleableLeafState = {description: string; value: StateValue};
44

@@ -10,10 +10,10 @@ function toStateValue(path: string[]): StateValue {
1010
return {[head]: toStateValue(rest)};
1111
}
1212

13-
function collectSettleableLeafStates(node: AnyStateNode): SettleableLeafState[] {
13+
function getSettleableLeafStates(node: AnyStateNode): SettleableLeafState[] {
1414
const children = Object.values(node.states);
1515
if (children.length > 0) {
16-
return children.flatMap(collectSettleableLeafStates);
16+
return children.flatMap(getSettleableLeafStates);
1717
}
1818
// A leaf with an `always` transition is a transient router that leaves on entry and never settles.
1919
if ((node.always?.length ?? 0) > 0) {
@@ -22,10 +22,6 @@ function collectSettleableLeafStates(node: AnyStateNode): SettleableLeafState[]
2222
return [{description: node.path.join('.'), value: toStateValue(node.path)}];
2323
}
2424

25-
function getSettleableLeafStates(machine: AnyStateMachine): SettleableLeafState[] {
26-
return collectSettleableLeafStates(machine.root);
27-
}
28-
2925
export default getSettleableLeafStates;
3026
export {toStateValue};
3127
export type {SettleableLeafState};

tests/utils/mfa/realUi/renderModal.tsx

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
1-
import {render, screen} from '@testing-library/react-native';
1+
import {render} from '@testing-library/react-native';
22
import React, {useEffect} from 'react';
33
import {SafeAreaProvider} from 'react-native-safe-area-context';
44
import ComposeProviders from '@components/ComposeProviders';
55
import {LocaleContextProvider} from '@components/LocaleContextProvider';
66
import {MultifactorAuthenticationContextProviders, useMultifactorAuthentication} from '@components/MultifactorAuthentication/Context';
77
import OnyxListItemProvider from '@components/OnyxListItemProvider';
88
import MultifactorAuthenticationModalNavigator from '@navigation/AppNavigator/Navigators/MultifactorAuthenticationModalNavigator';
9-
import waitForBatchedUpdatesWithAct from '../../waitForBatchedUpdatesWithAct';
10-
11-
/** The queryable markers the state tests assert against. `OutcomeScreenBase` is the success or failure
12-
* screen's testID, and the backdrop's `common.close` label only ever renders inside the mounted navigator. */
13-
const OUTCOME_SCREEN_TEST_ID = 'OutcomeScreenBase';
14-
const MODAL_OVERLAY_LABEL = 'Close';
159

1610
type MfaUiControls = {
1711
executeScenario: ReturnType<typeof useMultifactorAuthentication>['executeScenario'];
@@ -30,7 +24,6 @@ function MfaControlsCapture() {
3024
});
3125
return null;
3226
}
33-
MfaControlsCapture.displayName = 'MfaControlsCapture';
3427

3528
const INITIAL_SAFE_AREA_METRICS = {
3629
frame: {x: 0, y: 0, width: 390, height: 844},
@@ -54,23 +47,9 @@ function renderMfaUi() {
5447

5548
function getMfaControls(): MfaUiControls {
5649
if (!controlsHolder.current) {
57-
throw new Error('MFA UI controls were not captured. Call renderMfaUi() and flushMfaUi() first.');
50+
throw new Error('MFA UI controls were not captured. Call renderMfaUi() and await waitForBatchedUpdatesWithAct() first.');
5851
}
5952
return controlsHolder.current;
6053
}
6154

62-
async function flushMfaUi(): Promise<void> {
63-
await waitForBatchedUpdatesWithAct();
64-
}
65-
66-
/** True while the outcome screen (success/failure) is the visible route. */
67-
function isOutcomeScreenVisible(): boolean {
68-
return screen.queryAllByTestId(OUTCOME_SCREEN_TEST_ID).length > 0;
69-
}
70-
71-
/** True while the MFA overlay (navigator root + backdrop) is mounted. */
72-
function isModalOverlayMounted(): boolean {
73-
return screen.queryAllByLabelText(MODAL_OVERLAY_LABEL).length > 0;
74-
}
75-
76-
export {renderMfaUi, getMfaControls, flushMfaUi, isOutcomeScreenVisible, isModalOverlayMounted};
55+
export {renderMfaUi, getMfaControls};

tests/utils/mfa/realUi/userGestures.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import {act, fireEvent, screen} from '@testing-library/react-native';
22
import type {MfaEvent} from '@components/MultifactorAuthentication/machine/types';
33
import {handleInitialScreenLayout} from '@components/MultifactorAuthentication/mfaNavigation';
4+
import waitForBatchedUpdatesWithAct from '../../waitForBatchedUpdatesWithAct';
45
import {MFA_TEST_SCENARIO_NAME} from '../flowFixtures';
56
import {pendingModalClose} from './mocks';
6-
import {flushMfaUi, getMfaControls} from './renderModal';
7+
import {getMfaControls} from './renderModal';
78

89
type MfaEventType = MfaEvent['type'];
910
type MfaEventExecutor = () => Promise<void>;
1011

11-
/** Holds the translated `common.buttonConfirm` text shown on the outcome screen's confirm button and back button. */
12-
const CONFIRM_BUTTON_TEXT = 'Got it';
12+
/** The confirm button's testID on the outcome screen (see OutcomeScreenBase). Queried instead of the visible label. */
13+
const CONFIRM_BUTTON_TEST_ID = 'MultifactorAuthenticationOutcomeConfirmButton';
1314

1415
/**
1516
* Maps each machine event to the gesture (or system step) that produces it in production. The model
@@ -26,19 +27,19 @@ const mfaEventExecutors = {
2627
await act(async () => {
2728
await getMfaControls().executeScenario(MFA_TEST_SCENARIO_NAME);
2829
});
29-
await flushMfaUi();
30+
await waitForBatchedUpdatesWithAct();
3031
// The transparent initial screen's onLayout does not fire in jsdom. Calling the exact handler it
3132
// wires runs the buffered push to the outcome screen, just like a real layout pass.
3233
act(() => handleInitialScreenLayout());
33-
await flushMfaUi();
34+
await waitForBatchedUpdatesWithAct();
3435
},
3536
CLOSE_MODAL: async () => {
36-
fireEvent.press(screen.getByText(CONFIRM_BUTTON_TEXT));
37-
await flushMfaUi();
37+
fireEvent.press(screen.getByTestId(CONFIRM_BUTTON_TEST_ID));
38+
await waitForBatchedUpdatesWithAct();
3839
},
3940
MODAL_CLOSED: async () => {
4041
act(() => pendingModalClose.run());
41-
await flushMfaUi();
42+
await waitForBatchedUpdatesWithAct();
4243
},
4344
} satisfies Record<MfaEventType, MfaEventExecutor>;
4445
/* eslint-enable @typescript-eslint/naming-convention */

0 commit comments

Comments
 (0)