Skip to content

Commit f676301

Browse files
author
Sebastian Silbermann
committed
Add support for testing updates granularly
1 parent d78b532 commit f676301

10 files changed

+283
-140
lines changed

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

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

44
beforeEach(() => {
55
global.IS_REACT_ACT_ENVIRONMENT = true
66
})
77

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

97-
expect(container).toHaveTextContent('2')
17+
global.IS_REACT_ACT_ENVIRONMENT = false
18+
await expect(async () => {
19+
await act(() => {
20+
setState(1)
21+
})
22+
}).toErrorDev(
23+
'Warning: The current testing environment is not configured to support act(...)',
24+
{withoutStack: true},
25+
)
9826
})

src/__tests__/auto-cleanup-skip.js

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ 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
})

src/__tests__/end-to-end.js

+124-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import * as React from 'react'
2-
import {render, waitForElementToBeRemoved, screen, waitFor} from '../'
1+
let React, cleanup, render, screen, waitFor, waitForElementToBeRemoved
32

43
describe.each([
54
['real timers', () => jest.useRealTimers()],
@@ -9,10 +8,25 @@ describe.each([
98
'it waits for the data to be loaded in a macrotask using %s',
109
(label, useTimers) => {
1110
beforeEach(() => {
11+
jest.resetModules()
12+
global.IS_REACT_ACT_ENVIRONMENT = true
13+
process.env.RTL_SKIP_AUTO_CLEANUP = '0'
14+
1215
useTimers()
16+
17+
React = require('react')
18+
;({
19+
cleanup,
20+
render,
21+
screen,
22+
waitFor,
23+
waitForElementToBeRemoved,
24+
} = require('..'))
1325
})
1426

15-
afterEach(() => {
27+
afterEach(async () => {
28+
await cleanup()
29+
global.IS_REACT_ACT_ENVIRONMENT = false
1630
jest.useRealTimers()
1731
})
1832

@@ -83,10 +97,25 @@ describe.each([
8397
'it waits for the data to be loaded in many microtask using %s',
8498
(label, useTimers) => {
8599
beforeEach(() => {
100+
jest.resetModules()
101+
global.IS_REACT_ACT_ENVIRONMENT = true
102+
process.env.RTL_SKIP_AUTO_CLEANUP = '0'
103+
86104
useTimers()
105+
106+
React = require('react')
107+
;({
108+
cleanup,
109+
render,
110+
screen,
111+
waitFor,
112+
waitForElementToBeRemoved,
113+
} = require('..'))
87114
})
88115

89-
afterEach(() => {
116+
afterEach(async () => {
117+
await cleanup()
118+
global.IS_REACT_ACT_ENVIRONMENT = false
90119
jest.useRealTimers()
91120
})
92121

@@ -167,10 +196,25 @@ describe.each([
167196
'it waits for the data to be loaded in a microtask using %s',
168197
(label, useTimers) => {
169198
beforeEach(() => {
199+
jest.resetModules()
200+
global.IS_REACT_ACT_ENVIRONMENT = true
201+
process.env.RTL_SKIP_AUTO_CLEANUP = '0'
202+
170203
useTimers()
204+
205+
React = require('react')
206+
;({
207+
cleanup,
208+
render,
209+
screen,
210+
waitFor,
211+
waitForElementToBeRemoved,
212+
} = require('..'))
171213
})
172214

173-
afterEach(() => {
215+
afterEach(async () => {
216+
await cleanup()
217+
global.IS_REACT_ACT_ENVIRONMENT = false
174218
jest.useRealTimers()
175219
})
176220

@@ -218,3 +262,78 @@ describe.each([
218262
})
219263
},
220264
)
265+
266+
describe.each([
267+
['real timers', () => jest.useRealTimers()],
268+
['fake legacy timers', () => jest.useFakeTimers('legacy')],
269+
['fake modern timers', () => jest.useFakeTimers('modern')],
270+
])('testing intermediate states using %s', (label, useTimers) => {
271+
beforeEach(() => {
272+
jest.resetModules()
273+
global.IS_REACT_ACT_ENVIRONMENT = false
274+
process.env.RTL_SKIP_AUTO_CLEANUP = '0'
275+
276+
useTimers()
277+
278+
React = require('react')
279+
;({render, screen, waitFor, waitForElementToBeRemoved} = require('..'))
280+
})
281+
282+
afterEach(async () => {
283+
await cleanup()
284+
jest.useRealTimers()
285+
global.IS_REACT_ACT_ENVIRONMENT = true
286+
})
287+
288+
const fetchAMessageInAMicrotask = () =>
289+
Promise.resolve({
290+
status: 200,
291+
json: () => Promise.resolve({title: 'Hello World'}),
292+
})
293+
294+
function ComponentWithMicrotaskLoader() {
295+
const [fetchState, setFetchState] = React.useState({fetching: true})
296+
297+
React.useEffect(() => {
298+
if (fetchState.fetching) {
299+
fetchAMessageInAMicrotask().then(res => {
300+
return res.json().then(data => {
301+
setFetchState({todo: data.title, fetching: false})
302+
})
303+
})
304+
}
305+
}, [fetchState])
306+
307+
if (fetchState.fetching) {
308+
return <p>Loading..</p>
309+
}
310+
311+
return (
312+
<div data-testid="message">Loaded this message: {fetchState.todo}</div>
313+
)
314+
}
315+
316+
test('waitFor', async () => {
317+
await render(<ComponentWithMicrotaskLoader />)
318+
319+
await waitFor(() => {
320+
expect(screen.getByText('Loading..')).toBeInTheDocument()
321+
})
322+
323+
await waitFor(() => {
324+
expect(screen.getByText(/Loaded this message:/)).toBeInTheDocument()
325+
})
326+
327+
expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/)
328+
})
329+
330+
test('findBy', async () => {
331+
await render(<ComponentWithMicrotaskLoader />)
332+
333+
await screen.findByText('Loading..')
334+
335+
await screen.findByText(/Loaded this message:/)
336+
337+
expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/)
338+
})
339+
})

0 commit comments

Comments
 (0)