chore: ESLint 9 (#1728)

This commit is contained in:
Maciej Jastrzebski
2025-01-08 22:30:01 +01:00
committed by GitHub
parent 7f25adfe21
commit 1219944bbf
34 changed files with 1276 additions and 1341 deletions
+3
View File
@@ -3,6 +3,9 @@ coverage:
round: down
range: 70...100
status:
patch:
default:
target: 80% # Required patch coverage target
project:
default:
threshold: 0.5% # Allowable coverage drop in percentage points
-6
View File
@@ -1,6 +0,0 @@
flow-typed/
build/
experiments-rtl/
website/
jest-setup.ts
-15
View File
@@ -1,15 +0,0 @@
{
"extends": "@callstack",
"rules": {
"no-console": 1,
// Ignore certain webpack alias because it can't be resolved
"import/no-unresolved": [2, { "ignore": ["^@theme", "^@docusaurus", "^@generated"] }],
"react/no-multi-comp": 0,
"react-native/no-color-literals": "off",
"react-native/no-inline-styles": "off",
"react-native/no-raw-text": 0,
"react-native-a11y/has-valid-accessibility-descriptors": "off",
"react-native-a11y/has-valid-accessibility-ignores-invert-colors": 0,
"react-native-a11y/has-valid-accessibility-value": "off"
}
}
+35
View File
@@ -0,0 +1,35 @@
import tseslint from 'typescript-eslint';
import callstackConfig from '@callstack/eslint-config/react-native.flat.js';
export default [
{
ignores: [
'flow-typed/',
'build/',
'experiments-rtl/',
'website/',
'eslint.config.mjs',
'jest-setup.ts',
],
},
...callstackConfig,
...tseslint.configs.strict,
{
rules: {
'no-console': 'error',
},
},
{
files: ['**/*.test.{ts,tsx}', 'src/test-utils/**'],
rules: {
'react/no-multi-comp': 'off',
'react-native/no-color-literals': 'off',
'react-native/no-inline-styles': 'off',
'react-native/no-raw-text': 'off',
'react-native-a11y/has-valid-accessibility-descriptors': 'off',
'react-native-a11y/has-valid-accessibility-ignores-invert-colors': 'off',
'react-native-a11y/has-valid-accessibility-value': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
},
];
+4 -5
View File
@@ -73,7 +73,7 @@
"@babel/preset-typescript": "^7.26.0",
"@callstack/eslint-config": "^15.0.0",
"@react-native/babel-preset": "0.77.0-rc.0",
"@release-it/conventional-changelog": "^9.0.2",
"@release-it/conventional-changelog": "^10.0.0",
"@relmify/jest-serializer-strip-ansi": "^1.0.2",
"@types/jest": "^29.5.14",
"@types/react": "^18.3.12",
@@ -81,8 +81,7 @@
"babel-jest": "^29.7.0",
"babel-plugin-module-resolver": "^5.0.2",
"del-cli": "^6.0.0",
"eslint": "^8.57.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint": "^9.17.0",
"flow-bin": "~0.170.0",
"jest": "^29.7.0",
"prettier": "^2.8.8",
@@ -90,8 +89,8 @@
"react-native": "0.77.0-rc.0",
"react-test-renderer": "18.3.1",
"release-it": "^18.0.0",
"strip-ansi": "^6.0.1",
"typescript": "^5.6.3"
"typescript": "^5.6.3",
"typescript-eslint": "^8.19.1"
},
"publishConfig": {
"registry": "https://registry.npmjs.org"
@@ -288,6 +288,7 @@ exports[`debug with only props from TextInput components 1`] = `
exports[`debug: another custom message 1`] = `
"another custom message
<View>
<Text>
Is the banana fresh?
@@ -370,6 +371,7 @@ exports[`debug: another custom message 1`] = `
exports[`debug: with message 1`] = `
"my custom message
<View>
<Text>
Is the banana fresh?
+1
View File
@@ -4,6 +4,7 @@ import { View } from 'react-native';
let render: (element: React.ReactElement) => void;
beforeAll(() => {
process.env.RNTL_SKIP_AUTO_CLEANUP = 'true';
// eslint-disable-next-line @typescript-eslint/no-require-imports
const rntl = require('..');
render = rntl.render;
});
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable react/no-multi-comp */
import * as React from 'react';
import { View } from 'react-native';
import { cleanup, render } from '../pure';
+1 -1
View File
@@ -4,7 +4,7 @@ import { render, screen, userEvent } from '..';
type QuestionsBoardProps = {
questions: string[];
onSubmit: (obj: {}) => void;
onSubmit: (obj: object) => void;
};
jest.useFakeTimers();
+9 -12
View File
@@ -1,6 +1,5 @@
import * as React from 'react';
import { Pressable, Text, TextInput, View } from 'react-native';
import stripAnsi from 'strip-ansi';
import { configure, fireEvent, render, screen } from '..';
import { logger } from '../helpers/logger';
@@ -98,9 +97,9 @@ test('debug', () => {
screen.debug({ message: 'another custom message' });
const mockCalls = jest.mocked(logger.info).mock.calls;
expect(stripAnsi(mockCalls[0][0])).toMatchSnapshot();
expect(stripAnsi(mockCalls[1][0] + mockCalls[1][1])).toMatchSnapshot('with message');
expect(stripAnsi(mockCalls[2][0] + mockCalls[2][1])).toMatchSnapshot('another custom message');
expect(mockCalls[0][0]).toMatchSnapshot();
expect(`${mockCalls[1][0]}\n${mockCalls[1][1]}`).toMatchSnapshot('with message');
expect(`${mockCalls[2][0]}\n${mockCalls[2][1]}`).toMatchSnapshot('another custom message');
const mockWarnCalls = jest.mocked(logger.warn).mock.calls;
expect(mockWarnCalls[0]).toMatchInlineSnapshot(`
@@ -117,9 +116,7 @@ test('debug changing component', () => {
screen.debug();
const mockCalls = jest.mocked(logger.info).mock.calls;
expect(stripAnsi(mockCalls[0][0])).toMatchSnapshot(
'bananaFresh button message should now be "fresh"',
);
expect(mockCalls[0][0]).toMatchSnapshot('bananaFresh button message should now be "fresh"');
});
test('debug with only children prop', () => {
@@ -127,7 +124,7 @@ test('debug with only children prop', () => {
screen.debug({ mapProps: () => ({}) });
const mockCalls = jest.mocked(logger.info).mock.calls;
expect(stripAnsi(mockCalls[0][0])).toMatchSnapshot();
expect(mockCalls[0][0]).toMatchSnapshot();
});
test('debug with only prop whose value is bananaChef', () => {
@@ -145,7 +142,7 @@ test('debug with only prop whose value is bananaChef', () => {
});
const mockCalls = jest.mocked(logger.info).mock.calls;
expect(stripAnsi(mockCalls[0][0])).toMatchSnapshot();
expect(mockCalls[0][0]).toMatchSnapshot();
});
test('debug with only props from TextInput components', () => {
@@ -155,7 +152,7 @@ test('debug with only props from TextInput components', () => {
});
const mockCalls = jest.mocked(logger.info).mock.calls;
expect(stripAnsi(mockCalls[0][0])).toMatchSnapshot();
expect(mockCalls[0][0]).toMatchSnapshot();
});
test('debug should use debugOptions from config when no option is specified', () => {
@@ -169,7 +166,7 @@ test('debug should use debugOptions from config when no option is specified', ()
screen.debug();
const mockCalls = jest.mocked(logger.info).mock.calls;
expect(stripAnsi(mockCalls[0][0])).toMatchSnapshot();
expect(mockCalls[0][0]).toMatchSnapshot();
});
test('filtering out props through mapProps option should not modify component', () => {
@@ -190,5 +187,5 @@ test('debug should use given options over config debugOptions', () => {
screen.debug({ mapProps: (props) => props });
const mockCalls = jest.mocked(logger.info).mock.calls;
expect(stripAnsi(mockCalls[0][0])).toMatchSnapshot();
expect(mockCalls[0][0]).toMatchSnapshot();
});
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable no-console */
import * as React from 'react';
import { Pressable, Text, TextInput, View } from 'react-native';
import { fireEvent, render, RenderAPI, screen } from '..';
+2 -2
View File
@@ -19,7 +19,7 @@ class Banana extends React.Component<any> {
}
}
class BananaContainer extends React.Component<{}, any> {
class BananaContainer extends React.Component<object, any> {
state = { fresh: false };
onChangeFresh = async () => {
@@ -196,7 +196,7 @@ test.each([false, true])(
const blockThread = (timeToBlockThread: number, legacyFakeTimers: boolean) => {
jest.useRealTimers();
let end = Date.now() + timeToBlockThread;
const end = Date.now() + timeToBlockThread;
while (Date.now() < end) {
// do nothing
+4 -5
View File
@@ -8,6 +8,7 @@ type ReactAct = 0 extends 1 & typeof React.act ? typeof reactTestRendererAct : t
// See https://github.com/reactwg/react-18/discussions/102 for more context on global.IS_REACT_ACT_ENVIRONMENT
declare global {
// eslint-disable-next-line no-var
var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
}
@@ -45,13 +46,11 @@ function withGlobalActEnvironment(actImplementation: ReactAct) {
// eslint-disable-next-line promise/always-return
(returnValue) => {
setIsReactActEnvironment(previousActEnvironment);
// @ts-expect-error
resolve(returnValue);
resolve(returnValue as never);
},
(error) => {
setIsReactActEnvironment(previousActEnvironment);
// @ts-expect-error
reject(error);
reject(error as never);
},
);
},
@@ -69,7 +68,7 @@ function withGlobalActEnvironment(actImplementation: ReactAct) {
};
}
// @ts-expect-error
// @ts-expect-error: typings get too complex
const act = withGlobalActEnvironment(reactAct) as ReactAct;
export default act;
+3 -3
View File
@@ -1,6 +1,7 @@
import prettyFormat from 'pretty-format';
export class ErrorWithStack extends Error {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
constructor(message: string | undefined, callsite: Function) {
super(message);
if (Error.captureStackTrace) {
@@ -32,6 +33,7 @@ export const prepareErrorMessage = (
return errorMessage;
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
export const createQueryByError = (error: unknown, callsite: Function): null => {
if (error instanceof Error) {
if (error.message.includes('No instances found')) {
@@ -41,9 +43,7 @@ export const createQueryByError = (error: unknown, callsite: Function): null =>
}
throw new ErrorWithStack(
// generic refining of `unknown` is very hard, you cannot do `'toString' in error` or anything like that
// Converting as any with extra safe optional chaining will do the job just as well
`Query: caught unknown error type: ${typeof error}, value: ${(error as any)?.toString?.()}`,
`Query: caught unknown error type: ${typeof error}, value: ${error}`,
callsite,
);
};
+5 -5
View File
@@ -4,28 +4,28 @@ import chalk from 'chalk';
import redent from 'redent';
export const logger = {
debug(message: any, ...args: any[]) {
debug(message: unknown, ...args: unknown[]) {
const output = formatMessage('●', message, ...args);
nodeConsole.debug(chalk.dim(output));
},
info(message: any, ...args: any[]) {
info(message: unknown, ...args: unknown[]) {
const output = formatMessage('●', message, ...args);
nodeConsole.info(output);
},
warn(message: any, ...args: any[]) {
warn(message: unknown, ...args: unknown[]) {
const output = formatMessage('▲', message, ...args);
nodeConsole.warn(chalk.yellow(output));
},
error(message: any, ...args: any[]) {
error(message: unknown, ...args: unknown[]) {
const output = formatMessage('■', message, ...args);
nodeConsole.error(chalk.red(output));
},
};
function formatMessage(symbol: string, message: any, ...args: any[]) {
function formatMessage(symbol: string, message: unknown, ...args: unknown[]) {
const formatted = nodeUtil.format(message, ...args);
const indented = redent(formatted, 4);
return ` ${symbol} ${indented.trimStart()}\n`;
+1 -1
View File
@@ -1,4 +1,4 @@
export function pick<T extends {}>(object: T, keys: (keyof T)[]): Partial<T> {
export function pick<T extends object>(object: T, keys: (keyof T)[]): Partial<T> {
const result: Partial<T> = {};
keys.forEach((key) => {
if (object[key] !== undefined) {
+1 -1
View File
@@ -64,7 +64,7 @@ function getFakeTimersConfigFromType(type: FakeTimersTypes) {
const jestFakeTimersAreEnabled = (): boolean => Boolean(getJestFakeTimersType());
// we only run our tests in node, and setImmediate is supported in node.
function setImmediatePolyfill(fn: Function) {
function setImmediatePolyfill(fn: () => void) {
return globalObj.setTimeout(fn, 0);
}
-1
View File
@@ -10,7 +10,6 @@ if (!process?.env?.RNTL_SKIP_AUTO_CLEANUP) {
// if you don't like this then either import the `pure` module
// or set the RNTL_SKIP_AUTO_CLEANUP env variable to 'true'.
if (typeof afterEach === 'function') {
// eslint-disable-next-line no-undef
afterEach(async () => {
await flushMicroTasks();
cleanup();
@@ -123,7 +123,7 @@ test('toContainElement() handles non-element element', () => {
const view = screen.getByTestId('view');
expect(() =>
// @ts-expect-error
// @ts-expect-error: intentionally passing wrong element shape
expect(view).not.toContainElement({ name: 'non-element' }),
).toThrowErrorMatchingInlineSnapshot(`
"expect(received).not.toContainElement()
+4 -4
View File
@@ -4,7 +4,7 @@ import { render, screen } from '../..';
import { checkHostElement, formatElement } from '../utils';
function fakeMatcher() {
// Do nothing.
return { pass: true, message: () => 'fake' };
}
test('formatElement', () => {
@@ -15,7 +15,7 @@ test('checkHostElement allows host element', () => {
render(<View testID="view" />);
expect(() => {
// @ts-expect-error
// @ts-expect-error: intentionally passing wrong element shape
checkHostElement(screen.getByTestId('view'), fakeMatcher, {});
}).not.toThrow();
});
@@ -24,14 +24,14 @@ test('checkHostElement allows rejects composite element', () => {
render(<View testID="view" />);
expect(() => {
// @ts-expect-error
// @ts-expect-error: intentionally passing wrong element shape
checkHostElement(screen.UNSAFE_root, fakeMatcher, {});
}).toThrow(/value must be a host element./);
});
test('checkHostElement allows rejects null element', () => {
expect(() => {
// @ts-expect-error
// @ts-expect-error: intentionally passing wrong element shape
checkHostElement(null, fakeMatcher, {});
}).toThrowErrorMatchingInlineSnapshot(`
"expect(received).fakeMatcher()
+4 -2
View File
@@ -279,14 +279,16 @@ export interface JestNativeMatchers<R> {
// Implicit Jest global `expect`.
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace jest {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars
interface Matchers<R, T = {}> extends JestNativeMatchers<R> {}
}
}
// Explicit `@jest/globals` `expect` matchers.
// @ts-ignore
// @ts-expect-error: Invalid module name in augmentation, module '@jest/expect' cannot be found
declare module '@jest/expect' {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface Matchers<R extends void | Promise<void>> extends JestNativeMatchers<R> {}
}
+1 -1
View File
@@ -11,7 +11,7 @@ export type NativeState = {
contentOffsetForElement: WeakMap<ReactTestInstance, Point>;
};
export let nativeState: NativeState = {
export const nativeState: NativeState = {
valueForElement: new WeakMap(),
contentOffsetForElement: new WeakMap(),
};
+14 -16
View File
@@ -3,10 +3,10 @@ import prettyFormat from 'pretty-format';
import { ErrorWithStack, prepareErrorMessage } from '../helpers/errors';
import { createQueryByError } from '../helpers/errors';
const UNSAFE_getByProps = (
instance: ReactTestInstance,
): ((props: { [propName: string]: any }) => ReactTestInstance) =>
function getByPropsFn(props: { [propName: string]: any }) {
type Props = Record<string, unknown>;
const UNSAFE_getByProps = (instance: ReactTestInstance): ((props: Props) => ReactTestInstance) =>
function getByPropsFn(props: Props) {
try {
return instance.findByProps(props);
} catch (error) {
@@ -16,8 +16,8 @@ const UNSAFE_getByProps = (
const UNSAFE_getAllByProps = (
instance: ReactTestInstance,
): ((props: { [propName: string]: any }) => Array<ReactTestInstance>) =>
function getAllByPropsFn(props: { [propName: string]: any }) {
): ((props: Props) => Array<ReactTestInstance>) =>
function getAllByPropsFn(props: Props) {
const results = instance.findAllByProps(props);
if (results.length === 0) {
throw new ErrorWithStack(
@@ -30,8 +30,8 @@ const UNSAFE_getAllByProps = (
const UNSAFE_queryByProps = (
instance: ReactTestInstance,
): ((props: { [propName: string]: any }) => ReactTestInstance | null) =>
function queryByPropsFn(props: { [propName: string]: any }) {
): ((props: Props) => ReactTestInstance | null) =>
function queryByPropsFn(props: Props) {
try {
return UNSAFE_getByProps(instance)(props);
} catch (error) {
@@ -40,10 +40,8 @@ const UNSAFE_queryByProps = (
};
const UNSAFE_queryAllByProps =
(
instance: ReactTestInstance,
): ((props: { [propName: string]: any }) => Array<ReactTestInstance>) =>
(props: { [propName: string]: any }) => {
(instance: ReactTestInstance): ((props: Props) => Array<ReactTestInstance>) =>
(props: Props) => {
try {
return UNSAFE_getAllByProps(instance)(props);
} catch {
@@ -53,10 +51,10 @@ const UNSAFE_queryAllByProps =
// Unsafe aliases
export type UnsafeByPropsQueries = {
UNSAFE_getByProps: (props: { [key: string]: any }) => ReactTestInstance;
UNSAFE_getAllByProps: (props: { [key: string]: any }) => Array<ReactTestInstance>;
UNSAFE_queryByProps: (props: { [key: string]: any }) => ReactTestInstance | null;
UNSAFE_queryAllByProps: (props: { [key: string]: any }) => Array<ReactTestInstance>;
UNSAFE_getByProps: (props: Props) => ReactTestInstance;
UNSAFE_getAllByProps: (props: Props) => Array<ReactTestInstance>;
UNSAFE_queryByProps: (props: Props) => ReactTestInstance | null;
UNSAFE_queryAllByProps: (props: Props) => Array<ReactTestInstance>;
};
// TODO: migrate to makeQueries pattern
+11 -8
View File
@@ -3,10 +3,13 @@ import * as React from 'react';
import { ErrorWithStack, prepareErrorMessage } from '../helpers/errors';
import { createQueryByError } from '../helpers/errors';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type UnsafeComponentType = React.ComponentType<any>;
const UNSAFE_getByType = (
instance: ReactTestInstance,
): ((type: React.ComponentType<any>) => ReactTestInstance) =>
function getByTypeFn(type: React.ComponentType<any>) {
): ((type: UnsafeComponentType) => ReactTestInstance) =>
function getByTypeFn(type: UnsafeComponentType) {
try {
return instance.findByType(type);
} catch (error) {
@@ -16,8 +19,8 @@ const UNSAFE_getByType = (
const UNSAFE_getAllByType = (
instance: ReactTestInstance,
): ((type: React.ComponentType<any>) => Array<ReactTestInstance>) =>
function getAllByTypeFn(type: React.ComponentType<any>) {
): ((type: UnsafeComponentType) => Array<ReactTestInstance>) =>
function getAllByTypeFn(type: UnsafeComponentType) {
const results = instance.findAllByType(type);
if (results.length === 0) {
throw new ErrorWithStack('No instances found', getAllByTypeFn);
@@ -27,8 +30,8 @@ const UNSAFE_getAllByType = (
const UNSAFE_queryByType = (
instance: ReactTestInstance,
): ((type: React.ComponentType<any>) => ReactTestInstance | null) =>
function queryByTypeFn(type: React.ComponentType<any>) {
): ((type: UnsafeComponentType) => ReactTestInstance | null) =>
function queryByTypeFn(type: UnsafeComponentType) {
try {
return UNSAFE_getByType(instance)(type);
} catch (error) {
@@ -37,8 +40,8 @@ const UNSAFE_queryByType = (
};
const UNSAFE_queryAllByType =
(instance: ReactTestInstance): ((type: React.ComponentType<any>) => Array<ReactTestInstance>) =>
(type: React.ComponentType<any>) => {
(instance: ReactTestInstance): ((type: UnsafeComponentType) => Array<ReactTestInstance>) =>
(type: UnsafeComponentType) => {
try {
return UNSAFE_getAllByType(instance)(type);
} catch {
+2 -2
View File
@@ -10,10 +10,10 @@ export function renderWithAct(
// This will be called synchronously.
void act(() => {
// @ts-expect-error TestRenderer.create is not typed correctly
// @ts-expect-error `TestRenderer.create` is not typed correctly
renderer = TestRenderer.create(component, options);
});
// @ts-ignore act is synchronous, so renderer is already initialized here
// @ts-expect-error: `act` is synchronous, so `renderer` is already initialized here
return renderer;
}
+1
View File
@@ -10,6 +10,7 @@ export type RenderHookResult<Result, Props> = {
export type RenderHookOptions<Props> = {
initialProps?: Props;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
wrapper?: ComponentType<any>;
};
+8 -9
View File
@@ -19,8 +19,9 @@ import { getQueriesForElement } from './within';
export interface RenderOptions {
/**
* Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for creating
* reusable custom render functions for common data providers.
* reusable custom render functions for common data providers.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
wrapper?: React.ComponentType<any>;
/**
@@ -73,23 +74,21 @@ function renderWithStringValidation<T>(
component: React.ReactElement<T>,
options: Omit<RenderOptions, 'unstable_validateStringsRenderedWithinText'> = {},
) {
let renderer: ReactTestRenderer;
const { wrapper: Wrapper, ...testRendererOptions } = options ?? {};
const handleRender: React.ProfilerOnRenderCallback = (_, phase) => {
if (renderer && phase === 'update') {
validateStringsRenderedWithinText(renderer.toJSON());
}
};
const wrap = (element: React.ReactElement) => (
<Profiler id="renderProfiler" onRender={handleRender}>
{Wrapper ? <Wrapper>{element}</Wrapper> : element}
</Profiler>
);
renderer = renderWithAct(wrap(component), testRendererOptions);
const handleRender: React.ProfilerOnRenderCallback = (_, phase) => {
if (renderer && phase === 'update') {
validateStringsRenderedWithinText(renderer.toJSON());
}
};
const renderer: ReactTestRenderer = renderWithAct(wrap(component), testRendererOptions);
validateStringsRenderedWithinText(renderer.toJSON());
return buildRenderResult(renderer, wrap);
+1 -1
View File
@@ -1,7 +1,7 @@
import { BaseSyntheticEvent } from 'react';
/** Builds base syntentic event stub, with prop values as inspected in RN runtime. */
export function baseSyntheticEvent(): Partial<BaseSyntheticEvent<{}, unknown, unknown>> {
export function baseSyntheticEvent(): Partial<BaseSyntheticEvent<object, unknown, unknown>> {
return {
currentTarget: {},
target: {},
+1 -1
View File
@@ -169,7 +169,7 @@ function createInstance(config: UserEventConfig): UserEventInstance {
* @see https://github.com/testing-library/user-event/blob/7a305dee9ab833d6f338d567fc2e862b4838b76a/src/setup/setup.ts#L121
*/
function wrapAndBindImpl<
Args extends any[],
Args extends never[],
Impl extends (this: UserEventInstance, ...args: Args) => Promise<unknown>,
>(instance: UserEventInstance, impl: Impl) {
function method(...args: Args) {
+4 -6
View File
@@ -28,26 +28,24 @@ describe('wait()', () => {
expect(advanceTimers).toHaveBeenCalledWith(100);
});
it('wait with null delay does not wait with real timers', async () => {
it('wait with undefined delay does not wait with real timers', async () => {
jest.spyOn(globalThis, 'setTimeout');
const advanceTimers = jest.fn();
// @ts-expect-error
await wait({ delay: null, advanceTimers });
await wait({ advanceTimers });
expect(globalThis.setTimeout).not.toHaveBeenCalled();
expect(advanceTimers).not.toHaveBeenCalled();
});
it.each(['modern', 'legacy'])(
'wait with null delay does not wait with %s fake timers',
'wait with undefined delay does not wait with %s fake timers',
async (type) => {
jest.useFakeTimers({ legacyFakeTimers: type === 'legacy' });
jest.spyOn(globalThis, 'setTimeout');
const advanceTimers = jest.fn();
// @ts-expect-error
await wait({ delay: null, advanceTimers });
await wait({ advanceTimers });
expect(globalThis.setTimeout).not.toHaveBeenCalled();
expect(advanceTimers).not.toHaveBeenCalled();
+6 -3
View File
@@ -1,8 +1,11 @@
import { UserEventConfig } from '../setup';
type WaitConfig = {
delay?: number;
advanceTimers: (delay: number) => Promise<void> | void;
};
export function wait(config: UserEventConfig, durationInMs?: number) {
export function wait(config: WaitConfig, durationInMs?: number) {
const delay = durationInMs ?? config.delay;
if (typeof delay !== 'number') {
if (typeof delay !== 'number' || delay == null) {
return;
}
+2 -5
View File
@@ -43,7 +43,6 @@ function waitForInternal<T>(
// infinite loop. However, eslint isn't smart enough to know that we're
// setting finished inside `onDone` which will be called when we're done
// waiting or when we've timed out.
// eslint-disable-next-line no-unmodified-loop-condition
let fakeTimeRemaining = timeout;
while (!finished) {
if (!jestFakeTimersAreEnabled()) {
@@ -82,7 +81,6 @@ function waitForInternal<T>(
// to resolve before continuing. We don't need to take advantage
// of parallelization so we're fine.
// https://stackoverflow.com/a/59243586/971592
// eslint-disable-next-line no-await-in-loop
await flushMicroTasks();
}
} else {
@@ -127,10 +125,9 @@ function waitForInternal<T>(
try {
const result = expectation();
// @ts-ignore result can be a promise
// eslint-disable-next-line promise/prefer-await-to-then
// @ts-expect-error result can be a promise
if (typeof result?.then === 'function') {
const promiseResult: Promise<T> = result as any;
const promiseResult: Promise<T> = result as unknown as Promise<T>;
promiseStatus = 'pending';
// eslint-disable-next-line promise/catch-or-return, promise/prefer-await-to-then
promiseResult.then(
+1145 -1223
View File
File diff suppressed because it is too large Load Diff