77 configure as configureDTL ,
88} from '@testing-library/dom'
99import act , {
10+ actAsync ,
1011 getIsReactActEnvironment ,
1112 setReactActEnvironment ,
1213} from './act-compat'
@@ -196,6 +197,64 @@ function renderRoot(
196197 }
197198}
198199
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+
199258function render (
200259 ui ,
201260 {
@@ -258,6 +317,68 @@ function render(
258317 } )
259318}
260319
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+
261382function cleanup ( ) {
262383 mountedRootEntries . forEach ( ( { root, container} ) => {
263384 act ( ( ) => {
@@ -271,6 +392,21 @@ function cleanup() {
271392 mountedContainers . clear ( )
272393}
273394
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+
274410function renderHook ( renderCallback , options = { } ) {
275411 const { initialProps, ...renderOptions } = options
276412
@@ -310,8 +446,60 @@ function renderHook(renderCallback, options = {}) {
310446 return { result, rerender, unmount}
311447}
312448
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+
313488// just re-export everything from dom-testing-library
314489export * from '@testing-library/dom'
315- export { render , renderHook , cleanup , act , fireEvent , getConfig , configure }
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+ }
316504
317505/* eslint func-name-matching:0 */
0 commit comments