diff --git a/src/__tests__/waitFor.test.tsx b/src/__tests__/waitFor.test.tsx index 15daf8ac7..6e73fb3a5 100644 --- a/src/__tests__/waitFor.test.tsx +++ b/src/__tests__/waitFor.test.tsx @@ -263,3 +263,56 @@ test.each([false, true])( 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); + + // On mount, set the color to "red" in a promise microtask + React.useEffect(() => { + // eslint-disable-next-line promise/prefer-await-to-then, promise/catch-or-return + 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 + + + ); + } + + const onPress = jest.fn(); + const view = render(); + + // Required: this `waitFor` will succeed on first check, because the "root" view is there + // since the initial mount. + await waitFor(() => view.getByTestId('root')); + + // 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(() => view.getByText('red')); + + // Check that the `onPress` callback is called with the already-updated value of `syncedColor`. + fireEvent.press(view.getByText('Trigger')); + expect(onPress).toHaveBeenCalledWith('red'); + } +); diff --git a/src/flushMicroTasks.ts b/src/flushMicroTasks.ts index 011f01d6e..7b964a9d6 100644 --- a/src/flushMicroTasks.ts +++ b/src/flushMicroTasks.ts @@ -2,7 +2,7 @@ import { setImmediate } from './helpers/timers'; type Thenable = { then: (callback: () => T) => unknown }; -export function flushMicroTasks(): Thenable { +export function flushMicroTasks(): Thenable { return { // using "thenable" instead of a Promise, because otherwise it breaks when // using "modern" fake timers diff --git a/src/waitFor.ts b/src/waitFor.ts index 0077ee45a..58feb224a 100644 --- a/src/waitFor.ts +++ b/src/waitFor.ts @@ -1,6 +1,7 @@ /* globals jest */ import act, { setReactActEnvironment, getIsReactActEnvironment } from './act'; import { getConfig } from './config'; +import { flushMicroTasks } from './flushMicroTasks'; import { ErrorWithStack, copyStackTrace } from './helpers/errors'; import { setTimeout, @@ -196,7 +197,10 @@ export default async function waitFor( setReactActEnvironment(false); try { - return await waitForInternal(expectation, optionsWithStackTrace); + const result = await waitForInternal(expectation, optionsWithStackTrace); + // Flush the microtask queue before restoring the `act` environment + await flushMicroTasks(); + return result; } finally { setReactActEnvironment(previousActEnvironment); }