Skip to content

Commit f9730f5

Browse files
committed
Type the discard guard test mocks instead of disabling lint file-wide
1 parent e34626b commit f9730f5

2 files changed

Lines changed: 40 additions & 31 deletions

File tree

tests/ui/IOURequestStepDistanceManualTest.tsx

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
1-
// RNTL types `ReactTestInstance.props` as `any`, so reading a queried input's `.props.value` is an
2-
// unavoidably unsafe access, and the jest factories that spread `requireActual` results are untyped at
3-
// that boundary. The type assertion builds a route whose `action`/`backTo` params are typed `never`
4-
// but are read at runtime by the screen under test.
5-
// These are the only reason for the file-wide disables; nothing here silences a production-code rule.
6-
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
7-
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
8-
/* eslint-disable @typescript-eslint/no-unsafe-call */
9-
/* eslint-disable @typescript-eslint/no-unsafe-return */
10-
/* eslint-disable @typescript-eslint/no-unsafe-type-assertion */
111
import {act, fireEvent, render, screen} from '@testing-library/react-native';
122

133
import {CurrentUserPersonalDetailsProvider} from '@components/CurrentUserPersonalDetailsProvider';
144
import OnyxListItemProvider from '@components/OnyxListItemProvider';
155

6+
import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types';
7+
168
import DynamicIOURequestStepDistanceManual from '@pages/iou/request/step/DynamicIOURequestStepDistanceManual';
179

1810
import CONST from '@src/CONST';
@@ -32,6 +24,13 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'
3224
// what the guard reads. Capturing react-navigation's `usePreventRemove` flag instead does not work here:
3325
// `ScreenWrapper/index.tsx:216` calls that hook too and renders last, so its `false` wins.
3426
let mockGetHasUnsavedChanges: (() => boolean) | undefined;
27+
// Only the two React APIs this factory needs. A namespace import of 'react' trips no-restricted-imports,
28+
// and `typeof import(...)` is banned, so name them off the default import instead (types are erased).
29+
type ReactFactoryApi = {
30+
createContext: typeof React.createContext;
31+
createElement: typeof React.createElement;
32+
};
33+
3534
jest.mock('@hooks/useDiscardChangesConfirmation', () => ({
3635
__esModule: true,
3736
default: (options: {getHasUnsavedChanges: () => boolean}) => {
@@ -41,7 +40,7 @@ jest.mock('@hooks/useDiscardChangesConfirmation', () => ({
4140
}));
4241

4342
jest.mock('@components/LocaleContextProvider', () => {
44-
const React2 = require('react');
43+
const React2 = jest.requireActual<ReactFactoryApi>('react');
4544

4645
const defaultContextValue = {
4746
translate: (path: string) => path,
@@ -144,6 +143,17 @@ function createDistanceTransaction(): Transaction {
144143
};
145144
}
146145

146+
// The route types `action`/`backTo` as `never` (unused for navigation but read at runtime by the screen
147+
// under test), so the params object cannot be built without one assertion.
148+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- see comment above
149+
const EDIT_ROUTE_PARAMS = {
150+
action: CONST.IOU.ACTION.EDIT,
151+
iouType: CONST.IOU.TYPE.SUBMIT,
152+
reportID: REPORT_ID,
153+
transactionID: TRANSACTION_ID,
154+
backTo: undefined,
155+
} as unknown as MoneyRequestNavigatorParamList[typeof SCREENS.MONEY_REQUEST.DYNAMIC_STEP_DISTANCE_MANUAL];
156+
147157
function renderEditMode() {
148158
return render(
149159
<OnyxListItemProvider>
@@ -152,13 +162,7 @@ function renderEditMode() {
152162
route={{
153163
key: 'Dynamic_Money_Request_Step_Distance_Manual-test',
154164
name: SCREENS.MONEY_REQUEST.DYNAMIC_STEP_DISTANCE_MANUAL,
155-
params: {
156-
action: CONST.IOU.ACTION.EDIT as never,
157-
iouType: CONST.IOU.TYPE.SUBMIT,
158-
reportID: REPORT_ID,
159-
transactionID: TRANSACTION_ID,
160-
backTo: undefined as never,
161-
},
165+
params: EDIT_ROUTE_PARAMS,
162166
}}
163167
// @ts-expect-error minimal navigation for test
164168
navigation={undefined}

tests/ui/IOURequestStepDistanceOdometerDiscardGuardTest.tsx

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
1-
// RNTL types `ReactTestInstance.props` as `any`, so `odometerInput(...).props.value` and the
2-
// `'value' in element.props` filter below are unavoidably unsafe reads, and the jest factories that
3-
// spread `requireActual` results are untyped at the boundary. The non-null assertion is on the
4-
// `.find()` that picks the real TextInput out of the label query, which always matches on this screen.
5-
// These are the only reason for the file-wide disables; nothing here silences a production-code rule.
6-
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
7-
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
8-
/* eslint-disable @typescript-eslint/no-unsafe-call */
9-
/* eslint-disable @typescript-eslint/no-unsafe-return */
10-
/* eslint-disable @typescript-eslint/no-non-null-assertion */
111
import {act, fireEvent, render, screen} from '@testing-library/react-native';
122

133
import {CurrentUserPersonalDetailsProvider} from '@components/CurrentUserPersonalDetailsProvider';
144
import OnyxListItemProvider from '@components/OnyxListItemProvider';
155

6+
import type * as DiscardChangesNative from '@hooks/useDiscardChangesConfirmation/index.native';
7+
168
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
179
import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types';
1810

@@ -37,14 +29,21 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'
3729
// the flag stale, because React Compiler reuses the render-time result while the closure's captured values hold.
3830
const preventRemoveFlags: boolean[] = [];
3931

32+
// Only the two React APIs this factory needs. A namespace import of 'react' trips no-restricted-imports,
33+
// and `typeof import(...)` is banned, so name them off the default import instead (types are erased).
34+
type ReactFactoryApi = {
35+
createContext: typeof React.createContext;
36+
createElement: typeof React.createElement;
37+
};
38+
4039
jest.mock('@rnmapbox/maps', () => ({
4140
default: jest.fn(),
4241
MarkerView: jest.fn(),
4342
setAccessToken: jest.fn(),
4443
}));
4544

4645
jest.mock('@components/LocaleContextProvider', () => {
47-
const React2 = require('react');
46+
const React2 = jest.requireActual<ReactFactoryApi>('react');
4847
const defaultContextValue = {
4948
translate: (path: string) => path,
5049
numberFormat: (number: number) => String(number),
@@ -67,7 +66,7 @@ jest.mock('@components/LocaleContextProvider', () => {
6766
});
6867

6968
// The blink this PR fixes is iOS-only, so pin the native variant rather than whichever one module resolution picks.
70-
jest.mock('@hooks/useDiscardChangesConfirmation', () => jest.requireActual('@hooks/useDiscardChangesConfirmation/index.native.ts'));
69+
jest.mock('@hooks/useDiscardChangesConfirmation', () => jest.requireActual<typeof DiscardChangesNative>('@hooks/useDiscardChangesConfirmation/index.native.ts'));
7170

7271
jest.mock('@libs/actions/MapboxToken', () => ({
7372
init: jest.fn(),
@@ -204,7 +203,13 @@ function renderCreateOdometer() {
204203
}
205204

206205
// Returns the underlying TextInput (not the floating-label <Text>) for a given odometer field label
207-
const odometerInput = (labelKey: string) => screen.getAllByLabelText(labelKey).find((element) => 'value' in element.props)!;
206+
const odometerInput = (labelKey: string) => {
207+
const input = screen.getAllByLabelText(labelKey).find((element) => 'value' in element.props);
208+
if (!input) {
209+
throw new Error(`No editable odometer input found for ${labelKey}`);
210+
}
211+
return input;
212+
};
208213

209214
// ScreenWrapper calls usePreventRemove as well and always passes false here, so a single armed call in the render
210215
// pass can only have come from the discard guard. Reading the last flag alone would depend on render order.

0 commit comments

Comments
 (0)