diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json
index bf3237bb..d5850328 100644
--- a/.codesandbox/ci.json
+++ b/.codesandbox/ci.json
@@ -1,5 +1,5 @@
{
"installCommand": "install:csb",
"sandboxes": ["new", "github/kentcdodds/react-testing-library-examples"],
- "node": "12"
+ "node": "14"
}
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 0f99d084..5db8153c 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -24,7 +24,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- node: [12, 14, 16, 18]
+ node: [14, 16, 18]
react: [latest, next, experimental]
runs-on: ubuntu-latest
steps:
@@ -65,6 +65,7 @@ jobs:
permissions:
actions: write # to cancel/stop running workflows (styfle/cancel-workflow-action)
contents: write # to create release tags (cycjimmy/semantic-release-action)
+ issues: write # to post release that resolves an issue (cycjimmy/semantic-release-action)
needs: main
runs-on: ubuntu-latest
diff --git a/package.json b/package.json
index d2dd6a97..70aebdad 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"types": "types/index.d.ts",
"module": "dist/@testing-library/react.esm.js",
"engines": {
- "node": ">=12"
+ "node": ">=14"
},
"scripts": {
"prebuild": "rimraf dist",
@@ -46,15 +46,15 @@
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5",
- "@testing-library/dom": "^8.5.0",
+ "@testing-library/dom": "^9.0.0",
"@types/react-dom": "^18.0.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.11.6",
"chalk": "^4.1.2",
"dotenv-cli": "^4.0.0",
- "jest-diff": "^27.5.1",
- "kcd-scripts": "^11.1.0",
+ "jest-diff": "^29.4.1",
+ "kcd-scripts": "^13.0.0",
"npm-run-all": "^4.1.5",
"react": "^18.0.0",
"react-dom": "^18.0.0",
@@ -67,6 +67,9 @@
},
"eslintConfig": {
"extends": "./node_modules/kcd-scripts/eslint.js",
+ "parserOptions": {
+ "ecmaVersion": 2022
+ },
"globals": {
"globalThis": "readonly"
},
@@ -76,8 +79,11 @@
"import/no-unassigned-import": "off",
"import/named": "off",
"testing-library/no-container": "off",
+ "testing-library/no-debugging-utils": "off",
"testing-library/no-dom-import": "off",
"testing-library/no-unnecessary-act": "off",
+ "testing-library/prefer-explicit-assert": "off",
+ "testing-library/prefer-find-by": "off",
"testing-library/prefer-user-event": "off"
}
},
diff --git a/src/__tests__/cleanup.js b/src/__tests__/cleanup.js
index 4517c098..9f17c722 100644
--- a/src/__tests__/cleanup.js
+++ b/src/__tests__/cleanup.js
@@ -64,7 +64,7 @@ describe('fake timers and missing act warnings', () => {
let cancelled = false
Promise.resolve().then(() => {
microTaskSpy()
- // eslint-disable-next-line jest/no-if -- false positive
+ // eslint-disable-next-line jest/no-if, jest/no-conditional-in-test -- false positive
if (!cancelled) {
setDeferredCounter(counter)
}
@@ -96,6 +96,7 @@ describe('fake timers and missing act warnings', () => {
let cancelled = false
setTimeout(() => {
deferredStateUpdateSpy()
+ // eslint-disable-next-line jest/no-conditional-in-test -- false-positive
if (!cancelled) {
setDeferredCounter(counter)
}
diff --git a/src/__tests__/debug.js b/src/__tests__/debug.js
index f3aad595..c6a1d1fe 100644
--- a/src/__tests__/debug.js
+++ b/src/__tests__/debug.js
@@ -42,7 +42,7 @@ test('allows same arguments as prettyDOM', () => {
debug(container, 6, {highlight: false})
expect(console.log).toHaveBeenCalledTimes(1)
expect(console.log.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
+ [
...,
]
@@ -52,5 +52,4 @@ test('allows same arguments as prettyDOM', () => {
/*
eslint
no-console: "off",
- testing-library/no-debug: "off",
*/
diff --git a/src/__tests__/end-to-end.js b/src/__tests__/end-to-end.js
index cf222aec..005591d3 100644
--- a/src/__tests__/end-to-end.js
+++ b/src/__tests__/end-to-end.js
@@ -1,73 +1,164 @@
import * as React from 'react'
import {render, waitForElementToBeRemoved, screen, waitFor} from '../'
-const fetchAMessage = () =>
- new Promise(resolve => {
- // we are using random timeout here to simulate a real-time example
- // of an async operation calling a callback at a non-deterministic time
- const randomTimeout = Math.floor(Math.random() * 100)
- setTimeout(() => {
- resolve({returnedMessage: 'Hello World'})
- }, randomTimeout)
- })
-
-function ComponentWithLoader() {
- const [state, setState] = React.useState({data: undefined, loading: true})
- React.useEffect(() => {
- let cancelled = false
- fetchAMessage().then(data => {
- if (!cancelled) {
- setState({data, loading: false})
- }
+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 macrotask using %s',
+ (label, useTimers) => {
+ beforeEach(() => {
+ useTimers()
+ })
+
+ afterEach(() => {
+ jest.useRealTimers()
})
- return () => {
- cancelled = true
+ const fetchAMessageInAMacrotask = () =>
+ new Promise(resolve => {
+ // we are using random timeout here to simulate a real-time example
+ // of an async operation calling a callback at a non-deterministic time
+ const randomTimeout = Math.floor(Math.random() * 100)
+ setTimeout(() => {
+ resolve({returnedMessage: 'Hello World'})
+ }, randomTimeout)
+ })
+
+ function ComponentWithMacrotaskLoader() {
+ const [state, setState] = React.useState({data: undefined, loading: true})
+ React.useEffect(() => {
+ let cancelled = false
+ fetchAMessageInAMacrotask().then(data => {
+ if (!cancelled) {
+ setState({data, loading: false})
+ }
+ })
+
+ return () => {
+ cancelled = true
+ }
+ }, [])
+
+ if (state.loading) {
+ return
Loading...
+ }
+
+ return (
+
+ Loaded this message: {state.data.returnedMessage}!
+
+ )
}
- }, [])
- if (state.loading) {
- return
Loading...
- }
+ 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/)
+ })
- return (
-
- Loaded this message: {state.data.returnedMessage}!
-
- )
-}
+ test('findBy', async () => {
+ render(
)
+ await expect(screen.findByTestId('message')).resolves.toHaveTextContent(
+ /Hello World/,
+ )
+ })
+ },
+)
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 using %s', (label, useTimers) => {
- beforeEach(() => {
- useTimers()
- })
-
- afterEach(() => {
- jest.useRealTimers()
- })
-
- test('waitForElementToBeRemoved', async () => {
- render(
)
- const loading = () => screen.getByText('Loading...')
- await waitForElementToBeRemoved(loading)
- expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/)
- })
-
- test('waitFor', async () => {
- render(
)
- const message = () => screen.getByText(/Loaded this message:/)
- await waitFor(message)
- expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/)
- })
-
- test('findBy', async () => {
- render(
)
- await expect(screen.findByTestId('message')).resolves.toHaveTextContent(
- /Hello World/,
- )
- })
-})
+])(
+ '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()
+ // By spec, the runtime can only yield back to the event loop once
+ // the microtask queue is empty.
+ // So we ensure that we actually wait for that as well before yielding back from `waitFor`.
+ .then(data => data)
+ .then(data => data)
+ .then(data => data)
+ .then(data => data)
+ .then(data => data)
+ .then(data => data)
+ .then(data => data)
+ .then(data => data)
+ .then(data => data)
+ .then(data => data)
+ .then(data => data)
+ .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/,
+ )
+ })
+ },
+)
diff --git a/src/__tests__/new-act.js b/src/__tests__/new-act.js
index 4909d4a6..0412a8a3 100644
--- a/src/__tests__/new-act.js
+++ b/src/__tests__/new-act.js
@@ -47,8 +47,8 @@ test('async act recovers from errors', async () => {
}
expect(console.error).toHaveBeenCalledTimes(1)
expect(console.error.mock.calls).toMatchInlineSnapshot(`
- Array [
- Array [
+ [
+ [
call console.error,
],
]
@@ -65,8 +65,8 @@ test('async act recovers from sync errors', async () => {
}
expect(console.error).toHaveBeenCalledTimes(1)
expect(console.error.mock.calls).toMatchInlineSnapshot(`
- Array [
- Array [
+ [
+ [
call console.error,
],
]
diff --git a/src/__tests__/renderHook.js b/src/__tests__/renderHook.js
index f6b7a343..11b7009a 100644
--- a/src/__tests__/renderHook.js
+++ b/src/__tests__/renderHook.js
@@ -21,7 +21,7 @@ test('allows rerendering', () => {
const [left, setLeft] = React.useState('left')
const [right, setRight] = React.useState('right')
- // eslint-disable-next-line jest/no-if
+ // eslint-disable-next-line jest/no-if, jest/no-conditional-in-test -- false-positive
switch (branch) {
case 'left':
return [left, setLeft]
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)
}
diff --git a/tests/setup-env.js b/tests/setup-env.js
index a4ddfa17..c9b976f5 100644
--- a/tests/setup-env.js
+++ b/tests/setup-env.js
@@ -1,2 +1,5 @@
import '@testing-library/jest-dom/extend-expect'
import './failOnUnexpectedConsoleCalls'
+import {TextEncoder} from 'util'
+
+global.TextEncoder = TextEncoder
diff --git a/tests/toWarnDev.js b/tests/toWarnDev.js
index ac5f1b19..ca58346f 100644
--- a/tests/toWarnDev.js
+++ b/tests/toWarnDev.js
@@ -24,7 +24,7 @@ SOFTWARE.
*/
/* eslint-disable no-unsafe-finally */
/* eslint-disable no-negated-condition */
-/* eslint-disable @babel/no-invalid-this */
+/* eslint-disable no-invalid-this */
/* eslint-disable prefer-template */
/* eslint-disable func-names */
/* eslint-disable complexity */
diff --git a/types/test.tsx b/types/test.tsx
index 17ba7012..c33f07b6 100644
--- a/types/test.tsx
+++ b/types/test.tsx
@@ -188,7 +188,6 @@ export function testRenderHookProps() {
eslint
testing-library/prefer-explicit-assert: "off",
testing-library/no-wait-for-empty-callback: "off",
- testing-library/no-debug: "off",
testing-library/prefer-screen-queries: "off"
*/