/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @emails react-core */ 'use strict'; let React; let ReactDOM; let ReactDOMClient; let ReactDOMServer; let act; let SuspenseList; describe('ReactDOMServerSuspense', () => { beforeEach(() => { // Reset warning cache. jest.resetModules(); React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); act = require('internal-test-utils').act; if (gate(flags => flags.enableSuspenseList)) { SuspenseList = React.unstable_SuspenseList; } }); function Text(props) { return
{props.text}
; } function AsyncText(props) { throw new Promise(() => {}); } function getVisibleChildren(element) { const children = []; let node = element.firstChild; while (node) { if (node.nodeType === 1) { if ( node.tagName !== 'SCRIPT' && node.tagName !== 'TEMPLATE' && node.tagName !== 'template' && !node.hasAttribute('hidden') && !node.hasAttribute('aria-hidden') ) { const props = {}; const attributes = node.attributes; for (let i = 0; i < attributes.length; i++) { if ( attributes[i].name === 'id' && attributes[i].value.includes(':') ) { // We assume this is a React added ID that's a non-visual implementation detail. continue; } props[attributes[i].name] = attributes[i].value; } props.children = getVisibleChildren(node); children.push(React.createElement(node.tagName.toLowerCase(), props)); } } else if (node.nodeType === 3) { children.push(node.data); } node = node.nextSibling; } return children.length === 0 ? undefined : children.length === 1 ? children[0] : children; } it('should render the children when no promise is thrown', async () => { const container = document.createElement('div'); const html = ReactDOMServer.renderToString( }> , ); container.innerHTML = html; expect(getVisibleChildren(container)).toEqual(
Children
); }); it('should render the fallback when a promise thrown', async () => { const container = document.createElement('div'); const html = ReactDOMServer.renderToString( }> , ); container.innerHTML = html; expect(getVisibleChildren(container)).toEqual(
Fallback
); }); // @gate enableBrowserAPI it('hydrates browser-only content rendered with renderToString', async () => { function BrowserOnly() { React.use(ReactDOM.browser('Only render this content in the browser')); return ; } const app = ( }> ); const container = document.createElement('div'); container.innerHTML = ReactDOMServer.renderToString(app); expect(getVisibleChildren(container)).toEqual(
Fallback
); const recoverableErrors = []; await act(() => { ReactDOMClient.hydrateRoot(container, app, { onRecoverableError(error) { recoverableErrors.push(error); }, }); }); expect(recoverableErrors).toEqual([]); expect(getVisibleChildren(container)).toEqual(
Children
); }); // @gate enableBrowserAPI it('renders only the browser-only fallback with renderToStaticMarkup', () => { function BrowserOnly() { React.use(ReactDOM.browser('Only render this content in the browser')); return ; } const html = ReactDOMServer.renderToStaticMarkup( }> , ); expect(html).toBe('
Fallback
'); }); it('should work with nested suspense components', async () => { const container = document.createElement('div'); const html = ReactDOMServer.renderToString( }>
}>
, ); container.innerHTML = html; expect(getVisibleChildren(container)).toEqual(
Children
Fallback
, ); }); // @gate enableSuspenseList it('server renders a SuspenseList component and its children', async () => { const example = (
A
B
); const container = document.createElement('div'); const html = ReactDOMServer.renderToString(example); container.innerHTML = html; const divA = container.children[0]; expect(divA.tagName).toBe('DIV'); expect(divA.textContent).toBe('A'); const divB = container.children[1]; expect(divB.tagName).toBe('DIV'); expect(divB.textContent).toBe('B'); await act(() => { ReactDOMClient.hydrateRoot(container, example); }); const divA2 = container.children[0]; const divB2 = container.children[1]; expect(divA).toBe(divA2); expect(divB).toBe(divB2); }); it('throws when rendering a suspending component outside a Suspense node', async () => { expect(() => { ReactDOMServer.renderToString(
, ); }).toThrow('A component suspended while responding to synchronous input.'); }); it('does not get confused by throwing null', () => { function Bad() { // eslint-disable-next-line no-throw-literal throw null; } let didError; let error; try { ReactDOMServer.renderToString(); } catch (err) { didError = true; error = err; } expect(didError).toBe(true); expect(error).toBe(null); }); it('does not get confused by throwing undefined', () => { function Bad() { // eslint-disable-next-line no-throw-literal throw undefined; } let didError; let error; try { ReactDOMServer.renderToString(); } catch (err) { didError = true; error = err; } expect(didError).toBe(true); expect(error).toBe(undefined); }); it('does not get confused by throwing a primitive', () => { function Bad() { // eslint-disable-next-line no-throw-literal throw 'foo'; } let didError; let error; try { ReactDOMServer.renderToString(); } catch (err) { didError = true; error = err; } expect(didError).toBe(true); expect(error).toBe('foo'); }); });