diff --git a/jest.config.js b/jest.config.js
index 860358cd..fd76e2f2 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -6,14 +6,14 @@ module.exports = Object.assign(jestConfig, {
// Full coverage across the build matrix (React 18, 19) but not in a single job
// Ful coverage is checked via codecov
'./src/act-compat': {
- branches: 90,
+ branches: 80,
},
'./src/pure': {
// minimum coverage of jobs using React 18 and 19
- branches: 95,
- functions: 88,
- lines: 92,
- statements: 92,
+ branches: 90,
+ functions: 81,
+ lines: 91,
+ statements: 91,
},
},
})
diff --git a/package.json b/package.json
index 146c7d02..03f0629b 100644
--- a/package.json
+++ b/package.json
@@ -90,6 +90,7 @@
"react/no-adjacent-inline-elements": "off",
"import/no-unassigned-import": "off",
"import/named": "off",
+ "testing-library/no-await-sync-events": "off",
"testing-library/no-container": "off",
"testing-library/no-debugging-utils": "off",
"testing-library/no-dom-import": "off",
diff --git a/src/__tests__/act-compat.js b/src/__tests__/act-compat.js
new file mode 100644
index 00000000..89aacbc9
--- /dev/null
+++ b/src/__tests__/act-compat.js
@@ -0,0 +1,99 @@
+import * as React from 'react'
+import {render, fireEvent, screen} from '../'
+import {actIfEnabled} from '../act-compat'
+
+beforeEach(() => {
+ global.IS_REACT_ACT_ENVIRONMENT = true
+})
+
+test('render calls useEffect immediately', async () => {
+ const effectCb = jest.fn()
+ function MyUselessComponent() {
+ React.useEffect(effectCb)
+ return null
+ }
+ await render()
+ expect(effectCb).toHaveBeenCalledTimes(1)
+})
+
+test('findByTestId returns the element', async () => {
+ const ref = React.createRef()
+ await render(
)
+ expect(await screen.findByTestId('foo')).toBe(ref.current)
+})
+
+test('fireEvent triggers useEffect calls', async () => {
+ const effectCb = jest.fn()
+ function Counter() {
+ React.useEffect(effectCb)
+ const [count, setCount] = React.useState(0)
+ return
+ }
+ const {
+ container: {firstChild: buttonNode},
+ } = await render()
+
+ effectCb.mockClear()
+ // eslint-disable-next-line testing-library/no-await-sync-events -- TODO: Remove lint rule.
+ await fireEvent.click(buttonNode)
+ expect(buttonNode).toHaveTextContent('1')
+ expect(effectCb).toHaveBeenCalledTimes(1)
+})
+
+test('calls to hydrate will run useEffects', async () => {
+ const effectCb = jest.fn()
+ function MyUselessComponent() {
+ React.useEffect(effectCb)
+ return null
+ }
+ await render(, {hydrate: true})
+ expect(effectCb).toHaveBeenCalledTimes(1)
+})
+
+test('cleans up IS_REACT_ACT_ENVIRONMENT if its callback throws', async () => {
+ global.IS_REACT_ACT_ENVIRONMENT = false
+
+ await expect(() =>
+ actIfEnabled(() => {
+ throw new Error('threw')
+ }),
+ ).rejects.toThrow('threw')
+
+ expect(global.IS_REACT_ACT_ENVIRONMENT).toEqual(false)
+})
+
+test('cleans up IS_REACT_ACT_ENVIRONMENT if its async callback throws', async () => {
+ global.IS_REACT_ACT_ENVIRONMENT = false
+
+ await expect(() =>
+ actIfEnabled(async () => {
+ throw new Error('thenable threw')
+ }),
+ ).rejects.toThrow('thenable threw')
+
+ expect(global.IS_REACT_ACT_ENVIRONMENT).toEqual(false)
+})
+
+test('state update from microtask does not trigger "missing act" warning', async () => {
+ let triggerStateUpdateFromMicrotask
+ function App() {
+ const [state, setState] = React.useState(0)
+ triggerStateUpdateFromMicrotask = () => setState(1)
+ React.useEffect(() => {
+ // eslint-disable-next-line jest/no-conditional-in-test
+ if (state === 1) {
+ Promise.resolve().then(() => {
+ setState(2)
+ })
+ }
+ }, [state])
+ return state
+ }
+ const {container} = await render()
+
+ await actIfEnabled(() => {
+ triggerStateUpdateFromMicrotask()
+ })
+
+ expect(container).toHaveTextContent('2')
+})
diff --git a/src/__tests__/act.js b/src/__tests__/act.js
index 5430f28b..95803a39 100644
--- a/src/__tests__/act.js
+++ b/src/__tests__/act.js
@@ -1,69 +1,26 @@
import * as React from 'react'
-import {act, render, fireEvent, screen} from '../'
+import {act, render} from '../'
-test('render calls useEffect immediately', () => {
- const effectCb = jest.fn()
- function MyUselessComponent() {
- React.useEffect(effectCb)
- return null
- }
- render()
- expect(effectCb).toHaveBeenCalledTimes(1)
-})
-
-test('findByTestId returns the element', async () => {
- const ref = React.createRef()
- render()
- expect(await screen.findByTestId('foo')).toBe(ref.current)
-})
-
-test('fireEvent triggers useEffect calls', () => {
- const effectCb = jest.fn()
- function Counter() {
- React.useEffect(effectCb)
- const [count, setCount] = React.useState(0)
- return
- }
- const {
- container: {firstChild: buttonNode},
- } = render()
-
- effectCb.mockClear()
- fireEvent.click(buttonNode)
- expect(buttonNode).toHaveTextContent('1')
- expect(effectCb).toHaveBeenCalledTimes(1)
+beforeEach(() => {
+ global.IS_REACT_ACT_ENVIRONMENT = true
})
-test('calls to hydrate will run useEffects', () => {
- const effectCb = jest.fn()
- function MyUselessComponent() {
- React.useEffect(effectCb)
- return null
+test('does not work outside IS_REACT_ENVIRONMENT like React.act', async () => {
+ let setState
+ function Component() {
+ const [state, _setState] = React.useState(0)
+ setState = _setState
+ return state
}
- render(, {hydrate: true})
- expect(effectCb).toHaveBeenCalledTimes(1)
-})
+ await render()
-test('cleans up IS_REACT_ACT_ENVIRONMENT if its callback throws', () => {
global.IS_REACT_ACT_ENVIRONMENT = false
-
- expect(() =>
- act(() => {
- throw new Error('threw')
- }),
- ).toThrow('threw')
-
- expect(global.IS_REACT_ACT_ENVIRONMENT).toEqual(false)
-})
-
-test('cleans up IS_REACT_ACT_ENVIRONMENT if its async callback throws', async () => {
- global.IS_REACT_ACT_ENVIRONMENT = false
-
- await expect(() =>
- act(async () => {
- throw new Error('thenable threw')
- }),
- ).rejects.toThrow('thenable threw')
-
- expect(global.IS_REACT_ACT_ENVIRONMENT).toEqual(false)
+ await expect(async () => {
+ await act(() => {
+ setState(1)
+ })
+ }).toErrorDev(
+ 'The current testing environment is not configured to support act(...)',
+ {withoutStack: true},
+ )
})
diff --git a/src/__tests__/auto-cleanup-skip.js b/src/__tests__/auto-cleanup-skip.js
index 5696d4e3..89a99e9b 100644
--- a/src/__tests__/auto-cleanup-skip.js
+++ b/src/__tests__/auto-cleanup-skip.js
@@ -3,14 +3,15 @@ import * as React from 'react'
let render
beforeAll(() => {
process.env.RTL_SKIP_AUTO_CLEANUP = 'true'
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true
const rtl = require('../')
render = rtl.render
})
// This one verifies that if RTL_SKIP_AUTO_CLEANUP is set
// then we DON'T auto-wire up the afterEach for folks
-test('first', () => {
- render(
hi
)
+test('first', async () => {
+ await render(
hi
)
})
test('second', () => {
diff --git a/src/__tests__/auto-cleanup.js b/src/__tests__/auto-cleanup.js
index 450a6136..b796fa00 100644
--- a/src/__tests__/auto-cleanup.js
+++ b/src/__tests__/auto-cleanup.js
@@ -4,8 +4,8 @@ import {render} from '../'
// This just verifies that by importing RTL in an
// environment which supports afterEach (like jest)
// we'll get automatic cleanup between tests.
-test('first', () => {
- render(
hi
)
+test('first', async () => {
+ await render(
hi
)
})
test('second', () => {
diff --git a/src/__tests__/cleanup.js b/src/__tests__/cleanup.js
index 9f17c722..20e298f2 100644
--- a/src/__tests__/cleanup.js
+++ b/src/__tests__/cleanup.js
@@ -1,7 +1,7 @@
import * as React from 'react'
import {render, cleanup} from '../'
-test('cleans up the document', () => {
+test('cleans up the document', async () => {
const spy = jest.fn()
const divId = 'my-div'
@@ -16,18 +16,18 @@ test('cleans up the document', () => {
}
}
- render()
- cleanup()
+ await render()
+ await cleanup()
expect(document.body).toBeEmptyDOMElement()
expect(spy).toHaveBeenCalledTimes(1)
})
-test('cleanup does not error when an element is not a child', () => {
- render(, {container: document.createElement('div')})
- cleanup()
+test('cleanup does not error when an element is not a child', async () => {
+ await render(, {container: document.createElement('div')})
+ await cleanup()
})
-test('cleanup runs effect cleanup functions', () => {
+test('cleanup runs effect cleanup functions', async () => {
const spy = jest.fn()
const Test = () => {
@@ -36,11 +36,23 @@ test('cleanup runs effect cleanup functions', () => {
return null
}
- render()
- cleanup()
+ await render()
+ await cleanup()
expect(spy).toHaveBeenCalledTimes(1)
})
+test('cleanup cleans up every root and disconnects containers', async () => {
+ const {container: container1} = await render()
+ const {container: container2} = await render()
+
+ await cleanup()
+
+ expect(container1).toBeEmptyDOMElement()
+ expect(container1.isConnected).toBe(false)
+ expect(container2).toBeEmptyDOMElement()
+ expect(container2.isConnected).toBe(false)
+})
+
describe('fake timers and missing act warnings', () => {
beforeEach(() => {
jest.resetAllMocks()
@@ -55,7 +67,7 @@ describe('fake timers and missing act warnings', () => {
jest.useRealTimers()
})
- test('cleanup does not flush microtasks', () => {
+ test('cleanup does flush microtasks', async () => {
const microTaskSpy = jest.fn()
function Test() {
const counter = 1
@@ -72,22 +84,25 @@ describe('fake timers and missing act warnings', () => {
return () => {
cancelled = true
+ Promise.resolve().then(() => {
+ microTaskSpy()
+ })
}
}, [counter])
return null
}
- render()
-
- cleanup()
+ await render()
+ expect(microTaskSpy).toHaveBeenCalledTimes(1)
- expect(microTaskSpy).toHaveBeenCalledTimes(0)
+ await cleanup()
+ expect(microTaskSpy).toHaveBeenCalledTimes(2)
// console.error is mocked
// eslint-disable-next-line no-console
expect(console.error).toHaveBeenCalledTimes(0)
})
- test('cleanup does not swallow missing act warnings', () => {
+ test('cleanup does not swallow missing act warnings', async () => {
const deferredStateUpdateSpy = jest.fn()
function Test() {
const counter = 1
@@ -109,10 +124,10 @@ describe('fake timers and missing act warnings', () => {
return null
}
- render()
+ await render()
jest.runAllTimers()
- cleanup()
+ await cleanup()
expect(deferredStateUpdateSpy).toHaveBeenCalledTimes(1)
// console.error is mocked
diff --git a/src/__tests__/debug.js b/src/__tests__/debug.js
index c6a1d1fe..4c5ec311 100644
--- a/src/__tests__/debug.js
+++ b/src/__tests__/debug.js
@@ -9,9 +9,9 @@ afterEach(() => {
console.log.mockRestore()
})
-test('debug pretty prints the container', () => {
+test('debug pretty prints the container', async () => {
const HelloWorld = () =>