Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/__tests__/wait-for.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
30 changes: 24 additions & 6 deletions src/wait-for.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -94,18 +95,29 @@ function waitForInternal<T>(
} 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);
Expand All @@ -120,7 +132,7 @@ function waitForInternal<T>(
`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();
}
Expand Down Expand Up @@ -176,13 +188,19 @@ function waitForInternal<T>(
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 });
}
});
}
Expand Down