diff --git a/src/__tests__/wait-for.test.tsx b/src/__tests__/wait-for.test.tsx index d3e9c70bb..5d3305994 100644 --- a/src/__tests__/wait-for.test.tsx +++ b/src/__tests__/wait-for.test.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { Pressable, Text, TouchableOpacity, View } from 'react-native'; import { configure, fireEvent, render, screen, waitFor } from '..'; +import { cleanup } from '../pure'; import type { TimerType } from '../test-utils/timers'; import { setupTimeType } from '../test-utils/timers'; @@ -175,6 +176,24 @@ describe('timeout errors', () => { }), ).rejects.toThrow('Unable to find an element with text: Never appears'); }); + + test('rejects with error thrown by onTimeout callback', async () => { + const onTimeoutError = new Error('onTimeout failed'); + + await expect( + waitFor( + () => { + throw new Error('Original timeout error'); + }, + { + timeout: 10, + onTimeout: () => { + throw onTimeoutError; + }, + }, + ), + ).rejects.toBe(onTimeoutError); + }); }); describe('error handling', () => { @@ -235,6 +254,31 @@ describe('error handling', () => { 'Changed from using fake timers to real timers while using waitFor', ); }); + + test('cleanup stops real timer polling for pending waitFor calls', async () => { + const expectation = jest.fn(() => { + throw new Error('Not ready yet'); + }); + + async function ignoreWaitForRejection() { + try { + await waitFor(expectation, { timeout: 300, interval: 20 }); + } catch { + // This waitFor call is intentionally abandoned in the test. + } + } + + void ignoreWaitForRejection(); + + await new Promise((resolve) => setTimeout(resolve, 60)); + expect(expectation).toHaveBeenCalled(); + + await cleanup(); + const callsAfterCleanup = expectation.mock.calls.length; + + await new Promise((resolve) => setTimeout(resolve, 60)); + expect(expectation).toHaveBeenCalledTimes(callsAfterCleanup); + }); }); describe('configuration', () => { diff --git a/src/wait-for.ts b/src/wait-for.ts index 29ad802b4..bd5331fe8 100644 --- a/src/wait-for.ts +++ b/src/wait-for.ts @@ -1,5 +1,6 @@ /* globals jest */ import { act } from './act'; +import { addToCleanupQueue } from './cleanup'; import { getConfig } from './config'; import { flushMicroTasks } from './flush-micro-tasks'; import { copyStackTraceIfNeeded, ErrorWithStack } from './helpers/errors'; @@ -94,18 +95,29 @@ function waitForInternal( } else { overallTimeoutTimer = setTimeout(handleTimeout, timeout); intervalId = setInterval(checkRealTimersCallback, interval); + addToCleanupQueue(cleanupWaitFor); checkExpectation(); } - function onDone(done: { type: 'result'; result: T } | { type: 'error'; error: unknown }) { + function cleanupWaitFor() { finished = true; + if (overallTimeoutTimer) { clearTimeout(overallTimeoutTimer); + overallTimeoutTimer = null; } if (!fakeTimersType) { clearInterval(intervalId); } + } + + function onDone(done: { type: 'result'; result: T } | { type: 'error'; error: unknown }) { + if (finished) { + return; + } + + cleanupWaitFor(); if (done.type === 'error') { reject(done.error); @@ -120,7 +132,7 @@ function waitForInternal( `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`, ); copyStackTraceIfNeeded(error, stackTraceError); - return reject(error); + return onDone({ type: 'error', error }); } else { return checkExpectation(); } @@ -176,13 +188,19 @@ function waitForInternal( error = new Error('Timed out in waitFor.'); copyStackTraceIfNeeded(error, stackTraceError); } + + let errorForRejection: unknown = error; if (typeof onTimeout === 'function') { - const result = onTimeout(error); - if (result) { - error = result; + try { + const result = onTimeout(error); + if (result) { + errorForRejection = result; + } + } catch (onTimeoutError) { + errorForRejection = onTimeoutError; } } - onDone({ type: 'error', error }); + onDone({ type: 'error', error: errorForRejection }); } }); }