Skip to content

Commit 832381c

Browse files
eps1lonSebastian Silbermann
authored and
Sebastian Silbermann
committed
Manual fixes
1 parent 9812323 commit 832381c

File tree

6 files changed

+128
-142
lines changed

6 files changed

+128
-142
lines changed

src/__tests__/act.js

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,23 @@ beforeEach(() => {
55
global.IS_REACT_ACT_ENVIRONMENT = true
66
})
77

8-
test('render calls useEffect immediately', () => {
8+
test('render calls useEffect immediately', async () => {
99
const effectCb = jest.fn()
1010
function MyUselessComponent() {
1111
React.useEffect(effectCb)
1212
return null
1313
}
14-
render(<MyUselessComponent />)
14+
await render(<MyUselessComponent />)
1515
expect(effectCb).toHaveBeenCalledTimes(1)
1616
})
1717

1818
test('findByTestId returns the element', async () => {
1919
const ref = React.createRef()
20-
render(<div ref={ref} data-testid="foo" />)
20+
await render(<div ref={ref} data-testid="foo" />)
2121
expect(await screen.findByTestId('foo')).toBe(ref.current)
2222
})
2323

24-
test('fireEvent triggers useEffect calls', () => {
24+
test('fireEvent triggers useEffect calls', async () => {
2525
const effectCb = jest.fn()
2626
function Counter() {
2727
React.useEffect(effectCb)
@@ -30,32 +30,33 @@ test('fireEvent triggers useEffect calls', () => {
3030
}
3131
const {
3232
container: {firstChild: buttonNode},
33-
} = render(<Counter />)
33+
} = await render(<Counter />)
3434

3535
effectCb.mockClear()
36-
fireEvent.click(buttonNode)
36+
// eslint-disable-next-line testing-library/no-await-sync-events -- TODO: Remove lint rule.
37+
await fireEvent.click(buttonNode)
3738
expect(buttonNode).toHaveTextContent('1')
3839
expect(effectCb).toHaveBeenCalledTimes(1)
3940
})
4041

41-
test('calls to hydrate will run useEffects', () => {
42+
test('calls to hydrate will run useEffects', async () => {
4243
const effectCb = jest.fn()
4344
function MyUselessComponent() {
4445
React.useEffect(effectCb)
4546
return null
4647
}
47-
render(<MyUselessComponent />, {hydrate: true})
48+
await render(<MyUselessComponent />, {hydrate: true})
4849
expect(effectCb).toHaveBeenCalledTimes(1)
4950
})
5051

51-
test('cleans up IS_REACT_ACT_ENVIRONMENT if its callback throws', () => {
52+
test('cleans up IS_REACT_ACT_ENVIRONMENT if its callback throws', async () => {
5253
global.IS_REACT_ACT_ENVIRONMENT = false
5354

54-
expect(() =>
55+
await expect(() =>
5556
act(() => {
5657
throw new Error('threw')
5758
}),
58-
).toThrow('threw')
59+
).rejects.toThrow('threw')
5960

6061
expect(global.IS_REACT_ACT_ENVIRONMENT).toEqual(false)
6162
})

src/act-compat.js

Lines changed: 13 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -31,55 +31,22 @@ function getIsReactActEnvironment() {
3131
return getGlobalThis().IS_REACT_ACT_ENVIRONMENT
3232
}
3333

34-
function withGlobalActEnvironment(actImplementation) {
35-
return callback => {
36-
const previousActEnvironment = getIsReactActEnvironment()
37-
setIsReactActEnvironment(true)
38-
try {
39-
// The return value of `act` is always a thenable.
40-
let callbackNeedsToBeAwaited = false
41-
const actResult = actImplementation(() => {
42-
const result = callback()
43-
if (
44-
result !== null &&
45-
typeof result === 'object' &&
46-
typeof result.then === 'function'
47-
) {
48-
callbackNeedsToBeAwaited = true
49-
}
50-
return result
51-
})
52-
if (callbackNeedsToBeAwaited) {
53-
const thenable = actResult
54-
return {
55-
then: (resolve, reject) => {
56-
thenable.then(
57-
returnValue => {
58-
setIsReactActEnvironment(previousActEnvironment)
59-
resolve(returnValue)
60-
},
61-
error => {
62-
setIsReactActEnvironment(previousActEnvironment)
63-
reject(error)
64-
},
65-
)
66-
},
67-
}
68-
} else {
69-
setIsReactActEnvironment(previousActEnvironment)
70-
return actResult
71-
}
72-
} catch (error) {
73-
// Can't be a `finally {}` block since we don't know if we have to immediately restore IS_REACT_ACT_ENVIRONMENT
74-
// or if we have to await the callback first.
75-
setIsReactActEnvironment(previousActEnvironment)
76-
throw error
77-
}
34+
async function act(scope) {
35+
const previousActEnvironment = getIsReactActEnvironment()
36+
setIsReactActEnvironment(true)
37+
try {
38+
// scope passed to domAct needs to be `async` until React.act treats every scope as async.
39+
// We already enforce `await act()` (regardless of scope) to flush microtasks
40+
// inside the act scope.
41+
const result = await domAct(async () => {
42+
return scope()
43+
})
44+
return result
45+
} finally {
46+
setIsReactActEnvironment(previousActEnvironment)
7847
}
7948
}
8049

81-
const act = withGlobalActEnvironment(domAct)
82-
8350
export default act
8451
export {
8552
setIsReactActEnvironment as setReactActEnvironment,

src/index.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ if (typeof process === 'undefined' || !process.env?.RTL_SKIP_AUTO_CLEANUP) {
1010
// ignore teardown() in code coverage because Jest does not support it
1111
/* istanbul ignore else */
1212
if (typeof afterEach === 'function') {
13-
afterEach(() => {
14-
cleanup()
13+
afterEach(async () => {
14+
await cleanup()
1515
})
1616
} else if (typeof teardown === 'function') {
1717
// Block is guarded by `typeof` check.
1818
// eslint does not support `typeof` guards.
1919
// eslint-disable-next-line no-undef
20-
teardown(() => {
21-
cleanup()
20+
teardown(async () => {
21+
await cleanup()
2222
})
2323
}
2424

src/pure.js

Lines changed: 43 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,7 @@ configureDTL({
5757
}
5858
},
5959
eventWrapper: cb => {
60-
let result
61-
act(() => {
62-
result = cb()
63-
})
64-
return result
60+
return act(cb)
6561
},
6662
})
6763

@@ -76,13 +72,13 @@ const mountedContainers = new Set()
7672
*/
7773
const mountedRootEntries = []
7874

79-
function createConcurrentRoot(
75+
async function createConcurrentRoot(
8076
container,
8177
{hydrate, ui, wrapper: WrapperComponent},
8278
) {
8379
let root
8480
if (hydrate) {
85-
act(() => {
81+
await act(() => {
8682
root = ReactDOMClient.hydrateRoot(
8783
container,
8884
WrapperComponent ? React.createElement(WrapperComponent, null, ui) : ui,
@@ -103,29 +99,39 @@ function createConcurrentRoot(
10399
// Nothing to do since hydration happens when creating the root object.
104100
},
105101
render(element) {
106-
root.render(element)
102+
return act(() => {
103+
root.render(element)
104+
})
107105
},
108106
unmount() {
109-
root.unmount()
107+
return act(() => {
108+
root.unmount()
109+
})
110110
},
111111
}
112112
}
113113

114-
function createLegacyRoot(container) {
114+
async function createLegacyRoot(container) {
115115
return {
116116
hydrate(element) {
117-
ReactDOM.hydrate(element, container)
117+
return act(() => {
118+
ReactDOM.hydrate(element, container)
119+
})
118120
},
119121
render(element) {
120-
ReactDOM.render(element, container)
122+
return act(() => {
123+
ReactDOM.render(element, container)
124+
})
121125
},
122126
unmount() {
123-
ReactDOM.unmountComponentAtNode(container)
127+
return act(() => {
128+
ReactDOM.unmountComponentAtNode(container)
129+
})
124130
},
125131
}
126132
}
127133

128-
function renderRoot(
134+
async function renderRoot(
129135
ui,
130136
{baseElement, container, hydrate, queries, root, wrapper: WrapperComponent},
131137
) {
@@ -134,13 +140,11 @@ function renderRoot(
134140
? React.createElement(WrapperComponent, null, innerElement)
135141
: innerElement
136142

137-
act(() => {
138-
if (hydrate) {
139-
root.hydrate(wrapUiIfNeeded(ui), container)
140-
} else {
141-
root.render(wrapUiIfNeeded(ui), container)
142-
}
143-
})
143+
if (hydrate) {
144+
await root.hydrate(wrapUiIfNeeded(ui), container)
145+
} else {
146+
await root.render(wrapUiIfNeeded(ui), container)
147+
}
144148

145149
return {
146150
container,
@@ -152,12 +156,10 @@ function renderRoot(
152156
: // eslint-disable-next-line no-console,
153157
console.log(prettyDOM(el, maxLength, options)),
154158
unmount: () => {
155-
act(() => {
156-
root.unmount()
157-
})
159+
return root.unmount()
158160
},
159-
rerender: rerenderUi => {
160-
renderRoot(wrapUiIfNeeded(rerenderUi), {
161+
rerender: async rerenderUi => {
162+
await renderRoot(wrapUiIfNeeded(rerenderUi), {
161163
container,
162164
baseElement,
163165
root,
@@ -181,7 +183,7 @@ function renderRoot(
181183
}
182184
}
183185

184-
function render(
186+
async function render(
185187
ui,
186188
{
187189
container,
@@ -205,7 +207,7 @@ function render(
205207
// eslint-disable-next-line no-negated-condition -- we want to map the evolution of this over time. The root is created first. Only later is it re-used so we don't want to read the case that happens later first.
206208
if (!mountedContainers.has(container)) {
207209
const createRootImpl = legacyRoot ? createLegacyRoot : createConcurrentRoot
208-
root = createRootImpl(container, {hydrate, ui, wrapper})
210+
root = await createRootImpl(container, {hydrate, ui, wrapper})
209211

210212
mountedRootEntries.push({container, root})
211213
// we'll add it to the mounted containers regardless of whether it's actually
@@ -233,20 +235,22 @@ function render(
233235
})
234236
}
235237

236-
function cleanup() {
237-
mountedRootEntries.forEach(({root, container}) => {
238-
act(() => {
239-
root.unmount()
240-
})
241-
if (container.parentNode === document.body) {
242-
document.body.removeChild(container)
243-
}
244-
})
238+
async function cleanup() {
239+
await Promise.all(
240+
mountedRootEntries.map(async ({root, container}) => {
241+
await act(() => {
242+
root.unmount()
243+
})
244+
if (container.parentNode === document.body) {
245+
document.body.removeChild(container)
246+
}
247+
}),
248+
)
245249
mountedRootEntries.length = 0
246250
mountedContainers.clear()
247251
}
248252

249-
function renderHook(renderCallback, options = {}) {
253+
async function renderHook(renderCallback, options = {}) {
250254
const {initialProps, ...renderOptions} = options
251255
const result = React.createRef()
252256

@@ -260,7 +264,7 @@ function renderHook(renderCallback, options = {}) {
260264
return null
261265
}
262266

263-
const {rerender: baseRerender, unmount} = render(
267+
const {rerender: baseRerender, unmount} = await render(
264268
<TestComponent renderCallbackProps={initialProps} />,
265269
renderOptions,
266270
)

0 commit comments

Comments
 (0)