From 7cf22cea3a4f96e289399327c169255649e43631 Mon Sep 17 00:00:00 2001 From: eps1lon Date: Mon, 22 May 2023 17:36:14 +0200 Subject: [PATCH 1/4] fix: Remove redundant microtask flush --- src/pure.js | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/src/pure.js b/src/pure.js index 845aede1..94b3b2bd 100644 --- a/src/pure.js +++ b/src/pure.js @@ -12,20 +12,6 @@ import act, { } from './act-compat' import {fireEvent} from './fire-event' -function jestFakeTimersAreEnabled() { - /* istanbul ignore else */ - if (typeof jest !== 'undefined' && jest !== null) { - return ( - // legacy timers - setTimeout._isMockFunction === true || // modern timers - // eslint-disable-next-line prefer-object-has-own -- No Object.hasOwn in all target environments we support. - Object.prototype.hasOwnProperty.call(setTimeout, 'clock') - ) - } // istanbul ignore next - - return false -} - configureDTL({ unstable_advanceTimersWrapper: cb => { return act(cb) @@ -37,21 +23,7 @@ configureDTL({ const previousActEnvironment = getIsReactActEnvironment() setReactActEnvironment(false) try { - const result = await cb() - // Drain microtask queue. - // Otherwise we'll restore the previous act() environment, before we resolve the `waitFor` call. - // The caller would have no chance to wrap the in-flight Promises in `act()` - await new Promise(resolve => { - setTimeout(() => { - resolve() - }, 0) - - if (jestFakeTimersAreEnabled()) { - jest.advanceTimersByTime(0) - } - }) - - return result + return await cb() } finally { setReactActEnvironment(previousActEnvironment) } From f2d3dd0a93f63ccaa212dfa10fcdf9f49d3771f2 Mon Sep 17 00:00:00 2001 From: eps1lon Date: Mon, 22 May 2023 17:57:28 +0200 Subject: [PATCH 2/4] Add failing tests --- src/__tests__/end-to-end.js | 76 +++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/src/__tests__/end-to-end.js b/src/__tests__/end-to-end.js index 005591d3..b20684bd 100644 --- a/src/__tests__/end-to-end.js +++ b/src/__tests__/end-to-end.js @@ -2,8 +2,8 @@ import * as React from 'react' import {render, waitForElementToBeRemoved, screen, waitFor} from '../' describe.each([ - ['real timers', () => jest.useRealTimers()], - ['fake legacy timers', () => jest.useFakeTimers('legacy')], + // ['real timers', () => jest.useRealTimers()], + // ['fake legacy timers', () => jest.useFakeTimers('legacy')], ['fake modern timers', () => jest.useFakeTimers('modern')], ])( 'it waits for the data to be loaded in a macrotask using %s', @@ -80,7 +80,7 @@ describe.each([ ['fake legacy timers', () => jest.useFakeTimers('legacy')], ['fake modern timers', () => jest.useFakeTimers('modern')], ])( - 'it waits for the data to be loaded in a microtask using %s', + 'it waits for the data to be loaded in many microtask using %s', (label, useTimers) => { beforeEach(() => { useTimers() @@ -162,3 +162,73 @@ describe.each([ }) }, ) + +describe.each([ + ['real timers', () => jest.useRealTimers()], + ['fake legacy timers', () => jest.useFakeTimers('legacy')], + ['fake modern timers', () => jest.useFakeTimers('modern')], +])( + 'it waits for the data to be loaded in a microtask using %s', + (label, useTimers) => { + beforeEach(() => { + useTimers() + }) + + afterEach(() => { + jest.useRealTimers() + }) + + const fetchAMessageInAMicrotask = () => + Promise.resolve({ + status: 200, + json: () => Promise.resolve({title: 'Hello World'}), + }) + + function ComponentWithMicrotaskLoader() { + const [fetchState, setFetchState] = React.useState({fetching: true}) + + React.useEffect(() => { + if (fetchState.fetching) { + fetchAMessageInAMicrotask().then(res => { + return res.json().then(data => { + setFetchState({todo: data.title, fetching: false}) + }) + }) + } + }, [fetchState]) + + if (fetchState.fetching) { + return

Loading..

+ } + + return ( +
Loaded this message: {fetchState.todo}
+ ) + } + + test('waitForElementToBeRemoved', async () => { + render() + const loading = () => screen.getByText('Loading..') + await waitForElementToBeRemoved(loading) + expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) + }) + + test('waitFor', async () => { + render() + await waitFor(() => { + screen.getByText('Loading..') + }) + await waitFor(() => { + screen.getByText(/Loaded this message:/) + }) + expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) + }) + + test('findBy', async () => { + render() + await expect(screen.findByTestId('message')).resolves.toHaveTextContent( + /Hello World/, + ) + }) + }, +) From 3a060d116262f31c9c4adb9f05003b35697c846b Mon Sep 17 00:00:00 2001 From: eps1lon Date: Mon, 22 May 2023 18:00:31 +0200 Subject: [PATCH 3/4] Revert "fix: Remove redundant microtask flush" This reverts commit 7cf22cea3a4f96e289399327c169255649e43631. New tests showcase why we need this --- src/pure.js | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/pure.js b/src/pure.js index 94b3b2bd..845aede1 100644 --- a/src/pure.js +++ b/src/pure.js @@ -12,6 +12,20 @@ import act, { } from './act-compat' import {fireEvent} from './fire-event' +function jestFakeTimersAreEnabled() { + /* istanbul ignore else */ + if (typeof jest !== 'undefined' && jest !== null) { + return ( + // legacy timers + setTimeout._isMockFunction === true || // modern timers + // eslint-disable-next-line prefer-object-has-own -- No Object.hasOwn in all target environments we support. + Object.prototype.hasOwnProperty.call(setTimeout, 'clock') + ) + } // istanbul ignore next + + return false +} + configureDTL({ unstable_advanceTimersWrapper: cb => { return act(cb) @@ -23,7 +37,21 @@ configureDTL({ const previousActEnvironment = getIsReactActEnvironment() setReactActEnvironment(false) try { - return await cb() + const result = await cb() + // Drain microtask queue. + // Otherwise we'll restore the previous act() environment, before we resolve the `waitFor` call. + // The caller would have no chance to wrap the in-flight Promises in `act()` + await new Promise(resolve => { + setTimeout(() => { + resolve() + }, 0) + + if (jestFakeTimersAreEnabled()) { + jest.advanceTimersByTime(0) + } + }) + + return result } finally { setReactActEnvironment(previousActEnvironment) } From 7d25eececaf01b3b0d3d5e94fc23a91ecf06312b Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Sun, 28 May 2023 10:54:41 +0200 Subject: [PATCH 4/4] Update src/__tests__/end-to-end.js --- src/__tests__/end-to-end.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/end-to-end.js b/src/__tests__/end-to-end.js index b20684bd..f93c23be 100644 --- a/src/__tests__/end-to-end.js +++ b/src/__tests__/end-to-end.js @@ -2,8 +2,8 @@ import * as React from 'react' import {render, waitForElementToBeRemoved, screen, waitFor} from '../' describe.each([ - // ['real timers', () => jest.useRealTimers()], - // ['fake legacy timers', () => jest.useFakeTimers('legacy')], + ['real timers', () => jest.useRealTimers()], + ['fake legacy timers', () => jest.useFakeTimers('legacy')], ['fake modern timers', () => jest.useFakeTimers('modern')], ])( 'it waits for the data to be loaded in a macrotask using %s',