@tanstack/react-start-client 1.167.3 → 1.168.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/esm/GenericHydrate.d.ts +3 -0
  2. package/dist/esm/GenericHydrate.js +243 -0
  3. package/dist/esm/GenericHydrate.js.map +1 -0
  4. package/dist/esm/Hydrate.d.ts +31 -0
  5. package/dist/esm/Hydrate.js +34 -0
  6. package/dist/esm/Hydrate.js.map +1 -0
  7. package/dist/esm/hydration/generic.d.ts +7 -0
  8. package/dist/esm/hydration/generic.js +20 -0
  9. package/dist/esm/hydration/generic.js.map +1 -0
  10. package/dist/esm/hydration/idle.d.ts +3 -0
  11. package/dist/esm/hydration/idle.js +12 -0
  12. package/dist/esm/hydration/idle.js.map +1 -0
  13. package/dist/esm/hydration/load.d.ts +5 -0
  14. package/dist/esm/hydration/load.js +33 -0
  15. package/dist/esm/hydration/load.js.map +1 -0
  16. package/dist/esm/hydration/never.d.ts +4 -0
  17. package/dist/esm/hydration/never.js +56 -0
  18. package/dist/esm/hydration/never.js.map +1 -0
  19. package/dist/esm/hydration/visible.d.ts +5 -0
  20. package/dist/esm/hydration/visible.js +94 -0
  21. package/dist/esm/hydration/visible.js.map +1 -0
  22. package/dist/esm/hydration.d.ts +7 -0
  23. package/dist/esm/hydration.js +7 -0
  24. package/dist/esm/index.d.ts +2 -0
  25. package/dist/esm/index.js +3 -1
  26. package/dist/esm/tests/Hydrate.test-d.d.ts +1 -0
  27. package/dist/esm/tests/Hydrate.test.d.ts +1 -0
  28. package/package.json +10 -4
  29. package/src/GenericHydrate.tsx +436 -0
  30. package/src/Hydrate.tsx +107 -0
  31. package/src/hydration/generic.ts +43 -0
  32. package/src/hydration/idle.ts +22 -0
  33. package/src/hydration/load.tsx +49 -0
  34. package/src/hydration/never.tsx +97 -0
  35. package/src/hydration/visible.tsx +139 -0
  36. package/src/hydration.ts +22 -0
  37. package/src/index.tsx +16 -0
  38. package/src/tests/Hydrate.test-d.tsx +147 -0
  39. package/src/tests/Hydrate.test.tsx +676 -0
@@ -0,0 +1,676 @@
1
+ import * as React from 'react'
2
+ import { renderToString } from 'react-dom/server'
3
+ import { hydrateRoot } from 'react-dom/client'
4
+ import {
5
+ act,
6
+ cleanup,
7
+ fireEvent,
8
+ render,
9
+ screen,
10
+ waitFor,
11
+ } from '@testing-library/react'
12
+ import { afterEach, describe, expect, it, vi } from 'vitest'
13
+ import { hydrateIdAttribute } from '@tanstack/start-client-core/hydration/constants'
14
+ import { Hydrate } from '../Hydrate'
15
+ import { condition, idle, interaction, load, never } from '../hydration'
16
+ import type { HydrateProps, HydrationPrefetchStrategy } from '../Hydrate'
17
+
18
+ const InternalHydrate = Hydrate as React.ComponentType<
19
+ HydrateProps & { p?: () => Promise<void>; h?: string }
20
+ >
21
+
22
+ const hydrateIdSelector = `[${hydrateIdAttribute}]`
23
+
24
+ function getMarker() {
25
+ const marker = document.querySelector(hydrateIdSelector)
26
+
27
+ if (!marker) {
28
+ throw new Error('Expected Hydrate marker to exist')
29
+ }
30
+
31
+ return marker
32
+ }
33
+
34
+ function InteractiveChild() {
35
+ const [count, setCount] = React.useState(0)
36
+ const [hydrated, setHydrated] = React.useState(false)
37
+
38
+ React.useEffect(() => {
39
+ setHydrated(true)
40
+ }, [])
41
+
42
+ return (
43
+ <button
44
+ data-testid="child"
45
+ data-hydrated={hydrated ? 'true' : 'false'}
46
+ onClick={() => setCount((prev) => prev + 1)}
47
+ >
48
+ {count}
49
+ </button>
50
+ )
51
+ }
52
+
53
+ function NamedInteractiveChild(props: { id: string }) {
54
+ const [hydrated, setHydrated] = React.useState(false)
55
+
56
+ React.useEffect(() => {
57
+ setHydrated(true)
58
+ }, [])
59
+
60
+ return (
61
+ <button
62
+ data-testid={`child-${props.id}`}
63
+ data-hydrated={hydrated ? 'true' : 'false'}
64
+ >
65
+ {props.id}
66
+ </button>
67
+ )
68
+ }
69
+
70
+ function createSuspendingChild() {
71
+ let resolve!: () => void
72
+ let resolved = false
73
+ const promise = new Promise<void>((resolvePromise) => {
74
+ resolve = () => {
75
+ resolved = true
76
+ resolvePromise()
77
+ }
78
+ })
79
+
80
+ function SuspendingChild() {
81
+ if (!resolved) {
82
+ throw promise
83
+ }
84
+
85
+ return <div data-testid="child">child</div>
86
+ }
87
+
88
+ return { resolve, SuspendingChild }
89
+ }
90
+
91
+ async function expectNoHydrationAfterDefaultIntentEvents() {
92
+ const marker = getMarker()
93
+
94
+ expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe(
95
+ 'false',
96
+ )
97
+
98
+ await act(async () => {
99
+ fireEvent.pointerEnter(marker)
100
+ fireEvent.focusIn(marker)
101
+ fireEvent.pointerDown(marker)
102
+ fireEvent.click(marker)
103
+ await new Promise((resolve) => setTimeout(resolve, 20))
104
+ })
105
+
106
+ expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe(
107
+ 'false',
108
+ )
109
+ }
110
+
111
+ async function fireIntent(event: () => void) {
112
+ await act(async () => {
113
+ event()
114
+ await Promise.resolve()
115
+ })
116
+ }
117
+
118
+ async function renderAsync(ui: React.ReactElement) {
119
+ await act(async () => {
120
+ render(ui)
121
+ await Promise.resolve()
122
+ })
123
+ }
124
+
125
+ async function hydrateFromServer(ui: React.ReactElement) {
126
+ vi.stubGlobal('window', undefined)
127
+ const html = renderToString(ui)
128
+ vi.unstubAllGlobals()
129
+
130
+ const container = document.createElement('div')
131
+ document.body.append(container)
132
+ container.innerHTML = html
133
+
134
+ let root!: ReturnType<typeof hydrateRoot>
135
+ await act(async () => {
136
+ root = hydrateRoot(container, ui)
137
+ await Promise.resolve()
138
+ })
139
+
140
+ return { container, html, root }
141
+ }
142
+
143
+ async function unmountHydratedRoot(
144
+ root: ReturnType<typeof hydrateRoot>,
145
+ container: Element,
146
+ ) {
147
+ await act(async () => {
148
+ root.unmount()
149
+ })
150
+ container.remove()
151
+ }
152
+
153
+ afterEach(() => {
154
+ cleanup()
155
+ vi.unstubAllGlobals()
156
+ })
157
+
158
+ describe('Hydrate', () => {
159
+ it('uses a single custom interaction event instead of the default intent events', async () => {
160
+ const { container, html, root } = await hydrateFromServer(
161
+ <Hydrate
162
+ when={interaction({ events: 'dblclick' })}
163
+ fallback={<div data-testid="fallback">fallback</div>}
164
+ >
165
+ <InteractiveChild />
166
+ </Hydrate>,
167
+ )
168
+
169
+ try {
170
+ expect(html).toContain('data-testid="child"')
171
+ expect(html).not.toContain('data-testid="fallback"')
172
+ expect(screen.queryByTestId('fallback')).toBeNull()
173
+ await expectNoHydrationAfterDefaultIntentEvents()
174
+
175
+ await fireIntent(() =>
176
+ getMarker().dispatchEvent(
177
+ new MouseEvent('dblclick', { bubbles: true, cancelable: true }),
178
+ ),
179
+ )
180
+
181
+ await waitFor(() =>
182
+ expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe(
183
+ 'true',
184
+ ),
185
+ )
186
+ } finally {
187
+ await unmountHydratedRoot(root, container)
188
+ }
189
+ })
190
+
191
+ it('uses every event in a custom interaction event list', async () => {
192
+ const { container, root } = await hydrateFromServer(
193
+ <Hydrate
194
+ when={interaction({ events: ['contextmenu', 'dblclick'] })}
195
+ fallback={<div data-testid="fallback">fallback</div>}
196
+ >
197
+ <InteractiveChild />
198
+ </Hydrate>,
199
+ )
200
+
201
+ try {
202
+ expect(screen.queryByTestId('fallback')).toBeNull()
203
+ await expectNoHydrationAfterDefaultIntentEvents()
204
+
205
+ await fireIntent(() =>
206
+ getMarker().dispatchEvent(
207
+ new MouseEvent('contextmenu', { bubbles: true, cancelable: true }),
208
+ ),
209
+ )
210
+
211
+ await waitFor(() =>
212
+ expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe(
213
+ 'true',
214
+ ),
215
+ )
216
+ } finally {
217
+ await unmountHydratedRoot(root, container)
218
+ }
219
+ })
220
+
221
+ it('omits never content when mounted after the app is already hydrated', async () => {
222
+ await renderAsync(
223
+ <Hydrate when={never()}>
224
+ <InteractiveChild />
225
+ </Hydrate>,
226
+ )
227
+
228
+ expect(screen.queryByTestId('child')).toBeNull()
229
+ })
230
+
231
+ it('shows fallback for a client-only mount while children suspend', async () => {
232
+ const { resolve, SuspendingChild } = createSuspendingChild()
233
+
234
+ await renderAsync(
235
+ <Hydrate
236
+ when={load()}
237
+ fallback={<div data-testid="fallback">fallback</div>}
238
+ >
239
+ <SuspendingChild />
240
+ </Hydrate>,
241
+ )
242
+
243
+ expect(screen.getByTestId('fallback').textContent).toBe('fallback')
244
+ expect(screen.queryByTestId('child')).toBeNull()
245
+
246
+ await act(async () => {
247
+ resolve()
248
+ await Promise.resolve()
249
+ })
250
+
251
+ await screen.findByTestId('child')
252
+ expect(screen.queryByTestId('fallback')).toBeNull()
253
+ })
254
+
255
+ it('does not use fallback for an initial never boundary', async () => {
256
+ const { container, html, root } = await hydrateFromServer(
257
+ <Hydrate
258
+ when={never()}
259
+ fallback={<div data-testid="fallback">fallback</div>}
260
+ >
261
+ <InteractiveChild />
262
+ </Hydrate>,
263
+ )
264
+
265
+ try {
266
+ expect(html).toContain('data-testid="child"')
267
+ expect(html).not.toContain('data-testid="fallback"')
268
+ expect(screen.queryByTestId('fallback')).toBeNull()
269
+
270
+ fireEvent.click(screen.getByTestId('child'))
271
+ await new Promise((resolve) => setTimeout(resolve, 20))
272
+ expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe(
273
+ 'false',
274
+ )
275
+ expect(screen.getByTestId('child').textContent).toBe('0')
276
+ } finally {
277
+ await unmountHydratedRoot(root, container)
278
+ }
279
+ })
280
+
281
+ it('keeps repeated split boundaries independently gated', async () => {
282
+ const { container, root } = await hydrateFromServer(
283
+ <>
284
+ <InternalHydrate when={interaction()} h="shared-boundary">
285
+ <NamedInteractiveChild id="one" />
286
+ </InternalHydrate>
287
+ <InternalHydrate when={interaction()} h="shared-boundary">
288
+ <NamedInteractiveChild id="two" />
289
+ </InternalHydrate>
290
+ </>,
291
+ )
292
+
293
+ try {
294
+ const markers = container.querySelectorAll(hydrateIdSelector)
295
+
296
+ expect(markers).toHaveLength(2)
297
+ expect(markers[0]!.getAttribute(hydrateIdAttribute)).not.toBe(
298
+ markers[1]!.getAttribute(hydrateIdAttribute),
299
+ )
300
+ expect(
301
+ screen.getByTestId('child-one').getAttribute('data-hydrated'),
302
+ ).toBe('false')
303
+ expect(
304
+ screen.getByTestId('child-two').getAttribute('data-hydrated'),
305
+ ).toBe('false')
306
+
307
+ await fireIntent(() =>
308
+ markers[0]!.dispatchEvent(
309
+ new MouseEvent('click', { bubbles: true, cancelable: true }),
310
+ ),
311
+ )
312
+
313
+ await waitFor(() =>
314
+ expect(
315
+ screen.getByTestId('child-one').getAttribute('data-hydrated'),
316
+ ).toBe('true'),
317
+ )
318
+ expect(
319
+ screen.getByTestId('child-two').getAttribute('data-hydrated'),
320
+ ).toBe('false')
321
+ } finally {
322
+ await unmountHydratedRoot(root, container)
323
+ }
324
+ })
325
+
326
+ it('fires onHydrated once after the client hydration commit', async () => {
327
+ const onHydrated = vi.fn()
328
+ const app = (
329
+ <Hydrate when={load()} onHydrated={onHydrated}>
330
+ <div data-testid="child">child</div>
331
+ </Hydrate>
332
+ )
333
+
334
+ vi.stubGlobal('window', undefined)
335
+ const html = renderToString(app)
336
+ expect(html).toContain('child')
337
+ expect(onHydrated).not.toHaveBeenCalled()
338
+ vi.unstubAllGlobals()
339
+
340
+ const container = document.createElement('div')
341
+ document.body.append(container)
342
+ container.innerHTML = html
343
+
344
+ let root!: ReturnType<typeof hydrateRoot>
345
+ await act(async () => {
346
+ root = hydrateRoot(container, app)
347
+ })
348
+
349
+ await waitFor(() => expect(onHydrated).toHaveBeenCalledTimes(1))
350
+
351
+ fireEvent.click(screen.getByTestId('child'))
352
+ await new Promise((resolve) => setTimeout(resolve, 20))
353
+ expect(onHydrated).toHaveBeenCalledTimes(1)
354
+
355
+ await act(async () => {
356
+ root.unmount()
357
+ })
358
+ container.remove()
359
+ })
360
+
361
+ it('prefetches split children without hydrating the boundary', async () => {
362
+ const preload = vi.fn(() => Promise.resolve())
363
+
364
+ const { container, root } = await hydrateFromServer(
365
+ <InternalHydrate
366
+ when={interaction()}
367
+ prefetch={idle({ timeout: 1 })}
368
+ p={preload}
369
+ >
370
+ <InteractiveChild />
371
+ </InternalHydrate>,
372
+ )
373
+
374
+ try {
375
+ await waitFor(() => expect(preload).toHaveBeenCalledTimes(1))
376
+ expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe(
377
+ 'false',
378
+ )
379
+
380
+ await fireIntent(() =>
381
+ getMarker().dispatchEvent(
382
+ new MouseEvent('click', { bubbles: true, cancelable: true }),
383
+ ),
384
+ )
385
+
386
+ await waitFor(() =>
387
+ expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe(
388
+ 'true',
389
+ ),
390
+ )
391
+ expect(preload).toHaveBeenCalledTimes(1)
392
+ } finally {
393
+ await unmountHydratedRoot(root, container)
394
+ }
395
+ })
396
+
397
+ it('does not evaluate dynamic when callbacks on the server', async () => {
398
+ const when = vi.fn(() => interaction({ events: 'dblclick' }))
399
+
400
+ vi.stubGlobal('window', undefined)
401
+ const html = renderToString(
402
+ <Hydrate when={when}>
403
+ <InteractiveChild />
404
+ </Hydrate>,
405
+ )
406
+ vi.unstubAllGlobals()
407
+
408
+ expect(when).not.toHaveBeenCalled()
409
+ expect(html).toContain('data-ts-hydrate-when="dynamic"')
410
+
411
+ const container = document.createElement('div')
412
+ document.body.append(container)
413
+ container.innerHTML = html
414
+
415
+ let root!: ReturnType<typeof hydrateRoot>
416
+ try {
417
+ await act(async () => {
418
+ root = hydrateRoot(
419
+ container,
420
+ <Hydrate when={when}>
421
+ <InteractiveChild />
422
+ </Hydrate>,
423
+ )
424
+ await Promise.resolve()
425
+ })
426
+
427
+ expect(when).toHaveBeenCalled()
428
+ await expectNoHydrationAfterDefaultIntentEvents()
429
+
430
+ await fireIntent(() =>
431
+ getMarker().dispatchEvent(
432
+ new MouseEvent('dblclick', { bubbles: true, cancelable: true }),
433
+ ),
434
+ )
435
+
436
+ await waitFor(() =>
437
+ expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe(
438
+ 'true',
439
+ ),
440
+ )
441
+ } finally {
442
+ await unmountHydratedRoot(root, container)
443
+ }
444
+ })
445
+
446
+ it('replays an interaction captured before the Hydrate component hydrates', async () => {
447
+ const when = () => interaction({ events: 'click' })
448
+
449
+ vi.stubGlobal('window', undefined)
450
+ const html = renderToString(
451
+ <Hydrate when={when}>
452
+ <InteractiveChild />
453
+ </Hydrate>,
454
+ )
455
+ vi.unstubAllGlobals()
456
+
457
+ const container = document.createElement('div')
458
+ document.body.append(container)
459
+ container.innerHTML = html
460
+
461
+ const button = container.querySelector('[data-testid="child"]')
462
+ if (!button) {
463
+ throw new Error('Expected server-rendered child button')
464
+ }
465
+
466
+ button.dispatchEvent(
467
+ new MouseEvent('click', { bubbles: true, cancelable: true }),
468
+ )
469
+
470
+ let root!: ReturnType<typeof hydrateRoot>
471
+ try {
472
+ await act(async () => {
473
+ root = hydrateRoot(
474
+ container,
475
+ <Hydrate when={when}>
476
+ <InteractiveChild />
477
+ </Hydrate>,
478
+ )
479
+ await Promise.resolve()
480
+ })
481
+
482
+ await waitFor(() =>
483
+ expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe(
484
+ 'true',
485
+ ),
486
+ )
487
+ await waitFor(() =>
488
+ expect(screen.getByTestId('child').textContent).toBe('1'),
489
+ )
490
+ } finally {
491
+ await unmountHydratedRoot(root, container)
492
+ }
493
+ })
494
+
495
+ it('blocks hydration on awaited procedural prefetch work', async () => {
496
+ const preload = vi.fn(() => Promise.resolve())
497
+ let resolvePrefetch!: () => void
498
+ const prefetchBlocker = new Promise<void>((resolve) => {
499
+ resolvePrefetch = resolve
500
+ })
501
+ const waitReasons: Array<string> = []
502
+ const neverPrefetches = {
503
+ _t: 'idle',
504
+ _s: () => () => {},
505
+ } as HydrationPrefetchStrategy<'idle'>
506
+
507
+ const { container, root } = await hydrateFromServer(
508
+ <InternalHydrate
509
+ when={interaction()}
510
+ prefetch={async ({ waitFor, preload }) => {
511
+ waitReasons.push(await waitFor(neverPrefetches))
512
+ await preload()
513
+ await prefetchBlocker
514
+ }}
515
+ p={preload}
516
+ >
517
+ <InteractiveChild />
518
+ </InternalHydrate>,
519
+ )
520
+
521
+ try {
522
+ await fireIntent(() =>
523
+ getMarker().dispatchEvent(
524
+ new MouseEvent('click', { bubbles: true, cancelable: true }),
525
+ ),
526
+ )
527
+
528
+ await waitFor(() => expect(waitReasons).toEqual(['hydrate']))
529
+ expect(preload).toHaveBeenCalledTimes(1)
530
+ expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe(
531
+ 'false',
532
+ )
533
+
534
+ await act(async () => {
535
+ resolvePrefetch()
536
+ await prefetchBlocker
537
+ await Promise.resolve()
538
+ })
539
+
540
+ await waitFor(() =>
541
+ expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe(
542
+ 'true',
543
+ ),
544
+ )
545
+ } finally {
546
+ await unmountHydratedRoot(root, container)
547
+ }
548
+ })
549
+
550
+ it('hydrates when a condition strategy changes after the initial render', async () => {
551
+ function ConditionHarness() {
552
+ const [ready, setReady] = React.useState(false)
553
+
554
+ return (
555
+ <>
556
+ <button data-testid="ready" onClick={() => setReady(true)}>
557
+ ready
558
+ </button>
559
+ <Hydrate when={condition(ready)}>
560
+ <InteractiveChild />
561
+ </Hydrate>
562
+ </>
563
+ )
564
+ }
565
+
566
+ const { container, root } = await hydrateFromServer(<ConditionHarness />)
567
+
568
+ try {
569
+ expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe(
570
+ 'false',
571
+ )
572
+
573
+ await act(async () => {
574
+ fireEvent.click(screen.getByTestId('ready'))
575
+ await Promise.resolve()
576
+ })
577
+
578
+ await waitFor(() =>
579
+ expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe(
580
+ 'true',
581
+ ),
582
+ )
583
+ } finally {
584
+ await unmountHydratedRoot(root, container)
585
+ }
586
+ })
587
+
588
+ it('does not block hydration on fire-and-forget procedural prefetch work', async () => {
589
+ let resolvePrefetch!: () => void
590
+ const prefetchBlocker = new Promise<void>((resolve) => {
591
+ resolvePrefetch = resolve
592
+ })
593
+
594
+ const { container, root } = await hydrateFromServer(
595
+ <InternalHydrate
596
+ when={interaction()}
597
+ prefetch={() => {
598
+ void prefetchBlocker
599
+ }}
600
+ >
601
+ <InteractiveChild />
602
+ </InternalHydrate>,
603
+ )
604
+
605
+ try {
606
+ await fireIntent(() =>
607
+ getMarker().dispatchEvent(
608
+ new MouseEvent('click', { bubbles: true, cancelable: true }),
609
+ ),
610
+ )
611
+
612
+ await waitFor(() =>
613
+ expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe(
614
+ 'true',
615
+ ),
616
+ )
617
+
618
+ await act(async () => {
619
+ resolvePrefetch()
620
+ await prefetchBlocker
621
+ })
622
+ } finally {
623
+ await unmountHydratedRoot(root, container)
624
+ }
625
+ })
626
+
627
+ it('aborts procedural prefetch when the boundary unmounts', async () => {
628
+ const signals: Array<AbortSignal> = []
629
+
630
+ const { container, root } = await hydrateFromServer(
631
+ <InternalHydrate
632
+ when={interaction()}
633
+ prefetch={({ signal }) => {
634
+ signals.push(signal)
635
+ return new Promise<void>(() => {})
636
+ }}
637
+ >
638
+ <InteractiveChild />
639
+ </InternalHydrate>,
640
+ )
641
+
642
+ expect(signals).toHaveLength(1)
643
+ expect(signals[0]!.aborted).toBe(false)
644
+
645
+ await unmountHydratedRoot(root, container)
646
+ expect(signals[0]!.aborted).toBe(true)
647
+ })
648
+
649
+ it('delegates nested interaction boundaries at runtime', async () => {
650
+ const { container, root } = await hydrateFromServer(
651
+ <Hydrate when={idle({ timeout: 1000 })}>
652
+ <Hydrate when={interaction()}>
653
+ <InteractiveChild />
654
+ </Hydrate>
655
+ </Hydrate>,
656
+ )
657
+
658
+ try {
659
+ expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe(
660
+ 'false',
661
+ )
662
+
663
+ await fireIntent(() => {
664
+ fireEvent.click(screen.getByTestId('child'))
665
+ })
666
+
667
+ await waitFor(() =>
668
+ expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe(
669
+ 'true',
670
+ ),
671
+ )
672
+ } finally {
673
+ await unmountHydratedRoot(root, container)
674
+ }
675
+ })
676
+ })