Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 1bb540d

Browse files
committedNov 27, 2024··
Use dedicated entrypoint instead
Less renaming to-do
1 parent 571fbc8 commit 1bb540d

13 files changed

+768
-309
lines changed
 

Diff for: ‎async.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './types/pure-async'

Diff for: ‎async.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// makes it so people can import from '@testing-library/react/async'
2+
module.exports = require('./dist/async')

Diff for: ‎pure-async.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './types/pure-async'

Diff for: ‎pure-async.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// makes it so people can import from '@testing-library/react/pure-async'
2+
module.exports = require('./dist/pure-async')

Diff for: ‎src/__tests__/async.js

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// TODO: Upstream that the rule should check import source
2+
/* eslint-disable testing-library/no-await-sync-events */
3+
/* eslint-disable jest/no-conditional-in-test */
4+
/* eslint-disable jest/no-if */
5+
import * as React from 'react'
6+
import {act, render, fireEvent} from '../async'
7+
8+
test('async data requires async APIs', async () => {
9+
let resolve
10+
const promise = new Promise(_resolve => {
11+
resolve = _resolve
12+
})
13+
14+
function Component() {
15+
const value = React.use(promise)
16+
return <div>{value}</div>
17+
}
18+
19+
const {container} = await render(
20+
<React.Suspense fallback="loading...">
21+
<Component />
22+
</React.Suspense>,
23+
)
24+
25+
expect(container).toHaveTextContent('loading...')
26+
27+
await act(async () => {
28+
resolve('Hello, Dave!')
29+
})
30+
31+
expect(container).toHaveTextContent('Hello, Dave!')
32+
})
33+
34+
test('async fireEvent', async () => {
35+
let resolve
36+
function Component() {
37+
const [promise, setPromise] = React.useState('initial')
38+
const value = typeof promise === 'string' ? promise : React.use(promise)
39+
return (
40+
<button
41+
onClick={() =>
42+
setPromise(
43+
new Promise(_resolve => {
44+
resolve = _resolve
45+
}),
46+
)
47+
}
48+
>
49+
Value: {value}
50+
</button>
51+
)
52+
}
53+
54+
const {container} = await render(
55+
<React.Suspense fallback="loading...">
56+
<Component />
57+
</React.Suspense>,
58+
)
59+
60+
expect(container).toHaveTextContent('Value: initial')
61+
62+
await fireEvent.click(container.querySelector('button'))
63+
64+
expect(container).toHaveTextContent('loading...')
65+
66+
await act(() => {
67+
resolve('Hello, Dave!')
68+
})
69+
70+
expect(container).toHaveTextContent('Hello, Dave!')
71+
})

Diff for: ‎src/__tests__/renderAsync.js

-25
This file was deleted.

Diff for: ‎src/act-compat.js

-14
Original file line numberDiff line numberDiff line change
@@ -82,22 +82,8 @@ function withGlobalActEnvironment(actImplementation) {
8282

8383
const act = withGlobalActEnvironment(reactAct)
8484

85-
async function actAsync(scope) {
86-
const previousActEnvironment = getIsReactActEnvironment()
87-
setIsReactActEnvironment(true)
88-
try {
89-
// React.act isn't async yet so we need to force it.
90-
return await reactAct(async () => {
91-
scope()
92-
})
93-
} finally {
94-
setIsReactActEnvironment(previousActEnvironment)
95-
}
96-
}
97-
9885
export default act
9986
export {
100-
actAsync,
10187
setIsReactActEnvironment as setReactActEnvironment,
10288
getIsReactActEnvironment,
10389
}

Diff for: ‎src/async.js

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/* istanbul ignore file */
2+
import {getIsReactActEnvironment, setReactActEnvironment} from './act-compat'
3+
import {cleanup} from './pure-async'
4+
5+
// if we're running in a test runner that supports afterEach
6+
// or teardown then we'll automatically run cleanup afterEach test
7+
// this ensures that tests run in isolation from each other
8+
// if you don't like this then either import the `pure` module
9+
// or set the RTL_SKIP_AUTO_CLEANUP env variable to 'true'.
10+
if (typeof process === 'undefined' || !process.env?.RTL_SKIP_AUTO_CLEANUP) {
11+
// ignore teardown() in code coverage because Jest does not support it
12+
/* istanbul ignore else */
13+
if (typeof afterEach === 'function') {
14+
afterEach(async () => {
15+
await cleanup()
16+
})
17+
} else if (typeof teardown === 'function') {
18+
// Block is guarded by `typeof` check.
19+
// eslint does not support `typeof` guards.
20+
// eslint-disable-next-line no-undef
21+
teardown(async () => {
22+
await cleanup()
23+
})
24+
}
25+
26+
// No test setup with other test runners available
27+
/* istanbul ignore else */
28+
if (typeof beforeAll === 'function' && typeof afterAll === 'function') {
29+
// This matches the behavior of React < 18.
30+
let previousIsReactActEnvironment = getIsReactActEnvironment()
31+
beforeAll(() => {
32+
previousIsReactActEnvironment = getIsReactActEnvironment()
33+
setReactActEnvironment(true)
34+
})
35+
36+
afterAll(() => {
37+
setReactActEnvironment(previousIsReactActEnvironment)
38+
})
39+
}
40+
}
41+
42+
export * from './pure-async'

Diff for: ‎src/fire-event-async.js

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/* istanbul ignore file */
2+
import {fireEvent as dtlFireEvent} from '@testing-library/dom'
3+
4+
// react-testing-library's version of fireEvent will call
5+
// dom-testing-library's version of fireEvent. The reason
6+
// we make this distinction however is because we have
7+
// a few extra events that work a bit differently
8+
const fireEvent = (...args) => dtlFireEvent(...args)
9+
10+
Object.keys(dtlFireEvent).forEach(key => {
11+
fireEvent[key] = (...args) => dtlFireEvent[key](...args)
12+
})
13+
14+
// React event system tracks native mouseOver/mouseOut events for
15+
// running onMouseEnter/onMouseLeave handlers
16+
// @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/EnterLeaveEventPlugin.js#L24-L31
17+
const mouseEnter = fireEvent.mouseEnter
18+
const mouseLeave = fireEvent.mouseLeave
19+
fireEvent.mouseEnter = async (...args) => {
20+
await mouseEnter(...args)
21+
return fireEvent.mouseOver(...args)
22+
}
23+
fireEvent.mouseLeave = async (...args) => {
24+
await mouseLeave(...args)
25+
return fireEvent.mouseOut(...args)
26+
}
27+
28+
const pointerEnter = fireEvent.pointerEnter
29+
const pointerLeave = fireEvent.pointerLeave
30+
fireEvent.pointerEnter = async (...args) => {
31+
await pointerEnter(...args)
32+
return fireEvent.pointerOver(...args)
33+
}
34+
fireEvent.pointerLeave = async (...args) => {
35+
await pointerLeave(...args)
36+
return fireEvent.pointerOut(...args)
37+
}
38+
39+
const select = fireEvent.select
40+
fireEvent.select = async (node, init) => {
41+
await select(node, init)
42+
// React tracks this event only on focused inputs
43+
node.focus()
44+
45+
// React creates this event when one of the following native events happens
46+
// - contextMenu
47+
// - mouseUp
48+
// - dragEnd
49+
// - keyUp
50+
// - keyDown
51+
// so we can use any here
52+
// @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/SelectEventPlugin.js#L203-L224
53+
await fireEvent.keyUp(node, init)
54+
}
55+
56+
// React event system tracks native focusout/focusin events for
57+
// running blur/focus handlers
58+
// @link https://github.com/facebook/react/pull/19186
59+
const blur = fireEvent.blur
60+
const focus = fireEvent.focus
61+
fireEvent.blur = async (...args) => {
62+
await fireEvent.focusOut(...args)
63+
return blur(...args)
64+
}
65+
fireEvent.focus = async (...args) => {
66+
await fireEvent.focusIn(...args)
67+
return focus(...args)
68+
}
69+
70+
export {fireEvent}

Diff for: ‎src/pure-async.js

+330
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
/* istanbul ignore file */
2+
import * as React from 'react'
3+
import ReactDOM from 'react-dom'
4+
import * as ReactDOMClient from 'react-dom/client'
5+
import {
6+
getQueriesForElement,
7+
prettyDOM,
8+
configure as configureDTL,
9+
} from '@testing-library/dom'
10+
import {getIsReactActEnvironment, setReactActEnvironment} from './act-compat'
11+
import {fireEvent} from './fire-event'
12+
import {getConfig, configure} from './config'
13+
14+
async function act(scope) {
15+
const previousActEnvironment = getIsReactActEnvironment()
16+
setReactActEnvironment(true)
17+
try {
18+
// React.act isn't async yet so we need to force it.
19+
return await React.act(async () => {
20+
scope()
21+
})
22+
} finally {
23+
setReactActEnvironment(previousActEnvironment)
24+
}
25+
}
26+
27+
function jestFakeTimersAreEnabled() {
28+
/* istanbul ignore else */
29+
if (typeof jest !== 'undefined' && jest !== null) {
30+
return (
31+
// legacy timers
32+
setTimeout._isMockFunction === true || // modern timers
33+
// eslint-disable-next-line prefer-object-has-own -- No Object.hasOwn in all target environments we support.
34+
Object.prototype.hasOwnProperty.call(setTimeout, 'clock')
35+
)
36+
} // istanbul ignore next
37+
38+
return false
39+
}
40+
41+
configureDTL({
42+
unstable_advanceTimersWrapper: cb => {
43+
return act(cb)
44+
},
45+
// We just want to run `waitFor` without IS_REACT_ACT_ENVIRONMENT
46+
// But that's not necessarily how `asyncWrapper` is used since it's a public method.
47+
// Let's just hope nobody else is using it.
48+
asyncWrapper: async cb => {
49+
const previousActEnvironment = getIsReactActEnvironment()
50+
setReactActEnvironment(false)
51+
try {
52+
const result = await cb()
53+
// Drain microtask queue.
54+
// Otherwise we'll restore the previous act() environment, before we resolve the `waitFor` call.
55+
// The caller would have no chance to wrap the in-flight Promises in `act()`
56+
await new Promise(resolve => {
57+
setTimeout(() => {
58+
resolve()
59+
}, 0)
60+
61+
if (jestFakeTimersAreEnabled()) {
62+
jest.advanceTimersByTime(0)
63+
}
64+
})
65+
66+
return result
67+
} finally {
68+
setReactActEnvironment(previousActEnvironment)
69+
}
70+
},
71+
eventWrapper: async cb => {
72+
let result
73+
await act(() => {
74+
result = cb()
75+
})
76+
return result
77+
},
78+
})
79+
80+
// Ideally we'd just use a WeakMap where containers are keys and roots are values.
81+
// We use two variables so that we can bail out in constant time when we render with a new container (most common use case)
82+
/**
83+
* @type {Set<import('react-dom').Container>}
84+
*/
85+
const mountedContainers = new Set()
86+
/**
87+
* @type Array<{container: import('react-dom').Container, root: ReturnType<typeof createConcurrentRoot>}>
88+
*/
89+
const mountedRootEntries = []
90+
91+
function strictModeIfNeeded(innerElement) {
92+
return getConfig().reactStrictMode
93+
? React.createElement(React.StrictMode, null, innerElement)
94+
: innerElement
95+
}
96+
97+
function wrapUiIfNeeded(innerElement, wrapperComponent) {
98+
return wrapperComponent
99+
? React.createElement(wrapperComponent, null, innerElement)
100+
: innerElement
101+
}
102+
103+
async function createConcurrentRoot(
104+
container,
105+
{hydrate, ui, wrapper: WrapperComponent},
106+
) {
107+
let root
108+
if (hydrate) {
109+
await act(() => {
110+
root = ReactDOMClient.hydrateRoot(
111+
container,
112+
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
113+
)
114+
})
115+
} else {
116+
root = ReactDOMClient.createRoot(container)
117+
}
118+
119+
return {
120+
hydrate() {
121+
/* istanbul ignore if */
122+
if (!hydrate) {
123+
throw new Error(
124+
'Attempted to hydrate a non-hydrateable root. This is a bug in `@testing-library/react`.',
125+
)
126+
}
127+
// Nothing to do since hydration happens when creating the root object.
128+
},
129+
render(element) {
130+
root.render(element)
131+
},
132+
unmount() {
133+
root.unmount()
134+
},
135+
}
136+
}
137+
138+
function createLegacyRoot(container) {
139+
return {
140+
hydrate(element) {
141+
ReactDOM.hydrate(element, container)
142+
},
143+
render(element) {
144+
ReactDOM.render(element, container)
145+
},
146+
unmount() {
147+
ReactDOM.unmountComponentAtNode(container)
148+
},
149+
}
150+
}
151+
152+
async function renderRootAsync(
153+
ui,
154+
{baseElement, container, hydrate, queries, root, wrapper: WrapperComponent},
155+
) {
156+
await act(() => {
157+
if (hydrate) {
158+
root.hydrate(
159+
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
160+
container,
161+
)
162+
} else {
163+
root.render(
164+
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
165+
container,
166+
)
167+
}
168+
})
169+
170+
return {
171+
container,
172+
baseElement,
173+
debug: (el = baseElement, maxLength, options) =>
174+
Array.isArray(el)
175+
? // eslint-disable-next-line no-console
176+
el.forEach(e => console.log(prettyDOM(e, maxLength, options)))
177+
: // eslint-disable-next-line no-console,
178+
console.log(prettyDOM(el, maxLength, options)),
179+
unmount: async () => {
180+
await act(() => {
181+
root.unmount()
182+
})
183+
},
184+
rerender: async rerenderUi => {
185+
await renderRootAsync(rerenderUi, {
186+
container,
187+
baseElement,
188+
root,
189+
wrapper: WrapperComponent,
190+
})
191+
// Intentionally do not return anything to avoid unnecessarily complicating the API.
192+
// folks can use all the same utilities we return in the first place that are bound to the container
193+
},
194+
asFragment: () => {
195+
/* istanbul ignore else (old jsdom limitation) */
196+
if (typeof document.createRange === 'function') {
197+
return document
198+
.createRange()
199+
.createContextualFragment(container.innerHTML)
200+
} else {
201+
const template = document.createElement('template')
202+
template.innerHTML = container.innerHTML
203+
return template.content
204+
}
205+
},
206+
...getQueriesForElement(baseElement, queries),
207+
}
208+
}
209+
210+
async function render(
211+
ui,
212+
{
213+
container,
214+
baseElement = container,
215+
legacyRoot = false,
216+
queries,
217+
hydrate = false,
218+
wrapper,
219+
} = {},
220+
) {
221+
if (legacyRoot && typeof ReactDOM.render !== 'function') {
222+
const error = new Error(
223+
'`legacyRoot: true` is not supported in this version of React. ' +
224+
'If your app runs React 19 or later, you should remove this flag. ' +
225+
'If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.',
226+
)
227+
Error.captureStackTrace(error, render)
228+
throw error
229+
}
230+
231+
if (!baseElement) {
232+
// default to document.body instead of documentElement to avoid output of potentially-large
233+
// head elements (such as JSS style blocks) in debug output
234+
baseElement = document.body
235+
}
236+
if (!container) {
237+
container = baseElement.appendChild(document.createElement('div'))
238+
}
239+
240+
let root
241+
// 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.
242+
if (!mountedContainers.has(container)) {
243+
const createRootImpl = legacyRoot ? createLegacyRoot : createConcurrentRoot
244+
root = await createRootImpl(container, {hydrate, ui, wrapper})
245+
246+
mountedRootEntries.push({container, root})
247+
// we'll add it to the mounted containers regardless of whether it's actually
248+
// added to document.body so the cleanup method works regardless of whether
249+
// they're passing us a custom container or not.
250+
mountedContainers.add(container)
251+
} else {
252+
mountedRootEntries.forEach(rootEntry => {
253+
// Else is unreachable since `mountedContainers` has the `container`.
254+
// Only reachable if one would accidentally add the container to `mountedContainers` but not the root to `mountedRootEntries`
255+
/* istanbul ignore else */
256+
if (rootEntry.container === container) {
257+
root = rootEntry.root
258+
}
259+
})
260+
}
261+
262+
return renderRootAsync(ui, {
263+
container,
264+
baseElement,
265+
queries,
266+
hydrate,
267+
wrapper,
268+
root,
269+
})
270+
}
271+
272+
async function cleanup() {
273+
for (const {root, container} of mountedRootEntries) {
274+
// eslint-disable-next-line no-await-in-loop -- act calls can't overlap
275+
await act(() => {
276+
root.unmount()
277+
})
278+
if (container.parentNode === document.body) {
279+
document.body.removeChild(container)
280+
}
281+
}
282+
283+
mountedRootEntries.length = 0
284+
mountedContainers.clear()
285+
}
286+
287+
async function renderHook(renderCallback, options = {}) {
288+
const {initialProps, ...renderOptions} = options
289+
290+
if (renderOptions.legacyRoot && typeof ReactDOM.render !== 'function') {
291+
const error = new Error(
292+
'`legacyRoot: true` is not supported in this version of React. ' +
293+
'If your app runs React 19 or later, you should remove this flag. ' +
294+
'If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.',
295+
)
296+
Error.captureStackTrace(error, renderHook)
297+
throw error
298+
}
299+
300+
const result = React.createRef()
301+
302+
function TestComponent({renderCallbackProps}) {
303+
const pendingResult = renderCallback(renderCallbackProps)
304+
305+
React.useEffect(() => {
306+
result.current = pendingResult
307+
})
308+
309+
return null
310+
}
311+
312+
const {rerender: baseRerender, unmount} = await render(
313+
<TestComponent renderCallbackProps={initialProps} />,
314+
renderOptions,
315+
)
316+
317+
function rerender(rerenderCallbackProps) {
318+
return baseRerender(
319+
<TestComponent renderCallbackProps={rerenderCallbackProps} />,
320+
)
321+
}
322+
323+
return {result, rerender, unmount}
324+
}
325+
326+
// just re-export everything from dom-testing-library
327+
export * from '@testing-library/dom'
328+
export {render, renderHook, cleanup, act, fireEvent, getConfig, configure}
329+
330+
/* eslint func-name-matching:0 */

Diff for: ‎src/pure.js

+1-189
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
configure as configureDTL,
88
} from '@testing-library/dom'
99
import act, {
10-
actAsync,
1110
getIsReactActEnvironment,
1211
setReactActEnvironment,
1312
} from './act-compat'
@@ -197,64 +196,6 @@ function renderRoot(
197196
}
198197
}
199198

200-
async function renderRootAsync(
201-
ui,
202-
{baseElement, container, hydrate, queries, root, wrapper: WrapperComponent},
203-
) {
204-
await actAsync(() => {
205-
if (hydrate) {
206-
root.hydrate(
207-
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
208-
container,
209-
)
210-
} else {
211-
root.render(
212-
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
213-
container,
214-
)
215-
}
216-
})
217-
218-
return {
219-
container,
220-
baseElement,
221-
debug: (el = baseElement, maxLength, options) =>
222-
Array.isArray(el)
223-
? // eslint-disable-next-line no-console
224-
el.forEach(e => console.log(prettyDOM(e, maxLength, options)))
225-
: // eslint-disable-next-line no-console,
226-
console.log(prettyDOM(el, maxLength, options)),
227-
unmount: async () => {
228-
await actAsync(() => {
229-
root.unmount()
230-
})
231-
},
232-
rerender: async rerenderUi => {
233-
await renderRootAsync(rerenderUi, {
234-
container,
235-
baseElement,
236-
root,
237-
wrapper: WrapperComponent,
238-
})
239-
// Intentionally do not return anything to avoid unnecessarily complicating the API.
240-
// folks can use all the same utilities we return in the first place that are bound to the container
241-
},
242-
asFragment: () => {
243-
/* istanbul ignore else (old jsdom limitation) */
244-
if (typeof document.createRange === 'function') {
245-
return document
246-
.createRange()
247-
.createContextualFragment(container.innerHTML)
248-
} else {
249-
const template = document.createElement('template')
250-
template.innerHTML = container.innerHTML
251-
return template.content
252-
}
253-
},
254-
...getQueriesForElement(baseElement, queries),
255-
}
256-
}
257-
258199
function render(
259200
ui,
260201
{
@@ -317,68 +258,6 @@ function render(
317258
})
318259
}
319260

320-
function renderAsync(
321-
ui,
322-
{
323-
container,
324-
baseElement = container,
325-
legacyRoot = false,
326-
queries,
327-
hydrate = false,
328-
wrapper,
329-
} = {},
330-
) {
331-
if (legacyRoot && typeof ReactDOM.render !== 'function') {
332-
const error = new Error(
333-
'`legacyRoot: true` is not supported in this version of React. ' +
334-
'If your app runs React 19 or later, you should remove this flag. ' +
335-
'If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.',
336-
)
337-
Error.captureStackTrace(error, render)
338-
throw error
339-
}
340-
341-
if (!baseElement) {
342-
// default to document.body instead of documentElement to avoid output of potentially-large
343-
// head elements (such as JSS style blocks) in debug output
344-
baseElement = document.body
345-
}
346-
if (!container) {
347-
container = baseElement.appendChild(document.createElement('div'))
348-
}
349-
350-
let root
351-
// 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.
352-
if (!mountedContainers.has(container)) {
353-
const createRootImpl = legacyRoot ? createLegacyRoot : createConcurrentRoot
354-
root = createRootImpl(container, {hydrate, ui, wrapper})
355-
356-
mountedRootEntries.push({container, root})
357-
// we'll add it to the mounted containers regardless of whether it's actually
358-
// added to document.body so the cleanup method works regardless of whether
359-
// they're passing us a custom container or not.
360-
mountedContainers.add(container)
361-
} else {
362-
mountedRootEntries.forEach(rootEntry => {
363-
// Else is unreachable since `mountedContainers` has the `container`.
364-
// Only reachable if one would accidentally add the container to `mountedContainers` but not the root to `mountedRootEntries`
365-
/* istanbul ignore else */
366-
if (rootEntry.container === container) {
367-
root = rootEntry.root
368-
}
369-
})
370-
}
371-
372-
return renderRootAsync(ui, {
373-
container,
374-
baseElement,
375-
queries,
376-
hydrate,
377-
wrapper,
378-
root,
379-
})
380-
}
381-
382261
function cleanup() {
383262
mountedRootEntries.forEach(({root, container}) => {
384263
act(() => {
@@ -392,21 +271,6 @@ function cleanup() {
392271
mountedContainers.clear()
393272
}
394273

395-
async function cleanupAsync() {
396-
for (const {root, container} of mountedRootEntries) {
397-
// eslint-disable-next-line no-await-in-loop -- act calls can't overlap
398-
await actAsync(() => {
399-
root.unmount()
400-
})
401-
if (container.parentNode === document.body) {
402-
document.body.removeChild(container)
403-
}
404-
}
405-
406-
mountedRootEntries.length = 0
407-
mountedContainers.clear()
408-
}
409-
410274
function renderHook(renderCallback, options = {}) {
411275
const {initialProps, ...renderOptions} = options
412276

@@ -446,60 +310,8 @@ function renderHook(renderCallback, options = {}) {
446310
return {result, rerender, unmount}
447311
}
448312

449-
async function renderHookAsync(renderCallback, options = {}) {
450-
const {initialProps, ...renderOptions} = options
451-
452-
if (renderOptions.legacyRoot && typeof ReactDOM.render !== 'function') {
453-
const error = new Error(
454-
'`legacyRoot: true` is not supported in this version of React. ' +
455-
'If your app runs React 19 or later, you should remove this flag. ' +
456-
'If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.',
457-
)
458-
Error.captureStackTrace(error, renderHookAsync)
459-
throw error
460-
}
461-
462-
const result = React.createRef()
463-
464-
function TestComponent({renderCallbackProps}) {
465-
const pendingResult = renderCallback(renderCallbackProps)
466-
467-
React.useEffect(() => {
468-
result.current = pendingResult
469-
})
470-
471-
return null
472-
}
473-
474-
const {rerender: baseRerender, unmount} = await renderAsync(
475-
<TestComponent renderCallbackProps={initialProps} />,
476-
renderOptions,
477-
)
478-
479-
function rerender(rerenderCallbackProps) {
480-
return baseRerender(
481-
<TestComponent renderCallbackProps={rerenderCallbackProps} />,
482-
)
483-
}
484-
485-
return {result, rerender, unmount}
486-
}
487-
488313
// just re-export everything from dom-testing-library
489314
export * from '@testing-library/dom'
490-
export {
491-
render,
492-
renderAsync,
493-
renderHook,
494-
renderHookAsync,
495-
cleanup,
496-
cleanupAsync,
497-
act,
498-
actAsync,
499-
fireEvent,
500-
// TODO: fireEventAsync
501-
getConfig,
502-
configure,
503-
}
315+
export {render, renderHook, cleanup, act, fireEvent, getConfig, configure}
504316

505317
/* eslint func-name-matching:0 */

Diff for: ‎types/index.d.ts

-81
Original file line numberDiff line numberDiff line change
@@ -46,27 +46,6 @@ export type RenderResult<
4646
asFragment: () => DocumentFragment
4747
} & {[P in keyof Q]: BoundFunction<Q[P]>}
4848

49-
export type RenderAsyncResult<
50-
Q extends Queries = typeof queries,
51-
Container extends RendererableContainer | HydrateableContainer = HTMLElement,
52-
BaseElement extends RendererableContainer | HydrateableContainer = Container,
53-
> = {
54-
container: Container
55-
baseElement: BaseElement
56-
debug: (
57-
baseElement?:
58-
| RendererableContainer
59-
| HydrateableContainer
60-
| Array<RendererableContainer | HydrateableContainer>
61-
| undefined,
62-
maxLength?: number | undefined,
63-
options?: prettyFormat.OptionsReceived | undefined,
64-
) => void
65-
rerender: (ui: React.ReactNode) => Promise<void>
66-
unmount: () => Promise<void>
67-
asFragment: () => DocumentFragment
68-
} & {[P in keyof Q]: BoundFunction<Q[P]>}
69-
7049
/** @deprecated */
7150
export type BaseRenderOptions<
7251
Q extends Queries,
@@ -173,22 +152,6 @@ export function render(
173152
options?: Omit<RenderOptions, 'queries'> | undefined,
174153
): RenderResult
175154

176-
/**
177-
* Render into a container which is appended to document.body. It should be used with cleanup.
178-
*/
179-
export function renderAsync<
180-
Q extends Queries = typeof queries,
181-
Container extends RendererableContainer | HydrateableContainer = HTMLElement,
182-
BaseElement extends RendererableContainer | HydrateableContainer = Container,
183-
>(
184-
ui: React.ReactNode,
185-
options: RenderOptions<Q, Container, BaseElement>,
186-
): Promise<RenderAsyncResult<Q, Container, BaseElement>>
187-
export function renderAsync(
188-
ui: React.ReactNode,
189-
options?: Omit<RenderOptions, 'queries'> | undefined,
190-
): Promise<RenderAsyncResult>
191-
192155
export interface RenderHookResult<Result, Props> {
193156
/**
194157
* Triggers a re-render. The props will be passed to your renderHook callback.
@@ -211,28 +174,6 @@ export interface RenderHookResult<Result, Props> {
211174
unmount: () => void
212175
}
213176

214-
export interface RenderHookAsyncResult<Result, Props> {
215-
/**
216-
* Triggers a re-render. The props will be passed to your renderHook callback.
217-
*/
218-
rerender: (props?: Props) => Promise<void>
219-
/**
220-
* This is a stable reference to the latest value returned by your renderHook
221-
* callback
222-
*/
223-
result: {
224-
/**
225-
* The value returned by your renderHook callback
226-
*/
227-
current: Result
228-
}
229-
/**
230-
* Unmounts the test component. This is useful for when you need to test
231-
* any cleanup your useEffects have.
232-
*/
233-
unmount: () => Promise<void>
234-
}
235-
236177
/** @deprecated */
237178
export type BaseRenderHookOptions<
238179
Props,
@@ -301,31 +242,11 @@ export function renderHook<
301242
options?: RenderHookOptions<Props, Q, Container, BaseElement> | undefined,
302243
): RenderHookResult<Result, Props>
303244

304-
/**
305-
* Allows you to render a hook within a test React component without having to
306-
* create that component yourself.
307-
*/
308-
export function renderHookAsync<
309-
Result,
310-
Props,
311-
Q extends Queries = typeof queries,
312-
Container extends RendererableContainer | HydrateableContainer = HTMLElement,
313-
BaseElement extends RendererableContainer | HydrateableContainer = Container,
314-
>(
315-
render: (initialProps: Props) => Result,
316-
options?: RenderHookOptions<Props, Q, Container, BaseElement> | undefined,
317-
): Promise<RenderHookResult<Result, Props>>
318-
319245
/**
320246
* Unmounts React trees that were mounted with render.
321247
*/
322248
export function cleanup(): void
323249

324-
/**
325-
* Unmounts React trees that were mounted with render.
326-
*/
327-
export function cleanupAsync(): Promise<void>
328-
329250
/**
330251
* Simply calls React.act(cb)
331252
* If that's not available (older version of react) then it
@@ -335,5 +256,3 @@ export function cleanupAsync(): Promise<void>
335256
export const act: 0 extends 1 & typeof reactAct
336257
? typeof reactDeprecatedAct
337258
: typeof reactAct
338-
339-
export function actAsync(scope: () => void | Promise<void>): Promise<void>

Diff for: ‎types/pure-async.d.ts

+248
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
// TypeScript Version: 3.8
2+
// copy of ./index.d.ts but async
3+
import * as ReactDOMClient from 'react-dom/client'
4+
import {
5+
queries,
6+
Queries,
7+
BoundFunction,
8+
prettyFormat,
9+
Config as ConfigDTL,
10+
} from '@testing-library/dom'
11+
12+
export * from '@testing-library/dom'
13+
14+
export interface Config extends ConfigDTL {
15+
reactStrictMode: boolean
16+
}
17+
18+
export interface ConfigFn {
19+
(existingConfig: Config): Partial<Config>
20+
}
21+
22+
export function configure(configDelta: ConfigFn | Partial<Config>): void
23+
24+
export function getConfig(): Config
25+
26+
export type RenderResult<
27+
Q extends Queries = typeof queries,
28+
Container extends RendererableContainer | HydrateableContainer = HTMLElement,
29+
BaseElement extends RendererableContainer | HydrateableContainer = Container,
30+
> = {
31+
container: Container
32+
baseElement: BaseElement
33+
debug: (
34+
baseElement?:
35+
| RendererableContainer
36+
| HydrateableContainer
37+
| Array<RendererableContainer | HydrateableContainer>
38+
| undefined,
39+
maxLength?: number | undefined,
40+
options?: prettyFormat.OptionsReceived | undefined,
41+
) => void
42+
rerender: (ui: React.ReactNode) => Promise<void>
43+
unmount: () => Promise<void>
44+
asFragment: () => DocumentFragment
45+
} & {[P in keyof Q]: BoundFunction<Q[P]>}
46+
47+
/** @deprecated */
48+
export type BaseRenderOptions<
49+
Q extends Queries,
50+
Container extends RendererableContainer | HydrateableContainer,
51+
BaseElement extends RendererableContainer | HydrateableContainer,
52+
> = RenderOptions<Q, Container, BaseElement>
53+
54+
type RendererableContainer = ReactDOMClient.Container
55+
type HydrateableContainer = Parameters<typeof ReactDOMClient['hydrateRoot']>[0]
56+
/** @deprecated */
57+
export interface ClientRenderOptions<
58+
Q extends Queries,
59+
Container extends RendererableContainer,
60+
BaseElement extends RendererableContainer = Container,
61+
> extends BaseRenderOptions<Q, Container, BaseElement> {
62+
/**
63+
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
64+
* rendering and use ReactDOM.hydrate to mount your components.
65+
*
66+
* @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
67+
*/
68+
hydrate?: false | undefined
69+
}
70+
/** @deprecated */
71+
export interface HydrateOptions<
72+
Q extends Queries,
73+
Container extends HydrateableContainer,
74+
BaseElement extends HydrateableContainer = Container,
75+
> extends BaseRenderOptions<Q, Container, BaseElement> {
76+
/**
77+
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
78+
* rendering and use ReactDOM.hydrate to mount your components.
79+
*
80+
* @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
81+
*/
82+
hydrate: true
83+
}
84+
85+
export interface RenderOptions<
86+
Q extends Queries = typeof queries,
87+
Container extends RendererableContainer | HydrateableContainer = HTMLElement,
88+
BaseElement extends RendererableContainer | HydrateableContainer = Container,
89+
> {
90+
/**
91+
* By default, React Testing Library will create a div and append that div to the document.body. Your React component will be rendered in the created div. If you provide your own HTMLElement container via this option,
92+
* it will not be appended to the document.body automatically.
93+
*
94+
* For example: If you are unit testing a `<tbody>` element, it cannot be a child of a div. In this case, you can
95+
* specify a table as the render container.
96+
*
97+
* @see https://testing-library.com/docs/react-testing-library/api/#container
98+
*/
99+
container?: Container | undefined
100+
/**
101+
* Defaults to the container if the container is specified. Otherwise `document.body` is used for the default. This is used as
102+
* the base element for the queries as well as what is printed when you use `debug()`.
103+
*
104+
* @see https://testing-library.com/docs/react-testing-library/api/#baseelement
105+
*/
106+
baseElement?: BaseElement | undefined
107+
/**
108+
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
109+
* rendering and use ReactDOM.hydrate to mount your components.
110+
*
111+
* @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
112+
*/
113+
hydrate?: boolean | undefined
114+
/**
115+
* Only works if used with React 18.
116+
* Set to `true` if you want to force synchronous `ReactDOM.render`.
117+
* Otherwise `render` will default to concurrent React if available.
118+
*/
119+
legacyRoot?: boolean | undefined
120+
/**
121+
* Queries to bind. Overrides the default set from DOM Testing Library unless merged.
122+
*
123+
* @see https://testing-library.com/docs/react-testing-library/api/#queries
124+
*/
125+
queries?: Q | undefined
126+
/**
127+
* Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for creating
128+
* reusable custom render functions for common data providers. See setup for examples.
129+
*
130+
* @see https://testing-library.com/docs/react-testing-library/api/#wrapper
131+
*/
132+
wrapper?: React.JSXElementConstructor<{children: React.ReactNode}> | undefined
133+
}
134+
135+
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
136+
137+
/**
138+
* Render into a container which is appended to document.body. It should be used with cleanup.
139+
*/
140+
export function render<
141+
Q extends Queries = typeof queries,
142+
Container extends RendererableContainer | HydrateableContainer = HTMLElement,
143+
BaseElement extends RendererableContainer | HydrateableContainer = Container,
144+
>(
145+
ui: React.ReactNode,
146+
options: RenderOptions<Q, Container, BaseElement>,
147+
): Promise<RenderResult<Q, Container, BaseElement>>
148+
export function render(
149+
ui: React.ReactNode,
150+
options?: Omit<RenderOptions, 'queries'> | undefined,
151+
): Promise<RenderResult>
152+
153+
export interface RenderHookResult<Result, Props> {
154+
/**
155+
* Triggers a re-render. The props will be passed to your renderHook callback.
156+
*/
157+
rerender: (props?: Props) => Promise<void>
158+
/**
159+
* This is a stable reference to the latest value returned by your renderHook
160+
* callback
161+
*/
162+
result: {
163+
/**
164+
* The value returned by your renderHook callback
165+
*/
166+
current: Result
167+
}
168+
/**
169+
* Unmounts the test component. This is useful for when you need to test
170+
* any cleanup your useEffects have.
171+
*/
172+
unmount: () => Promise<void>
173+
}
174+
175+
/** @deprecated */
176+
export type BaseRenderHookOptions<
177+
Props,
178+
Q extends Queries,
179+
Container extends RendererableContainer | HydrateableContainer,
180+
BaseElement extends Element | DocumentFragment,
181+
> = RenderHookOptions<Props, Q, Container, BaseElement>
182+
183+
/** @deprecated */
184+
export interface ClientRenderHookOptions<
185+
Props,
186+
Q extends Queries,
187+
Container extends Element | DocumentFragment,
188+
BaseElement extends Element | DocumentFragment = Container,
189+
> extends BaseRenderHookOptions<Props, Q, Container, BaseElement> {
190+
/**
191+
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
192+
* rendering and use ReactDOM.hydrate to mount your components.
193+
*
194+
* @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
195+
*/
196+
hydrate?: false | undefined
197+
}
198+
199+
/** @deprecated */
200+
export interface HydrateHookOptions<
201+
Props,
202+
Q extends Queries,
203+
Container extends Element | DocumentFragment,
204+
BaseElement extends Element | DocumentFragment = Container,
205+
> extends BaseRenderHookOptions<Props, Q, Container, BaseElement> {
206+
/**
207+
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
208+
* rendering and use ReactDOM.hydrate to mount your components.
209+
*
210+
* @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
211+
*/
212+
hydrate: true
213+
}
214+
215+
export interface RenderHookOptions<
216+
Props,
217+
Q extends Queries = typeof queries,
218+
Container extends RendererableContainer | HydrateableContainer = HTMLElement,
219+
BaseElement extends RendererableContainer | HydrateableContainer = Container,
220+
> extends BaseRenderOptions<Q, Container, BaseElement> {
221+
/**
222+
* The argument passed to the renderHook callback. Can be useful if you plan
223+
* to use the rerender utility to change the values passed to your hook.
224+
*/
225+
initialProps?: Props | undefined
226+
}
227+
228+
/**
229+
* Allows you to render a hook within a test React component without having to
230+
* create that component yourself.
231+
*/
232+
export function renderHook<
233+
Result,
234+
Props,
235+
Q extends Queries = typeof queries,
236+
Container extends RendererableContainer | HydrateableContainer = HTMLElement,
237+
BaseElement extends RendererableContainer | HydrateableContainer = Container,
238+
>(
239+
render: (initialProps: Props) => Result,
240+
options?: RenderHookOptions<Props, Q, Container, BaseElement> | undefined,
241+
): Promise<RenderHookResult<Result, Props>>
242+
243+
/**
244+
* Unmounts React trees that were mounted with render.
245+
*/
246+
export function cleanup(): Promise<void>
247+
248+
export function act(cb: () => void | Promise<void>): Promise<void>

0 commit comments

Comments
 (0)
Please sign in to comment.