packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js JAVASCRIPT 10,928 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 10,928.
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  // @gate enableBrowserAPI410  it('can opt a component into browser-only rendering', async () => {411    let resolveBrowserText;412    const browserText = new Promise(resolve => {413      resolveBrowserText = resolve;414    });415    let browserReason;416    const initializeReason = jest.fn(() => {417      browserReason = Object.freeze(418        new Error('Only render this content in a browser'),419      );420      return browserReason;421    });422    const browserOnly = ReactDOM.browser(initializeReason);423424    function BrowserOnly() {425      use(browserOnly);426      const text = use(browserText);427      Scheduler.log(text);428      return <span>{text}</span>;429    }430431    function App() {432      return (433        <div>434          <Suspense fallback={<span>Fallback</span>}>435            <BrowserOnly />436          </Suspense>437        </div>438      );439    }440441    const serverErrors = [];442    const browserBailouts = [];443    await act(() => {444      const {pipe} = renderToPipeableStream(<App />, {445        onError(error) {446          serverErrors.push(error);447        },448        onBrowserBailout(error, errorInfo) {449          browserBailouts.push({error, errorInfo});450        },451      });452      pipe(writable);453    });454455    expect(serverErrors).toEqual([]);456    expect(initializeReason).toHaveBeenCalledTimes(1);457    expect(browserBailouts).toHaveLength(1);458    expect(browserBailouts[0].error).toBeInstanceOf(Error);459    expect(browserBailouts[0].error.message).toBe(460      'Browser-only rendering was requested by `browser()`.',461    );462    expect(browserBailouts[0].error.stack).toContain('BrowserOnly');463    expect(browserBailouts[0].error.cause).toBe(browserReason);464    expect(465      normalizeCodeLocInfo(browserBailouts[0].errorInfo.componentStack),466    ).toBe(componentStack(['BrowserOnly', 'Suspense', 'div', 'App']));467    expect(getVisibleChildren(container)).toEqual(468      <div>469        <span>Fallback</span>470      </div>,471    );472    const recoverableErrors = [];473    ReactDOMClient.hydrateRoot(container, <App />, {474      onRecoverableError(error) {475        recoverableErrors.push(error);476      },477    });478    await waitForAll([]);479480    expect(getVisibleChildren(container)).toEqual(481      <div>482        <span>Fallback</span>483      </div>,484    );485486    await clientAct(() => {487      resolveBrowserText('Browser');488    });489    assertLog(['Browser']);490491    expect(recoverableErrors).toEqual([]);492    expect(initializeReason).toHaveBeenCalledTimes(1);493    expect(getVisibleChildren(container)).toEqual(494      <div>495        <span>Browser</span>496      </div>,497    );498  });499500  // @gate enableBrowserAPI501  it('can opt a component into browser-only rendering after streaming the fallback', async () => {502    let resolveServerReady;503    const serverReady = new Promise(resolve => {504      resolveServerReady = resolve;505    });506    const initializeReason = jest.fn(507      () => 'Only render this content in a browser',508    );509510    function BrowserOnly() {511      use(serverReady);512      use(ReactDOM.browser(initializeReason));513      return <span>Browser</span>;514    }515516    function App() {517      return (518        <div>519          <Suspense fallback={<span>Fallback</span>}>520            <BrowserOnly />521          </Suspense>522        </div>523      );524    }525526    const serverErrors = [];527    const browserBailouts = [];528    await act(() => {529      const {pipe} = renderToPipeableStream(<App />, {530        onError(error) {531          serverErrors.push(error);532        },533        onBrowserBailout(error) {534          browserBailouts.push(error);535        },536      });537      pipe(writable);538    });539540    expect(getVisibleChildren(container)).toEqual(541      <div>542        <span>Fallback</span>543      </div>,544    );545546    await act(() => {547      resolveServerReady();548    });549550    expect(serverErrors).toEqual([]);551    expect(initializeReason).toHaveBeenCalledTimes(1);552    expect(browserBailouts).toHaveLength(1);553    expect(browserBailouts[0].message).toBe(554      'Browser-only rendering was requested by `browser()`.',555    );556    expect(browserBailouts[0].stack).toContain('BrowserOnly');557    expect(browserBailouts[0].cause).toBe(558      'Only render this content in a browser',559    );560561    const recoverableErrors = [];562    ReactDOMClient.hydrateRoot(container, <App />, {563      onRecoverableError(error) {564        recoverableErrors.push(error);565      },566    });567    await waitForAll([]);568569    expect(recoverableErrors).toEqual([]);570    expect(initializeReason).toHaveBeenCalledTimes(1);571    expect(getVisibleChildren(container)).toEqual(572      <div>573        <span>Browser</span>574      </div>,575    );576  });577578  // @gate enableBrowserAPI579  it('supports omitted and direct string browser reasons', async () => {580    const directReason = 'Only render this content in a browser';581    const withoutReason = ReactDOM.browser();582    const withDirectReason = ReactDOM.browser(directReason);583584    function WithoutReason() {585      use(withoutReason);586      return <span>Browser</span>;587    }588589    function WithDirectReason() {590      use(withDirectReason);591      return <span>Browser</span>;592    }593594    const serverErrors = [];595    const browserBailouts = [];596    await act(() => {597      const {pipe} = renderToPipeableStream(598        <>599          <Suspense fallback={<span>Fallback A</span>}>600            <WithoutReason />601          </Suspense>602          <Suspense fallback={<span>Fallback B</span>}>603            <WithDirectReason />604          </Suspense>605        </>,606        {607          onError(error) {608            serverErrors.push(error);609          },610          onBrowserBailout(error) {611            browserBailouts.push(error);612          },613        },614      );615      pipe(writable);616    });617618    expect(serverErrors).toEqual([]);619    expect(browserBailouts).toHaveLength(2);620    expect(browserBailouts[0].message).toBe(621      'Browser-only rendering was requested by `browser()`.',622    );623    expect(browserBailouts[0].stack).toContain('WithoutReason');624    expect(625      Object.prototype.hasOwnProperty.call(browserBailouts[0], 'cause'),626    ).toBe(false);627    expect(browserBailouts[1].message).toBe(628      'Browser-only rendering was requested by `browser()`.',629    );630    expect(browserBailouts[1].stack).toContain('WithDirectReason');631    expect(browserBailouts[1].cause).toBe(directReason);632  });633634  // @gate enableBrowserAPI635  it('supports any value returned by a browser reason initializer', async () => {636    const reasonValues = [undefined, null, 42, Symbol('browser reason')];637    const initializeReasons = reasonValues.map(reason => jest.fn(() => reason));638    const browserValues = initializeReasons.map(initializeReason =>639      ReactDOM.browser(initializeReason),640    );641642    function BrowserOnly({browserValue}) {643      use(browserValue);644      return <span>Browser</span>;645    }646647    const serverErrors = [];648    const browserBailouts = [];649    await act(() => {650      const {pipe} = renderToPipeableStream(651        <>652          {browserValues.map((browserValue, index) => (653            <Suspense key={index} fallback={<span>Fallback</span>}>654              <BrowserOnly browserValue={browserValue} />655            </Suspense>656          ))}657        </>,658        {659          onError(error) {660            serverErrors.push(error);661          },662          onBrowserBailout(error) {663            browserBailouts.push(error);664          },665        },666      );667      pipe(writable);668    });669670    expect(serverErrors).toEqual([]);671    expect(browserBailouts).toHaveLength(reasonValues.length);672    initializeReasons.forEach(initializeReason => {673      expect(initializeReason).toHaveBeenCalledTimes(1);674    });675    browserBailouts.forEach((error, index) => {676      expect(error).toBeInstanceOf(Error);677      expect(error.message).toBe(678        'Browser-only rendering was requested by `browser()`.',679      );680      expect(Object.prototype.hasOwnProperty.call(error, 'cause')).toBe(true);681      expect(error.cause).toBe(reasonValues[index]);682    });683  });684685  // @gate enableBrowserAPI686  it('initializes a shared browser reason at each use site', async () => {687    const browserReasons = [];688    const initializeReason = jest.fn(() => {689      const browserReason = {index: browserReasons.length};690      browserReasons.push(browserReason);691      return browserReason;692    });693    const browserValue = ReactDOM.browser(initializeReason);694695    function BrowserOnlyA() {696      use(browserValue);697      return <span>Browser A</span>;698    }699700    function BrowserOnlyB() {701      use(browserValue);702      return <span>Browser B</span>;703    }704705    const browserBailouts = [];706    await act(() => {707      const {pipe} = renderToPipeableStream(708        <>709          <Suspense fallback={<span>Fallback A</span>}>710            <BrowserOnlyA />711          </Suspense>712          <Suspense fallback={<span>Fallback B</span>}>713            <BrowserOnlyB />714          </Suspense>715        </>,716        {717          onBrowserBailout(error) {718            browserBailouts.push(error);719          },720        },721      );722      pipe(writable);723    });724725    expect(initializeReason).toHaveBeenCalledTimes(2);726    expect(browserBailouts).toHaveLength(2);727    expect(browserBailouts[0]).not.toBe(browserBailouts[1]);728    expect(browserBailouts[0].cause).toBe(browserReasons[0]);729    expect(browserBailouts[0].stack).toContain('BrowserOnlyA');730    expect(browserBailouts[1].cause).toBe(browserReasons[1]);731    expect(browserBailouts[1].stack).toContain('BrowserOnlyB');732  });733734  // @gate enableBrowserAPI735  it('uses a fallback if a browser reason initializer throws', async () => {736    const reasonError = new Error('Failed to initialize browser reason');737    const initializeReason = jest.fn(() => {738      throw reasonError;739    });740    const browserValue = ReactDOM.browser(initializeReason);741742    function BrowserOnly() {743      use(browserValue);744      return <span>Browser</span>;745    }746747    const serverErrors = [];748    const browserBailouts = [];749    await act(() => {750      const {pipe} = renderToPipeableStream(751        <Suspense fallback={<span>Fallback</span>}>752          <BrowserOnly />753        </Suspense>,754        {755          onError(error) {756            serverErrors.push(error);757          },758          onBrowserBailout(error) {759            browserBailouts.push(error);760          },761        },762      );763      pipe(writable);764    });765766    expect(initializeReason).toHaveBeenCalledTimes(1);767    expect(serverErrors).toEqual([]);768    expect(browserBailouts).toHaveLength(1);769    expect(browserBailouts[0].cause).toBe(770      'The reason for browser-only rendering could not be determined because ' +771        'its initializer threw.',772    );773    expect(getVisibleChildren(container)).toEqual(<span>Fallback</span>);774  });775776  // @gate enableBrowserAPI777  it('errors if browser-only content is rendered outside Suspense', async () => {778    const browserReason = 'Only render this content in a browser';779    const browserValue = ReactDOM.browser(browserReason);780781    function BrowserOnly() {782      use(browserValue);783      return <span>Browser</span>;784    }785786    const reportedErrors = [];787    const browserBailouts = [];788    let shellReady = false;789    let shellError;790    await act(() => {791      renderToPipeableStream(<BrowserOnly />, {792        onError(error) {793          reportedErrors.push(error);794        },795        onBrowserBailout(error) {796          browserBailouts.push(error);797        },798        onShellReady() {799          shellReady = true;800        },801        onShellError(error) {802          shellError = error;803        },804      });805    });806807    expect(shellError).toBeInstanceOf(Error);808    expect(shellError.message).toBe(809      'The server render could not complete because client rendering was ' +810        "requested outside a Suspense boundary. See this error's cause for " +811        'additional details.',812    );813    expect(shellError.cause).toBe(browserReason);814    expect(shellError.stack).toContain('BrowserOnly');815    expect(shellError.stack.split('\n')[0]).toBe(816      'Error: ' + shellError.message,817    );818    expect(shellReady).toBe(false);819    expect(reportedErrors).toEqual([shellError]);820    expect(browserBailouts).toEqual([]);821  });822823  // @gate enableBrowserAPI824  it('can abort all pending boundaries into browser-only rendering', async () => {825    const never = new Promise(() => {});826    let isClient = false;827828    function Pending({children}) {829      if (!isClient) {830        use(never);831      }832      return <span>{children}</span>;833    }834835    function App() {836      return (837        <div>838          <span>Shell</span>839          <Suspense fallback={<span>Loading A</span>}>840            <Pending>A</Pending>841          </Suspense>842          <Suspense fallback={<span>Loading B</span>}>843            <Pending>B</Pending>844          </Suspense>845        </div>846      );847    }848849    const serverErrors = [];850    const browserBailouts = [];851    const browserReason = {code: 'render-pending-content-in-browser'};852    const initializeReason = jest.fn(() => browserReason);853    const browserValue = ReactDOM.browser(initializeReason);854    let abort;855    await act(() => {856      const controls = renderToPipeableStream(<App />, {857        onError(error) {858          serverErrors.push(error);859        },860        onBrowserBailout(error) {861          browserBailouts.push(error);862        },863      });864      abort = controls.abort;865      controls.pipe(writable);866    });867868    expect(getVisibleChildren(container)).toEqual(869      <div>870        <span>Shell</span>871        <span>Loading A</span>872        <span>Loading B</span>873      </div>,874    );875876    await act(() => {877      function abortToBrowser() {878        abort(browserValue);879      }880      abortToBrowser();881    });882883    expect(serverErrors).toEqual([]);884    expect(initializeReason).toHaveBeenCalledTimes(1);885    expect(browserBailouts).toHaveLength(2);886    expect(browserBailouts[0]).toBeInstanceOf(Error);887    expect(browserBailouts[0].message).toBe(888      'Browser-only rendering was requested by `browser()`.',889    );890    expect(browserBailouts[0].stack).toContain('abortToBrowser');891    expect(browserBailouts[0].cause).toBe(browserReason);892    expect(browserBailouts[1]).toBe(browserBailouts[0]);893894    isClient = true;895    const recoverableErrors = [];896    ReactDOMClient.hydrateRoot(container, <App />, {897      onRecoverableError(error) {898        recoverableErrors.push(error);899      },900    });901    await waitForAll([]);902903    expect(recoverableErrors).toEqual([]);904    expect(getVisibleChildren(container)).toEqual(905      <div>906        <span>Shell</span>907        <span>A</span>908        <span>B</span>909      </div>,910    );911  });912913  // @gate enableBrowserAPI914  it('errors if aborted with browser() before the shell completes', async () => {915    const never = new Promise(() => {});916    let browserReason;917    const initializeReason = jest.fn(() => {918      browserReason = new Error('Only abort this render on the server');919      return browserReason;920    });921    const browserValue = ReactDOM.browser(initializeReason);922923    function PendingRoot() {924      use(never);925      return <span>Root</span>;926    }927928    const reportedErrors = [];929    const browserBailouts = [];930    let shellReady = false;931    let shellError;932    let abort;933    await act(() => {934      const controls = renderToPipeableStream(<PendingRoot />, {935        onError(error) {936          reportedErrors.push(error);937        },938        onBrowserBailout(error) {939          browserBailouts.push(error);940        },941        onShellReady() {942          shellReady = true;943        },944        onShellError(error) {945          shellError = error;946        },947      });948      abort = controls.abort;949    });950951    await act(() => {952      function abortToBrowser() {953        abort(browserValue);954      }955      abortToBrowser();956    });957958    expect(shellError).toBeInstanceOf(Error);959    expect(initializeReason).toHaveBeenCalledTimes(1);960    expect(shellError.message).toBe(961      'The server render could not complete because client rendering was ' +962        "requested outside a Suspense boundary. See this error's cause for " +963        'additional details.',964    );965    expect(shellError.cause).toBe(browserReason);966    expect(shellError.stack).toContain('abortToBrowser');967    expect(shellReady).toBe(false);968    expect(reportedErrors).toEqual([shellError]);969    expect(browserBailouts).toEqual([]);970  });971972  // @gate enableBrowserAPI973  it('reports nested browser bailouts if aborting fatals the shell', async () => {974    const never = new Promise(() => {});975    const browserReason = 'Abort pending work into browser rendering';976    const browserValue = ReactDOM.browser(browserReason);977978    function Pending() {979      use(never);980      return <span>Pending</span>;981    }982983    const reportedErrors = [];984    const browserBailouts = [];985    let shellError;986    let abort;987    await act(() => {988      const controls = renderToPipeableStream(989        <>990          <Suspense fallback={<span>Fallback</span>}>991            <Pending />992          </Suspense>993          <Pending />994          <Suspense fallback={<span>Fallback</span>}>995            <Pending />996          </Suspense>997          <Pending />998        </>,999        {1000          onError(error) {1001            reportedErrors.push(error);1002          },1003          onBrowserBailout(error) {1004            browserBailouts.push(error);1005          },1006          onShellError(error) {1007            shellError = error;1008          },1009        },1010      );1011      abort = controls.abort;1012    });10131014    await act(() => {1015      abort(browserValue);1016    });10171018    expect(shellError).toBeInstanceOf(Error);1019    expect(shellError.message).toBe(1020      'The server render could not complete because client rendering was ' +1021        "requested outside a Suspense boundary. See this error's cause for " +1022        'additional details.',1023    );1024    expect(shellError.cause).toBe(browserReason);1025    expect(reportedErrors).toHaveLength(2);1026    expect(reportedErrors[0]).toBe(shellError);1027    expect(reportedErrors[1].message).toBe(shellError.message);1028    expect(reportedErrors[1].cause).toBe(browserReason);1029    expect(browserBailouts).toHaveLength(2);1030    expect(browserBailouts[0]).toBe(browserBailouts[1]);1031    expect(browserBailouts[0]).not.toBe(shellError);1032    expect(browserBailouts[0].message).toBe(1033      'Browser-only rendering was requested by `browser()`.',1034    );1035    expect(browserBailouts[0].cause).toBe(browserReason);1036  });10371038  // @gate enableBrowserAPI1039  it('uses a fallback if a browser reason initializer throws during abort', async () => {1040    const never = new Promise(() => {});1041    const reasonError = new Error('Failed to initialize browser reason');1042    const initializeReason = jest.fn(() => {1043      throw reasonError;1044    });1045    const browserValue = ReactDOM.browser(initializeReason);10461047    function PendingRoot() {1048      use(never);1049      return <span>Root</span>;1050    }10511052    const reportedErrors = [];1053    const browserBailouts = [];1054    let shellError;1055    let abort;1056    await act(() => {1057      const controls = renderToPipeableStream(<PendingRoot />, {1058        onError(error) {1059          reportedErrors.push(error);1060        },1061        onBrowserBailout(error) {1062          browserBailouts.push(error);1063        },1064        onShellError(error) {1065          shellError = error;1066        },1067      });1068      abort = controls.abort;1069    });10701071    await act(() => {1072      abort(browserValue);1073    });10741075    expect(initializeReason).toHaveBeenCalledTimes(1);1076    expect(shellError).toBeInstanceOf(Error);1077    expect(shellError.cause).toBe(1078      'The reason for browser-only rendering could not be determined because ' +1079        'its initializer threw.',1080    );1081    expect(reportedErrors).toEqual([shellError]);1082    expect(browserBailouts).toEqual([]);1083  });10841085  // @gate enableBrowserAPI1086  it('reports the browser value if it is thrown instead of passed to use', async () => {1087    const initializeReason = jest.fn(1088      () => new Error('Only render this content in a browser'),1089    );1090    const browserValue = ReactDOM.browser(initializeReason);10911092    function BrowserOnly() {1093      throw browserValue;1094    }10951096    const reportedErrors = [];1097    const browserBailouts = [];1098    await act(() => {1099      const {pipe} = renderToPipeableStream(1100        <Suspense fallback={<span>Fallback</span>}>1101          <BrowserOnly />1102        </Suspense>,1103        {1104          onError(error) {1105            reportedErrors.push(error);1106          },1107          onBrowserBailout(error) {1108            browserBailouts.push(error);1109          },1110        },1111      );1112      pipe(writable);1113    });11141115    expect(reportedErrors).toEqual([browserValue]);1116    expect(browserBailouts).toEqual([]);1117    expect(initializeReason).not.toHaveBeenCalled();1118    expect(getVisibleChildren(container)).toEqual(<span>Fallback</span>);1119  });11201121  ['', 'BROWSER'].forEach(userDigest => {1122    it(`does not reserve the ${JSON.stringify(1123      userDigest,1124    )} user error digest for browser rendering`, async () => {1125      let isClient = false;1126      const serverError = new Error('Server error');11271128      function ServerError() {1129        if (!isClient) {1130          throw serverError;1131        }1132        return <span>Client</span>;1133      }11341135      function App() {1136        return (1137          <Suspense fallback={<span>Fallback</span>}>1138            <ServerError />1139          </Suspense>1140        );1141      }11421143      const serverErrors = [];1144      await act(() => {1145        const {pipe} = renderToPipeableStream(<App />, {1146          onError(error) {1147            serverErrors.push(error);1148            return userDigest;1149          },1150        });1151        pipe(writable);1152      });11531154      expect(serverErrors).toEqual([serverError]);1155      expect(getVisibleChildren(container)).toEqual(<span>Fallback</span>);11561157      isClient = true;1158      const recoverableErrors = [];1159      ReactDOMClient.hydrateRoot(container, <App />, {1160        onRecoverableError(error) {1161          recoverableErrors.push(error);1162        },1163      });1164      await waitForAll([]);11651166      expect(recoverableErrors).toHaveLength(1);1167      expect(recoverableErrors[0].digest).toBe(userDigest || undefined);1168      expect(getVisibleChildren(container)).toEqual(<span>Client</span>);1169    });1170  });11711172  it('should asynchronously load a lazy component', async () => {1173    let resolveA;1174    const LazyA = React.lazy(() => {1175      return new Promise(r => {1176        resolveA = r;1177      });1178    });11791180    let resolveB;1181    const LazyB = React.lazy(() => {1182      return new Promise(r => {1183        resolveB = r;1184      });1185    });11861187    class TextWithPunctuation extends React.Component {1188      render() {1189        return <Text text={this.props.text + this.props.punctuation} />;1190      }1191    }11921193    // This tests that default props of the inner element is resolved.1194    TextWithPunctuation.defaultProps = {1195      punctuation: '!',1196    };11971198    await act(() => {1199      const {pipe} = renderToPipeableStream(1200        <div>1201          <div>1202            <Suspense fallback={<Text text="Loading..." />}>1203              <LazyA text="Hello" />1204            </Suspense>1205          </div>1206          <div>1207            <Suspense fallback={<Text text="Loading..." />}>1208              <LazyB text="world" />1209            </Suspense>1210          </div>1211        </div>,1212      );1213      pipe(writable);1214    });12151216    expect(getVisibleChildren(container)).toEqual(1217      <div>1218        <div>Loading...</div>1219        <div>Loading...</div>1220      </div>,1221    );1222    await act(() => {1223      resolveA({default: Text});1224    });1225    expect(getVisibleChildren(container)).toEqual(1226      <div>1227        <div>Hello</div>1228        <div>Loading...</div>1229      </div>,1230    );1231    await act(() => {1232      resolveB({default: TextWithPunctuation});1233    });1234    expect(getVisibleChildren(container)).toEqual(1235      <div>1236        <div>Hello</div>1237        <div>world!</div>1238      </div>,1239    );1240  });12411242  it('#23331: does not warn about hydration mismatches if something suspended in an earlier sibling', async () => {1243    const makeApp = () => {1244      let resolve;1245      const imports = new Promise(r => {1246        resolve = () => r({default: () => <span id="async">async</span>});1247      });1248      const Lazy = React.lazy(() => imports);12491250      const App = () => (1251        <div>1252          <Suspense fallback={<span>Loading...</span>}>1253            <Lazy />1254            <span id="after">after</span>1255          </Suspense>1256        </div>1257      );12581259      return [App, resolve];1260    };12611262    // Server-side1263    const [App, resolve] = makeApp();1264    await act(() => {1265      const {pipe} = renderToPipeableStream(<App />);1266      pipe(writable);1267    });1268    expect(getVisibleChildren(container)).toEqual(1269      <div>1270        <span>Loading...</span>1271      </div>,1272    );1273    await act(() => {1274      resolve();1275    });1276    expect(getVisibleChildren(container)).toEqual(1277      <div>1278        <span id="async">async</span>1279        <span id="after">after</span>1280      </div>,1281    );12821283    // Client-side1284    const [HydrateApp, hydrateResolve] = makeApp();1285    await act(() => {1286      ReactDOMClient.hydrateRoot(container, <HydrateApp />);1287    });12881289    expect(getVisibleChildren(container)).toEqual(1290      <div>1291        <span id="async">async</span>1292        <span id="after">after</span>1293      </div>,1294    );12951296    await act(() => {1297      hydrateResolve();1298    });1299    expect(getVisibleChildren(container)).toEqual(1300      <div>1301        <span id="async">async</span>1302        <span id="after">after</span>1303      </div>,1304    );1305  });13061307  it('should support nonce for bootstrap and runtime scripts', async () => {1308    CSPnonce = 'R4nd0m';1309    try {1310      let resolve;1311      const Lazy = React.lazy(() => {1312        return new Promise(r => {1313          resolve = r;1314        });1315      });13161317      await act(() => {1318        const {pipe} = renderToPipeableStream(1319          <div>1320            <Suspense fallback={<Text text="Loading..." />}>1321              <Lazy text="Hello" />1322            </Suspense>1323          </div>,1324          {1325            nonce: 'R4nd0m',1326            bootstrapScriptContent: 'function noop(){}',1327            bootstrapScripts: [1328              'init.js',1329              {src: 'init2.js', integrity: 'init2hash'},1330            ],1331            bootstrapModules: [1332              'init.mjs',1333              {src: 'init2.mjs', integrity: 'init2hash'},1334            ],1335          },1336        );1337        pipe(writable);1338      });13391340      expect(getVisibleChildren(container)).toEqual([1341        <link1342          rel="preload"1343          fetchpriority="low"1344          href="init.js"1345          as="script"1346          nonce={CSPnonce}1347        />,1348        <link1349          rel="preload"1350          fetchpriority="low"1351          href="init2.js"1352          as="script"1353          nonce={CSPnonce}1354          integrity="init2hash"1355        />,1356        <link1357          rel="modulepreload"1358          fetchpriority="low"1359          href="init.mjs"1360          nonce={CSPnonce}1361        />,1362        <link1363          rel="modulepreload"1364          fetchpriority="low"1365          href="init2.mjs"1366          nonce={CSPnonce}1367          integrity="init2hash"1368        />,1369        <div>Loading...</div>,1370      ]);13711372      // check that there are 6 scripts with a matching nonce:1373      // The runtime script or initial paint time, an inline bootstrap script, two bootstrap scripts and two bootstrap modules1374      expect(1375        Array.from(container.getElementsByTagName('script')).filter(1376          node => node.getAttribute('nonce') === CSPnonce,1377        ).length,1378      ).toEqual(6);13791380      await act(() => {1381        resolve({default: Text});1382      });1383      expect(getVisibleChildren(container)).toEqual([1384        <link1385          rel="preload"1386          fetchpriority="low"1387          href="init.js"1388          as="script"1389          nonce={CSPnonce}1390        />,1391        <link1392          rel="preload"1393          fetchpriority="low"1394          href="init2.js"1395          as="script"1396          nonce={CSPnonce}1397          integrity="init2hash"1398        />,1399        <link1400          rel="modulepreload"1401          fetchpriority="low"1402          href="init.mjs"1403          nonce={CSPnonce}1404        />,1405        <link1406          rel="modulepreload"1407          fetchpriority="low"1408          href="init2.mjs"1409          nonce={CSPnonce}1410          integrity="init2hash"1411        />,1412        <div>Hello</div>,1413      ]);1414    } finally {1415      CSPnonce = null;1416    }1417  });14181419  it('should not automatically add nonce to rendered scripts', async () => {1420    CSPnonce = 'R4nd0m';1421    try {1422      await act(async () => {1423        const {pipe} = renderToPipeableStream(1424          <html>1425            <body>1426              <script nonce={CSPnonce}>{'try { foo() } catch (e) {} ;'}</script>1427              <script nonce={CSPnonce} src="foo" async={true} />1428              <script src="bar" />1429              <script src="baz" integrity="qux" async={true} />1430              <script type="module" src="quux" async={true} />1431              <script type="module" src="corge" async={true} />1432              <script1433                type="module"1434                src="grault"1435                integrity="garply"1436                async={true}1437              />1438            </body>1439          </html>,1440          {1441            nonce: CSPnonce,1442          },1443        );1444        pipe(writable);1445      });14461447      expect(1448        stripExternalRuntimeInNodes(1449          document.getElementsByTagName('script'),1450          renderOptions.unstable_externalRuntimeSrc,1451        ).map(n => n.outerHTML),1452      ).toEqual([1453        `<script nonce="${CSPnonce}" src="foo" async=""></script>`,1454        `<script src="baz" integrity="qux" async=""></script>`,1455        `<script type="module" src="quux" async=""></script>`,1456        `<script type="module" src="corge" async=""></script>`,1457        `<script type="module" src="grault" integrity="garply" async=""></script>`,1458        `<script nonce="${CSPnonce}">try { foo() } catch (e) {} ;</script>`,1459        `<script src="bar"></script>`,1460      ]);1461    } finally {1462      CSPnonce = null;1463    }1464  });14651466  it('should client render a boundary if a lazy component rejects', async () => {1467    let rejectComponent;1468    const promise = new Promise((resolve, reject) => {1469      rejectComponent = reject;1470    });1471    const LazyComponent = React.lazy(() => {1472      return promise;1473    });14741475    const LazyLazy = React.lazy(async () => {1476      return {1477        default: LazyComponent,1478      };1479    });14801481    function Wrapper({children}) {1482      return children;1483    }1484    const LazyWrapper = React.lazy(() => {1485      return {1486        then(callback) {1487          callback({1488            default: Wrapper,1489          });1490        },1491      };1492    });14931494    function App({isClient}) {1495      return (1496        <div>1497          <Suspense fallback={<Text text="Loading..." />}>1498            <LazyWrapper>1499              {isClient ? <Text text="Hello" /> : <LazyLazy text="Hello" />}1500            </LazyWrapper>1501          </Suspense>1502        </div>1503      );1504    }15051506    let bootstrapped = false;1507    const errors = [];1508    window.__INIT__ = function () {1509      bootstrapped = true;1510      // Attempt to hydrate the content.1511      ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {1512        onRecoverableError(error, errorInfo) {1513          errors.push({error, errorInfo});1514        },1515      });1516    };15171518    const theError = new Error('Test');1519    const loggedErrors = [];1520    function onError(x, errorInfo) {1521      loggedErrors.push(x);1522      return 'Hash of (' + x.message + ')';1523    }1524    const expectedDigest = onError(theError);1525    loggedErrors.length = 0;15261527    await act(() => {1528      const {pipe} = renderToPipeableStream(<App isClient={false} />, {1529        bootstrapScriptContent: '__INIT__();',1530        onError,1531      });1532      pipe(writable);1533    });15341535    expect(loggedErrors).toEqual([]);1536    expect(bootstrapped).toBe(true);15371538    await waitForAll([]);15391540    // We're still loading because we're waiting for the server to stream more content.1541    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);15421543    expect(loggedErrors).toEqual([]);15441545    await act(() => {1546      rejectComponent(theError);1547    });15481549    expect(loggedErrors).toEqual([theError]);15501551    // We haven't ran the client hydration yet.1552    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);15531554    // Now we can client render it instead.1555    await waitForAll([]);1556    expectErrors(1557      errors,1558      [1559        [1560          'Switched to client rendering because the server rendering errored:\n\n' +1561            theError.message,1562          expectedDigest,1563          componentStack(['Lazy', 'Wrapper', 'Suspense', 'div', 'App']),1564        ],1565      ],1566      [1567        [1568          'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1569          expectedDigest,1570        ],1571      ],1572    );15731574    // The client rendered HTML is now in place.1575    expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);15761577    expect(loggedErrors).toEqual([theError]);1578  });15791580  it('should have special stacks if Suspense fallback', async () => {1581    const infinitePromise = new Promise(() => {});1582    const InfiniteComponent = React.lazy(() => {1583      return infinitePromise;1584    });15851586    function Throw({text}) {1587      throw new Error(text);1588    }15891590    function App() {1591      return (1592        <Suspense fallback="Loading">1593          <div>1594            <Suspense fallback={<Throw text="Bye" />}>1595              <InfiniteComponent text="Hi" />1596            </Suspense>1597          </div>1598        </Suspense>1599      );1600    }16011602    const loggedErrors = [];1603    function onError(x, errorInfo) {1604      loggedErrors.push({1605        message: x.message,1606        componentStack: errorInfo.componentStack,1607      });1608      return 'Hash of (' + x.message + ')';1609    }1610    loggedErrors.length = 0;16111612    await act(() => {1613      const {pipe} = renderToPipeableStream(<App />, {1614        onError,1615      });1616      pipe(writable);1617    });16181619    expect(loggedErrors.length).toBe(1);1620    expect(loggedErrors[0].message).toBe('Bye');1621    expect(normalizeCodeLocInfo(loggedErrors[0].componentStack)).toBe(1622      componentStack(['Throw', 'Suspense Fallback', 'div', 'Suspense', 'App']),1623    );1624  });16251626  it('should asynchronously load a lazy element', async () => {1627    let resolveElement;1628    const lazyElement = React.lazy(() => {1629      return new Promise(r => {1630        resolveElement = r;1631      });1632    });16331634    await act(() => {1635      const {pipe} = renderToPipeableStream(1636        <div>1637          <Suspense fallback={<Text text="Loading..." />}>1638            {lazyElement}1639          </Suspense>1640        </div>,1641      );1642      pipe(writable);1643    });1644    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);1645    // Because there is no content inside the Suspense boundary that could've1646    // been written, we expect to not see any additional partial data flushed1647    // yet.1648    expect(1649      stripExternalRuntimeInNodes(1650        container.childNodes,1651        renderOptions.unstable_externalRuntimeSrc,1652      ).length,1653    ).toBe(gate(flags => flags.shouldUseFizzExternalRuntime) ? 1 : 2);1654    await act(() => {1655      resolveElement({default: <Text text="Hello" />});1656    });1657    expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);1658  });16591660  it('should client render a boundary if a lazy element rejects', async () => {1661    let rejectElement;1662    const element = <Text text="Hello" />;1663    const lazyElement = React.lazy(() => {1664      return new Promise((resolve, reject) => {1665        rejectElement = reject;1666      });1667    });16681669    const theError = new Error('Test');1670    const loggedErrors = [];1671    function onError(x, errorInfo) {1672      loggedErrors.push(x);1673      return 'hash of (' + x.message + ')';1674    }1675    const expectedDigest = onError(theError);1676    loggedErrors.length = 0;16771678    function App({isClient}) {1679      return (1680        <div>1681          <Suspense fallback={<Text text="Loading..." />}>1682            {isClient ? element : lazyElement}1683          </Suspense>1684        </div>1685      );1686    }16871688    await act(() => {1689      const {pipe} = renderToPipeableStream(<App isClient={false} />, {1690        onError,1691      });1692      pipe(writable);1693    });1694    expect(loggedErrors).toEqual([]);16951696    const errors = [];1697    // Attempt to hydrate the content.1698    ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {1699      onRecoverableError(error, errorInfo) {1700        errors.push({error, errorInfo});1701      },1702    });1703    await waitForAll([]);17041705    // We're still loading because we're waiting for the server to stream more content.1706    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);17071708    expect(loggedErrors).toEqual([]);17091710    await act(() => {1711      rejectElement(theError);1712    });17131714    expect(loggedErrors).toEqual([theError]);17151716    // We haven't ran the client hydration yet.1717    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);17181719    // Now we can client render it instead.1720    await waitForAll([]);17211722    expectErrors(1723      errors,1724      [1725        [1726          'Switched to client rendering because the server rendering errored:\n\n' +1727            theError.message,1728          expectedDigest,1729          componentStack(['Suspense', 'div', 'App']),1730        ],1731      ],1732      [1733        [1734          'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1735          expectedDigest,1736        ],1737      ],1738    );17391740    // The client rendered HTML is now in place.1741    // expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);17421743    expect(loggedErrors).toEqual([theError]);1744  });17451746  it('Errors in boundaries should be sent to the client and reported on client render - Error before flushing', async () => {1747    function Indirection({level, children}) {1748      if (level > 0) {1749        return <Indirection level={level - 1}>{children}</Indirection>;1750      }1751      return children;1752    }17531754    const theError = new Error('uh oh');17551756    function Erroring({isClient}) {1757      if (isClient) {1758        return 'Hello World';1759      }1760      throw theError;1761    }17621763    function App({isClient}) {1764      return (1765        <div>1766          <Suspense fallback={<span>loading...</span>}>1767            <Indirection level={2}>1768              <Erroring isClient={isClient} />1769            </Indirection>1770          </Suspense>1771        </div>1772      );1773    }17741775    const loggedErrors = [];1776    function onError(x) {1777      loggedErrors.push(x);1778      return 'hash(' + x.message + ')';1779    }1780    const expectedDigest = onError(theError);1781    loggedErrors.length = 0;17821783    await act(() => {1784      const {pipe} = renderToPipeableStream(1785        <App />,17861787        {1788          onError,1789        },1790      );1791      pipe(writable);1792    });1793    expect(loggedErrors).toEqual([theError]);17941795    const errors = [];1796    // Attempt to hydrate the content.1797    ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {1798      onRecoverableError(error, errorInfo) {1799        errors.push({error, errorInfo});1800      },1801    });1802    await waitForAll([]);18031804    expect(getVisibleChildren(container)).toEqual(<div>Hello World</div>);18051806    expectErrors(1807      errors,1808      [1809        [1810          'Switched to client rendering because the server rendering errored:\n\n' +1811            theError.message,1812          expectedDigest,1813          componentStack([1814            'Erroring',1815            'Indirection',1816            'Indirection',1817            'Indirection',1818            'Suspense',1819            'div',1820            'App',1821          ]),1822        ],1823      ],1824      [1825        [1826          'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1827          expectedDigest,1828        ],1829      ],1830    );1831  });18321833  it('Errors in boundaries should be sent to the client and reported on client render - Error after flushing', async () => {1834    let rejectComponent;1835    const LazyComponent = React.lazy(() => {1836      return new Promise((resolve, reject) => {1837        rejectComponent = reject;1838      });1839    });18401841    function App({isClient}) {1842      return (1843        <div>1844          <Suspense fallback={<Text text="Loading..." />}>1845            {isClient ? <Text text="Hello" /> : <LazyComponent text="Hello" />}1846          </Suspense>1847        </div>1848      );1849    }18501851    const loggedErrors = [];1852    const theError = new Error('uh oh');1853    function onError(x) {1854      loggedErrors.push(x);1855      return 'hash(' + x.message + ')';1856    }1857    const expectedDigest = onError(theError);1858    loggedErrors.length = 0;18591860    await act(() => {1861      const {pipe} = renderToPipeableStream(1862        <App />,18631864        {1865          onError,1866        },1867      );1868      pipe(writable);1869    });1870    expect(loggedErrors).toEqual([]);18711872    const errors = [];1873    // Attempt to hydrate the content.1874    ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {1875      onRecoverableError(error, errorInfo) {1876        errors.push({error, errorInfo});1877      },1878    });1879    await waitForAll([]);18801881    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);18821883    await act(() => {1884      rejectComponent(theError);1885    });18861887    expect(loggedErrors).toEqual([theError]);1888    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);18891890    // Now we can client render it instead.1891    await waitForAll([]);18921893    expectErrors(1894      errors,1895      [1896        [1897          'Switched to client rendering because the server rendering errored:\n\n' +1898            theError.message,1899          expectedDigest,1900          componentStack(['Lazy', 'Suspense', 'div', 'App']),1901        ],1902      ],1903      [1904        [1905          'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1906          expectedDigest,1907        ],1908      ],1909    );19101911    // The client rendered HTML is now in place.1912    expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);1913    expect(loggedErrors).toEqual([theError]);1914  });19151916  it('should asynchronously load the suspense boundary', async () => {1917    await act(() => {1918      const {pipe} = renderToPipeableStream(1919        <div>1920          <Suspense fallback={<Text text="Loading..." />}>1921            <AsyncText text="Hello World" />1922          </Suspense>1923        </div>,1924      );1925      pipe(writable);1926    });1927    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);1928    await act(() => {1929      resolveText('Hello World');1930    });1931    expect(getVisibleChildren(container)).toEqual(<div>Hello World</div>);1932  });19331934  it('waits for pending content to come in from the server and then hydrates it', async () => {1935    const ref = React.createRef();19361937    function App() {1938      return (1939        <div>1940          <Suspense fallback="Loading...">1941            <h1 ref={ref}>1942              <AsyncText text="Hello" />1943            </h1>1944          </Suspense>1945        </div>1946      );1947    }19481949    let bootstrapped = false;1950    window.__INIT__ = function () {1951      bootstrapped = true;1952      // Attempt to hydrate the content.1953      ReactDOMClient.hydrateRoot(container, <App />);1954    };19551956    await act(() => {1957      const {pipe} = renderToPipeableStream(<App />, {1958        bootstrapScriptContent: '__INIT__();',1959      });1960      pipe(writable);1961    });19621963    // We're still showing a fallback.1964    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);19651966    // We already bootstrapped.1967    expect(bootstrapped).toBe(true);19681969    // Attempt to hydrate the content.1970    await waitForAll([]);19711972    // We're still loading because we're waiting for the server to stream more content.1973    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);19741975    // The server now updates the content in place in the fallback.1976    await act(() => {1977      resolveText('Hello');1978    });19791980    // The final HTML is now in place.1981    expect(getVisibleChildren(container)).toEqual(1982      <div>1983        <h1>Hello</h1>1984      </div>,1985    );1986    const h1 = container.getElementsByTagName('h1')[0];19871988    // But it is not yet hydrated.1989    expect(ref.current).toBe(null);19901991    await waitForAll([]);19921993    // Now it's hydrated.1994    expect(ref.current).toBe(h1);1995  });19961997  it('handles an error on the client if the server ends up erroring', async () => {1998    const ref = React.createRef();19992000    class ErrorBoundary extends React.Component {

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.