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 f78839b

Browse files
authored
fix: Prevent "missing act" warning for queued microtasks (#1137)
* Add intended behavior * fix: Prevent "missing act" warning for in-flight promises * Disable TL lint rules in tests * Implementation without macrotask * Now I member
1 parent 6653c23 commit f78839b

File tree

3 files changed

+182
-61
lines changed

3 files changed

+182
-61
lines changed

‎package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@
8282
"testing-library/no-debugging-utils": "off",
8383
"testing-library/no-dom-import": "off",
8484
"testing-library/no-unnecessary-act": "off",
85+
"testing-library/prefer-explicit-assert": "off",
86+
"testing-library/prefer-find-by": "off",
8587
"testing-library/prefer-user-event": "off"
8688
}
8789
},

‎src/__tests__/end-to-end.js‎

Lines changed: 151 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,164 @@
11
import * as React from 'react'
22
import {render, waitForElementToBeRemoved, screen, waitFor} from '../'
33

4-
const fetchAMessage = () =>
5-
new Promise(resolve => {
6-
// we are using random timeout here to simulate a real-time example
7-
// of an async operation calling a callback at a non-deterministic time
8-
const randomTimeout = Math.floor(Math.random() * 100)
9-
setTimeout(() => {
10-
resolve({returnedMessage: 'Hello World'})
11-
}, randomTimeout)
12-
})
13-
14-
function ComponentWithLoader() {
15-
const [state, setState] = React.useState({data: undefined, loading: true})
16-
React.useEffect(() => {
17-
let cancelled = false
18-
fetchAMessage().then(data => {
19-
if (!cancelled) {
20-
setState({data, loading: false})
21-
}
4+
describe.each([
5+
['real timers', () => jest.useRealTimers()],
6+
['fake legacy timers', () => jest.useFakeTimers('legacy')],
7+
['fake modern timers', () => jest.useFakeTimers('modern')],
8+
])(
9+
'it waits for the data to be loaded in a macrotask using %s',
10+
(label, useTimers) => {
11+
beforeEach(() => {
12+
useTimers()
13+
})
14+
15+
afterEach(() => {
16+
jest.useRealTimers()
2217
})
2318

24-
return () => {
25-
cancelled = true
19+
const fetchAMessageInAMacrotask = () =>
20+
new Promise(resolve => {
21+
// we are using random timeout here to simulate a real-time example
22+
// of an async operation calling a callback at a non-deterministic time
23+
const randomTimeout = Math.floor(Math.random() * 100)
24+
setTimeout(() => {
25+
resolve({returnedMessage: 'Hello World'})
26+
}, randomTimeout)
27+
})
28+
29+
function ComponentWithMacrotaskLoader() {
30+
const [state, setState] = React.useState({data: undefined, loading: true})
31+
React.useEffect(() => {
32+
let cancelled = false
33+
fetchAMessageInAMacrotask().then(data => {
34+
if (!cancelled) {
35+
setState({data, loading: false})
36+
}
37+
})
38+
39+
return () => {
40+
cancelled = true
41+
}
42+
}, [])
43+
44+
if (state.loading) {
45+
return <div>Loading...</div>
46+
}
47+
48+
return (
49+
<div data-testid="message">
50+
Loaded this message: {state.data.returnedMessage}!
51+
</div>
52+
)
2653
}
27-
}, [])
2854

29-
if (state.loading) {
30-
return <div>Loading...</div>
31-
}
55+
test('waitForElementToBeRemoved', async () => {
56+
render(<ComponentWithMacrotaskLoader />)
57+
const loading = () => screen.getByText('Loading...')
58+
await waitForElementToBeRemoved(loading)
59+
expect(screen.getByTestId('message')).toHaveTextContent(/HelloWorld/)
60+
})
61+
62+
test('waitFor', async () => {
63+
render(<ComponentWithMacrotaskLoader />)
64+
await waitFor(() => screen.getByText(/Loading../))
65+
await waitFor(() => screen.getByText(/Loadedthismessage:/))
66+
expect(screen.getByTestId('message')).toHaveTextContent(/HelloWorld/)
67+
})
3268

33-
return (
34-
<div data-testid="message">
35-
Loaded this message: {state.data.returnedMessage}!
36-
</div>
37-
)
38-
}
69+
test('findBy', async () => {
70+
render(<ComponentWithMacrotaskLoader />)
71+
await expect(screen.findByTestId('message')).resolves.toHaveTextContent(
72+
/HelloWorld/,
73+
)
74+
})
75+
},
76+
)
3977

4078
describe.each([
4179
['real timers', () => jest.useRealTimers()],
4280
['fake legacy timers', () => jest.useFakeTimers('legacy')],
4381
['fake modern timers', () => jest.useFakeTimers('modern')],
44-
])('it waits for the data to be loaded using %s', (label, useTimers) => {
45-
beforeEach(() => {
46-
useTimers()
47-
})
48-
49-
afterEach(() => {
50-
jest.useRealTimers()
51-
})
52-
53-
test('waitForElementToBeRemoved', async () => {
54-
render(<ComponentWithLoader />)
55-
const loading = () => screen.getByText('Loading...')
56-
await waitForElementToBeRemoved(loading)
57-
expect(screen.getByTestId('message')).toHaveTextContent(/HelloWorld/)
58-
})
59-
60-
test('waitFor', async () => {
61-
render(<ComponentWithLoader />)
62-
const message = () => screen.getByText(/Loadedthismessage:/)
63-
await waitFor(message)
64-
expect(screen.getByTestId('message')).toHaveTextContent(/HelloWorld/)
65-
})
66-
67-
test('findBy', async () => {
68-
render(<ComponentWithLoader />)
69-
await expect(screen.findByTestId('message')).resolves.toHaveTextContent(
70-
/HelloWorld/,
71-
)
72-
})
73-
})
82+
])(
83+
'it waits for the data to be loaded in a microtask using %s',
84+
(label, useTimers) => {
85+
beforeEach(() => {
86+
useTimers()
87+
})
88+
89+
afterEach(() => {
90+
jest.useRealTimers()
91+
})
92+
93+
const fetchAMessageInAMicrotask = () =>
94+
Promise.resolve({
95+
status: 200,
96+
json: () => Promise.resolve({title: 'Hello World'}),
97+
})
98+
99+
function ComponentWithMicrotaskLoader() {
100+
const [fetchState, setFetchState] = React.useState({fetching: true})
101+
102+
React.useEffect(() => {
103+
if (fetchState.fetching) {
104+
fetchAMessageInAMicrotask().then(res => {
105+
return (
106+
res
107+
.json()
108+
// By spec, the runtime can only yield back to the event loop once
109+
// the microtask queue is empty.
110+
// So we ensure that we actually wait for that as well before yielding back from `waitFor`.
111+
.then(data => data)
112+
.then(data => data)
113+
.then(data => data)
114+
.then(data => data)
115+
.then(data => data)
116+
.then(data => data)
117+
.then(data => data)
118+
.then(data => data)
119+
.then(data => data)
120+
.then(data => data)
121+
.then(data => data)
122+
.then(data => {
123+
setFetchState({todo: data.title, fetching: false})
124+
})
125+
)
126+
})
127+
}
128+
}, [fetchState])
129+
130+
if (fetchState.fetching) {
131+
return <p>Loading..</p>
132+
}
133+
134+
return (
135+
<div data-testid="message">Loaded this message: {fetchState.todo}</div>
136+
)
137+
}
138+
139+
test('waitForElementToBeRemoved', async () => {
140+
render(<ComponentWithMicrotaskLoader />)
141+
const loading = () => screen.getByText('Loading..')
142+
await waitForElementToBeRemoved(loading)
143+
expect(screen.getByTestId('message')).toHaveTextContent(/HelloWorld/)
144+
})
145+
146+
test('waitFor', async () => {
147+
render(<ComponentWithMicrotaskLoader />)
148+
await waitFor(() => {
149+
screen.getByText('Loading..')
150+
})
151+
await waitFor(() => {
152+
screen.getByText(/Loadedthismessage:/)
153+
})
154+
expect(screen.getByTestId('message')).toHaveTextContent(/HelloWorld/)
155+
})
156+
157+
test('findBy', async () => {
158+
render(<ComponentWithMicrotaskLoader />)
159+
await expect(screen.findByTestId('message')).resolves.toHaveTextContent(
160+
/HelloWorld/,
161+
)
162+
})
163+
},
164+
)

‎src/pure.js‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@ import act, {
1212
} from './act-compat'
1313
import {fireEvent} from './fire-event'
1414

15+
function jestFakeTimersAreEnabled() {
16+
/* istanbul ignore else */
17+
if (typeof jest !== 'undefined' && jest !== null) {
18+
return (
19+
// legacy timers
20+
setTimeout._isMockFunction === true || // modern timers
21+
// eslint-disable-next-line prefer-object-has-own -- No Object.hasOwn in all target environments we support.
22+
Object.prototype.hasOwnProperty.call(setTimeout, 'clock')
23+
)
24+
} // istanbul ignore next
25+
26+
return false
27+
}
28+
1529
configureDTL({
1630
unstable_advanceTimersWrapper: cb => {
1731
return act(cb)
@@ -23,7 +37,21 @@ configureDTL({
2337
const previousActEnvironment = getIsReactActEnvironment()
2438
setReactActEnvironment(false)
2539
try {
26-
return await cb()
40+
const result = await cb()
41+
// Drain microtask queue.
42+
// Otherwise we'll restore the previous act() environment, before we resolve the `waitFor` call.
43+
// The caller would have no chance to wrap the in-flight Promises in `act()`
44+
await new Promise(resolve => {
45+
setTimeout(() => {
46+
resolve()
47+
}, 0)
48+
49+
if (jestFakeTimersAreEnabled()) {
50+
jest.advanceTimersByTime(0)
51+
}
52+
})
53+
54+
return result
2755
} finally {
2856
setReactActEnvironment(previousActEnvironment)
2957
}

0 commit comments

Comments
(0)

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