Skip to content

Commit 930b2b0

Browse files
committed
Manual fixes
1 parent 7036392 commit 930b2b0

File tree

6 files changed

+135
-150
lines changed

6 files changed

+135
-150
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
@@ -33,55 +33,22 @@ function getIsReactActEnvironment() {
3333
return getGlobalThis().IS_REACT_ACT_ENVIRONMENT
3434
}
3535

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

83-
const act = withGlobalActEnvironment(reactAct)
84-
8552
export default act
8653
export {
8754
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: 49 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,7 @@ configureDTL({
5858
}
5959
},
6060
eventWrapper: cb => {
61-
let result
62-
act(() => {
63-
result = cb()
64-
})
65-
return result
61+
return act(cb)
6662
},
6763
})
6864

@@ -89,13 +85,13 @@ function wrapUiIfNeeded(innerElement, wrapperComponent) {
8985
: innerElement
9086
}
9187

92-
function createConcurrentRoot(
88+
async function createConcurrentRoot(
9389
container,
9490
{hydrate, ui, wrapper: WrapperComponent},
9591
) {
9692
let root
9793
if (hydrate) {
98-
act(() => {
94+
await act(() => {
9995
root = ReactDOMClient.hydrateRoot(
10096
container,
10197
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
@@ -116,45 +112,53 @@ function createConcurrentRoot(
116112
// Nothing to do since hydration happens when creating the root object.
117113
},
118114
render(element) {
119-
root.render(element)
115+
return act(() => {
116+
root.render(element)
117+
})
120118
},
121119
unmount() {
122-
root.unmount()
120+
return act(() => {
121+
root.unmount()
122+
})
123123
},
124124
}
125125
}
126126

127-
function createLegacyRoot(container) {
127+
async function createLegacyRoot(container) {
128128
return {
129129
hydrate(element) {
130-
ReactDOM.hydrate(element, container)
130+
return act(() => {
131+
ReactDOM.hydrate(element, container)
132+
})
131133
},
132134
render(element) {
133-
ReactDOM.render(element, container)
135+
return act(() => {
136+
ReactDOM.render(element, container)
137+
})
134138
},
135139
unmount() {
136-
ReactDOM.unmountComponentAtNode(container)
140+
return act(() => {
141+
ReactDOM.unmountComponentAtNode(container)
142+
})
137143
},
138144
}
139145
}
140146

141-
function renderRoot(
147+
async function renderRoot(
142148
ui,
143149
{baseElement, container, hydrate, queries, root, wrapper: WrapperComponent},
144150
) {
145-
act(() => {
146-
if (hydrate) {
147-
root.hydrate(
148-
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
149-
container,
150-
)
151-
} else {
152-
root.render(
153-
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
154-
container,
155-
)
156-
}
157-
})
151+
if (hydrate) {
152+
await root.hydrate(
153+
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
154+
container,
155+
)
156+
} else {
157+
await root.render(
158+
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
159+
container,
160+
)
161+
}
158162

159163
return {
160164
container,
@@ -166,12 +170,10 @@ function renderRoot(
166170
: // eslint-disable-next-line no-console,
167171
console.log(prettyDOM(el, maxLength, options)),
168172
unmount: () => {
169-
act(() => {
170-
root.unmount()
171-
})
173+
return root.unmount()
172174
},
173-
rerender: rerenderUi => {
174-
renderRoot(rerenderUi, {
175+
rerender: async rerenderUi => {
176+
await renderRoot(rerenderUi, {
175177
container,
176178
baseElement,
177179
root,
@@ -196,7 +198,7 @@ function renderRoot(
196198
}
197199
}
198200

199-
function render(
201+
async function render(
200202
ui,
201203
{
202204
container,
@@ -230,7 +232,7 @@ function render(
230232
// 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.
231233
if (!mountedContainers.has(container)) {
232234
const createRootImpl = legacyRoot ? createLegacyRoot : createConcurrentRoot
233-
root = createRootImpl(container, {hydrate, ui, wrapper})
235+
root = await createRootImpl(container, {hydrate, ui, wrapper})
234236

235237
mountedRootEntries.push({container, root})
236238
// we'll add it to the mounted containers regardless of whether it's actually
@@ -258,20 +260,22 @@ function render(
258260
})
259261
}
260262

261-
function cleanup() {
262-
mountedRootEntries.forEach(({root, container}) => {
263-
act(() => {
264-
root.unmount()
265-
})
266-
if (container.parentNode === document.body) {
267-
document.body.removeChild(container)
268-
}
269-
})
263+
async function cleanup() {
264+
await Promise.all(
265+
mountedRootEntries.map(async ({root, container}) => {
266+
await act(() => {
267+
root.unmount()
268+
})
269+
if (container.parentNode === document.body) {
270+
document.body.removeChild(container)
271+
}
272+
}),
273+
)
270274
mountedRootEntries.length = 0
271275
mountedContainers.clear()
272276
}
273277

274-
function renderHook(renderCallback, options = {}) {
278+
async function renderHook(renderCallback, options = {}) {
275279
const {initialProps, ...renderOptions} = options
276280

277281
if (renderOptions.legacyRoot && typeof ReactDOM.render !== 'function') {
@@ -296,7 +300,7 @@ function renderHook(renderCallback, options = {}) {
296300
return null
297301
}
298302

299-
const {rerender: baseRerender, unmount} = render(
303+
const {rerender: baseRerender, unmount} = await render(
300304
<TestComponent renderCallbackProps={initialProps} />,
301305
renderOptions,
302306
)

0 commit comments

Comments
 (0)