7
* @emails react-core
8
* @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
9
*/
10
-
10
let JSDOM;
11
let React;
12
let ReactDOMClient;
23
let hasErrored = false;
24
let fatalError = undefined;
25
let waitForPaint;
26
+let SuspenseList;
27
+let assertConsoleErrorDev;
28
29
describe('useId', () => {
30
beforeEach(() => {
33
React = require('react');
34
ReactDOMClient = require('react-dom/client');
35
clientAct = require('internal-test-utils').act;
36
+ assertConsoleErrorDev =
37
+ require('internal-test-utils').assertConsoleErrorDev;
38
ReactDOMFizzServer = require('react-dom/server');
39
Stream = require('stream');
40
Suspense = React.Suspense;
41
useId = React.useId;
42
useState = React.useState;
43
+ if (gate(flags => flags.enableSuspenseList)) {
44
+ SuspenseList = React.unstable_SuspenseList;
45
+ }
46
47
const InternalTestUtils = require('internal-test-utils');
48
waitForPaint = InternalTestUtils.waitForPaint;
381
`);
382
});
383
384
+ // @gate enableSuspenseList
385
+ it('Supports SuspenseList (reveal order independent)', async () => {
386
+ function Baz({id, children}) {
387
+ return <span id={id}>{children}</span>;
388
+ }
389
+
390
+ function Bar({children}) {
391
+ const id = useId();
392
+ return <Baz id={id}>{children}</Baz>;
393
+ }
394
+
395
+ function Foo() {
396
+ return (
397
+ <SuspenseList revealOrder="independent">
398
+ <Bar>A</Bar>
399
+ <Bar>B</Bar>
400
+ </SuspenseList>
401
+ );
402
+ }
403
+
404
+ await serverAct(async () => {
405
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
406
+ pipe(writable);
407
+ });
408
+ expect(container).toMatchInlineSnapshot(`
409
+ <div
410
+ id="container"
411
+ >
412
+ <span
413
+ id="_R_1_"
414
+ >
415
+ A
416
+ </span>
417
+ <span
418
+ id="_R_2_"
419
+ >
420
+ B
421
+ </span>
422
+ </div>
423
+ `);
424
+
425
+ await clientAct(async () => {
426
+ ReactDOMClient.hydrateRoot(container, <Foo />);
427
+ });
428
+
429
+ expect(container).toMatchInlineSnapshot(`
430
+ <div
431
+ id="container"
432
+ >
433
+ <span
434
+ id="_R_1_"
435
+ >
436
+ A
437
+ </span>
438
+ <span
439
+ id="_R_2_"
440
+ >
441
+ B
442
+ </span>
443
+ </div>
444
+ `);
445
+ });
446
+
447
+ // @gate enableSuspenseList
448
+ it('Supports SuspenseList (reveal order "together")', async () => {
449
+ function Baz({id, children}) {
450
+ return <span id={id}>{children}</span>;
451
+ }
452
+
453
+ function Bar({children}) {
454
+ const id = useId();
455
+ return <Baz id={id}>{children}</Baz>;
456
+ }
457
+
458
+ function Foo() {
459
+ return (
460
+ <SuspenseList revealOrder="together">
461
+ <Bar>A</Bar>
462
+ <Bar>B</Bar>
463
+ </SuspenseList>
464
+ );
465
+ }
466
+
467
+ await serverAct(async () => {
468
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
469
+ pipe(writable);
470
+ });
471
+ expect(container).toMatchInlineSnapshot(`
472
+ <div
473
+ id="container"
474
+ >
475
+ <span
476
+ id="_R_1_"
477
+ >
478
+ A
479
+ </span>
480
+ <span
481
+ id="_R_2_"
482
+ >
483
+ B
484
+ </span>
485
+ </div>
486
+ `);
487
+
488
+ await clientAct(async () => {
489
+ ReactDOMClient.hydrateRoot(container, <Foo />);
490
+ });
491
+
492
+ expect(container).toMatchInlineSnapshot(`
493
+ <div
494
+ id="container"
495
+ >
496
+ <span
497
+ id="_R_1_"
498
+ >
499
+ A
500
+ </span>
501
+ <span
502
+ id="_R_2_"
503
+ >
504
+ B
505
+ </span>
506
+ </div>
507
+ `);
508
+ });
509
+
510
+ // @gate enableSuspenseList
511
+ it('Supports SuspenseList (reveal order "forwards")', async () => {
512
+ function Baz({id, children}) {
513
+ return <span id={id}>{children}</span>;
514
+ }
515
+
516
+ function Bar({children}) {
517
+ const id = useId();
518
+ return <Baz id={id}>{children}</Baz>;
519
+ }
520
+
521
+ function Foo() {
522
+ return (
523
+ <SuspenseList revealOrder="forwards" tail="visible">
524
+ <Bar>A</Bar>
525
+ <Bar>B</Bar>
526
+ </SuspenseList>
527
+ );
528
+ }
529
+
530
+ await serverAct(async () => {
531
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
532
+ pipe(writable);
533
+ });
534
+ expect(container).toMatchInlineSnapshot(`
535
+ <div
536
+ id="container"
537
+ >
538
+ <span
539
+ id="_R_1_"
540
+ >
541
+ A
542
+ </span>
543
+ <span
544
+ id="_R_2_"
545
+ >
546
+ B
547
+ </span>
548
+ </div>
549
+ `);
550
+
551
+ await clientAct(async () => {
552
+ ReactDOMClient.hydrateRoot(container, <Foo />);
553
+ });
554
+
555
+ expect(container).toMatchInlineSnapshot(`
556
+ <div
557
+ id="container"
558
+ >
559
+ <span
560
+ id="_R_1_"
561
+ >
562
+ A
563
+ </span>
564
+ <span
565
+ id="_R_2_"
566
+ >
567
+ B
568
+ </span>
569
+ </div>
570
+ `);
571
+ });
572
+
573
+ // @gate enableSuspenseList
574
+ it('Supports SuspenseList (reveal order "backwards") with a single child in a list of many', async () => {
575
+ function Baz({id, children}) {
576
+ return <span id={id}>{children}</span>;
577
+ }
578
+
579
+ function Bar({children}) {
580
+ const id = useId();
581
+ return <Baz id={id}>{children}</Baz>;
582
+ }
583
+
584
+ function Foo() {
585
+ return (
586
+ <SuspenseList revealOrder="unstable_legacy-backwards" tail="visible">
587
+ {null}
588
+ <Bar>A</Bar>
589
+ {null}
590
+ </SuspenseList>
591
+ );
592
+ }
593
+
594
+ await serverAct(async () => {
595
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
596
+ pipe(writable);
597
+ });
598
+ expect(container).toMatchInlineSnapshot(`
599
+ <div
600
+ id="container"
601
+ >
602
+ <span
603
+ id="_R_2_"
604
+ >
605
+ A
606
+ </span>
607
+ <!-- -->
608
+ </div>
609
+ `);
610
+
611
+ await clientAct(async () => {
612
+ ReactDOMClient.hydrateRoot(container, <Foo />);
613
+ });
614
+
615
+ expect(container).toMatchInlineSnapshot(`
616
+ <div
617
+ id="container"
618
+ >
619
+ <span
620
+ id="_R_2_"
621
+ >
622
+ A
623
+ </span>
624
+ <!-- -->
625
+ </div>
626
+ `);
627
+ });
628
+
629
+ // @gate enableSuspenseList
630
+ it('Supports SuspenseList (reveal order "backwards")', async () => {
631
+ function Baz({id, children}) {
632
+ return <span id={id}>{children}</span>;
633
+ }
634
+
635
+ function Bar({children}) {
636
+ const id = useId();
637
+ return <Baz id={id}>{children}</Baz>;
638
+ }
639
+
640
+ function Foo() {
641
+ return (
642
+ <SuspenseList revealOrder="unstable_legacy-backwards" tail="visible">
643
+ <Bar>A</Bar>
644
+ <Bar>B</Bar>
645
+ </SuspenseList>
646
+ );
647
+ }
648
+
649
+ await serverAct(async () => {
650
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
651
+ pipe(writable);
652
+ });
653
+ expect(container).toMatchInlineSnapshot(`
654
+ <div
655
+ id="container"
656
+ >
657
+ <span
658
+ id="_R_1_"
659
+ >
660
+ A
661
+ </span>
662
+ <span
663
+ id="_R_2_"
664
+ >
665
+ B
666
+ </span>
667
+ </div>
668
+ `);
669
+
670
+ if (gate(flags => flags.favorSafetyOverHydrationPerf)) {
671
+ // TODO: This is a bug with revealOrder="backwards" in that it hydrates in reverse.
672
+ await expect(async () => {
673
+ await clientAct(async () => {
674
+ ReactDOMClient.hydrateRoot(container, <Foo />);
675
+ });
676
+ }).rejects.toThrowError(
677
+ `Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client.`,
678
+ );
679
+
680
+ expect(container).toMatchInlineSnapshot(`
681
+ <div
682
+ id="container"
683
+ >
684
+ <span
685
+ id="_r_1_"
686
+ >
687
+ A
688
+ </span>
689
+ <span
690
+ id="_r_0_"
691
+ >
692
+ B
693
+ </span>
694
+ </div>
695
+ `);
696
+ } else {
697
+ await clientAct(async () => {
698
+ ReactDOMClient.hydrateRoot(container, <Foo />);
699
+ });
700
+
701
+ // TODO: This is a bug with revealOrder="backwards" in that it hydrates in reverse.
702
+ assertConsoleErrorDev([
703
+ `A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:
704
+
705
+- A server/client branch \`if (typeof window !== 'undefined')\`.
706
+- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
707
+- Date formatting in a user's locale which doesn't match the server.
708
+- External changing data without sending a snapshot of it along with the HTML.
709
+- Invalid HTML tag nesting.
710
+
711
+It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
712
+
713
+https://react.dev/link/hydration-mismatch
714
+
715
+ <Foo>
716
+ <SuspenseList revealOrder="unstable_l..." tail="visible">
717
+ <Bar>
718
+ <Bar>
719
+ <Baz id="_R_2_">
720
+ <span
721
++ id="_R_2_"
722
+- id="_R_1_"
723
+ >
724
++ B
725
+- A
726
+`,
727
+ ]);
728
+
729
+ expect(container).toMatchInlineSnapshot(`
730
+ <div
731
+ id="container"
732
+ >
733
+ <span
734
+ id="_R_1_"
735
+ >
736
+ A
737
+ </span>
738
+ <span
739
+ id="_R_2_"
740
+ >
741
+ B
742
+ </span>
743
+ </div>
744
+ `);
745
+ }
746
+ });
747
+
748
it('basic incremental hydration', async () => {
749
function App() {
750
return (