7
7
configure as configureDTL ,
8
8
} from '@testing-library/dom'
9
9
import act , {
10
+ actAsync ,
10
11
getIsReactActEnvironment ,
11
12
setReactActEnvironment ,
12
13
} from './act-compat'
@@ -196,6 +197,64 @@ function renderRoot(
196
197
}
197
198
}
198
199
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
+
199
258
function render (
200
259
ui ,
201
260
{
@@ -258,6 +317,68 @@ function render(
258
317
} )
259
318
}
260
319
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
+
261
382
function cleanup ( ) {
262
383
mountedRootEntries . forEach ( ( { root, container} ) => {
263
384
act ( ( ) => {
@@ -271,6 +392,21 @@ function cleanup() {
271
392
mountedContainers . clear ( )
272
393
}
273
394
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
+
274
410
function renderHook ( renderCallback , options = { } ) {
275
411
const { initialProps, ...renderOptions } = options
276
412
@@ -310,8 +446,60 @@ function renderHook(renderCallback, options = {}) {
310
446
return { result, rerender, unmount}
311
447
}
312
448
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
+
313
488
// just re-export everything from dom-testing-library
314
489
export * 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
+ }
316
504
317
505
/* eslint func-name-matching:0 */
0 commit comments