Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 4567a0d

Browse files
committedJan 16, 2025··
Use async act
1 parent c3e3d90 commit 4567a0d

24 files changed

+692
-479
lines changed
 

‎jest.config.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ module.exports = Object.assign(jestConfig, {
66
// Full coverage across the build matrix (React 18, 19) but not in a single job
77
// Ful coverage is checked via codecov
88
'./src/act-compat': {
9-
branches: 90,
9+
branches: 80,
1010
},
1111
'./src/pure': {
1212
// minimum coverage of jobs using React 18 and 19
13-
branches: 95,
14-
functions: 88,
15-
lines: 92,
16-
statements: 92,
13+
branches: 90,
14+
functions: 81,
15+
lines: 91,
16+
statements: 91,
1717
},
1818
},
1919
})

‎package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@
5757
"jest-diff": "^29.7.0",
5858
"kcd-scripts": "^13.0.0",
5959
"npm-run-all": "^4.1.5",
60-
"react": "^19.0.0",
61-
"react-dom": "^19.0.0",
60+
"react": "^18.3.1",
61+
"react-dom": "^18.3.1",
6262
"rimraf": "^3.0.2",
6363
"typescript": "^4.1.2"
6464
},
@@ -90,6 +90,7 @@
9090
"react/no-adjacent-inline-elements": "off",
9191
"import/no-unassigned-import": "off",
9292
"import/named": "off",
93+
"testing-library/no-await-sync-events": "off",
9394
"testing-library/no-container": "off",
9495
"testing-library/no-debugging-utils": "off",
9596
"testing-library/no-dom-import": "off",

‎src/__tests__/act-compat.js

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import * as React from 'react'
2+
import {render, fireEvent, screen} from '../'
3+
import {actIfEnabled} from '../act-compat'
4+
5+
beforeEach(() => {
6+
global.IS_REACT_ACT_ENVIRONMENT = true
7+
})
8+
9+
test('render calls useEffect immediately', async () => {
10+
const effectCb = jest.fn()
11+
function MyUselessComponent() {
12+
React.useEffect(effectCb)
13+
return null
14+
}
15+
await render(<MyUselessComponent />)
16+
expect(effectCb).toHaveBeenCalledTimes(1)
17+
})
18+
19+
test('findByTestId returns the element', async () => {
20+
const ref = React.createRef()
21+
await render(<div ref={ref} data-testid="foo" />)
22+
expect(await screen.findByTestId('foo')).toBe(ref.current)
23+
})
24+
25+
test('fireEvent triggers useEffect calls', async () => {
26+
const effectCb = jest.fn()
27+
function Counter() {
28+
React.useEffect(effectCb)
29+
const [count, setCount] = React.useState(0)
30+
return <button onClick={() => setCount(count + 1)}>{count}</button>
31+
}
32+
const {
33+
container: {firstChild: buttonNode},
34+
} = await render(<Counter />)
35+
36+
effectCb.mockClear()
37+
// eslint-disable-next-line testing-library/no-await-sync-events -- TODO: Remove lint rule.
38+
await fireEvent.click(buttonNode)
39+
expect(buttonNode).toHaveTextContent('1')
40+
expect(effectCb).toHaveBeenCalledTimes(1)
41+
})
42+
43+
test('calls to hydrate will run useEffects', async () => {
44+
const effectCb = jest.fn()
45+
function MyUselessComponent() {
46+
React.useEffect(effectCb)
47+
return null
48+
}
49+
await render(<MyUselessComponent />, {hydrate: true})
50+
expect(effectCb).toHaveBeenCalledTimes(1)
51+
})
52+
53+
test('cleans up IS_REACT_ACT_ENVIRONMENT if its callback throws', async () => {
54+
global.IS_REACT_ACT_ENVIRONMENT = false
55+
56+
await expect(() =>
57+
actIfEnabled(() => {
58+
throw new Error('threw')
59+
}),
60+
).rejects.toThrow('threw')
61+
62+
expect(global.IS_REACT_ACT_ENVIRONMENT).toEqual(false)
63+
})
64+
65+
test('cleans up IS_REACT_ACT_ENVIRONMENT if its async callback throws', async () => {
66+
global.IS_REACT_ACT_ENVIRONMENT = false
67+
68+
await expect(() =>
69+
actIfEnabled(async () => {
70+
throw new Error('thenable threw')
71+
}),
72+
).rejects.toThrow('thenable threw')
73+
74+
expect(global.IS_REACT_ACT_ENVIRONMENT).toEqual(false)
75+
})
76+
77+
test('state update from microtask does not trigger "missing act" warning', async () => {
78+
let triggerStateUpdateFromMicrotask
79+
function App() {
80+
const [state, setState] = React.useState(0)
81+
triggerStateUpdateFromMicrotask = () => setState(1)
82+
React.useEffect(() => {
83+
// eslint-disable-next-line jest/no-conditional-in-test
84+
if (state === 1) {
85+
Promise.resolve().then(() => {
86+
setState(2)
87+
})
88+
}
89+
}, [state])
90+
return state
91+
}
92+
const {container} = await render(<App />)
93+
94+
await actIfEnabled(() => {
95+
triggerStateUpdateFromMicrotask()
96+
})
97+
98+
expect(container).toHaveTextContent('2')
99+
})

‎src/__tests__/act.js

+18-61
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,26 @@
11
import * as React from 'react'
2-
import {act, render, fireEvent, screen} from '../'
2+
import {act, render} from '../'
33

4-
test('render calls useEffect immediately', () => {
5-
const effectCb = jest.fn()
6-
function MyUselessComponent() {
7-
React.useEffect(effectCb)
8-
return null
9-
}
10-
render(<MyUselessComponent />)
11-
expect(effectCb).toHaveBeenCalledTimes(1)
12-
})
13-
14-
test('findByTestId returns the element', async () => {
15-
const ref = React.createRef()
16-
render(<div ref={ref} data-testid="foo" />)
17-
expect(await screen.findByTestId('foo')).toBe(ref.current)
18-
})
19-
20-
test('fireEvent triggers useEffect calls', () => {
21-
const effectCb = jest.fn()
22-
function Counter() {
23-
React.useEffect(effectCb)
24-
const [count, setCount] = React.useState(0)
25-
return <button onClick={() => setCount(count + 1)}>{count}</button>
26-
}
27-
const {
28-
container: {firstChild: buttonNode},
29-
} = render(<Counter />)
30-
31-
effectCb.mockClear()
32-
fireEvent.click(buttonNode)
33-
expect(buttonNode).toHaveTextContent('1')
34-
expect(effectCb).toHaveBeenCalledTimes(1)
4+
beforeEach(() => {
5+
global.IS_REACT_ACT_ENVIRONMENT = true
356
})
367

37-
test('calls to hydrate will run useEffects', () => {
38-
const effectCb = jest.fn()
39-
function MyUselessComponent() {
40-
React.useEffect(effectCb)
41-
return null
8+
test('does not work outside IS_REACT_ENVIRONMENT like React.act', async () => {
9+
let setState
10+
function Component() {
11+
const [state, _setState] = React.useState(0)
12+
setState = _setState
13+
return state
4214
}
43-
render(<MyUselessComponent />, {hydrate: true})
44-
expect(effectCb).toHaveBeenCalledTimes(1)
45-
})
15+
await render(<Component />)
4616

47-
test('cleans up IS_REACT_ACT_ENVIRONMENT if its callback throws', () => {
4817
global.IS_REACT_ACT_ENVIRONMENT = false
49-
50-
expect(() =>
51-
act(() => {
52-
throw new Error('threw')
53-
}),
54-
).toThrow('threw')
55-
56-
expect(global.IS_REACT_ACT_ENVIRONMENT).toEqual(false)
57-
})
58-
59-
test('cleans up IS_REACT_ACT_ENVIRONMENT if its async callback throws', async () => {
60-
global.IS_REACT_ACT_ENVIRONMENT = false
61-
62-
await expect(() =>
63-
act(async () => {
64-
throw new Error('thenable threw')
65-
}),
66-
).rejects.toThrow('thenable threw')
67-
68-
expect(global.IS_REACT_ACT_ENVIRONMENT).toEqual(false)
18+
await expect(async () => {
19+
await act(() => {
20+
setState(1)
21+
})
22+
}).toErrorDev(
23+
'The current testing environment is not configured to support act(...)',
24+
{withoutStack: true},
25+
)
6926
})

‎src/__tests__/auto-cleanup-skip.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import * as React from 'react'
33
let render
44
beforeAll(() => {
55
process.env.RTL_SKIP_AUTO_CLEANUP = 'true'
6+
globalThis.IS_REACT_ACT_ENVIRONMENT = true
67
const rtl = require('../')
78
render = rtl.render
89
})
910

1011
// This one verifies that if RTL_SKIP_AUTO_CLEANUP is set
1112
// then we DON'T auto-wire up the afterEach for folks
12-
test('first', () => {
13-
render(<div>hi</div>)
13+
test('first', async () => {
14+
await render(<div>hi</div>)
1415
})
1516

1617
test('second', () => {

‎src/__tests__/auto-cleanup.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import {render} from '../'
44
// This just verifies that by importing RTL in an
55
// environment which supports afterEach (like jest)
66
// we'll get automatic cleanup between tests.
7-
test('first', () => {
8-
render(<div>hi</div>)
7+
test('first', async () => {
8+
await render(<div>hi</div>)
99
})
1010

1111
test('second', () => {

‎src/__tests__/cleanup.js

+32-17
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react'
22
import {render, cleanup} from '../'
33

4-
test('cleans up the document', () => {
4+
test('cleans up the document', async () => {
55
const spy = jest.fn()
66
const divId = 'my-div'
77

@@ -16,18 +16,18 @@ test('cleans up the document', () => {
1616
}
1717
}
1818

19-
render(<Test />)
20-
cleanup()
19+
await render(<Test />)
20+
await cleanup()
2121
expect(document.body).toBeEmptyDOMElement()
2222
expect(spy).toHaveBeenCalledTimes(1)
2323
})
2424

25-
test('cleanup does not error when an element is not a child', () => {
26-
render(<div />, {container: document.createElement('div')})
27-
cleanup()
25+
test('cleanup does not error when an element is not a child', async () => {
26+
await render(<div />, {container: document.createElement('div')})
27+
await cleanup()
2828
})
2929

30-
test('cleanup runs effect cleanup functions', () => {
30+
test('cleanup runs effect cleanup functions', async () => {
3131
const spy = jest.fn()
3232

3333
const Test = () => {
@@ -36,11 +36,23 @@ test('cleanup runs effect cleanup functions', () => {
3636
return null
3737
}
3838

39-
render(<Test />)
40-
cleanup()
39+
await render(<Test />)
40+
await cleanup()
4141
expect(spy).toHaveBeenCalledTimes(1)
4242
})
4343

44+
test('cleanup cleans up every root and disconnects containers', async () => {
45+
const {container: container1} = await render(<div />)
46+
const {container: container2} = await render(<span />)
47+
48+
await cleanup()
49+
50+
expect(container1).toBeEmptyDOMElement()
51+
expect(container1.isConnected).toBe(false)
52+
expect(container2).toBeEmptyDOMElement()
53+
expect(container2.isConnected).toBe(false)
54+
})
55+
4456
describe('fake timers and missing act warnings', () => {
4557
beforeEach(() => {
4658
jest.resetAllMocks()
@@ -55,7 +67,7 @@ describe('fake timers and missing act warnings', () => {
5567
jest.useRealTimers()
5668
})
5769

58-
test('cleanup does not flush microtasks', () => {
70+
test('cleanup does flush microtasks', async () => {
5971
const microTaskSpy = jest.fn()
6072
function Test() {
6173
const counter = 1
@@ -72,22 +84,25 @@ describe('fake timers and missing act warnings', () => {
7284

7385
return () => {
7486
cancelled = true
87+
Promise.resolve().then(() => {
88+
microTaskSpy()
89+
})
7590
}
7691
}, [counter])
7792

7893
return null
7994
}
80-
render(<Test />)
81-
82-
cleanup()
95+
await render(<Test />)
96+
expect(microTaskSpy).toHaveBeenCalledTimes(1)
8397

84-
expect(microTaskSpy).toHaveBeenCalledTimes(0)
98+
await cleanup()
99+
expect(microTaskSpy).toHaveBeenCalledTimes(2)
85100
// console.error is mocked
86101
// eslint-disable-next-line no-console
87102
expect(console.error).toHaveBeenCalledTimes(0)
88103
})
89104

90-
test('cleanup does not swallow missing act warnings', () => {
105+
test('cleanup does not swallow missing act warnings', async () => {
91106
const deferredStateUpdateSpy = jest.fn()
92107
function Test() {
93108
const counter = 1
@@ -109,10 +124,10 @@ describe('fake timers and missing act warnings', () => {
109124

110125
return null
111126
}
112-
render(<Test />)
127+
await render(<Test />)
113128

114129
jest.runAllTimers()
115-
cleanup()
130+
await cleanup()
116131

117132
expect(deferredStateUpdateSpy).toHaveBeenCalledTimes(1)
118133
// console.error is mocked

0 commit comments

Comments
 (0)
Please sign in to comment.