main
js 855 lines 27.5 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
9 */
10
11 'use strict';
12
13 let React;
14 let ReactDOM;
15 let ReactDOMClient;
16 let ReactDOMServer;
17 let ReactDOMServerBrowser;
18 let waitForAll;
19 let act;
20 let assertConsoleErrorDev;
21 let assertConsoleWarnDev;
22
23 // These tests rely both on ReactDOMServer and ReactDOM.
24 // If a test only needs ReactDOMServer, put it in ReactServerRendering-test instead.
25 describe('ReactDOMServerHydration', () => {
26 beforeEach(() => {
27 jest.resetModules();
28 React = require('react');
29 ReactDOM = require('react-dom');
30 ReactDOMClient = require('react-dom/client');
31 ReactDOMServer = require('react-dom/server');
32 ReactDOMServerBrowser = require('react-dom/server.browser');
33
34 const InternalTestUtils = require('internal-test-utils');
35 waitForAll = InternalTestUtils.waitForAll;
36 act = InternalTestUtils.act;
37 assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
38 assertConsoleWarnDev = InternalTestUtils.assertConsoleWarnDev;
39 });
40
41 it('should have the correct mounting behavior', async () => {
42 let mountCount = 0;
43 let numClicks = 0;
44
45 class TestComponent extends React.Component {
46 spanRef = React.createRef();
47
48 componentDidMount() {
49 mountCount++;
50 }
51
52 click = () => {
53 numClicks++;
54 };
55
56 render() {
57 return (
58 <span ref={this.spanRef} onClick={this.click}>
59 Name: {this.props.name}
60 </span>
61 );
62 }
63 }
64
65 const element = document.createElement('div');
66 document.body.appendChild(element);
67 try {
68 let root = ReactDOMClient.createRoot(element);
69 await act(() => {
70 root.render(<TestComponent />);
71 });
72
73 let lastMarkup = element.innerHTML;
74
75 // Exercise the update path. Markup should not change,
76 // but some lifecycle methods should be run again.
77 await act(() => {
78 root.render(<TestComponent name="x" />);
79 });
80 expect(mountCount).toEqual(1);
81
82 // Unmount and remount. We should get another mount event and
83 // we should get different markup, as the IDs are unique each time.
84 root.unmount();
85 expect(element.innerHTML).toEqual('');
86 root = ReactDOMClient.createRoot(element);
87 await act(() => {
88 root.render(<TestComponent name="x" />);
89 });
90
91 expect(mountCount).toEqual(2);
92 expect(element.innerHTML).not.toEqual(lastMarkup);
93
94 // Now kill the node and render it on top of server-rendered markup, as if
95 // we used server rendering. We should mount again, but the markup should
96 // be unchanged. We will append a sentinel at the end of innerHTML to be
97 // sure that innerHTML was not changed.
98 await act(() => {
99 root.unmount();
100 });
101 expect(element.innerHTML).toEqual('');
102
103 lastMarkup = ReactDOMServer.renderToString(<TestComponent name="x" />);
104 element.innerHTML = lastMarkup;
105
106 let instance;
107
108 root = await act(() => {
109 return ReactDOMClient.hydrateRoot(
110 element,
111 <TestComponent name="x" ref={current => (instance = current)} />,
112 );
113 });
114 expect(mountCount).toEqual(3);
115 expect(element.innerHTML).toBe(lastMarkup);
116
117 // Ensure the events system works after mount into server markup
118 expect(numClicks).toEqual(0);
119 instance.spanRef.current.click();
120 expect(numClicks).toEqual(1);
121
122 await act(() => {
123 root.unmount();
124 });
125 expect(element.innerHTML).toEqual('');
126
127 // Now simulate a situation where the app is not idempotent. React should
128 // warn but do the right thing.
129 element.innerHTML = lastMarkup;
130 root = await act(() => {
131 return ReactDOMClient.hydrateRoot(
132 element,
133 <TestComponent
134 name="y"
135 ref={current => {
136 instance = current;
137 }}
138 />,
139 {
140 onRecoverableError: error => {},
141 },
142 );
143 });
144
145 expect(mountCount).toEqual(4);
146 expect(element.innerHTML.length > 0).toBe(true);
147 expect(element.innerHTML).not.toEqual(lastMarkup);
148
149 // Ensure the events system works after markup mismatch.
150 expect(numClicks).toEqual(1);
151 instance.spanRef.current.click();
152 expect(numClicks).toEqual(2);
153 } finally {
154 document.body.removeChild(element);
155 }
156 });
157
158 // We have a polyfill for autoFocus on the client, but we intentionally don't
159 // want it to call focus() when hydrating because this can mess up existing
160 // focus before the JS has loaded.
161 it('should emit autofocus on the server but not focus() when hydrating', async () => {
162 const element = document.createElement('div');
163 element.innerHTML = ReactDOMServer.renderToString(
164 <input autoFocus={true} />,
165 );
166 expect(element.firstChild.autofocus).toBe(true);
167
168 // It should not be called on mount.
169 element.firstChild.focus = jest.fn();
170 const root = await act(() =>
171 ReactDOMClient.hydrateRoot(element, <input autoFocus={true} />),
172 );
173 expect(element.firstChild.focus).not.toHaveBeenCalled();
174
175 // Or during an update.
176 await act(() => {
177 root.render(<input autoFocus={true} />);
178 });
179 expect(element.firstChild.focus).not.toHaveBeenCalled();
180 });
181
182 it('should not focus on either server or client with autofocus={false}', async () => {
183 const element = document.createElement('div');
184 element.innerHTML = ReactDOMServer.renderToString(
185 <input autoFocus={false} />,
186 );
187 expect(element.firstChild.autofocus).toBe(false);
188
189 element.firstChild.focus = jest.fn();
190 const root = await act(() =>
191 ReactDOMClient.hydrateRoot(element, <input autoFocus={false} />),
192 );
193
194 expect(element.firstChild.focus).not.toHaveBeenCalled();
195
196 await act(() => {
197 root.render(<input autoFocus={false} />);
198 });
199 expect(element.firstChild.focus).not.toHaveBeenCalled();
200 });
201
202 // Regression test for https://github.com/facebook/react/issues/11726
203 it('should not focus on either server or client with autofocus={false} even if there is a markup mismatch', async () => {
204 const element = document.createElement('div');
205 element.innerHTML = ReactDOMServer.renderToString(
206 <button autoFocus={false}>server</button>,
207 );
208 expect(element.firstChild.autofocus).toBe(false);
209 const onFocusBeforeHydration = jest.fn();
210 const onFocusAfterHydration = jest.fn();
211 element.firstChild.focus = onFocusBeforeHydration;
212
213 await act(() => {
214 ReactDOMClient.hydrateRoot(
215 element,
216 <button autoFocus={false} onFocus={onFocusAfterHydration}>
217 client
218 </button>,
219 {onRecoverableError: error => {}},
220 );
221 });
222
223 expect(onFocusBeforeHydration).not.toHaveBeenCalled();
224 expect(onFocusAfterHydration).not.toHaveBeenCalled();
225 });
226
227 it('should warn when the style property differs', async () => {
228 const element = document.createElement('div');
229 element.innerHTML = ReactDOMServer.renderToString(
230 <div style={{textDecoration: 'none', color: 'black', height: '10px'}} />,
231 );
232 expect(element.firstChild.style.textDecoration).toBe('none');
233 expect(element.firstChild.style.color).toBe('black');
234
235 await act(() => {
236 ReactDOMClient.hydrateRoot(
237 element,
238 <div
239 style={{textDecoration: 'none', color: 'white', height: '10px'}}
240 />,
241 );
242 });
243 assertConsoleErrorDev([
244 "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
245 "This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
246 '\n' +
247 "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
248 "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
249 "- Date formatting in a user's locale which doesn't match the server.\n" +
250 '- External changing data without sending a snapshot of it along with the HTML.\n' +
251 '- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension ' +
252 'installed which messes with the HTML before React loaded.\n' +
253 '\n' +
254 'https://react.dev/link/hydration-mismatch\n' +
255 '\n' +
256 ' <div\n style={{\n+ textDecoration: "none"\n' +
257 '+ color: "white"\n' +
258 '- color: "black"\n' +
259 '+ height: "10px"\n' +
260 '- height: "10px"\n' +
261 '- text-decoration: "none"\n' +
262 ' }}\n' +
263 ' >\n' +
264 '\n in div (at **)',
265 ]);
266 });
267
268 it('should not warn when the style property differs on whitespace or order in IE', async () => {
269 document.documentMode = 11;
270 jest.resetModules();
271 React = require('react');
272 ReactDOMClient = require('react-dom/client');
273 ReactDOMServer = require('react-dom/server');
274 try {
275 const element = document.createElement('div');
276
277 // Simulate IE normalizing the style attribute. IE makes it equal to
278 // what's available under `node.style.cssText`.
279 element.innerHTML =
280 '<div style="height: 10px; color: black; text-decoration: none;"></div>';
281
282 await act(() => {
283 ReactDOMClient.hydrateRoot(
284 element,
285 <div
286 style={{textDecoration: 'none', color: 'black', height: '10px'}}
287 />,
288 );
289 });
290 } finally {
291 delete document.documentMode;
292 }
293 });
294
295 it('should warn when the style property differs on whitespace in non-IE browsers', async () => {
296 const element = document.createElement('div');
297
298 element.innerHTML =
299 '<div style="text-decoration: none; color: black; height: 10px;"></div>';
300
301 await act(() => {
302 ReactDOMClient.hydrateRoot(
303 element,
304 <div
305 style={{textDecoration: 'none', color: 'black', height: '10px'}}
306 />,
307 );
308 });
309 assertConsoleErrorDev([
310 "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
311 "This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
312 '\n' +
313 "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
314 "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
315 "- Date formatting in a user's locale which doesn't match the server.\n" +
316 '- External changing data without sending a snapshot of it along with the HTML.\n' +
317 '- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension ' +
318 'installed which messes with the HTML before React loaded.\n' +
319 '\n' +
320 'https://react.dev/link/hydration-mismatch\n' +
321 '\n' +
322 ' <div\n' +
323 ' style={{\n' +
324 '+ textDecoration: "none"\n' +
325 '+ color: "black"\n' +
326 '- color: "black"\n' +
327 '+ height: "10px"\n' +
328 '- height: "10px"\n' +
329 '- text-decoration: "none"\n' +
330 ' }}\n' +
331 ' >\n' +
332 '\n in div (at **)',
333 ]);
334 });
335
336 it('should throw rendering portals on the server', () => {
337 const div = document.createElement('div');
338 expect(() => {
339 ReactDOMServer.renderToString(
340 <div>{ReactDOM.createPortal(<div />, div)}</div>,
341 );
342 }).toThrow(
343 'Portals are not currently supported by the server renderer. ' +
344 'Render them conditionally so that they only appear on the client render.',
345 );
346 });
347
348 it('should be able to render and hydrate Mode components', async () => {
349 class ComponentWithWarning extends React.Component {
350 componentWillMount() {
351 // Expected warning
352 }
353 render() {
354 return 'Hi';
355 }
356 }
357
358 const markup = (
359 <React.StrictMode>
360 <ComponentWithWarning />
361 </React.StrictMode>
362 );
363
364 const element = document.createElement('div');
365 element.innerHTML = ReactDOMServer.renderToString(markup);
366 assertConsoleWarnDev([
367 'componentWillMount has been renamed, and is not recommended for use. ' +
368 'See https://react.dev/link/unsafe-component-lifecycles for details.\n' +
369 '\n' +
370 '* Move code from componentWillMount to componentDidMount (preferred in most cases) or the constructor.\n' +
371 '\n' +
372 'Please update the following components: ComponentWithWarning\n' +
373 ' in ComponentWithWarning (at **)',
374 ]);
375 expect(element.textContent).toBe('Hi');
376
377 await act(() => {
378 ReactDOMClient.hydrateRoot(element, markup);
379 });
380 assertConsoleWarnDev([
381 'componentWillMount has been renamed, and is not recommended for use. ' +
382 'See https://react.dev/link/unsafe-component-lifecycles for details.\n' +
383 '\n' +
384 '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' +
385 '* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. ' +
386 'In React 18.x, only the UNSAFE_ name will work. ' +
387 'To rename all deprecated lifecycles to their new names, ' +
388 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
389 '\n' +
390 'Please update the following components: ComponentWithWarning',
391 ]);
392 expect(element.textContent).toBe('Hi');
393 });
394
395 it('replays effects when hydrating a StrictMode subtree', async () => {
396 const log = [];
397 function Child() {
398 React.useLayoutEffect(() => {
399 log.push('layout mount');
400 return () => log.push('layout unmount');
401 }, []);
402 React.useEffect(() => {
403 log.push('effect mount');
404 return () => log.push('effect unmount');
405 }, []);
406 return <span>Hello</span>;
407 }
408
409 function App() {
410 return (
411 <div>
412 <Child />
413 </div>
414 );
415 }
416
417 const markup = (
418 <React.StrictMode>
419 <App />
420 </React.StrictMode>
421 );
422
423 const element = document.createElement('div');
424 element.innerHTML = ReactDOMServer.renderToString(markup);
425 expect(element.textContent).toBe('Hello');
426
427 await act(() => {
428 ReactDOMClient.hydrateRoot(element, markup);
429 });
430
431 if (__DEV__) {
432 expect(log).toEqual([
433 'layout mount',
434 'effect mount',
435 'layout unmount',
436 'effect unmount',
437 'layout mount',
438 'effect mount',
439 ]);
440 } else {
441 expect(log).toEqual(['layout mount', 'effect mount']);
442 }
443 });
444
445 it('should be able to render and hydrate forwardRef components', async () => {
446 const FunctionComponent = ({label, forwardedRef}) => (
447 <div ref={forwardedRef}>{label}</div>
448 );
449 const WrappedFunctionComponent = React.forwardRef((props, ref) => (
450 <FunctionComponent {...props} forwardedRef={ref} />
451 ));
452
453 const ref = React.createRef();
454 const markup = <WrappedFunctionComponent ref={ref} label="Hi" />;
455
456 const element = document.createElement('div');
457 element.innerHTML = ReactDOMServer.renderToString(markup);
458 expect(element.textContent).toBe('Hi');
459 expect(ref.current).toBe(null);
460
461 await act(() => {
462 ReactDOMClient.hydrateRoot(element, markup);
463 });
464 expect(element.textContent).toBe('Hi');
465 expect(ref.current.tagName).toBe('DIV');
466 });
467
468 it('should be able to render and hydrate Profiler components', async () => {
469 const callback = jest.fn();
470 const markup = (
471 <React.Profiler id="profiler" onRender={callback}>
472 <div>Hi</div>
473 </React.Profiler>
474 );
475
476 const element = document.createElement('div');
477 element.innerHTML = ReactDOMServer.renderToString(markup);
478 expect(element.textContent).toBe('Hi');
479 expect(callback).not.toHaveBeenCalled();
480
481 await act(() => {
482 ReactDOMClient.hydrateRoot(element, markup);
483 });
484 expect(element.textContent).toBe('Hi');
485 if (__DEV__) {
486 expect(callback).toHaveBeenCalledTimes(1);
487 const [id, phase] = callback.mock.calls[0];
488 expect(id).toBe('profiler');
489 expect(phase).toBe('mount');
490 } else {
491 expect(callback).toHaveBeenCalledTimes(0);
492 }
493 });
494
495 // Regression test for https://github.com/facebook/react/issues/11423
496 it('should ignore noscript content on the client and not warn about mismatches', async () => {
497 const callback = jest.fn();
498 const TestComponent = ({onRender}) => {
499 onRender();
500 return <div>Enable JavaScript to run this app.</div>;
501 };
502 const markup = (
503 <noscript>
504 <TestComponent onRender={callback} />
505 </noscript>
506 );
507
508 const element = document.createElement('div');
509 element.innerHTML = ReactDOMServer.renderToString(markup);
510 expect(callback).toHaveBeenCalledTimes(1);
511 expect(element.textContent).toBe(
512 '<div>Enable JavaScript to run this app.</div>',
513 );
514
515 await act(() => {
516 ReactDOMClient.hydrateRoot(element, markup);
517 });
518 expect(callback).toHaveBeenCalledTimes(1);
519 expect(element.textContent).toBe(
520 '<div>Enable JavaScript to run this app.</div>',
521 );
522 });
523
524 it('should be able to use lazy components after hydrating', async () => {
525 let resolveLazy;
526 const Lazy = React.lazy(
527 () =>
528 new Promise(resolve => {
529 resolveLazy = () => {
530 resolve({
531 default: function World() {
532 return 'world';
533 },
534 });
535 };
536 }),
537 );
538 class HelloWorld extends React.Component {
539 state = {isClient: false};
540 componentDidMount() {
541 this.setState({
542 isClient: true,
543 });
544 }
545 render() {
546 return (
547 <div>
548 Hello{' '}
549 {this.state.isClient && (
550 <React.Suspense fallback="loading">
551 <Lazy />
552 </React.Suspense>
553 )}
554 </div>
555 );
556 }
557 }
558
559 const element = document.createElement('div');
560 element.innerHTML = ReactDOMServer.renderToString(<HelloWorld />);
561 expect(element.textContent).toBe('Hello ');
562
563 await act(() => {
564 ReactDOMClient.hydrateRoot(element, <HelloWorld />);
565 });
566 expect(element.textContent).toBe('Hello loading');
567
568 // Resolve Lazy component
569 await act(() => resolveLazy());
570 expect(element.textContent).toBe('Hello world');
571 });
572
573 it('does not re-enter hydration after committing the first one', async () => {
574 const finalHTML = ReactDOMServer.renderToString(<div />);
575 const container = document.createElement('div');
576 container.innerHTML = finalHTML;
577 const root = await act(() =>
578 ReactDOMClient.hydrateRoot(container, <div />),
579 );
580 await act(() => root.render(null));
581 // This should not reenter hydration state and therefore not trigger hydration
582 // warnings.
583 await act(() => root.render(<div />));
584 });
585
586 // regression test for https://github.com/facebook/react/issues/17170
587 it('should not warn if dangerouslySetInnerHtml=undefined', async () => {
588 const domElement = document.createElement('div');
589 const reactElement = (
590 <div dangerouslySetInnerHTML={undefined}>
591 <p>Hello, World!</p>
592 </div>
593 );
594 const markup = ReactDOMServer.renderToStaticMarkup(reactElement);
595 domElement.innerHTML = markup;
596
597 await act(() => {
598 ReactDOMClient.hydrateRoot(domElement, reactElement);
599 });
600
601 expect(domElement.innerHTML).toEqual(markup);
602 });
603
604 it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined and children on the client', async () => {
605 const domElement = document.createElement('div');
606 const markup = ReactDOMServer.renderToStaticMarkup(
607 <div dangerouslySetInnerHTML={{__html: '<p>server</p>'}} />,
608 );
609 domElement.innerHTML = markup;
610
611 await act(() => {
612 ReactDOMClient.hydrateRoot(
613 domElement,
614 <div dangerouslySetInnerHTML={undefined}>
615 <p>client</p>
616 </div>,
617 {onRecoverableError: error => {}},
618 );
619 });
620
621 expect(domElement.innerHTML).not.toEqual(markup);
622 });
623
624 it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined on the client', async () => {
625 const domElement = document.createElement('div');
626 const markup = ReactDOMServer.renderToStaticMarkup(
627 <div dangerouslySetInnerHTML={{__html: '<p>server</p>'}} />,
628 );
629 domElement.innerHTML = markup;
630
631 await act(() => {
632 ReactDOMClient.hydrateRoot(
633 domElement,
634 <div dangerouslySetInnerHTML={undefined} />,
635 {onRecoverableError: error => {}},
636 );
637 });
638
639 expect(domElement.innerHTML).not.toEqual(markup);
640 });
641
642 it('should warn when hydrating read-only properties', async () => {
643 const readOnlyProperties = [
644 'offsetParent',
645 'offsetTop',
646 'offsetLeft',
647 'offsetWidth',
648 'offsetHeight',
649 'isContentEditable',
650 'outerText',
651 'outerHTML',
652 ];
653 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
654 for (const readOnlyProperty of readOnlyProperties) {
655 const props = {};
656 props[readOnlyProperty] = 'hello';
657 const jsx = React.createElement('my-custom-element', props);
658 const element = document.createElement('div');
659 element.innerHTML = ReactDOMServer.renderToString(jsx);
660 await act(() => {
661 ReactDOMClient.hydrateRoot(element, jsx);
662 });
663 assertConsoleErrorDev([
664 `Assignment to read-only property will result in a no-op: \`${readOnlyProperty}\`
665 in my-custom-element (at **)`,
666 ]);
667 }
668 });
669
670 it('should not re-assign properties on hydration', async () => {
671 const container = document.createElement('div');
672 document.body.appendChild(container);
673
674 const jsx = React.createElement('my-custom-element', {
675 str: 'string',
676 obj: {foo: 'bar'},
677 });
678
679 container.innerHTML = ReactDOMServer.renderToString(jsx);
680 const customElement = container.querySelector('my-custom-element');
681
682 // Install setters to activate `in` check
683 Object.defineProperty(customElement, 'str', {
684 set: function (x) {
685 this._str = x;
686 },
687 get: function () {
688 return this._str;
689 },
690 });
691 Object.defineProperty(customElement, 'obj', {
692 set: function (x) {
693 this._obj = x;
694 },
695 get: function () {
696 return this._obj;
697 },
698 });
699
700 await act(() => {
701 ReactDOMClient.hydrateRoot(container, jsx);
702 });
703
704 expect(customElement.getAttribute('str')).toBe('string');
705 expect(customElement.getAttribute('obj')).toBe(null);
706 expect(customElement.str).toBe(undefined);
707 expect(customElement.obj).toBe(undefined);
708 });
709
710 it('refers users to apis that support Suspense when something suspends', async () => {
711 const theInfinitePromise = new Promise(() => {});
712 function InfiniteSuspend() {
713 throw theInfinitePromise;
714 }
715
716 function App({isClient}) {
717 return (
718 <div>
719 <React.Suspense fallback={'fallback'}>
720 {isClient ? 'resolved' : <InfiniteSuspend />}
721 </React.Suspense>
722 </div>
723 );
724 }
725 const container = document.createElement('div');
726 container.innerHTML = ReactDOMServer.renderToString(
727 <App isClient={false} />,
728 );
729
730 const errors = [];
731 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
732 onRecoverableError(error, errorInfo) {
733 errors.push(error.message);
734 },
735 });
736
737 await waitForAll([]);
738 expect(errors.length).toBe(1);
739 if (__DEV__) {
740 expect(errors[0]).toBe(
741 'Switched to client rendering because the server rendering aborted due to:\n\n' +
742 'The server used "renderToString" ' +
743 'which does not support Suspense. If you intended for this Suspense boundary to render ' +
744 'the fallback content on the server consider throwing an Error somewhere within the ' +
745 'Suspense boundary. If you intended to have the server wait for the suspended component ' +
746 'please switch to "renderToPipeableStream" which supports Suspense on the server',
747 );
748 } else {
749 expect(errors[0]).toBe(
750 'The server could not finish this Suspense boundary, likely due to ' +
751 'an error during server rendering. Switched to client rendering.',
752 );
753 }
754 });
755
756 it('refers users to apis that support Suspense when something suspends (browser)', async () => {
757 const theInfinitePromise = new Promise(() => {});
758 function InfiniteSuspend() {
759 throw theInfinitePromise;
760 }
761
762 function App({isClient}) {
763 return (
764 <div>
765 <React.Suspense fallback={'fallback'}>
766 {isClient ? 'resolved' : <InfiniteSuspend />}
767 </React.Suspense>
768 </div>
769 );
770 }
771 const container = document.createElement('div');
772 container.innerHTML = ReactDOMServerBrowser.renderToString(
773 <App isClient={false} />,
774 );
775
776 const errors = [];
777 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
778 onRecoverableError(error, errorInfo) {
779 errors.push(error.message);
780 },
781 });
782
783 await waitForAll([]);
784 expect(errors.length).toBe(1);
785 if (__DEV__) {
786 expect(errors[0]).toBe(
787 'Switched to client rendering because the server rendering aborted due to:\n\n' +
788 'The server used "renderToString" ' +
789 'which does not support Suspense. If you intended for this Suspense boundary to render ' +
790 'the fallback content on the server consider throwing an Error somewhere within the ' +
791 'Suspense boundary. If you intended to have the server wait for the suspended component ' +
792 'please switch to "renderToReadableStream" which supports Suspense on the server',
793 );
794 } else {
795 expect(errors[0]).toBe(
796 'The server could not finish this Suspense boundary, likely due to ' +
797 'an error during server rendering. Switched to client rendering.',
798 );
799 }
800 });
801
802 it('allows rendering extra hidden inputs in a form', async () => {
803 const element = document.createElement('div');
804 element.innerHTML =
805 '<form>' +
806 '<input type="hidden" /><input type="hidden" name="a" value="A" />' +
807 '<input type="hidden" /><input type="submit" name="b" value="B" />' +
808 '<input type="hidden" /><button name="c" value="C"></button>' +
809 '<input type="hidden" />' +
810 '</form>';
811 const form = element.firstChild;
812 const ref = React.createRef();
813 const a = React.createRef();
814 const b = React.createRef();
815 const c = React.createRef();
816 await act(async () => {
817 ReactDOMClient.hydrateRoot(
818 element,
819 <form ref={ref}>
820 <input type="hidden" name="a" value="A" ref={a} />
821 <input type="submit" name="b" value="B" ref={b} />
822 <button name="c" value="C" ref={c} />
823 </form>,
824 );
825 });
826
827 // The content should not have been client rendered.
828 expect(ref.current).toBe(form);
829
830 expect(a.current.name).toBe('a');
831 expect(a.current.value).toBe('A');
832 expect(b.current.name).toBe('b');
833 expect(b.current.value).toBe('B');
834 expect(c.current.name).toBe('c');
835 expect(c.current.value).toBe('C');
836 });
837
838 it('allows rendering extra hidden inputs immediately before a text instance', async () => {
839 const element = document.createElement('div');
840 element.innerHTML =
841 '<button><input name="a" value="A" type="hidden" />Click <!-- -->me</button>';
842 const button = element.firstChild;
843 const ref = React.createRef();
844 const extraText = 'me';
845
846 await act(() => {
847 ReactDOMClient.hydrateRoot(
848 element,
849 <button ref={ref}>Click {extraText}</button>,
850 );
851 });
852
853 expect(ref.current).toBe(button);
854 });
855 });