diff --git a/src/__tests__/cleanup.js b/src/__tests__/cleanup.js index c0f1676d..4b67814a 100644 --- a/src/__tests__/cleanup.js +++ b/src/__tests__/cleanup.js @@ -1,7 +1,7 @@ import React from 'react' import {render, cleanup} from '../' -test('cleans up the document', async () => { +test('cleans up the document', () => { const spy = jest.fn() const divId = 'my-div' @@ -17,17 +17,17 @@ test('cleans up the document', async () => { } render() - await cleanup() + cleanup() expect(document.body).toBeEmptyDOMElement() expect(spy).toHaveBeenCalledTimes(1) }) -test('cleanup does not error when an element is not a child', async () => { +test('cleanup does not error when an element is not a child', () => { render(
, {container: document.createElement('div')}) - await cleanup() + cleanup() }) -test('cleanup runs effect cleanup functions', async () => { +test('cleanup runs effect cleanup functions', () => { const spy = jest.fn() const Test = () => { @@ -37,6 +37,87 @@ test('cleanup runs effect cleanup functions', async () => { } render() - await cleanup() + cleanup() expect(spy).toHaveBeenCalledTimes(1) }) + +describe('fake timers and missing act warnings', () => { + beforeEach(() => { + jest.resetAllMocks() + jest.spyOn(console, 'error').mockImplementation(() => { + // assert messages explicitly + }) + jest.useFakeTimers() + }) + + afterEach(() => { + jest.useRealTimers() + }) + + test('cleanup does not flush immediates', () => { + const microTaskSpy = jest.fn() + function Test() { + const counter = 1 + const [, setDeferredCounter] = React.useState(null) + React.useEffect(() => { + let cancelled = false + setImmediate(() => { + microTaskSpy() + if (!cancelled) { + setDeferredCounter(counter) + } + }) + + return () => { + cancelled = true + } + }, [counter]) + + return null + } + render() + + cleanup() + + expect(microTaskSpy).toHaveBeenCalledTimes(0) + // console.error is mocked + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalledTimes(0) + }) + + test('cleanup does not swallow missing act warnings', () => { + const deferredStateUpdateSpy = jest.fn() + function Test() { + const counter = 1 + const [, setDeferredCounter] = React.useState(null) + React.useEffect(() => { + let cancelled = false + setImmediate(() => { + deferredStateUpdateSpy() + if (!cancelled) { + setDeferredCounter(counter) + } + }) + + return () => { + cancelled = true + } + }, [counter]) + + return null + } + render() + + jest.runAllImmediates() + cleanup() + + expect(deferredStateUpdateSpy).toHaveBeenCalledTimes(1) + // console.error is mocked + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalledTimes(1) + // eslint-disable-next-line no-console + expect(console.error.mock.calls[0][0]).toMatch( + 'a test was not wrapped in act(...)', + ) + }) +}) diff --git a/src/flush-microtasks.js b/src/flush-microtasks.js deleted file mode 100644 index e1d8fe6f..00000000 --- a/src/flush-microtasks.js +++ /dev/null @@ -1,67 +0,0 @@ -/* istanbul ignore file */ -// the part of this file that we need tested is definitely being run -// and the part that is not cannot easily have useful tests written -// anyway. So we're just going to ignore coverage for this file -/** - * copied and modified from React's enqueueTask.js - */ - -function getIsUsingFakeTimers() { - return ( - typeof jest !== 'undefined' && - typeof setTimeout !== 'undefined' && - (setTimeout.hasOwnProperty('_isMockFunction') || - setTimeout.hasOwnProperty('clock')) - ) -} - -let didWarnAboutMessageChannel = false -let enqueueTask - -try { - // read require off the module object to get around the bundlers. - // we don't want them to detect a require and bundle a Node polyfill. - const requireString = `require${Math.random()}`.slice(0, 7) - const nodeRequire = module && module[requireString] - // assuming we're in node, let's try to get node's - // version of setImmediate, bypassing fake timers if any. - enqueueTask = nodeRequire.call(module, 'timers').setImmediate -} catch (_err) { - // we're in a browser - // we can't use regular timers because they may still be faked - // so we try MessageChannel+postMessage instead - enqueueTask = callback => { - const supportsMessageChannel = typeof MessageChannel === 'function' - if (supportsMessageChannel) { - const channel = new MessageChannel() - channel.port1.onmessage = callback - channel.port2.postMessage(undefined) - } else if (didWarnAboutMessageChannel === false) { - didWarnAboutMessageChannel = true - - // eslint-disable-next-line no-console - console.error( - 'This browser does not have a MessageChannel implementation, ' + - 'so enqueuing tasks via await act(async () => ...) will fail. ' + - 'Please file an issue at https://github.com/facebook/react/issues ' + - 'if you encounter this warning.', - ) - } - } -} - -export default function flushMicroTasks() { - return { - then(resolve) { - if (getIsUsingFakeTimers()) { - // without this, a test using fake timers would never get microtasks - // actually flushed. I spent several days on this... Really hard to - // reproduce the problem, so there's no test for it. But it works! - jest.advanceTimersByTime(0) - resolve() - } else { - enqueueTask(resolve) - } - }, - } -} diff --git a/src/pure.js b/src/pure.js index f2f3438f..8a062038 100644 --- a/src/pure.js +++ b/src/pure.js @@ -7,7 +7,6 @@ import { } from '@testing-library/dom' import act, {asyncAct} from './act-compat' import {fireEvent} from './fire-event' -import flush from './flush-microtasks' configureDTL({ asyncWrapper: async cb => { @@ -100,17 +99,16 @@ function render( } } -async function cleanup() { +function cleanup() { mountedContainers.forEach(cleanupAtContainer) - // flush microtask queue after unmounting in case - // unmount sequence generates new microtasks - await flush() } // maybe one day we'll expose this (perhaps even as a utility returned by render). // but let's wait until someone asks for it. function cleanupAtContainer(container) { - ReactDOM.unmountComponentAtNode(container) + act(() => { + ReactDOM.unmountComponentAtNode(container) + }) if (container.parentNode === document.body) { document.body.removeChild(container) }