From dfff0c55f376b758b6961e597d1e8877ffa50e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gilles=20Gre=CC=81goire?= Date: Mon, 15 Jun 2026 13:33:51 +0200 Subject: [PATCH 01/12] fix cleanup of waitFor timers when an error occurs during onTimeout printout --- src/__tests__/wait-for.test.tsx | 44 +++++++++++++++++++++++++++++++++ src/wait-for.ts | 30 +++++++++++++++++----- 2 files changed, 68 insertions(+), 6 deletions(-) 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 }); } }); } From 2329703db00a977a442c832449f78ee3e318abd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 22 Jun 2026 16:22:41 +0200 Subject: [PATCH 02/12] preserve original error context onTimeout --- src/__tests__/wait-for.test.tsx | 13 ++++++++----- src/wait-for.ts | 10 +++++++++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/__tests__/wait-for.test.tsx b/src/__tests__/wait-for.test.tsx index 5d3305994..aa62f588a 100644 --- a/src/__tests__/wait-for.test.tsx +++ b/src/__tests__/wait-for.test.tsx @@ -177,9 +177,7 @@ 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'); - + test('chains original timeout error when onTimeout callback throws', async () => { await expect( waitFor( () => { @@ -188,11 +186,16 @@ describe('timeout errors', () => { { timeout: 10, onTimeout: () => { - throw onTimeoutError; + throw new Error('onTimeout failed'); }, }, ), - ).rejects.toBe(onTimeoutError); + ).rejects.toMatchObject({ + message: '`onTimeout` threw while handling `waitFor` timeout: onTimeout failed', + cause: expect.objectContaining({ + message: 'Original timeout error', + }), + }); }); }); diff --git a/src/wait-for.ts b/src/wait-for.ts index bd5331fe8..4bf794090 100644 --- a/src/wait-for.ts +++ b/src/wait-for.ts @@ -197,7 +197,15 @@ function waitForInternal( errorForRejection = result; } } catch (onTimeoutError) { - errorForRejection = onTimeoutError; + const onTimeoutMessage = + onTimeoutError instanceof Error ? onTimeoutError.message : String(onTimeoutError); + + errorForRejection = new Error( + `\`onTimeout\` threw while handling \`waitFor\` timeout: ${onTimeoutMessage}`, + { + cause: error, + }, + ); } } onDone({ type: 'error', error: errorForRejection }); From 26d0ffdf83b10d37fb8a2d72886a758e0dc0ef09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 22 Jun 2026 16:26:00 +0200 Subject: [PATCH 03/12] reject pending waitFor promise on cleanup --- src/__tests__/wait-for.test.tsx | 14 ++++---------- src/wait-for.ts | 8 ++++++-- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/__tests__/wait-for.test.tsx b/src/__tests__/wait-for.test.tsx index aa62f588a..b225dc348 100644 --- a/src/__tests__/wait-for.test.tsx +++ b/src/__tests__/wait-for.test.tsx @@ -258,25 +258,19 @@ describe('error handling', () => { ); }); - test('cleanup stops real timer polling for pending waitFor calls', async () => { + test('cleanup rejects pending waitFor calls and stops real timer polling', 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(); + const waitForPromise = waitFor(expectation, { timeout: 300, interval: 20 }); await new Promise((resolve) => setTimeout(resolve, 60)); expect(expectation).toHaveBeenCalled(); await cleanup(); + await expect(waitForPromise).rejects.toThrow('waitFor was aborted by cleanup'); + const callsAfterCleanup = expectation.mock.calls.length; await new Promise((resolve) => setTimeout(resolve, 60)); diff --git a/src/wait-for.ts b/src/wait-for.ts index 4bf794090..7a9929cee 100644 --- a/src/wait-for.ts +++ b/src/wait-for.ts @@ -95,11 +95,15 @@ function waitForInternal( } else { overallTimeoutTimer = setTimeout(handleTimeout, timeout); intervalId = setInterval(checkRealTimersCallback, interval); - addToCleanupQueue(cleanupWaitFor); + addToCleanupQueue(() => cleanupWaitFor({ rejectOnAbort: true })); checkExpectation(); } - function cleanupWaitFor() { + function cleanupWaitFor({ rejectOnAbort = false } = {}) { + if (rejectOnAbort && !finished) { + reject(new Error('waitFor was aborted by cleanup')); + } + finished = true; if (overallTimeoutTimer) { From 92687cb40de5d4faf82020b243194344fa3cf887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 22 Jun 2026 16:34:08 +0200 Subject: [PATCH 04/12] remove cleanup after waitFor settles normally --- src/__tests__/wait-for.test.tsx | 7 +++++++ src/cleanup.ts | 8 ++++++++ src/wait-for.ts | 6 ++++-- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/__tests__/wait-for.test.tsx b/src/__tests__/wait-for.test.tsx index b225dc348..8cf9f44b8 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 { getCleanupQueueSize } from '../cleanup'; import { cleanup } from '../pure'; import type { TimerType } from '../test-utils/timers'; import { setupTimeType } from '../test-utils/timers'; @@ -72,6 +73,12 @@ describe('successful waiting', () => { }); }); + test('does not retain cleanup callback after waitFor settles', async () => { + await waitFor(() => true); + + expect(getCleanupQueueSize()).toBe(0); + }); + test.each(['real', 'fake', 'fake-legacy'] as const)( 'flushes scheduled updates before returning with %s timers', async (timerType) => { diff --git a/src/cleanup.ts b/src/cleanup.ts index 5b6f056c0..c49660f4a 100644 --- a/src/cleanup.ts +++ b/src/cleanup.ts @@ -17,3 +17,11 @@ export async function cleanup() { export function addToCleanupQueue(fn: CleanUpFunction) { cleanupQueue.add(fn); } + +export function removeFromCleanupQueue(fn: CleanUpFunction) { + cleanupQueue.delete(fn); +} + +export function getCleanupQueueSize() { + return cleanupQueue.size; +} diff --git a/src/wait-for.ts b/src/wait-for.ts index 7a9929cee..a9f4bd6a2 100644 --- a/src/wait-for.ts +++ b/src/wait-for.ts @@ -1,6 +1,6 @@ /* globals jest */ import { act } from './act'; -import { addToCleanupQueue } from './cleanup'; +import { addToCleanupQueue, removeFromCleanupQueue } from './cleanup'; import { getConfig } from './config'; import { flushMicroTasks } from './flush-micro-tasks'; import { copyStackTraceIfNeeded, ErrorWithStack } from './helpers/errors'; @@ -41,6 +41,7 @@ function waitForInternal( let promiseStatus = 'idle'; let overallTimeoutTimer: NodeJS.Timeout | null = null; + const cleanupQueueCallback = () => cleanupWaitFor({ rejectOnAbort: true }); const fakeTimersType = getJestFakeTimersType(); @@ -95,7 +96,7 @@ function waitForInternal( } else { overallTimeoutTimer = setTimeout(handleTimeout, timeout); intervalId = setInterval(checkRealTimersCallback, interval); - addToCleanupQueue(() => cleanupWaitFor({ rejectOnAbort: true })); + addToCleanupQueue(cleanupQueueCallback); checkExpectation(); } @@ -122,6 +123,7 @@ function waitForInternal( } cleanupWaitFor(); + removeFromCleanupQueue(cleanupQueueCallback); if (done.type === 'error') { reject(done.error); From 95146b191e06f24494034055606c01c5c38f22de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 22 Jun 2026 16:40:23 +0200 Subject: [PATCH 05/12] make cleanupWaitFor deliberately idempotent --- src/wait-for.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/wait-for.ts b/src/wait-for.ts index a9f4bd6a2..afd5d8d75 100644 --- a/src/wait-for.ts +++ b/src/wait-for.ts @@ -36,11 +36,12 @@ function waitForInternal( // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { - let lastError: unknown, intervalId: ReturnType; + let lastError: unknown; + let intervalId: ReturnType | null = null; let finished = false; let promiseStatus = 'idle'; - let overallTimeoutTimer: NodeJS.Timeout | null = null; + let overallTimeoutTimer: ReturnType | null = null; const cleanupQueueCallback = () => cleanupWaitFor({ rejectOnAbort: true }); const fakeTimersType = getJestFakeTimersType(); @@ -101,19 +102,24 @@ function waitForInternal( } function cleanupWaitFor({ rejectOnAbort = false } = {}) { - if (rejectOnAbort && !finished) { - reject(new Error('waitFor was aborted by cleanup')); + if (finished) { + return; } finished = true; + if (rejectOnAbort) { + reject(new Error('waitFor was aborted by cleanup')); + } + if (overallTimeoutTimer) { clearTimeout(overallTimeoutTimer); overallTimeoutTimer = null; } - if (!fakeTimersType) { + if (intervalId) { clearInterval(intervalId); + intervalId = null; } } From cd7c40409ea464aa6c3fc7488297a73fdce71739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 22 Jun 2026 16:50:51 +0200 Subject: [PATCH 06/12] simplify --- src/wait-for.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/wait-for.ts b/src/wait-for.ts index afd5d8d75..f236e01de 100644 --- a/src/wait-for.ts +++ b/src/wait-for.ts @@ -209,14 +209,9 @@ function waitForInternal( errorForRejection = result; } } catch (onTimeoutError) { - const onTimeoutMessage = - onTimeoutError instanceof Error ? onTimeoutError.message : String(onTimeoutError); - errorForRejection = new Error( - `\`onTimeout\` threw while handling \`waitFor\` timeout: ${onTimeoutMessage}`, - { - cause: error, - }, + `\`onTimeout\` threw while handling \`waitFor\` timeout: ${onTimeoutError instanceof Error ? onTimeoutError.message : String(onTimeoutError)}`, + { cause: error }, ); } } From f6685df5be0e1c9fc21edba73d0e82b1e4594c95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 22 Jun 2026 17:05:45 +0200 Subject: [PATCH 07/12] codecov --- src/__tests__/wait-for.test.tsx | 44 +++++++++++++++++++++++++++++++++ src/wait-for.ts | 1 + 2 files changed, 45 insertions(+) diff --git a/src/__tests__/wait-for.test.tsx b/src/__tests__/wait-for.test.tsx index 8cf9f44b8..177028bbf 100644 --- a/src/__tests__/wait-for.test.tsx +++ b/src/__tests__/wait-for.test.tsx @@ -204,6 +204,29 @@ describe('timeout errors', () => { }), }); }); + + test('chains original timeout error when onTimeout throws a non-Error value', async () => { + await expect( + waitFor( + () => { + throw new Error('Original timeout error'); + }, + { + timeout: 10, + onTimeout: () => { + // eslint-disable-next-line @typescript-eslint/only-throw-error + throw 'onTimeout failed with string'; + }, + }, + ), + ).rejects.toMatchObject({ + message: + '`onTimeout` threw while handling `waitFor` timeout: onTimeout failed with string', + cause: expect.objectContaining({ + message: 'Original timeout error', + }), + }); + }); }); describe('error handling', () => { @@ -283,6 +306,27 @@ describe('error handling', () => { await new Promise((resolve) => setTimeout(resolve, 60)); expect(expectation).toHaveBeenCalledTimes(callsAfterCleanup); }); + + test('async expectation resolving after cleanup abort has no effect', async () => { + let resolveExpectation!: (value: string) => void; + const expectationPromise = new Promise((resolve) => { + resolveExpectation = resolve; + }); + + const waitForPromise = waitFor(() => expectationPromise, { timeout: 300, interval: 20 }); + + // Wait for at least one interval tick so the expectation is called and its promise is pending + await new Promise((resolve) => setTimeout(resolve, 30)); + + // Start cleanup while the expectation promise is still pending + const cleanupPromise = cleanup(); + + // Resolve the expectation promise while cleanup is in progress + resolveExpectation('success'); + + await cleanupPromise; + await expect(waitForPromise).rejects.toThrow('waitFor was aborted by cleanup'); + }); }); describe('configuration', () => { diff --git a/src/wait-for.ts b/src/wait-for.ts index f236e01de..b826a262f 100644 --- a/src/wait-for.ts +++ b/src/wait-for.ts @@ -102,6 +102,7 @@ function waitForInternal( } function cleanupWaitFor({ rejectOnAbort = false } = {}) { + /* istanbul ignore next */ if (finished) { return; } From 71c5faf81f116449e467c6ee0a0d9c22a9faa773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 22 Jun 2026 17:09:38 +0200 Subject: [PATCH 08/12] naming --- src/wait-for.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wait-for.ts b/src/wait-for.ts index b826a262f..d4232ced4 100644 --- a/src/wait-for.ts +++ b/src/wait-for.ts @@ -42,7 +42,7 @@ function waitForInternal( let promiseStatus = 'idle'; let overallTimeoutTimer: ReturnType | null = null; - const cleanupQueueCallback = () => cleanupWaitFor({ rejectOnAbort: true }); + const cleanupQueueCallback = () => finalizeWaitFor({ rejectOnAbort: true }); const fakeTimersType = getJestFakeTimersType(); @@ -101,7 +101,7 @@ function waitForInternal( checkExpectation(); } - function cleanupWaitFor({ rejectOnAbort = false } = {}) { + function finalizeWaitFor({ rejectOnAbort = false } = {}) { /* istanbul ignore next */ if (finished) { return; @@ -129,7 +129,7 @@ function waitForInternal( return; } - cleanupWaitFor(); + finalizeWaitFor(); removeFromCleanupQueue(cleanupQueueCallback); if (done.type === 'error') { From 1ba06aa72256ee6afe21fb89ca25bfb7fa4ac214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 22 Jun 2026 17:13:56 +0200 Subject: [PATCH 09/12] fix format --- src/__tests__/wait-for.test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/__tests__/wait-for.test.tsx b/src/__tests__/wait-for.test.tsx index 177028bbf..bae304531 100644 --- a/src/__tests__/wait-for.test.tsx +++ b/src/__tests__/wait-for.test.tsx @@ -220,8 +220,7 @@ describe('timeout errors', () => { }, ), ).rejects.toMatchObject({ - message: - '`onTimeout` threw while handling `waitFor` timeout: onTimeout failed with string', + message: '`onTimeout` threw while handling `waitFor` timeout: onTimeout failed with string', cause: expect.objectContaining({ message: 'Original timeout error', }), From bb649b87ea3e0ec0d799f9e9dda35b8f45059aa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 22 Jun 2026 17:40:07 +0200 Subject: [PATCH 10/12] expose actual onTimeout error --- src/__tests__/wait-for.test.tsx | 18 ++++-------------- src/wait-for.ts | 5 +---- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/__tests__/wait-for.test.tsx b/src/__tests__/wait-for.test.tsx index bae304531..f4f39f2c6 100644 --- a/src/__tests__/wait-for.test.tsx +++ b/src/__tests__/wait-for.test.tsx @@ -184,7 +184,7 @@ describe('timeout errors', () => { ).rejects.toThrow('Unable to find an element with text: Never appears'); }); - test('chains original timeout error when onTimeout callback throws', async () => { + test('rejects with onTimeout error when onTimeout callback throws', async () => { await expect( waitFor( () => { @@ -197,15 +197,10 @@ describe('timeout errors', () => { }, }, ), - ).rejects.toMatchObject({ - message: '`onTimeout` threw while handling `waitFor` timeout: onTimeout failed', - cause: expect.objectContaining({ - message: 'Original timeout error', - }), - }); + ).rejects.toThrow('onTimeout failed'); }); - test('chains original timeout error when onTimeout throws a non-Error value', async () => { + test('rejects with onTimeout value when onTimeout throws a non-Error value', async () => { await expect( waitFor( () => { @@ -219,12 +214,7 @@ describe('timeout errors', () => { }, }, ), - ).rejects.toMatchObject({ - message: '`onTimeout` threw while handling `waitFor` timeout: onTimeout failed with string', - cause: expect.objectContaining({ - message: 'Original timeout error', - }), - }); + ).rejects.toBe('onTimeout failed with string'); }); }); diff --git a/src/wait-for.ts b/src/wait-for.ts index d4232ced4..6a2921889 100644 --- a/src/wait-for.ts +++ b/src/wait-for.ts @@ -210,10 +210,7 @@ function waitForInternal( errorForRejection = result; } } catch (onTimeoutError) { - errorForRejection = new Error( - `\`onTimeout\` threw while handling \`waitFor\` timeout: ${onTimeoutError instanceof Error ? onTimeoutError.message : String(onTimeoutError)}`, - { cause: error }, - ); + errorForRejection = onTimeoutError; } } onDone({ type: 'error', error: errorForRejection }); From 6f9490bbe6ea0ff9e7b26a68f0f2dfad7b601c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 22 Jun 2026 17:52:00 +0200 Subject: [PATCH 11/12] symetric cleanup --- src/__tests__/wait-for.test.tsx | 22 +++++++++++++++++++++- src/wait-for.ts | 3 ++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/__tests__/wait-for.test.tsx b/src/__tests__/wait-for.test.tsx index f4f39f2c6..79f81b0db 100644 --- a/src/__tests__/wait-for.test.tsx +++ b/src/__tests__/wait-for.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { Pressable, Text, TouchableOpacity, View } from 'react-native'; import { configure, fireEvent, render, screen, waitFor } from '..'; -import { getCleanupQueueSize } from '../cleanup'; +import { addToCleanupQueue, getCleanupQueueSize } from '../cleanup'; import { cleanup } from '../pure'; import type { TimerType } from '../test-utils/timers'; import { setupTimeType } from '../test-utils/timers'; @@ -296,6 +296,26 @@ describe('error handling', () => { expect(expectation).toHaveBeenCalledTimes(callsAfterCleanup); }); + test('cleanup abort removes pending waitFor callback before cleanup queue is cleared', async () => { + const expectation = jest.fn(() => { + throw new Error('Not ready yet'); + }); + + const waitForPromise = waitFor(expectation, { timeout: 300, interval: 20 }); + + await new Promise((resolve) => setTimeout(resolve, 30)); + + const queueSizes: number[] = []; + addToCleanupQueue(() => { + queueSizes.push(getCleanupQueueSize()); + }); + + await cleanup(); + await expect(waitForPromise).rejects.toThrow('waitFor was aborted by cleanup'); + + expect(queueSizes).toEqual([1]); + }); + test('async expectation resolving after cleanup abort has no effect', async () => { let resolveExpectation!: (value: string) => void; const expectationPromise = new Promise((resolve) => { diff --git a/src/wait-for.ts b/src/wait-for.ts index 6a2921889..15b3e93bf 100644 --- a/src/wait-for.ts +++ b/src/wait-for.ts @@ -109,6 +109,8 @@ function waitForInternal( finished = true; + removeFromCleanupQueue(cleanupQueueCallback); + if (rejectOnAbort) { reject(new Error('waitFor was aborted by cleanup')); } @@ -130,7 +132,6 @@ function waitForInternal( } finalizeWaitFor(); - removeFromCleanupQueue(cleanupQueueCallback); if (done.type === 'error') { reject(done.error); From 64c586a3dc8238db4c040e956320648cd470b5be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 22 Jun 2026 19:09:49 +0200 Subject: [PATCH 12/12] simplify --- src/__tests__/wait-for.test.tsx | 27 --------------------------- src/cleanup.ts | 4 ---- 2 files changed, 31 deletions(-) diff --git a/src/__tests__/wait-for.test.tsx b/src/__tests__/wait-for.test.tsx index 79f81b0db..08c428b3e 100644 --- a/src/__tests__/wait-for.test.tsx +++ b/src/__tests__/wait-for.test.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { Pressable, Text, TouchableOpacity, View } from 'react-native'; import { configure, fireEvent, render, screen, waitFor } from '..'; -import { addToCleanupQueue, getCleanupQueueSize } from '../cleanup'; import { cleanup } from '../pure'; import type { TimerType } from '../test-utils/timers'; import { setupTimeType } from '../test-utils/timers'; @@ -73,12 +72,6 @@ describe('successful waiting', () => { }); }); - test('does not retain cleanup callback after waitFor settles', async () => { - await waitFor(() => true); - - expect(getCleanupQueueSize()).toBe(0); - }); - test.each(['real', 'fake', 'fake-legacy'] as const)( 'flushes scheduled updates before returning with %s timers', async (timerType) => { @@ -296,26 +289,6 @@ describe('error handling', () => { expect(expectation).toHaveBeenCalledTimes(callsAfterCleanup); }); - test('cleanup abort removes pending waitFor callback before cleanup queue is cleared', async () => { - const expectation = jest.fn(() => { - throw new Error('Not ready yet'); - }); - - const waitForPromise = waitFor(expectation, { timeout: 300, interval: 20 }); - - await new Promise((resolve) => setTimeout(resolve, 30)); - - const queueSizes: number[] = []; - addToCleanupQueue(() => { - queueSizes.push(getCleanupQueueSize()); - }); - - await cleanup(); - await expect(waitForPromise).rejects.toThrow('waitFor was aborted by cleanup'); - - expect(queueSizes).toEqual([1]); - }); - test('async expectation resolving after cleanup abort has no effect', async () => { let resolveExpectation!: (value: string) => void; const expectationPromise = new Promise((resolve) => { diff --git a/src/cleanup.ts b/src/cleanup.ts index c49660f4a..7b012c44a 100644 --- a/src/cleanup.ts +++ b/src/cleanup.ts @@ -21,7 +21,3 @@ export function addToCleanupQueue(fn: CleanUpFunction) { export function removeFromCleanupQueue(fn: CleanUpFunction) { cleanupQueue.delete(fn); } - -export function getCleanupQueueSize() { - return cleanupQueue.size; -}