packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js JAVASCRIPT 10,949 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 10,949.
1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 *7 * @emails react-core8 * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment9 */1011'use strict';12import {13  insertNodesAndExecuteScripts,14  mergeOptions,15  stripExternalRuntimeInNodes,16  getVisibleChildren,17} from '../test-utils/FizzTestUtils';1819let JSDOM;20let Stream;21let Scheduler;22let React;23let ReactDOM;24let ReactDOMClient;25let ReactDOMFizzServer;26let ReactDOMFizzStatic;27let Suspense;28let SuspenseList;2930let assertConsoleErrorDev;31let useSyncExternalStore;32let useSyncExternalStoreWithSelector;33let use;34let useActionState;35let PropTypes;36let textCache;37let writable;38let CSPnonce = null;39let container;40let buffer = '';41let hasErrored = false;42let fatalError = undefined;43let renderOptions;44let waitFor;45let waitForAll;46let assertLog;47let waitForPaint;48let clientAct;49let streamingContainer;5051function normalizeError(msg) {52  // Take the first sentence to make it easier to assert on.53  const idx = msg.indexOf('.');54  if (idx > -1) {55    return msg.slice(0, idx + 1);56  }57  return msg;58}5960describe('ReactDOMFizzServer', () => {61  beforeEach(() => {62    jest.resetModules();63    JSDOM = require('jsdom').JSDOM;6465    const jsdom = new JSDOM(66      '<!DOCTYPE html><html><head></head><body><div id="container">',67      {68        runScripts: 'dangerously',69      },70    );71    // We mock matchMedia. for simplicity it only matches 'all' or '' and misses everything else72    Object.defineProperty(jsdom.window, 'matchMedia', {73      writable: true,74      value: jest.fn().mockImplementation(query => ({75        matches: query === 'all' || query === '',76        media: query,77      })),78    });79    streamingContainer = null;80    global.window = jsdom.window;81    global.document = global.window.document;82    global.navigator = global.window.navigator;83    global.Node = global.window.Node;84    global.addEventListener = global.window.addEventListener;85    global.MutationObserver = global.window.MutationObserver;86    // The Fizz runtime assumes requestAnimationFrame exists so we need to polyfill it.87    global.requestAnimationFrame = global.window.requestAnimationFrame = cb =>88      setTimeout(cb);89    container = document.getElementById('container');9091    CSPnonce = null;92    Scheduler = require('scheduler');93    React = require('react');94    ReactDOM = require('react-dom');95    ReactDOMClient = require('react-dom/client');96    ReactDOMFizzServer = require('react-dom/server');97    ReactDOMFizzStatic = require('react-dom/static');98    Stream = require('stream');99    Suspense = React.Suspense;100    use = React.use;101    if (gate(flags => flags.enableSuspenseList)) {102      SuspenseList = React.unstable_SuspenseList;103    }104    PropTypes = require('prop-types');105    if (__VARIANT__) {106      const originalConsoleError = console.error;107      console.error = (error, ...args) => {108        if (109          typeof error !== 'string' ||110          error.indexOf('ReactDOM.useFormState has been renamed') === -1111        ) {112          originalConsoleError(error, ...args);113        }114      };115116      // Remove after API is deleted.117      useActionState = ReactDOM.useFormState;118    } else {119      useActionState = React.useActionState;120    }121122    ({123      assertConsoleErrorDev,124      assertLog,125      act: clientAct,126      waitFor,127      waitForAll,128      waitForPaint,129    } = require('internal-test-utils'));130131    if (gate(flags => flags.source)) {132      // The `with-selector` module composes the main `use-sync-external-store`133      // entrypoint. In the compiled artifacts, this is resolved to the `shim`134      // implementation by our build config, but when running the tests against135      // the source files, we need to tell Jest how to resolve it. Because this136      // is a source module, this mock has no affect on the build tests.137      jest.mock('use-sync-external-store/src/useSyncExternalStore', () =>138        jest.requireActual('react'),139      );140    }141    useSyncExternalStore = React.useSyncExternalStore;142    useSyncExternalStoreWithSelector =143      require('use-sync-external-store/with-selector').useSyncExternalStoreWithSelector;144145    textCache = new Map();146147    buffer = '';148    hasErrored = false;149150    writable = new Stream.PassThrough();151    writable.setEncoding('utf8');152    writable.on('data', chunk => {153      buffer += chunk;154    });155    writable.on('error', error => {156      hasErrored = true;157      fatalError = error;158    });159160    renderOptions = {};161    if (gate(flags => flags.shouldUseFizzExternalRuntime)) {162      renderOptions.unstable_externalRuntimeSrc =163        'react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js';164    }165  });166167  function expectErrors(errorsArr, toBeDevArr, toBeProdArr) {168    const mappedErrows = errorsArr.map(({error, errorInfo}) => {169      const stack = errorInfo && errorInfo.componentStack;170      const digest = error.digest;171      if (stack) {172        return [error.message, digest, normalizeCodeLocInfo(stack)];173      } else if (digest) {174        return [error.message, digest];175      }176      return error.message;177    });178    if (__DEV__) {179      expect(mappedErrows).toEqual(toBeDevArr);180    } else {181      expect(mappedErrows).toEqual(toBeProdArr);182    }183  }184185  function componentStack(components) {186    return components187      .map(component => `\n    in ${component} (at **)`)188      .join('');189  }190191  const bodyStartMatch = /<body(?:>| .*?>)/;192  const headStartMatch = /<head(?:>| .*?>)/;193194  async function act(callback) {195    await callback();196    // Await one turn around the event loop.197    // This assumes that we'll flush everything we have so far.198    await new Promise(resolve => {199      setImmediate(resolve);200    });201    if (hasErrored) {202      throw fatalError;203    }204    // JSDOM doesn't support stream HTML parser so we need to give it a proper fragment.205    // We also want to execute any scripts that are embedded.206    // We assume that we have now received a proper fragment of HTML.207    let bufferedContent = buffer;208    buffer = '';209210    if (!bufferedContent) {211      jest.runAllTimers();212      return;213    }214215    const bodyMatch = bufferedContent.match(bodyStartMatch);216    const headMatch = bufferedContent.match(headStartMatch);217218    if (streamingContainer === null) {219      // This is the first streamed content. We decide here where to insert it. If we get <html>, <head>, or <body>220      // we abandon the pre-built document and start from scratch. If we get anything else we assume it goes into the221      // container. This is not really production behavior because you can't correctly stream into a deep div effectively222      // but it's pragmatic for tests.223224      if (225        bufferedContent.startsWith('<head>') ||226        bufferedContent.startsWith('<head ') ||227        bufferedContent.startsWith('<body>') ||228        bufferedContent.startsWith('<body ')229      ) {230        // wrap in doctype to normalize the parsing process231        bufferedContent = '<!DOCTYPE html><html>' + bufferedContent;232      } else if (233        bufferedContent.startsWith('<html>') ||234        bufferedContent.startsWith('<html ')235      ) {236        throw new Error(237          'Recieved <html> without a <!DOCTYPE html> which is almost certainly a bug in React',238        );239      }240241      if (bufferedContent.startsWith('<!DOCTYPE html>')) {242        // we can just use the whole document243        const tempDom = new JSDOM(bufferedContent);244245        // Wipe existing head and body content246        document.head.innerHTML = '';247        document.body.innerHTML = '';248249        // Copy the <html> attributes over250        const tempHtmlNode = tempDom.window.document.documentElement;251        for (let i = 0; i < tempHtmlNode.attributes.length; i++) {252          const attr = tempHtmlNode.attributes[i];253          document.documentElement.setAttribute(attr.name, attr.value);254        }255256        if (headMatch) {257          // We parsed a head open tag. we need to copy head attributes and insert future258          // content into <head>259          streamingContainer = document.head;260          const tempHeadNode = tempDom.window.document.head;261          for (let i = 0; i < tempHeadNode.attributes.length; i++) {262            const attr = tempHeadNode.attributes[i];263            document.head.setAttribute(attr.name, attr.value);264          }265          const source = document.createElement('head');266          source.innerHTML = tempHeadNode.innerHTML;267          await insertNodesAndExecuteScripts(source, document.head, CSPnonce);268        }269270        if (bodyMatch) {271          // We parsed a body open tag. we need to copy head attributes and insert future272          // content into <body>273          streamingContainer = document.body;274          const tempBodyNode = tempDom.window.document.body;275          for (let i = 0; i < tempBodyNode.attributes.length; i++) {276            const attr = tempBodyNode.attributes[i];277            document.body.setAttribute(attr.name, attr.value);278          }279          const source = document.createElement('body');280          source.innerHTML = tempBodyNode.innerHTML;281          await insertNodesAndExecuteScripts(source, document.body, CSPnonce);282        }283284        if (!headMatch && !bodyMatch) {285          throw new Error('expected <head> or <body> after <html>');286        }287      } else {288        // we assume we are streaming into the default container'289        streamingContainer = container;290        const div = document.createElement('div');291        div.innerHTML = bufferedContent;292        await insertNodesAndExecuteScripts(div, container, CSPnonce);293      }294    } else if (streamingContainer === document.head) {295      bufferedContent = '<!DOCTYPE html><html><head>' + bufferedContent;296      const tempDom = new JSDOM(bufferedContent);297298      const tempHeadNode = tempDom.window.document.head;299      const source = document.createElement('head');300      source.innerHTML = tempHeadNode.innerHTML;301      await insertNodesAndExecuteScripts(source, document.head, CSPnonce);302303      if (bodyMatch) {304        streamingContainer = document.body;305306        const tempBodyNode = tempDom.window.document.body;307        for (let i = 0; i < tempBodyNode.attributes.length; i++) {308          const attr = tempBodyNode.attributes[i];309          document.body.setAttribute(attr.name, attr.value);310        }311        const bodySource = document.createElement('body');312        bodySource.innerHTML = tempBodyNode.innerHTML;313        await insertNodesAndExecuteScripts(bodySource, document.body, CSPnonce);314      }315    } else {316      const div = document.createElement('div');317      div.innerHTML = bufferedContent;318      await insertNodesAndExecuteScripts(div, streamingContainer, CSPnonce);319    }320    // Let throttled boundaries reveal321    jest.runAllTimers();322  }323324  function resolveText(text) {325    const record = textCache.get(text);326    if (record === undefined) {327      const newRecord = {328        status: 'resolved',329        value: text,330      };331      textCache.set(text, newRecord);332    } else if (record.status === 'pending') {333      const thenable = record.value;334      record.status = 'resolved';335      record.value = text;336      thenable.pings.forEach(t => t());337    }338  }339340  function rejectText(text, error) {341    const record = textCache.get(text);342    if (record === undefined) {343      const newRecord = {344        status: 'rejected',345        value: error,346      };347      textCache.set(text, newRecord);348    } else if (record.status === 'pending') {349      const thenable = record.value;350      record.status = 'rejected';351      record.value = error;352      thenable.pings.forEach(t => t());353    }354  }355356  function readText(text) {357    const record = textCache.get(text);358    if (record !== undefined) {359      switch (record.status) {360        case 'pending':361          throw record.value;362        case 'rejected':363          throw record.value;364        case 'resolved':365          return record.value;366      }367    } else {368      const thenable = {369        pings: [],370        then(resolve) {371          if (newRecord.status === 'pending') {372            thenable.pings.push(resolve);373          } else {374            Promise.resolve().then(() => resolve(newRecord.value));375          }376        },377      };378379      const newRecord = {380        status: 'pending',381        value: thenable,382      };383      textCache.set(text, newRecord);384385      throw thenable;386    }387  }388389  function Text({text}) {390    return text;391  }392393  function AsyncText({text}) {394    return readText(text);395  }396397  function AsyncTextWrapped({as, text}) {398    const As = as;399    return <As>{readText(text)}</As>;400  }401  function renderToPipeableStream(jsx, options) {402    // Merge options with renderOptions, which may contain featureFlag specific behavior403    return ReactDOMFizzServer.renderToPipeableStream(404      jsx,405      mergeOptions(options, renderOptions),406    );407  }408409  it('can opt a component into browser-only rendering', async () => {410    let resolveBrowserText;411    const browserText = new Promise(resolve => {412      resolveBrowserText = resolve;413    });414    let browserReason;415    const initializeReason = jest.fn(() => {416      browserReason = Object.freeze(417        new Error('Only render this content in a browser'),418      );419      return browserReason;420    });421    const browserOnly = ReactDOM.browser(initializeReason);422423    function BrowserOnly() {424      use(browserOnly);425      const text = use(browserText);426      Scheduler.log(text);427      return <span>{text}</span>;428    }429430    function App() {431      return (432        <div>433          <Suspense fallback={<span>Fallback</span>}>434            <BrowserOnly />435          </Suspense>436        </div>437      );438    }439440    const serverErrors = [];441    const browserBailouts = [];442    await act(() => {443      const {pipe} = renderToPipeableStream(<App />, {444        onError(error) {445          serverErrors.push(error);446        },447        onBrowserBailout(error, errorInfo) {448          browserBailouts.push({error, errorInfo});449        },450      });451      pipe(writable);452    });453454    expect(serverErrors).toEqual([]);455    expect(initializeReason).toHaveBeenCalledTimes(1);456    expect(browserBailouts).toHaveLength(1);457    expect(browserBailouts[0].error).toBeInstanceOf(Error);458    expect(browserBailouts[0].error.message).toBe(459      'Browser-only rendering was requested by `browser()`.',460    );461    expect(browserBailouts[0].error.stack).toContain('BrowserOnly');462    expect(browserBailouts[0].error.cause).toBe(browserReason);463    expect(464      normalizeCodeLocInfo(browserBailouts[0].errorInfo.componentStack),465    ).toBe(componentStack(['BrowserOnly', 'Suspense', 'div', 'App']));466    expect(getVisibleChildren(container)).toEqual(467      <div>468        <span>Fallback</span>469      </div>,470    );471    const recoverableErrors = [];472    ReactDOMClient.hydrateRoot(container, <App />, {473      onRecoverableError(error) {474        recoverableErrors.push(error);475      },476    });477    await waitForAll([]);478479    expect(getVisibleChildren(container)).toEqual(480      <div>481        <span>Fallback</span>482      </div>,483    );484485    await clientAct(() => {486      resolveBrowserText('Browser');487    });488    assertLog(['Browser']);489490    expect(recoverableErrors).toEqual([]);491    expect(initializeReason).toHaveBeenCalledTimes(1);492    expect(getVisibleChildren(container)).toEqual(493      <div>494        <span>Browser</span>495      </div>,496    );497  });498499  it('can opt a component into browser-only rendering after streaming the fallback', async () => {500    let resolveServerReady;501    const serverReady = new Promise(resolve => {502      resolveServerReady = resolve;503    });504    const initializeReason = jest.fn(505      () => 'Only render this content in a browser',506    );507508    function BrowserOnly() {509      use(serverReady);510      use(ReactDOM.browser(initializeReason));511      return <span>Browser</span>;512    }513514    function App() {515      return (516        <div>517          <Suspense fallback={<span>Fallback</span>}>518            <BrowserOnly />519          </Suspense>520        </div>521      );522    }523524    const serverErrors = [];525    const browserBailouts = [];526    await act(() => {527      const {pipe} = renderToPipeableStream(<App />, {528        onError(error) {529          serverErrors.push(error);530        },531        onBrowserBailout(error) {532          browserBailouts.push(error);533        },534      });535      pipe(writable);536    });537538    expect(getVisibleChildren(container)).toEqual(539      <div>540        <span>Fallback</span>541      </div>,542    );543544    await act(() => {545      resolveServerReady();546    });547548    expect(serverErrors).toEqual([]);549    expect(initializeReason).toHaveBeenCalledTimes(1);550    expect(browserBailouts).toHaveLength(1);551    expect(browserBailouts[0].message).toBe(552      'Browser-only rendering was requested by `browser()`.',553    );554    expect(browserBailouts[0].stack).toContain('BrowserOnly');555    expect(browserBailouts[0].cause).toBe(556      'Only render this content in a browser',557    );558559    const recoverableErrors = [];560    ReactDOMClient.hydrateRoot(container, <App />, {561      onRecoverableError(error) {562        recoverableErrors.push(error);563      },564    });565    await waitForAll([]);566567    expect(recoverableErrors).toEqual([]);568    expect(initializeReason).toHaveBeenCalledTimes(1);569    expect(getVisibleChildren(container)).toEqual(570      <div>571        <span>Browser</span>572      </div>,573    );574  });575576  it('supports omitted and direct string browser reasons', async () => {577    const directReason = 'Only render this content in a browser';578    const withoutReason = ReactDOM.browser();579    const withDirectReason = ReactDOM.browser(directReason);580581    function WithoutReason() {582      use(withoutReason);583      return <span>Browser</span>;584    }585586    function WithDirectReason() {587      use(withDirectReason);588      return <span>Browser</span>;589    }590591    const serverErrors = [];592    const browserBailouts = [];593    await act(() => {594      const {pipe} = renderToPipeableStream(595        <>596          <Suspense fallback={<span>Fallback A</span>}>597            <WithoutReason />598          </Suspense>599          <Suspense fallback={<span>Fallback B</span>}>600            <WithDirectReason />601          </Suspense>602        </>,603        {604          onError(error) {605            serverErrors.push(error);606          },607          onBrowserBailout(error) {608            browserBailouts.push(error);609          },610        },611      );612      pipe(writable);613    });614615    expect(serverErrors).toEqual([]);616    expect(browserBailouts).toHaveLength(2);617    expect(browserBailouts[0].message).toBe(618      'Browser-only rendering was requested by `browser()`.',619    );620    expect(browserBailouts[0].stack).toContain('WithoutReason');621    expect(622      Object.prototype.hasOwnProperty.call(browserBailouts[0], 'cause'),623    ).toBe(false);624    expect(browserBailouts[1].message).toBe(625      'Browser-only rendering was requested by `browser()`.',626    );627    expect(browserBailouts[1].stack).toContain('WithDirectReason');628    expect(browserBailouts[1].cause).toBe(directReason);629  });630631  it('supports any value returned by a browser reason initializer', async () => {632    const reasonValues = [undefined, null, 42, Symbol('browser reason')];633    const initializeReasons = reasonValues.map(reason => jest.fn(() => reason));634    const browserValues = initializeReasons.map(initializeReason =>635      ReactDOM.browser(initializeReason),636    );637638    function BrowserOnly({browserValue}) {639      use(browserValue);640      return <span>Browser</span>;641    }642643    const serverErrors = [];644    const browserBailouts = [];645    await act(() => {646      const {pipe} = renderToPipeableStream(647        <>648          {browserValues.map((browserValue, index) => (649            <Suspense key={index} fallback={<span>Fallback</span>}>650              <BrowserOnly browserValue={browserValue} />651            </Suspense>652          ))}653        </>,654        {655          onError(error) {656            serverErrors.push(error);657          },658          onBrowserBailout(error) {659            browserBailouts.push(error);660          },661        },662      );663      pipe(writable);664    });665666    expect(serverErrors).toEqual([]);667    expect(browserBailouts).toHaveLength(reasonValues.length);668    initializeReasons.forEach(initializeReason => {669      expect(initializeReason).toHaveBeenCalledTimes(1);670    });671    browserBailouts.forEach((error, index) => {672      expect(error).toBeInstanceOf(Error);673      expect(error.message).toBe(674        'Browser-only rendering was requested by `browser()`.',675      );676      expect(Object.prototype.hasOwnProperty.call(error, 'cause')).toBe(true);677      expect(error.cause).toBe(reasonValues[index]);678    });679  });680681  it('initializes a shared browser reason at each use site', async () => {682    const browserReasons = [];683    const initializeReason = jest.fn(() => {684      const browserReason = {index: browserReasons.length};685      browserReasons.push(browserReason);686      return browserReason;687    });688    const browserValue = ReactDOM.browser(initializeReason);689690    function BrowserOnlyA() {691      use(browserValue);692      return <span>Browser A</span>;693    }694695    function BrowserOnlyB() {696      use(browserValue);697      return <span>Browser B</span>;698    }699700    const browserBailouts = [];701    await act(() => {702      const {pipe} = renderToPipeableStream(703        <>704          <Suspense fallback={<span>Fallback A</span>}>705            <BrowserOnlyA />706          </Suspense>707          <Suspense fallback={<span>Fallback B</span>}>708            <BrowserOnlyB />709          </Suspense>710        </>,711        {712          onBrowserBailout(error) {713            browserBailouts.push(error);714          },715        },716      );717      pipe(writable);718    });719720    expect(initializeReason).toHaveBeenCalledTimes(2);721    expect(browserBailouts).toHaveLength(2);722    expect(browserBailouts[0]).not.toBe(browserBailouts[1]);723    expect(browserBailouts[0].cause).toBe(browserReasons[0]);724    expect(browserBailouts[0].stack).toContain('BrowserOnlyA');725    expect(browserBailouts[1].cause).toBe(browserReasons[1]);726    expect(browserBailouts[1].stack).toContain('BrowserOnlyB');727  });728729  it('uses a fallback if a browser reason initializer throws', async () => {730    const reasonError = new Error('Failed to initialize browser reason');731    const initializeReason = jest.fn(() => {732      throw reasonError;733    });734    const browserValue = ReactDOM.browser(initializeReason);735736    function BrowserOnly() {737      use(browserValue);738      return <span>Browser</span>;739    }740741    const serverErrors = [];742    const browserBailouts = [];743    await act(() => {744      const {pipe} = renderToPipeableStream(745        <Suspense fallback={<span>Fallback</span>}>746          <BrowserOnly />747        </Suspense>,748        {749          onError(error) {750            serverErrors.push(error);751          },752          onBrowserBailout(error) {753            browserBailouts.push(error);754          },755        },756      );757      pipe(writable);758    });759760    expect(initializeReason).toHaveBeenCalledTimes(1);761    expect(serverErrors).toEqual([]);762    expect(browserBailouts).toHaveLength(1);763    expect(browserBailouts[0].cause).toBe(764      'The reason for browser-only rendering could not be determined because ' +765        'its initializer threw.',766    );767    expect(getVisibleChildren(container)).toEqual(<span>Fallback</span>);768  });769770  it('errors if browser-only content is rendered outside Suspense', async () => {771    const browserReason = 'Only render this content in a browser';772    const browserValue = ReactDOM.browser(browserReason);773774    function BrowserOnly() {775      use(browserValue);776      return <span>Browser</span>;777    }778779    const reportedErrors = [];780    const browserBailouts = [];781    let shellReady = false;782    let shellError;783    await act(() => {784      renderToPipeableStream(<BrowserOnly />, {785        onError(error) {786          reportedErrors.push(error);787        },788        onBrowserBailout(error) {789          browserBailouts.push(error);790        },791        onShellReady() {792          shellReady = true;793        },794        onShellError(error) {795          shellError = error;796        },797      });798    });799800    expect(shellError).toBeInstanceOf(Error);801    expect(shellError.message).toBe(802      'The server render could not complete because client rendering was ' +803        "requested outside a Suspense boundary. See this error's cause for " +804        'additional details.',805    );806    expect(shellError.cause).toBe(browserReason);807    expect(shellError.stack).toContain('BrowserOnly');808    expect(shellError.stack.split('\n')[0]).toBe(809      'Error: ' + shellError.message,810    );811    expect(shellReady).toBe(false);812    expect(reportedErrors).toEqual([shellError]);813    expect(browserBailouts).toEqual([]);814  });815816  it('can abort all pending boundaries into browser-only rendering', async () => {817    const never = new Promise(() => {});818    let isClient = false;819820    function Pending({children}) {821      if (!isClient) {822        use(never);823      }824      return <span>{children}</span>;825    }826827    function App() {828      return (829        <div>830          <span>Shell</span>831          <Suspense fallback={<span>Loading A</span>}>832            <Pending>A</Pending>833          </Suspense>834          <Suspense fallback={<span>Loading B</span>}>835            <Pending>B</Pending>836          </Suspense>837        </div>838      );839    }840841    const serverErrors = [];842    const browserBailouts = [];843    const browserReason = {code: 'render-pending-content-in-browser'};844    const initializeReason = jest.fn(() => browserReason);845    const browserValue = ReactDOM.browser(initializeReason);846    let abort;847    await act(() => {848      const controls = renderToPipeableStream(<App />, {849        onError(error) {850          serverErrors.push(error);851        },852        onBrowserBailout(error) {853          browserBailouts.push(error);854        },855      });856      abort = controls.abort;857      controls.pipe(writable);858    });859860    expect(getVisibleChildren(container)).toEqual(861      <div>862        <span>Shell</span>863        <span>Loading A</span>864        <span>Loading B</span>865      </div>,866    );867868    await act(() => {869      function abortToBrowser() {870        abort(browserValue);871      }872      abortToBrowser();873    });874875    expect(serverErrors).toEqual([]);876    expect(initializeReason).toHaveBeenCalledTimes(1);877    expect(browserBailouts).toHaveLength(2);878    expect(browserBailouts[0]).toBeInstanceOf(Error);879    expect(browserBailouts[0].message).toBe(880      'Browser-only rendering was requested by `browser()`.',881    );882    expect(browserBailouts[0].stack).toContain('abortToBrowser');883    expect(browserBailouts[0].cause).toBe(browserReason);884    expect(browserBailouts[1]).toBe(browserBailouts[0]);885886    isClient = true;887    const recoverableErrors = [];888    ReactDOMClient.hydrateRoot(container, <App />, {889      onRecoverableError(error) {890        recoverableErrors.push(error);891      },892    });893    await waitForAll([]);894895    expect(recoverableErrors).toEqual([]);896    expect(getVisibleChildren(container)).toEqual(897      <div>898        <span>Shell</span>899        <span>A</span>900        <span>B</span>901      </div>,902    );903  });904905  it('errors if aborted with browser() before the shell completes', async () => {906    const never = new Promise(() => {});907    let browserReason;908    const initializeReason = jest.fn(() => {909      browserReason = new Error('Only abort this render on the server');910      return browserReason;911    });912    const browserValue = ReactDOM.browser(initializeReason);913914    function PendingRoot() {915      use(never);916      return <span>Root</span>;917    }918919    const reportedErrors = [];920    const browserBailouts = [];921    let shellReady = false;922    let shellError;923    let abort;924    await act(() => {925      const controls = renderToPipeableStream(<PendingRoot />, {926        onError(error) {927          reportedErrors.push(error);928        },929        onBrowserBailout(error) {930          browserBailouts.push(error);931        },932        onShellReady() {933          shellReady = true;934        },935        onShellError(error) {936          shellError = error;937        },938      });939      abort = controls.abort;940    });941942    await act(() => {943      function abortToBrowser() {944        abort(browserValue);945      }946      abortToBrowser();947    });948949    expect(shellError).toBeInstanceOf(Error);950    expect(initializeReason).toHaveBeenCalledTimes(1);951    expect(shellError.message).toBe(952      'The server render could not complete because client rendering was ' +953        "requested outside a Suspense boundary. See this error's cause for " +954        'additional details.',955    );956    expect(shellError.cause).toBe(browserReason);957    expect(shellError.stack).toContain('abortToBrowser');958    expect(shellReady).toBe(false);959    expect(reportedErrors).toEqual([shellError]);960    expect(browserBailouts).toEqual([]);961  });962963  it('reports nested browser bailouts if aborting fatals the shell', async () => {964    const never = new Promise(() => {});965    const browserReason = 'Abort pending work into browser rendering';966    const browserValue = ReactDOM.browser(browserReason);967968    function Pending() {969      use(never);970      return <span>Pending</span>;971    }972973    const reportedErrors = [];974    const browserBailouts = [];975    let shellError;976    let abort;977    await act(() => {978      const controls = renderToPipeableStream(979        <>980          <Suspense fallback={<span>Fallback</span>}>981            <Pending />982          </Suspense>983          <Pending />984          <Suspense fallback={<span>Fallback</span>}>985            <Pending />986          </Suspense>987          <Pending />988        </>,989        {990          onError(error) {991            reportedErrors.push(error);992          },993          onBrowserBailout(error) {994            browserBailouts.push(error);995          },996          onShellError(error) {997            shellError = error;998          },999        },1000      );1001      abort = controls.abort;1002    });10031004    await act(() => {1005      abort(browserValue);1006    });10071008    expect(shellError).toBeInstanceOf(Error);1009    expect(shellError.message).toBe(1010      'The server render could not complete because client rendering was ' +1011        "requested outside a Suspense boundary. See this error's cause for " +1012        'additional details.',1013    );1014    expect(shellError.cause).toBe(browserReason);1015    expect(reportedErrors).toHaveLength(2);1016    expect(reportedErrors[0]).toBe(shellError);1017    expect(reportedErrors[1].message).toBe(shellError.message);1018    expect(reportedErrors[1].cause).toBe(browserReason);1019    expect(browserBailouts).toHaveLength(2);1020    expect(browserBailouts[0]).toBe(browserBailouts[1]);1021    expect(browserBailouts[0]).not.toBe(shellError);1022    expect(browserBailouts[0].message).toBe(1023      'Browser-only rendering was requested by `browser()`.',1024    );1025    expect(browserBailouts[0].cause).toBe(browserReason);1026  });10271028  it('uses a fallback if a browser reason initializer throws during abort', async () => {1029    const never = new Promise(() => {});1030    const reasonError = new Error('Failed to initialize browser reason');1031    const initializeReason = jest.fn(() => {1032      throw reasonError;1033    });1034    const browserValue = ReactDOM.browser(initializeReason);10351036    function PendingRoot() {1037      use(never);1038      return <span>Root</span>;1039    }10401041    const reportedErrors = [];1042    const browserBailouts = [];1043    let shellError;1044    let abort;1045    await act(() => {1046      const controls = renderToPipeableStream(<PendingRoot />, {1047        onError(error) {1048          reportedErrors.push(error);1049        },1050        onBrowserBailout(error) {1051          browserBailouts.push(error);1052        },1053        onShellError(error) {1054          shellError = error;1055        },1056      });1057      abort = controls.abort;1058    });10591060    await act(() => {1061      abort(browserValue);1062    });10631064    expect(initializeReason).toHaveBeenCalledTimes(1);1065    expect(shellError).toBeInstanceOf(Error);1066    expect(shellError.cause).toBe(1067      'The reason for browser-only rendering could not be determined because ' +1068        'its initializer threw.',1069    );1070    expect(reportedErrors).toEqual([shellError]);1071    expect(browserBailouts).toEqual([]);1072  });10731074  it('reports the browser value if it is thrown instead of passed to use', async () => {1075    const initializeReason = jest.fn(1076      () => new Error('Only render this content in a browser'),1077    );1078    const browserValue = ReactDOM.browser(initializeReason);10791080    function BrowserOnly() {1081      throw browserValue;1082    }10831084    const reportedErrors = [];1085    const browserBailouts = [];1086    await act(() => {1087      const {pipe} = renderToPipeableStream(1088        <Suspense fallback={<span>Fallback</span>}>1089          <BrowserOnly />1090        </Suspense>,1091        {1092          onError(error) {1093            reportedErrors.push(error);1094          },1095          onBrowserBailout(error) {1096            browserBailouts.push(error);1097          },1098        },1099      );1100      pipe(writable);1101    });11021103    expect(reportedErrors).toEqual([browserValue]);1104    expect(browserBailouts).toEqual([]);1105    expect(initializeReason).not.toHaveBeenCalled();1106    expect(getVisibleChildren(container)).toEqual(<span>Fallback</span>);1107  });11081109  ['', 'BROWSER'].forEach(userDigest => {1110    it(`does not reserve the ${JSON.stringify(1111      userDigest,1112    )} user error digest for browser rendering`, async () => {1113      let isClient = false;1114      const serverError = new Error('Server error');11151116      function ServerError() {1117        if (!isClient) {1118          throw serverError;1119        }1120        return <span>Client</span>;1121      }11221123      function App() {1124        return (1125          <Suspense fallback={<span>Fallback</span>}>1126            <ServerError />1127          </Suspense>1128        );1129      }11301131      const serverErrors = [];1132      await act(() => {1133        const {pipe} = renderToPipeableStream(<App />, {1134          onError(error) {1135            serverErrors.push(error);1136            return userDigest;1137          },1138        });1139        pipe(writable);1140      });11411142      expect(serverErrors).toEqual([serverError]);1143      expect(getVisibleChildren(container)).toEqual(<span>Fallback</span>);11441145      isClient = true;1146      const recoverableErrors = [];1147      ReactDOMClient.hydrateRoot(container, <App />, {1148        onRecoverableError(error) {1149          recoverableErrors.push(error);1150        },1151      });1152      await waitForAll([]);11531154      expect(recoverableErrors).toHaveLength(1);1155      expect(recoverableErrors[0].digest).toBe(userDigest || undefined);1156      expect(getVisibleChildren(container)).toEqual(<span>Client</span>);1157    });1158  });11591160  it('should asynchronously load a lazy component', async () => {1161    let resolveA;1162    const LazyA = React.lazy(() => {1163      return new Promise(r => {1164        resolveA = r;1165      });1166    });11671168    let resolveB;1169    const LazyB = React.lazy(() => {1170      return new Promise(r => {1171        resolveB = r;1172      });1173    });11741175    class TextWithPunctuation extends React.Component {1176      render() {1177        return <Text text={this.props.text + this.props.punctuation} />;1178      }1179    }11801181    // This tests that default props of the inner element is resolved.1182    TextWithPunctuation.defaultProps = {1183      punctuation: '!',1184    };11851186    await act(() => {1187      const {pipe} = renderToPipeableStream(1188        <div>1189          <div>1190            <Suspense fallback={<Text text="Loading..." />}>1191              <LazyA text="Hello" />1192            </Suspense>1193          </div>1194          <div>1195            <Suspense fallback={<Text text="Loading..." />}>1196              <LazyB text="world" />1197            </Suspense>1198          </div>1199        </div>,1200      );1201      pipe(writable);1202    });12031204    expect(getVisibleChildren(container)).toEqual(1205      <div>1206        <div>Loading...</div>1207        <div>Loading...</div>1208      </div>,1209    );1210    await act(() => {1211      resolveA({default: Text});1212    });1213    expect(getVisibleChildren(container)).toEqual(1214      <div>1215        <div>Hello</div>1216        <div>Loading...</div>1217      </div>,1218    );1219    await act(() => {1220      resolveB({default: TextWithPunctuation});1221    });1222    expect(getVisibleChildren(container)).toEqual(1223      <div>1224        <div>Hello</div>1225        <div>world!</div>1226      </div>,1227    );1228  });12291230  it('#23331: does not warn about hydration mismatches if something suspended in an earlier sibling', async () => {1231    const makeApp = () => {1232      let resolve;1233      const imports = new Promise(r => {1234        resolve = () => r({default: () => <span id="async">async</span>});1235      });1236      const Lazy = React.lazy(() => imports);12371238      const App = () => (1239        <div>1240          <Suspense fallback={<span>Loading...</span>}>1241            <Lazy />1242            <span id="after">after</span>1243          </Suspense>1244        </div>1245      );12461247      return [App, resolve];1248    };12491250    // Server-side1251    const [App, resolve] = makeApp();1252    await act(() => {1253      const {pipe} = renderToPipeableStream(<App />);1254      pipe(writable);1255    });1256    expect(getVisibleChildren(container)).toEqual(1257      <div>1258        <span>Loading...</span>1259      </div>,1260    );1261    await act(() => {1262      resolve();1263    });1264    expect(getVisibleChildren(container)).toEqual(1265      <div>1266        <span id="async">async</span>1267        <span id="after">after</span>1268      </div>,1269    );12701271    // Client-side1272    const [HydrateApp, hydrateResolve] = makeApp();1273    await act(() => {1274      ReactDOMClient.hydrateRoot(container, <HydrateApp />);1275    });12761277    expect(getVisibleChildren(container)).toEqual(1278      <div>1279        <span id="async">async</span>1280        <span id="after">after</span>1281      </div>,1282    );12831284    await act(() => {1285      hydrateResolve();1286    });1287    expect(getVisibleChildren(container)).toEqual(1288      <div>1289        <span id="async">async</span>1290        <span id="after">after</span>1291      </div>,1292    );1293  });12941295  it('should support nonce for bootstrap and runtime scripts', async () => {1296    CSPnonce = 'R4nd0m';1297    try {1298      let resolve;1299      const Lazy = React.lazy(() => {1300        return new Promise(r => {1301          resolve = r;1302        });1303      });13041305      await act(() => {1306        const {pipe} = renderToPipeableStream(1307          <div>1308            <Suspense fallback={<Text text="Loading..." />}>1309              <Lazy text="Hello" />1310            </Suspense>1311          </div>,1312          {1313            nonce: 'R4nd0m',1314            bootstrapScriptContent: 'function noop(){}',1315            bootstrapScripts: [1316              'init.js',1317              {src: 'init2.js', integrity: 'init2hash'},1318            ],1319            bootstrapModules: [1320              'init.mjs',1321              {src: 'init2.mjs', integrity: 'init2hash'},1322            ],1323          },1324        );1325        pipe(writable);1326      });13271328      expect(getVisibleChildren(container)).toEqual([1329        <link1330          rel="preload"1331          fetchpriority="low"1332          href="init.js"1333          as="script"1334          nonce={CSPnonce}1335        />,1336        <link1337          rel="preload"1338          fetchpriority="low"1339          href="init2.js"1340          as="script"1341          nonce={CSPnonce}1342          integrity="init2hash"1343        />,1344        <link1345          rel="modulepreload"1346          fetchpriority="low"1347          href="init.mjs"1348          nonce={CSPnonce}1349        />,1350        <link1351          rel="modulepreload"1352          fetchpriority="low"1353          href="init2.mjs"1354          nonce={CSPnonce}1355          integrity="init2hash"1356        />,1357        <div>Loading...</div>,1358      ]);13591360      // check that there are 6 scripts with a matching nonce:1361      // The runtime script or initial paint time, an inline bootstrap script, two bootstrap scripts and two bootstrap modules1362      expect(1363        Array.from(container.getElementsByTagName('script')).filter(1364          node => node.getAttribute('nonce') === CSPnonce,1365        ).length,1366      ).toEqual(6);13671368      await act(() => {1369        resolve({default: Text});1370      });1371      expect(getVisibleChildren(container)).toEqual([1372        <link1373          rel="preload"1374          fetchpriority="low"1375          href="init.js"1376          as="script"1377          nonce={CSPnonce}1378        />,1379        <link1380          rel="preload"1381          fetchpriority="low"1382          href="init2.js"1383          as="script"1384          nonce={CSPnonce}1385          integrity="init2hash"1386        />,1387        <link1388          rel="modulepreload"1389          fetchpriority="low"1390          href="init.mjs"1391          nonce={CSPnonce}1392        />,1393        <link1394          rel="modulepreload"1395          fetchpriority="low"1396          href="init2.mjs"1397          nonce={CSPnonce}1398          integrity="init2hash"1399        />,1400        <div>Hello</div>,1401      ]);1402    } finally {1403      CSPnonce = null;1404    }1405  });14061407  it('should not automatically add nonce to rendered scripts', async () => {1408    CSPnonce = 'R4nd0m';1409    try {1410      await act(async () => {1411        const {pipe} = renderToPipeableStream(1412          <html>1413            <body>1414              <script nonce={CSPnonce}>{'try { foo() } catch (e) {} ;'}</script>1415              <script nonce={CSPnonce} src="foo" async={true} />1416              <script src="bar" />1417              <script src="baz" integrity="qux" async={true} />1418              <script type="module" src="quux" async={true} />1419              <script type="module" src="corge" async={true} />1420              <script1421                type="module"1422                src="grault"1423                integrity="garply"1424                async={true}1425              />1426            </body>1427          </html>,1428          {1429            nonce: CSPnonce,1430          },1431        );1432        pipe(writable);1433      });14341435      expect(1436        stripExternalRuntimeInNodes(1437          document.getElementsByTagName('script'),1438          renderOptions.unstable_externalRuntimeSrc,1439        ).map(n => n.outerHTML),1440      ).toEqual([1441        `<script nonce="${CSPnonce}" src="foo" async=""></script>`,1442        `<script src="baz" integrity="qux" async=""></script>`,1443        `<script type="module" src="quux" async=""></script>`,1444        `<script type="module" src="corge" async=""></script>`,1445        `<script type="module" src="grault" integrity="garply" async=""></script>`,1446        `<script nonce="${CSPnonce}">try { foo() } catch (e) {} ;</script>`,1447        `<script src="bar"></script>`,1448      ]);1449    } finally {1450      CSPnonce = null;1451    }1452  });14531454  it('should client render a boundary if a lazy component rejects', async () => {1455    let rejectComponent;1456    const promise = new Promise((resolve, reject) => {1457      rejectComponent = reject;1458    });1459    const LazyComponent = React.lazy(() => {1460      return promise;1461    });14621463    const LazyLazy = React.lazy(async () => {1464      return {1465        default: LazyComponent,1466      };1467    });14681469    function Wrapper({children}) {1470      return children;1471    }1472    const LazyWrapper = React.lazy(() => {1473      return {1474        then(callback) {1475          callback({1476            default: Wrapper,1477          });1478        },1479      };1480    });14811482    function App({isClient}) {1483      return (1484        <div>1485          <Suspense fallback={<Text text="Loading..." />}>1486            <LazyWrapper>1487              {isClient ? <Text text="Hello" /> : <LazyLazy text="Hello" />}1488            </LazyWrapper>1489          </Suspense>1490        </div>1491      );1492    }14931494    let bootstrapped = false;1495    const errors = [];1496    window.__INIT__ = function () {1497      bootstrapped = true;1498      // Attempt to hydrate the content.1499      ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {1500        onRecoverableError(error, errorInfo) {1501          errors.push({error, errorInfo});1502        },1503      });1504    };15051506    const theError = new Error('Test');1507    const loggedErrors = [];1508    function onError(x, errorInfo) {1509      loggedErrors.push(x);1510      return 'Hash of (' + x.message + ')';1511    }1512    const expectedDigest = onError(theError);1513    loggedErrors.length = 0;15141515    await act(() => {1516      const {pipe} = renderToPipeableStream(<App isClient={false} />, {1517        bootstrapScriptContent: '__INIT__();',1518        onError,1519      });1520      pipe(writable);1521    });15221523    expect(loggedErrors).toEqual([]);1524    expect(bootstrapped).toBe(true);15251526    await waitForAll([]);15271528    // We're still loading because we're waiting for the server to stream more content.1529    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);15301531    expect(loggedErrors).toEqual([]);15321533    await act(() => {1534      rejectComponent(theError);1535    });15361537    expect(loggedErrors).toEqual([theError]);15381539    // We haven't ran the client hydration yet.1540    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);15411542    // Now we can client render it instead.1543    await waitForAll([]);1544    expectErrors(1545      errors,1546      [1547        [1548          'Switched to client rendering because the server rendering errored:\n\n' +1549            theError.message,1550          expectedDigest,1551          componentStack(['Lazy', 'Wrapper', 'Suspense', 'div', 'App']),1552        ],1553      ],1554      [1555        [1556          'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1557          expectedDigest,1558        ],1559      ],1560    );15611562    // The client rendered HTML is now in place.1563    expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);15641565    expect(loggedErrors).toEqual([theError]);1566  });15671568  it('should have special stacks if Suspense fallback', async () => {1569    const infinitePromise = new Promise(() => {});1570    const InfiniteComponent = React.lazy(() => {1571      return infinitePromise;1572    });15731574    function Throw({text}) {1575      throw new Error(text);1576    }15771578    function App() {1579      return (1580        <Suspense fallback="Loading">1581          <div>1582            <Suspense fallback={<Throw text="Bye" />}>1583              <InfiniteComponent text="Hi" />1584            </Suspense>1585          </div>1586        </Suspense>1587      );1588    }15891590    const loggedErrors = [];1591    function onError(x, errorInfo) {1592      loggedErrors.push({1593        message: x.message,1594        componentStack: errorInfo.componentStack,1595      });1596      return 'Hash of (' + x.message + ')';1597    }1598    loggedErrors.length = 0;15991600    await act(() => {1601      const {pipe} = renderToPipeableStream(<App />, {1602        onError,1603      });1604      pipe(writable);1605    });16061607    expect(loggedErrors.length).toBe(1);1608    expect(loggedErrors[0].message).toBe('Bye');1609    expect(normalizeCodeLocInfo(loggedErrors[0].componentStack)).toBe(1610      componentStack(['Throw', 'Suspense Fallback', 'div', 'Suspense', 'App']),1611    );1612  });16131614  it('should asynchronously load a lazy element', async () => {1615    let resolveElement;1616    const lazyElement = React.lazy(() => {1617      return new Promise(r => {1618        resolveElement = r;1619      });1620    });16211622    await act(() => {1623      const {pipe} = renderToPipeableStream(1624        <div>1625          <Suspense fallback={<Text text="Loading..." />}>1626            {lazyElement}1627          </Suspense>1628        </div>,1629      );1630      pipe(writable);1631    });1632    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);1633    // Because there is no content inside the Suspense boundary that could've1634    // been written, we expect to not see any additional partial data flushed1635    // yet.1636    expect(1637      stripExternalRuntimeInNodes(1638        container.childNodes,1639        renderOptions.unstable_externalRuntimeSrc,1640      ).length,1641    ).toBe(gate(flags => flags.shouldUseFizzExternalRuntime) ? 1 : 2);1642    await act(() => {1643      resolveElement({default: <Text text="Hello" />});1644    });1645    expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);1646  });16471648  it('should client render a boundary if a lazy element rejects', async () => {1649    let rejectElement;1650    const element = <Text text="Hello" />;1651    const lazyElement = React.lazy(() => {1652      return new Promise((resolve, reject) => {1653        rejectElement = reject;1654      });1655    });16561657    const theError = new Error('Test');1658    const loggedErrors = [];1659    function onError(x, errorInfo) {1660      loggedErrors.push(x);1661      return 'hash of (' + x.message + ')';1662    }1663    const expectedDigest = onError(theError);1664    loggedErrors.length = 0;16651666    function App({isClient}) {1667      return (1668        <div>1669          <Suspense fallback={<Text text="Loading..." />}>1670            {isClient ? element : lazyElement}1671          </Suspense>1672        </div>1673      );1674    }16751676    await act(() => {1677      const {pipe} = renderToPipeableStream(<App isClient={false} />, {1678        onError,1679      });1680      pipe(writable);1681    });1682    expect(loggedErrors).toEqual([]);16831684    const errors = [];1685    // Attempt to hydrate the content.1686    ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {1687      onRecoverableError(error, errorInfo) {1688        errors.push({error, errorInfo});1689      },1690    });1691    await waitForAll([]);16921693    // We're still loading because we're waiting for the server to stream more content.1694    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);16951696    expect(loggedErrors).toEqual([]);16971698    await act(() => {1699      rejectElement(theError);1700    });17011702    expect(loggedErrors).toEqual([theError]);17031704    // We haven't ran the client hydration yet.1705    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);17061707    // Now we can client render it instead.1708    await waitForAll([]);17091710    expectErrors(1711      errors,1712      [1713        [1714          'Switched to client rendering because the server rendering errored:\n\n' +1715            theError.message,1716          expectedDigest,1717          componentStack(['Suspense', 'div', 'App']),1718        ],1719      ],1720      [1721        [1722          'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1723          expectedDigest,1724        ],1725      ],1726    );17271728    // The client rendered HTML is now in place.1729    expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);17301731    expect(loggedErrors).toEqual([theError]);1732  });17331734  it('Errors in boundaries should be sent to the client and reported on client render - Error before flushing', async () => {1735    function Indirection({level, children}) {1736      if (level > 0) {1737        return <Indirection level={level - 1}>{children}</Indirection>;1738      }1739      return children;1740    }17411742    const theError = new Error('uh oh');17431744    function Erroring({isClient}) {1745      if (isClient) {1746        return 'Hello World';1747      }1748      throw theError;1749    }17501751    function App({isClient}) {1752      return (1753        <div>1754          <Suspense fallback={<span>loading...</span>}>1755            <Indirection level={2}>1756              <Erroring isClient={isClient} />1757            </Indirection>1758          </Suspense>1759        </div>1760      );1761    }17621763    const loggedErrors = [];1764    function onError(x) {1765      loggedErrors.push(x);1766      return 'hash(' + x.message + ')';1767    }1768    const expectedDigest = onError(theError);1769    loggedErrors.length = 0;17701771    await act(() => {1772      const {pipe} = renderToPipeableStream(1773        <App />,17741775        {1776          onError,1777        },1778      );1779      pipe(writable);1780    });1781    expect(loggedErrors).toEqual([theError]);17821783    const errors = [];1784    // Attempt to hydrate the content.1785    ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {1786      onRecoverableError(error, errorInfo) {1787        errors.push({error, errorInfo});1788      },1789    });1790    await waitForAll([]);17911792    expect(getVisibleChildren(container)).toEqual(<div>Hello World</div>);17931794    expectErrors(1795      errors,1796      [1797        [1798          'Switched to client rendering because the server rendering errored:\n\n' +1799            theError.message,1800          expectedDigest,1801          componentStack([1802            'Erroring',1803            'Indirection',1804            'Indirection',1805            'Indirection',1806            'Suspense',1807            'div',1808            'App',1809          ]),1810        ],1811      ],1812      [1813        [1814          'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1815          expectedDigest,1816        ],1817      ],1818    );1819  });18201821  it('Errors in boundaries should be sent to the client and reported on client render - Error after flushing', async () => {1822    let rejectComponent;1823    const LazyComponent = React.lazy(() => {1824      return new Promise((resolve, reject) => {1825        rejectComponent = reject;1826      });1827    });18281829    function App({isClient}) {1830      return (1831        <div>1832          <Suspense fallback={<Text text="Loading..." />}>1833            {isClient ? <Text text="Hello" /> : <LazyComponent text="Hello" />}1834          </Suspense>1835        </div>1836      );1837    }18381839    const loggedErrors = [];1840    const theError = new Error('uh oh');1841    function onError(x) {1842      loggedErrors.push(x);1843      return 'hash(' + x.message + ')';1844    }1845    const expectedDigest = onError(theError);1846    loggedErrors.length = 0;18471848    await act(() => {1849      const {pipe} = renderToPipeableStream(1850        <App />,18511852        {1853          onError,1854        },1855      );1856      pipe(writable);1857    });1858    expect(loggedErrors).toEqual([]);18591860    const errors = [];1861    // Attempt to hydrate the content.1862    ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {1863      onRecoverableError(error, errorInfo) {1864        errors.push({error, errorInfo});1865      },1866    });1867    await waitForAll([]);18681869    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);18701871    await act(() => {1872      rejectComponent(theError);1873    });18741875    expect(loggedErrors).toEqual([theError]);1876    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);18771878    // Now we can client render it instead.1879    await waitForAll([]);18801881    expectErrors(1882      errors,1883      [1884        [1885          'Switched to client rendering because the server rendering errored:\n\n' +1886            theError.message,1887          expectedDigest,1888          componentStack(['Lazy', 'Suspense', 'div', 'App']),1889        ],1890      ],1891      [1892        [1893          'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1894          expectedDigest,1895        ],1896      ],1897    );18981899    // The client rendered HTML is now in place.1900    expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);1901    expect(loggedErrors).toEqual([theError]);1902  });19031904  it('should asynchronously load the suspense boundary', async () => {1905    await act(() => {1906      const {pipe} = renderToPipeableStream(1907        <div>1908          <Suspense fallback={<Text text="Loading..." />}>1909            <AsyncText text="Hello World" />1910          </Suspense>1911        </div>,1912      );1913      pipe(writable);1914    });1915    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);1916    await act(() => {1917      resolveText('Hello World');1918    });1919    expect(getVisibleChildren(container)).toEqual(<div>Hello World</div>);1920  });19211922  it('waits for pending content to come in from the server and then hydrates it', async () => {1923    const ref = React.createRef();19241925    function App() {1926      return (1927        <div>1928          <Suspense fallback="Loading...">1929            <h1 ref={ref}>1930              <AsyncText text="Hello" />1931            </h1>1932          </Suspense>1933        </div>1934      );1935    }19361937    let bootstrapped = false;1938    window.__INIT__ = function () {1939      bootstrapped = true;1940      // Attempt to hydrate the content.1941      ReactDOMClient.hydrateRoot(container, <App />);1942    };19431944    await act(() => {1945      const {pipe} = renderToPipeableStream(<App />, {1946        bootstrapScriptContent: '__INIT__();',1947      });1948      pipe(writable);1949    });19501951    // We're still showing a fallback.1952    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);19531954    // We already bootstrapped.1955    expect(bootstrapped).toBe(true);19561957    // Attempt to hydrate the content.1958    await waitForAll([]);19591960    // We're still loading because we're waiting for the server to stream more content.1961    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);19621963    // The server now updates the content in place in the fallback.1964    await act(() => {1965      resolveText('Hello');1966    });19671968    // The final HTML is now in place.1969    expect(getVisibleChildren(container)).toEqual(1970      <div>1971        <h1>Hello</h1>1972      </div>,1973    );1974    const h1 = container.getElementsByTagName('h1')[0];19751976    // But it is not yet hydrated.1977    expect(ref.current).toBe(null);19781979    await waitForAll([]);19801981    // Now it's hydrated.1982    expect(ref.current).toBe(h1);1983  });19841985  it('handles an error on the client if the server ends up erroring', async () => {1986    const ref = React.createRef();19871988    class ErrorBoundary extends React.Component {1989      state = {error: null};1990      static getDerivedStateFromError(error) {1991        return {error};1992      }1993      render() {1994        if (this.state.error) {1995          return <b ref={ref}>{this.state.error.message}</b>;1996        }1997        return this.props.children;1998      }1999    }

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.

Generated by searchcode.com · powered by code_get_file + code_get_findings