diff --git a/src/__tests__/wait-for.test.tsx b/src/__tests__/wait-for.test.tsx index 88c8429d..03caed81 100644 --- a/src/__tests__/wait-for.test.tsx +++ b/src/__tests__/wait-for.test.tsx @@ -2,249 +2,268 @@ import * as React from 'react'; import { Pressable, Text, TouchableOpacity, View } from 'react-native'; import { configure, fireEvent, render, screen, waitFor } from '..'; +import type { TimerType } from '../test-utils/timers'; +import { setupTimeType } from '../test-utils/timers'; -class Banana extends React.Component { - changeFresh = () => { - this.props.onChangeFresh(); - }; - - render() { - return ( - - {this.props.fresh && Fresh} - - Change freshness! - - - ); - } -} +beforeEach(() => { + jest.useRealTimers(); +}); -class BananaContainer extends React.Component { - state = { fresh: false }; +describe('successful waiting', () => { + test('waits for expect() assertion to pass', async () => { + const mockFunction = jest.fn(); - onChangeFresh = async () => { - await new Promise((resolve) => setTimeout(resolve, 300)); - this.setState({ fresh: true }); - }; + function AsyncComponent() { + React.useEffect(() => { + setTimeout(() => mockFunction(), 100); + }, []); - render() { - return ; - } -} + return ; + } -afterEach(() => { - jest.useRealTimers(); -}); + await render(); + await waitFor(() => expect(mockFunction).toHaveBeenCalled()); + expect(mockFunction).toHaveBeenCalledTimes(1); + }); -test('waits for element until it stops throwing', async () => { - await render(); + test.each(['real', 'fake', 'fake-legacy'] as const)( + 'waits for query with %s timers', + async (timerType) => { + setupTimeType(timerType as TimerType); + function AsyncComponent() { + const [text, setText] = React.useState('Loading...'); - await fireEvent.press(screen.getByText('Change freshness!')); + React.useEffect(() => { + setTimeout(() => setText('Loaded'), 100); + }, []); - expect(screen.queryByText('Fresh')).toBeNull(); + return {text}; + } - const freshBananaText = await waitFor(() => screen.getByText('Fresh')); + setupTimeType(timerType); + await render(); + await waitFor(() => screen.getByText('Loaded')); + expect(screen.getByText('Loaded')).toBeOnTheScreen(); + }, + ); - expect(freshBananaText.props.children).toBe('Fresh'); -}); + test('waits for async event with fireEvent', async () => { + const Component = ({ onDelayedPress }: { onDelayedPress: () => void }) => { + const [state, setState] = React.useState(false); -test('waits for element until timeout is met', async () => { - await render(); + React.useEffect(() => { + if (state) { + onDelayedPress(); + } + }, [state, onDelayedPress]); - await fireEvent.press(screen.getByText('Change freshness!')); + return ( + { + await Promise.resolve(); + setState(true); + }} + > + Press + + ); + }; - await expect(waitFor(() => screen.getByText('Fresh'), { timeout: 100 })).rejects.toThrow(); + const onDelayedPress = jest.fn(); + await render(); - // Async action ends after 300ms and we only waited 100ms, so we need to wait - // for the remaining async actions to finish - await waitFor(() => screen.getByText('Fresh')); -}); + await fireEvent.press(screen.getByText('Press')); + await waitFor(() => { + expect(onDelayedPress).toHaveBeenCalled(); + }); + }); -test('waitFor defaults to asyncWaitTimeout config option', async () => { - configure({ asyncUtilTimeout: 100 }); - await render(); + test.each(['real', 'fake', 'fake-legacy'] as const)( + 'flushes scheduled updates before returning with %s timers', + async (timerType) => { + setupTimeType(timerType); + + function Component({ onPress }: { onPress: (color: string) => void }) { + const [color, setColor] = React.useState('green'); + const [syncedColor, setSyncedColor] = React.useState(color); + + // On mount, set the color to "red" in a promise microtask + React.useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises, promise/catch-or-return, promise/prefer-await-to-then + Promise.resolve('red').then((c) => setColor(c)); + }, []); + + // Sync the `color` state to `syncedColor` state, but with a delay caused by the effect + React.useEffect(() => { + setSyncedColor(color); + }, [color]); + + return ( + + {color} + onPress(syncedColor)}> + Trigger + + + ); + } - await fireEvent.press(screen.getByText('Change freshness!')); - await expect(waitFor(() => screen.getByText('Fresh'))).rejects.toThrow(); + const onPress = jest.fn(); + await render(); - // Async action ends after 300ms and we only waited 100ms, so we need to wait - // for the remaining async actions to finish - await waitFor(() => screen.getByText('Fresh'), { timeout: 1000 }); -}); + // Required: this `waitFor` will succeed on first check, because the "root" view is there + // since the initial mount. + await waitFor(() => screen.getByTestId('root')); -test('waitFor timeout option takes precendence over `asyncWaitTimeout` config option', async () => { - configure({ asyncUtilTimeout: 2000 }); - await render(); + // This `waitFor` will also succeed on first check, because the promise that sets the + // `color` state to "red" resolves right after the previous `await waitFor` statement. + await waitFor(() => screen.getByText('red')); - await fireEvent.press(screen.getByText('Change freshness!')); - await expect(waitFor(() => screen.getByText('Fresh'), { timeout: 100 })).rejects.toThrow(); + // Check that the `onPress` callback is called with the already-updated value of `syncedColor`. + await fireEvent.press(screen.getByText('Trigger')); + expect(onPress).toHaveBeenCalledWith('red'); + }, + ); - // Async action ends after 300ms and we only waited 100ms, so we need to wait - // for the remaining async actions to finish - await waitFor(() => screen.getByText('Fresh')); -}); + test('continues waiting when expectation returns a promise that rejects', async () => { + let attemptCount = 0; + const maxAttempts = 3; + + await waitFor( + () => { + attemptCount++; + if (attemptCount < maxAttempts) { + return Promise.reject(new Error('Not ready yet')); + } + return Promise.resolve('Success'); + }, + { timeout: 1000 }, + ); -test('waits for element with custom interval', async () => { - const mockFn = jest.fn(() => { - throw Error('test'); + expect(attemptCount).toBe(maxAttempts); }); - - try { - await waitFor(() => mockFn(), { timeout: 400, interval: 200 }); - } catch { - // suppress - } - - expect(mockFn).toHaveBeenCalledTimes(2); }); -// this component is convoluted on purpose. It is not a good react pattern, but it is valid -// react code that will run differently between different react versions (17 and 18), so we need -// explicit tests for it -const Comp = ({ onPress }: { onPress: () => void }) => { - const [state, setState] = React.useState(false); - - React.useEffect(() => { - if (state) { - onPress(); +describe('timeout errors', () => { + test('throws timeout error when condition never becomes true', async () => { + function Component() { + return Hello; } - }, [state, onPress]); - - return ( - { - await Promise.resolve(); - setState(true); - }} - > - Trigger - - ); -}; -test('waits for async event with fireEvent', async () => { - const spy = jest.fn(); - await render(); + await render(); + await expect( + waitFor(() => screen.getByText('Never appears'), { timeout: 100 }), + ).rejects.toThrow('Unable to find an element with text: Never appears'); + }); + + test('throws timeout error with fake timers when condition never becomes true', async () => { + jest.useFakeTimers(); + await render(); - await fireEvent.press(screen.getByText('Trigger')); + const waitForPromise = waitFor(() => screen.getByText('Never appears'), { timeout: 100 }); - await waitFor(() => { - expect(spy).toHaveBeenCalled(); + await jest.advanceTimersByTimeAsync(100); + await expect(waitForPromise).rejects.toThrow( + 'Unable to find an element with text: Never appears', + ); }); -}); -test.each([false, true])( - 'waits for element until it stops throwing using fake timers (legacyFakeTimers = %s)', - async (legacyFakeTimers) => { - jest.useFakeTimers({ legacyFakeTimers }); - await render(); + test('throws generic timeout error when promise rejects with falsy value until timeout', async () => { + await expect( + waitFor(() => Promise.reject(null), { + timeout: 100, + }), + ).rejects.toThrow('Timed out in waitFor.'); + }); - await fireEvent.press(screen.getByText('Change freshness!')); - expect(screen.queryByText('Fresh')).toBeNull(); + test('uses custom error from onTimeout callback when timeout occurs', async () => { + const customErrorMessage = 'Custom timeout error: Element never appeared'; - jest.advanceTimersByTime(300); - const freshBananaText = await waitFor(() => screen.getByText('Fresh')); + await render(); + await expect( + waitFor(() => screen.getByText('Never appears'), { + timeout: 100, + onTimeout: () => new Error(customErrorMessage), + }), + ).rejects.toThrow(customErrorMessage); + }); - expect(freshBananaText.props.children).toBe('Fresh'); - }, -); + test('onTimeout callback returning falsy value keeps original error', async () => { + await render(); + // When onTimeout returns null/undefined/false, the original error should be kept (line 181 false branch) + await expect( + waitFor(() => screen.getByText('Never appears'), { + timeout: 100, + onTimeout: () => null as any, + }), + ).rejects.toThrow('Unable to find an element with text: Never appears'); + }); +}); -test.each([false, true])( - 'waits for assertion until timeout is met with fake timers (legacyFakeTimers = %s)', - async (legacyFakeTimers) => { - jest.useFakeTimers({ legacyFakeTimers }); +describe('error handling', () => { + test('throws TypeError when expectation is not a function', async () => { + await expect(waitFor(null as any)).rejects.toThrow( + 'Received `expectation` arg must be a function', + ); + }); - const mockFn = jest.fn(() => { - throw Error('test'); - }); + test('converts non-Error thrown value to Error when timeout occurs', async () => { + const errorMessage = 'Custom string error'; + let caughtError: unknown; try { - await waitFor(() => mockFn(), { timeout: 400, interval: 200 }); - } catch { - // suppress + await waitFor( + () => { + throw errorMessage; + }, + { timeout: 50 }, + ); + } catch (error) { + caughtError = error; } - expect(mockFn).toHaveBeenCalledTimes(3); - }, -); + expect(caughtError).toBeInstanceOf(Error); + expect((caughtError as Error).message).toBe(errorMessage); + }); -test.each([false, true])( - 'waits for assertion until timeout is met with fake timers (legacyFakeTimers = %s)', - async (legacyFakeTimers) => { - jest.useFakeTimers({ legacyFakeTimers }); + test('throws error when switching from real timers to fake timers during waitFor', async () => { + await render(); - const mockErrorFn = jest.fn(() => { - throw Error('test'); + const waitForPromise = waitFor(() => { + // This will never pass, but we'll switch timers before timeout + return screen.getByText('Never appears'); }); - const mockHandleFn = jest.fn((e) => e); + // Switch to fake timers while waitFor is running + jest.useFakeTimers(); - try { - await waitFor(() => mockErrorFn(), { - timeout: 400, - interval: 200, - onTimeout: mockHandleFn, - }); - } catch { - // suppress - } + await expect(waitForPromise).rejects.toThrow( + 'Changed from using real timers to fake timers while using waitFor', + ); + }); - expect(mockErrorFn).toHaveBeenCalledTimes(3); - expect(mockHandleFn).toHaveBeenCalledTimes(1); - }, -); + test('throws error when switching from fake timers to real timers during waitFor', async () => { + jest.useFakeTimers(); + await render(); -const blockThread = (timeToBlockThread: number, legacyFakeTimers: boolean) => { - jest.useRealTimers(); - const end = Date.now() + timeToBlockThread; - - while (Date.now() < end) { - // do nothing - } - - jest.useFakeTimers({ legacyFakeTimers }); -}; - -test.each([true, false])( - 'it should not depend on real time when using fake timers (legacyFakeTimers = %s)', - async (legacyFakeTimers) => { - jest.useFakeTimers({ legacyFakeTimers }); - const WAIT_FOR_INTERVAL = 20; - const WAIT_FOR_TIMEOUT = WAIT_FOR_INTERVAL * 5; - - const mockErrorFn = jest.fn(() => { - // Wait 2 times interval so that check time is longer than interval - blockThread(WAIT_FOR_INTERVAL * 2, legacyFakeTimers); - throw new Error('test'); + const waitForPromise = waitFor(() => { + // This will never pass, but we'll switch timers before timeout + return screen.getByText('Never appears'); }); - await expect( - async () => - await waitFor(mockErrorFn, { - timeout: WAIT_FOR_TIMEOUT, - interval: WAIT_FOR_INTERVAL, - }), - ).rejects.toThrow(); - - // Verify that the `waitFor` callback has been called the expected number of times - // (timeout / interval + 1), so it confirms that the real duration of callback did not - // cause the real clock timeout when running using fake timers. - expect(mockErrorFn).toHaveBeenCalledTimes(WAIT_FOR_TIMEOUT / WAIT_FOR_INTERVAL + 1); - }, -); - -test.each([false, true])( - 'awaiting something that succeeds before timeout works with fake timers (legacyFakeTimers = %s)', - async (legacyFakeTimers) => { - jest.useFakeTimers({ legacyFakeTimers }); - - let calls = 0; + // Switch to real timers while waitFor is running + jest.useRealTimers(); + + await expect(waitForPromise).rejects.toThrow( + 'Changed from using fake timers to real timers while using waitFor', + ); + }); +}); + +describe('configuration', () => { + test('waits for element with custom interval', async () => { const mockFn = jest.fn(() => { - calls += 1; - if (calls < 3) { - throw Error('test'); - } + throw Error('test'); }); try { @@ -253,97 +272,150 @@ test.each([false, true])( // suppress } - expect(mockFn).toHaveBeenCalledTimes(3); - }, -); - -test.each([ - [false, false], - [true, false], - [true, true], -])( - 'flushes scheduled updates before returning (fakeTimers = %s, legacyFakeTimers = %s)', - async (fakeTimers, legacyFakeTimers) => { - if (fakeTimers) { - jest.useFakeTimers({ legacyFakeTimers }); - } - - function Apple({ onPress }: { onPress: (color: string) => void }) { - const [color, setColor] = React.useState('green'); - const [syncedColor, setSyncedColor] = React.useState(color); + expect(mockFn).toHaveBeenCalledTimes(2); + }); - // On mount, set the color to "red" in a promise microtask - React.useEffect(() => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises, promise/catch-or-return, promise/prefer-await-to-then - Promise.resolve('red').then((c) => setColor(c)); - }, []); + test('waitFor defaults to asyncUtilTimeout config option', async () => { + function Component() { + const [active, setActive] = React.useState(false); - // Sync the `color` state to `syncedColor` state, but with a delay caused by the effect - React.useEffect(() => { - setSyncedColor(color); - }, [color]); + const handlePress = () => { + setTimeout(() => setActive(true), 300); + }; return ( - - {color} - onPress(syncedColor)}> - Trigger + + {active && Active} + + Activate ); } - const onPress = jest.fn(); - await render(); + configure({ asyncUtilTimeout: 100 }); + await render(); + await fireEvent.press(screen.getByText('Activate')); + expect(screen.queryByText('Active')).toBeNull(); + await expect(waitFor(() => screen.getByText('Active'))).rejects.toThrow(); + + // Async action ends after 300ms and we only waited 100ms, so we need to wait + // for the remaining async actions to finish + await waitFor(() => screen.getByText('Active'), { timeout: 1000 }); + }); + + test('waitFor timeout option takes precedence over asyncUtilTimeout config option', async () => { + function AsyncTextToggle() { + const [active, setActive] = React.useState(false); - // Required: this `waitFor` will succeed on first check, because the "root" view is there - // since the initial mount. - await waitFor(() => screen.getByTestId('root')); + const handlePress = () => { + setTimeout(() => setActive(true), 300); + }; - // This `waitFor` will also succeed on first check, because the promise that sets the - // `color` state to "red" resolves right after the previous `await waitFor` statement. - await waitFor(() => screen.getByText('red')); + return ( + + {active && Active} + + Activate + + + ); + } - // Check that the `onPress` callback is called with the already-updated value of `syncedColor`. - await fireEvent.press(screen.getByText('Trigger')); - expect(onPress).toHaveBeenCalledWith('red'); - }, -); + configure({ asyncUtilTimeout: 2000 }); + await render(); + await fireEvent.press(screen.getByText('Activate')); + expect(screen.queryByText('Active')).toBeNull(); + await expect(waitFor(() => screen.getByText('Active'), { timeout: 100 })).rejects.toThrow(); -test('waitFor throws if expectation is not a function', async () => { - await expect( - // @ts-expect-error intentionally passing non-function - waitFor('not a function'), - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Received \`expectation\` arg must be a function"`); + // Async action ends after 300ms and we only waited 100ms, so we need to wait + // for the remaining async actions to finish + await waitFor(() => screen.getByText('Active')); + }); }); -test.each([false, true])( - 'waitFor throws clear error when switching from fake timers to real timers (legacyFakeTimers = %s)', - async (legacyFakeTimers) => { - jest.useFakeTimers({ legacyFakeTimers }); +describe('fake timers behavior', () => { + test.each(['fake', 'fake-legacy'] as const)( + 'should not depend on real time when using %s timers', + async (timerType) => { + setupTimeType(timerType); + const WAIT_FOR_INTERVAL = 20; + const WAIT_FOR_TIMEOUT = WAIT_FOR_INTERVAL * 5; + + const blockThread = (timeToBlockThread: number, timerType: TimerType): void => { + jest.useRealTimers(); + const end = Date.now() + timeToBlockThread; + + while (Date.now() < end) { + // do nothing + } + + setupTimeType(timerType); + }; + + const mockErrorFn = jest.fn(() => { + // Wait 2 times interval so that check time is longer than interval + blockThread(WAIT_FOR_INTERVAL * 2, timerType); + throw new Error('test'); + }); - const waitForPromise = waitFor(() => { - // Switch to real timers during waitFor - this should trigger an error - jest.useRealTimers(); - throw new Error('test'); - }); + await expect( + async () => + await waitFor(mockErrorFn, { + timeout: WAIT_FOR_TIMEOUT, + interval: WAIT_FOR_INTERVAL, + }), + ).rejects.toThrow(); + + // Verify that the `waitFor` callback has been called the expected number of times + // (timeout / interval + 1), so it confirms that the real duration of callback did not + // cause the real clock timeout when running using fake timers. + expect(mockErrorFn).toHaveBeenCalledTimes(WAIT_FOR_TIMEOUT / WAIT_FOR_INTERVAL + 1); + }, + ); - await expect(waitForPromise).rejects.toThrow( - 'Changed from using fake timers to real timers while using waitFor', - ); - }, -); + test.each(['fake', 'fake-legacy'] as const)( + 'waits for assertion until timeout is met with %s timers and interval', + async (timerType) => { + setupTimeType(timerType); -test('waitFor throws clear error when switching from real timers to fake timers', async () => { - jest.useRealTimers(); + const mockFn = jest.fn(() => { + throw Error('test'); + }); - const waitForPromise = waitFor(() => { - // Switch to fake timers during waitFor - this should trigger an error - jest.useFakeTimers(); - throw new Error('test'); - }); + try { + await waitFor(() => mockFn(), { timeout: 400, interval: 200 }); + } catch { + // suppress + } + + expect(mockFn).toHaveBeenCalledTimes(3); + }, + ); + + test.each(['fake', 'fake-legacy'] as const)( + 'waits for assertion until timeout is met with %s timers, interval, and onTimeout', + async (timerType) => { + setupTimeType(timerType); + + const mockErrorFn = jest.fn(() => { + throw Error('test'); + }); + + const mockHandleFn = jest.fn((e) => e); + + try { + await waitFor(() => mockErrorFn(), { + timeout: 400, + interval: 200, + onTimeout: mockHandleFn, + }); + } catch { + // suppress + } - await expect(waitForPromise).rejects.toThrow( - 'Changed from using real timers to fake timers while using waitFor', + expect(mockErrorFn).toHaveBeenCalledTimes(3); + expect(mockHandleFn).toHaveBeenCalledTimes(1); + }, ); }); diff --git a/src/helpers/__tests__/errors.test.ts b/src/helpers/__tests__/errors.test.ts index 09fc7abe..e39a0a83 100644 --- a/src/helpers/__tests__/errors.test.ts +++ b/src/helpers/__tests__/errors.test.ts @@ -1,4 +1,4 @@ -import { copyStackTrace, ErrorWithStack } from '../errors'; +import { copyStackTraceIfNeeded, ErrorWithStack } from '../errors'; describe('ErrorWithStack', () => { test('should create an error with message', () => { @@ -29,13 +29,13 @@ describe('ErrorWithStack', () => { }); }); -describe('copyStackTrace', () => { +describe('copyStackTraceIfNeeded', () => { test('should copy stack trace from source to target when both are Error instances', () => { const target = new Error('Target error'); const source = new Error('Source error'); source.stack = 'Error: Source error\n at test.js:1:1'; - copyStackTrace(target, source); + copyStackTraceIfNeeded(target, source); expect(target.stack).toBe('Error: Target error\n at test.js:1:1'); const target2 = new Error('Target error'); @@ -43,7 +43,7 @@ describe('copyStackTrace', () => { source2.stack = 'Error: Source error\n at test.js:1:1\nError: Source error\n at test.js:2:2'; - copyStackTrace(target2, source2); + copyStackTraceIfNeeded(target2, source2); // Should replace only the first occurrence expect(target2.stack).toBe( 'Error: Target error\n at test.js:1:1\nError: Source error\n at test.js:2:2', @@ -55,20 +55,26 @@ describe('copyStackTrace', () => { const source = new Error('Source error'); source.stack = 'Error: Source error\n at test.js:1:1'; - copyStackTrace(targetNotError, source); + copyStackTraceIfNeeded(targetNotError, source); expect(targetNotError).toEqual({ message: 'Not an error' }); const target = new Error('Target error'); const originalStack = target.stack; - const sourceNotError = { message: 'Not an error' }; - copyStackTrace(target, sourceNotError as Error); + copyStackTraceIfNeeded(target, undefined); + expect(target.stack).toBe(originalStack); + + copyStackTraceIfNeeded(target, null as unknown as Error); + expect(target.stack).toBe(originalStack); + + const sourceNotError = { message: 'Not an error' }; + copyStackTraceIfNeeded(target, sourceNotError as Error); expect(target.stack).toBe(originalStack); const sourceNoStack = new Error('Source error'); delete sourceNoStack.stack; - copyStackTrace(target, sourceNoStack); + copyStackTraceIfNeeded(target, sourceNoStack); expect(target.stack).toBe(originalStack); }); }); diff --git a/src/helpers/errors.ts b/src/helpers/errors.ts index a6965619..20c56a74 100644 --- a/src/helpers/errors.ts +++ b/src/helpers/errors.ts @@ -8,8 +8,8 @@ export class ErrorWithStack extends Error { } } -export function copyStackTrace(target: unknown, stackTraceSource: Error) { - if (target instanceof Error && stackTraceSource.stack) { +export function copyStackTraceIfNeeded(target: unknown, stackTraceSource: Error | undefined) { + if (stackTraceSource != null && target instanceof Error && stackTraceSource.stack) { target.stack = stackTraceSource.stack.replace(stackTraceSource.message, target.message); } } diff --git a/src/test-utils/timers.ts b/src/test-utils/timers.ts new file mode 100644 index 00000000..78bd4653 --- /dev/null +++ b/src/test-utils/timers.ts @@ -0,0 +1,11 @@ +export type TimerType = 'real' | 'fake' | 'fake-legacy'; + +export function setupTimeType(type: TimerType): void { + if (type === 'fake-legacy') { + jest.useFakeTimers({ legacyFakeTimers: true }); + } else if (type === 'fake') { + jest.useFakeTimers({ legacyFakeTimers: false }); + } else { + jest.useRealTimers(); + } +} diff --git a/src/wait-for.ts b/src/wait-for.ts index 409623a8..29ad802b 100644 --- a/src/wait-for.ts +++ b/src/wait-for.ts @@ -2,7 +2,7 @@ import { act } from './act'; import { getConfig } from './config'; import { flushMicroTasks } from './flush-micro-tasks'; -import { copyStackTrace, ErrorWithStack } from './helpers/errors'; +import { copyStackTraceIfNeeded, ErrorWithStack } from './helpers/errors'; import { clearTimeout, getJestFakeTimersType, @@ -55,9 +55,7 @@ function waitForInternal( const error = new Error( `Changed from using fake timers to real timers while using waitFor. This is not allowed and will result in very strange behavior. Please ensure you're awaiting all async things your test is doing before changing to real timers. For more info, please go to https://github.com/testing-library/dom-testing-library/issues/830`, ); - if (stackTraceError) { - copyStackTrace(error, stackTraceError); - } + copyStackTraceIfNeeded(error, stackTraceError); reject(error); return; } @@ -121,9 +119,7 @@ function waitForInternal( const error = new Error( `Changed from using real timers to fake timers while using waitFor. This is not allowed and will result in very strange behavior. Please ensure you're awaiting all async things your test is doing before changing to fake timers. For more info, please go to https://github.com/testing-library/dom-testing-library/issues/830`, ); - if (stackTraceError) { - copyStackTrace(error, stackTraceError); - } + copyStackTraceIfNeeded(error, stackTraceError); return reject(error); } else { return checkExpectation(); @@ -131,7 +127,11 @@ function waitForInternal( } function checkExpectation() { - if (promiseStatus === 'pending') return; + /* istanbul ignore next */ + if (promiseStatus === 'pending') { + return; + } + try { const result = expectation(); @@ -171,14 +171,10 @@ function waitForInternal( error = new Error(String(lastError)); } - if (stackTraceError) { - copyStackTrace(error, stackTraceError); - } + copyStackTraceIfNeeded(error, stackTraceError); } else { error = new Error('Timed out in waitFor.'); - if (stackTraceError) { - copyStackTrace(error, stackTraceError); - } + copyStackTraceIfNeeded(error, stackTraceError); } if (typeof onTimeout === 'function') { const result = onTimeout(error);