Skip to content

Commit f79d41d

Browse files
Flush all pending microtasks and updates before returning from waitFor (#1366)
* Flush all pending microtasks and updates before returning from waitFor * Disable a linter error, its advice is not good * refactor: use `flushMicroTasks` util * refactor: add legacy fake timer test * refactor: tweaks --------- Co-authored-by: Maciej Jastrzębski <[email protected]>
1 parent 0783af9 commit f79d41d

File tree

3 files changed

+59
-2
lines changed

3 files changed

+59
-2
lines changed

src/__tests__/waitFor.test.tsx

+53
Original file line numberDiff line numberDiff line change
@@ -263,3 +263,56 @@ test.each([false, true])(
263263
expect(mockFn).toHaveBeenCalledTimes(3);
264264
}
265265
);
266+
267+
test.each([
268+
[false, false],
269+
[true, false],
270+
[true, true],
271+
])(
272+
'flushes scheduled updates before returning (fakeTimers = %s, legacyFakeTimers = %s)',
273+
async (fakeTimers, legacyFakeTimers) => {
274+
if (fakeTimers) {
275+
jest.useFakeTimers({ legacyFakeTimers });
276+
}
277+
278+
function Apple({ onPress }: { onPress: (color: string) => void }) {
279+
const [color, setColor] = React.useState('green');
280+
const [syncedColor, setSyncedColor] = React.useState(color);
281+
282+
// On mount, set the color to "red" in a promise microtask
283+
React.useEffect(() => {
284+
// eslint-disable-next-line promise/prefer-await-to-then, promise/catch-or-return
285+
Promise.resolve('red').then((c) => setColor(c));
286+
}, []);
287+
288+
// Sync the `color` state to `syncedColor` state, but with a delay caused by the effect
289+
React.useEffect(() => {
290+
setSyncedColor(color);
291+
}, [color]);
292+
293+
return (
294+
<View testID="root">
295+
<Text>{color}</Text>
296+
<Pressable onPress={() => onPress(syncedColor)}>
297+
<Text>Trigger</Text>
298+
</Pressable>
299+
</View>
300+
);
301+
}
302+
303+
const onPress = jest.fn();
304+
const view = render(<Apple onPress={onPress} />);
305+
306+
// Required: this `waitFor` will succeed on first check, because the "root" view is there
307+
// since the initial mount.
308+
await waitFor(() => view.getByTestId('root'));
309+
310+
// This `waitFor` will also succeed on first check, because the promise that sets the
311+
// `color` state to "red" resolves right after the previous `await waitFor` statement.
312+
await waitFor(() => view.getByText('red'));
313+
314+
// Check that the `onPress` callback is called with the already-updated value of `syncedColor`.
315+
fireEvent.press(view.getByText('Trigger'));
316+
expect(onPress).toHaveBeenCalledWith('red');
317+
}
318+
);

src/flushMicroTasks.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { setImmediate } from './helpers/timers';
22

33
type Thenable<T> = { then: (callback: () => T) => unknown };
44

5-
export function flushMicroTasks<T>(): Thenable<T> {
5+
export function flushMicroTasks(): Thenable<void> {
66
return {
77
// using "thenable" instead of a Promise, because otherwise it breaks when
88
// using "modern" fake timers

src/waitFor.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* globals jest */
22
import act, { setReactActEnvironment, getIsReactActEnvironment } from './act';
33
import { getConfig } from './config';
4+
import { flushMicroTasks } from './flushMicroTasks';
45
import { ErrorWithStack, copyStackTrace } from './helpers/errors';
56
import {
67
setTimeout,
@@ -196,7 +197,10 @@ export default async function waitFor<T>(
196197
setReactActEnvironment(false);
197198

198199
try {
199-
return await waitForInternal(expectation, optionsWithStackTrace);
200+
const result = await waitForInternal(expectation, optionsWithStackTrace);
201+
// Flush the microtask queue before restoring the `act` environment
202+
await flushMicroTasks();
203+
return result;
200204
} finally {
201205
setReactActEnvironment(previousActEnvironment);
202206
}

0 commit comments

Comments
 (0)