Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 4fd9f05

Browse files
Sebastian Silbermanneps1lon
Sebastian Silbermann
authored andcommitted
Add support for testing updates granularly
1 parent 293e79f commit 4fd9f05

File tree

9 files changed

+290
-139
lines changed

9 files changed

+290
-139
lines changed

‎src/__tests__/act-compat.js‎

Lines changed: 99 additions & 0 deletions
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‎

Lines changed: 16 additions & 88 deletions
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‎

Lines changed: 1 addition & 0 deletions
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‎

Lines changed: 124 additions & 5 deletions
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(/Loadedthismessage:/)).toBeInTheDocument()
325+
})
326+
327+
expect(screen.getByTestId('message')).toHaveTextContent(/HelloWorld/)
328+
})
329+
330+
test('findBy', async () => {
331+
await render(<ComponentWithMicrotaskLoader />)
332+
333+
await screen.findByText('Loading..')
334+
335+
await screen.findByText(/Loadedthismessage:/)
336+
337+
expect(screen.getByTestId('message')).toHaveTextContent(/HelloWorld/)
338+
})
339+
})

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /