packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js JAVASCRIPT 9,985 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 9,985.
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('should asynchronously load a lazy component', async () => {410    let resolveA;411    const LazyA = React.lazy(() => {412      return new Promise(r => {413        resolveA = r;414      });415    });416417    let resolveB;418    const LazyB = React.lazy(() => {419      return new Promise(r => {420        resolveB = r;421      });422    });423424    class TextWithPunctuation extends React.Component {425      render() {426        return <Text text={this.props.text + this.props.punctuation} />;427      }428    }429430    // This tests that default props of the inner element is resolved.431    TextWithPunctuation.defaultProps = {432      punctuation: '!',433    };434435    await act(() => {436      const {pipe} = renderToPipeableStream(437        <div>438          <div>439            <Suspense fallback={<Text text="Loading..." />}>440              <LazyA text="Hello" />441            </Suspense>442          </div>443          <div>444            <Suspense fallback={<Text text="Loading..." />}>445              <LazyB text="world" />446            </Suspense>447          </div>448        </div>,449      );450      pipe(writable);451    });452453    expect(getVisibleChildren(container)).toEqual(454      <div>455        <div>Loading...</div>456        <div>Loading...</div>457      </div>,458    );459    await act(() => {460      resolveA({default: Text});461    });462    expect(getVisibleChildren(container)).toEqual(463      <div>464        <div>Hello</div>465        <div>Loading...</div>466      </div>,467    );468    await act(() => {469      resolveB({default: TextWithPunctuation});470    });471    expect(getVisibleChildren(container)).toEqual(472      <div>473        <div>Hello</div>474        <div>world!</div>475      </div>,476    );477  });478479  it('#23331: does not warn about hydration mismatches if something suspended in an earlier sibling', async () => {480    const makeApp = () => {481      let resolve;482      const imports = new Promise(r => {483        resolve = () => r({default: () => <span id="async">async</span>});484      });485      const Lazy = React.lazy(() => imports);486487      const App = () => (488        <div>489          <Suspense fallback={<span>Loading...</span>}>490            <Lazy />491            <span id="after">after</span>492          </Suspense>493        </div>494      );495496      return [App, resolve];497    };498499    // Server-side500    const [App, resolve] = makeApp();501    await act(() => {502      const {pipe} = renderToPipeableStream(<App />);503      pipe(writable);504    });505    expect(getVisibleChildren(container)).toEqual(506      <div>507        <span>Loading...</span>508      </div>,509    );510    await act(() => {511      resolve();512    });513    expect(getVisibleChildren(container)).toEqual(514      <div>515        <span id="async">async</span>516        <span id="after">after</span>517      </div>,518    );519520    // Client-side521    const [HydrateApp, hydrateResolve] = makeApp();522    await act(() => {523      ReactDOMClient.hydrateRoot(container, <HydrateApp />);524    });525526    expect(getVisibleChildren(container)).toEqual(527      <div>528        <span id="async">async</span>529        <span id="after">after</span>530      </div>,531    );532533    await act(() => {534      hydrateResolve();535    });536    expect(getVisibleChildren(container)).toEqual(537      <div>538        <span id="async">async</span>539        <span id="after">after</span>540      </div>,541    );542  });543544  it('should support nonce for bootstrap and runtime scripts', async () => {545    CSPnonce = 'R4nd0m';546    try {547      let resolve;548      const Lazy = React.lazy(() => {549        return new Promise(r => {550          resolve = r;551        });552      });553554      await act(() => {555        const {pipe} = renderToPipeableStream(556          <div>557            <Suspense fallback={<Text text="Loading..." />}>558              <Lazy text="Hello" />559            </Suspense>560          </div>,561          {562            nonce: 'R4nd0m',563            bootstrapScriptContent: 'function noop(){}',564            bootstrapScripts: [565              'init.js',566              {src: 'init2.js', integrity: 'init2hash'},567            ],568            bootstrapModules: [569              'init.mjs',570              {src: 'init2.mjs', integrity: 'init2hash'},571            ],572          },573        );574        pipe(writable);575      });576577      expect(getVisibleChildren(container)).toEqual([578        <link579          rel="preload"580          fetchpriority="low"581          href="init.js"582          as="script"583          nonce={CSPnonce}584        />,585        <link586          rel="preload"587          fetchpriority="low"588          href="init2.js"589          as="script"590          nonce={CSPnonce}591          integrity="init2hash"592        />,593        <link594          rel="modulepreload"595          fetchpriority="low"596          href="init.mjs"597          nonce={CSPnonce}598        />,599        <link600          rel="modulepreload"601          fetchpriority="low"602          href="init2.mjs"603          nonce={CSPnonce}604          integrity="init2hash"605        />,606        <div>Loading...</div>,607      ]);608609      // check that there are 6 scripts with a matching nonce:610      // The runtime script or initial paint time, an inline bootstrap script, two bootstrap scripts and two bootstrap modules611      expect(612        Array.from(container.getElementsByTagName('script')).filter(613          node => node.getAttribute('nonce') === CSPnonce,614        ).length,615      ).toEqual(6);616617      await act(() => {618        resolve({default: Text});619      });620      expect(getVisibleChildren(container)).toEqual([621        <link622          rel="preload"623          fetchpriority="low"624          href="init.js"625          as="script"626          nonce={CSPnonce}627        />,628        <link629          rel="preload"630          fetchpriority="low"631          href="init2.js"632          as="script"633          nonce={CSPnonce}634          integrity="init2hash"635        />,636        <link637          rel="modulepreload"638          fetchpriority="low"639          href="init.mjs"640          nonce={CSPnonce}641        />,642        <link643          rel="modulepreload"644          fetchpriority="low"645          href="init2.mjs"646          nonce={CSPnonce}647          integrity="init2hash"648        />,649        <div>Hello</div>,650      ]);651    } finally {652      CSPnonce = null;653    }654  });655656  it('should not automatically add nonce to rendered scripts', async () => {657    CSPnonce = 'R4nd0m';658    try {659      await act(async () => {660        const {pipe} = renderToPipeableStream(661          <html>662            <body>663              <script nonce={CSPnonce}>{'try { foo() } catch (e) {} ;'}</script>664              <script nonce={CSPnonce} src="foo" async={true} />665              <script src="bar" />666              <script src="baz" integrity="qux" async={true} />667              <script type="module" src="quux" async={true} />668              <script type="module" src="corge" async={true} />669              <script670                type="module"671                src="grault"672                integrity="garply"673                async={true}674              />675            </body>676          </html>,677          {678            nonce: CSPnonce,679          },680        );681        pipe(writable);682      });683684      expect(685        stripExternalRuntimeInNodes(686          document.getElementsByTagName('script'),687          renderOptions.unstable_externalRuntimeSrc,688        ).map(n => n.outerHTML),689      ).toEqual([690        `<script nonce="${CSPnonce}" src="foo" async=""></script>`,691        `<script src="baz" integrity="qux" async=""></script>`,692        `<script type="module" src="quux" async=""></script>`,693        `<script type="module" src="corge" async=""></script>`,694        `<script type="module" src="grault" integrity="garply" async=""></script>`,695        `<script nonce="${CSPnonce}">try { foo() } catch (e) {} ;</script>`,696        `<script src="bar"></script>`,697      ]);698    } finally {699      CSPnonce = null;700    }701  });702703  it('should client render a boundary if a lazy component rejects', async () => {704    let rejectComponent;705    const promise = new Promise((resolve, reject) => {706      rejectComponent = reject;707    });708    const LazyComponent = React.lazy(() => {709      return promise;710    });711712    const LazyLazy = React.lazy(async () => {713      return {714        default: LazyComponent,715      };716    });717718    function Wrapper({children}) {719      return children;720    }721    const LazyWrapper = React.lazy(() => {722      return {723        then(callback) {724          callback({725            default: Wrapper,726          });727        },728      };729    });730731    function App({isClient}) {732      return (733        <div>734          <Suspense fallback={<Text text="Loading..." />}>735            <LazyWrapper>736              {isClient ? <Text text="Hello" /> : <LazyLazy text="Hello" />}737            </LazyWrapper>738          </Suspense>739        </div>740      );741    }742743    let bootstrapped = false;744    const errors = [];745    window.__INIT__ = function () {746      bootstrapped = true;747      // Attempt to hydrate the content.748      ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {749        onRecoverableError(error, errorInfo) {750          errors.push({error, errorInfo});751        },752      });753    };754755    const theError = new Error('Test');756    const loggedErrors = [];757    function onError(x, errorInfo) {758      loggedErrors.push(x);759      return 'Hash of (' + x.message + ')';760    }761    const expectedDigest = onError(theError);762    loggedErrors.length = 0;763764    await act(() => {765      const {pipe} = renderToPipeableStream(<App isClient={false} />, {766        bootstrapScriptContent: '__INIT__();',767        onError,768      });769      pipe(writable);770    });771772    expect(loggedErrors).toEqual([]);773    expect(bootstrapped).toBe(true);774775    await waitForAll([]);776777    // We're still loading because we're waiting for the server to stream more content.778    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);779780    expect(loggedErrors).toEqual([]);781782    await act(() => {783      rejectComponent(theError);784    });785786    expect(loggedErrors).toEqual([theError]);787788    // We haven't ran the client hydration yet.789    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);790791    // Now we can client render it instead.792    await waitForAll([]);793    expectErrors(794      errors,795      [796        [797          'Switched to client rendering because the server rendering errored:\n\n' +798            theError.message,799          expectedDigest,800          componentStack(['Lazy', 'Wrapper', 'Suspense', 'div', 'App']),801        ],802      ],803      [804        [805          'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',806          expectedDigest,807        ],808      ],809    );810811    // The client rendered HTML is now in place.812    expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);813814    expect(loggedErrors).toEqual([theError]);815  });816817  it('should have special stacks if Suspense fallback', async () => {818    const infinitePromise = new Promise(() => {});819    const InfiniteComponent = React.lazy(() => {820      return infinitePromise;821    });822823    function Throw({text}) {824      throw new Error(text);825    }826827    function App() {828      return (829        <Suspense fallback="Loading">830          <div>831            <Suspense fallback={<Throw text="Bye" />}>832              <InfiniteComponent text="Hi" />833            </Suspense>834          </div>835        </Suspense>836      );837    }838839    const loggedErrors = [];840    function onError(x, errorInfo) {841      loggedErrors.push({842        message: x.message,843        componentStack: errorInfo.componentStack,844      });845      return 'Hash of (' + x.message + ')';846    }847    loggedErrors.length = 0;848849    await act(() => {850      const {pipe} = renderToPipeableStream(<App />, {851        onError,852      });853      pipe(writable);854    });855856    expect(loggedErrors.length).toBe(1);857    expect(loggedErrors[0].message).toBe('Bye');858    expect(normalizeCodeLocInfo(loggedErrors[0].componentStack)).toBe(859      componentStack(['Throw', 'Suspense Fallback', 'div', 'Suspense', 'App']),860    );861  });862863  it('should asynchronously load a lazy element', async () => {864    let resolveElement;865    const lazyElement = React.lazy(() => {866      return new Promise(r => {867        resolveElement = r;868      });869    });870871    await act(() => {872      const {pipe} = renderToPipeableStream(873        <div>874          <Suspense fallback={<Text text="Loading..." />}>875            {lazyElement}876          </Suspense>877        </div>,878      );879      pipe(writable);880    });881    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);882    // Because there is no content inside the Suspense boundary that could've883    // been written, we expect to not see any additional partial data flushed884    // yet.885    expect(886      stripExternalRuntimeInNodes(887        container.childNodes,888        renderOptions.unstable_externalRuntimeSrc,889      ).length,890    ).toBe(gate(flags => flags.shouldUseFizzExternalRuntime) ? 1 : 2);891    await act(() => {892      resolveElement({default: <Text text="Hello" />});893    });894    expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);895  });896897  it('should client render a boundary if a lazy element rejects', async () => {898    let rejectElement;899    const element = <Text text="Hello" />;900    const lazyElement = React.lazy(() => {901      return new Promise((resolve, reject) => {902        rejectElement = reject;903      });904    });905906    const theError = new Error('Test');907    const loggedErrors = [];908    function onError(x, errorInfo) {909      loggedErrors.push(x);910      return 'hash of (' + x.message + ')';911    }912    const expectedDigest = onError(theError);913    loggedErrors.length = 0;914915    function App({isClient}) {916      return (917        <div>918          <Suspense fallback={<Text text="Loading..." />}>919            {isClient ? element : lazyElement}920          </Suspense>921        </div>922      );923    }924925    await act(() => {926      const {pipe} = renderToPipeableStream(<App isClient={false} />, {927        onError,928      });929      pipe(writable);930    });931    expect(loggedErrors).toEqual([]);932933    const errors = [];934    // Attempt to hydrate the content.935    ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {936      onRecoverableError(error, errorInfo) {937        errors.push({error, errorInfo});938      },939    });940    await waitForAll([]);941942    // We're still loading because we're waiting for the server to stream more content.943    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);944945    expect(loggedErrors).toEqual([]);946947    await act(() => {948      rejectElement(theError);949    });950951    expect(loggedErrors).toEqual([theError]);952953    // We haven't ran the client hydration yet.954    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);955956    // Now we can client render it instead.957    await waitForAll([]);958959    expectErrors(960      errors,961      [962        [963          'Switched to client rendering because the server rendering errored:\n\n' +964            theError.message,965          expectedDigest,966          componentStack(['Suspense', 'div', 'App']),967        ],968      ],969      [970        [971          'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',972          expectedDigest,973        ],974      ],975    );976977    // The client rendered HTML is now in place.978    // expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);979980    expect(loggedErrors).toEqual([theError]);981  });982983  it('Errors in boundaries should be sent to the client and reported on client render - Error before flushing', async () => {984    function Indirection({level, children}) {985      if (level > 0) {986        return <Indirection level={level - 1}>{children}</Indirection>;987      }988      return children;989    }990991    const theError = new Error('uh oh');992993    function Erroring({isClient}) {994      if (isClient) {995        return 'Hello World';996      }997      throw theError;998    }9991000    function App({isClient}) {1001      return (1002        <div>1003          <Suspense fallback={<span>loading...</span>}>1004            <Indirection level={2}>1005              <Erroring isClient={isClient} />1006            </Indirection>1007          </Suspense>1008        </div>1009      );1010    }10111012    const loggedErrors = [];1013    function onError(x) {1014      loggedErrors.push(x);1015      return 'hash(' + x.message + ')';1016    }1017    const expectedDigest = onError(theError);1018    loggedErrors.length = 0;10191020    await act(() => {1021      const {pipe} = renderToPipeableStream(1022        <App />,10231024        {1025          onError,1026        },1027      );1028      pipe(writable);1029    });1030    expect(loggedErrors).toEqual([theError]);10311032    const errors = [];1033    // Attempt to hydrate the content.1034    ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {1035      onRecoverableError(error, errorInfo) {1036        errors.push({error, errorInfo});1037      },1038    });1039    await waitForAll([]);10401041    expect(getVisibleChildren(container)).toEqual(<div>Hello World</div>);10421043    expectErrors(1044      errors,1045      [1046        [1047          'Switched to client rendering because the server rendering errored:\n\n' +1048            theError.message,1049          expectedDigest,1050          componentStack([1051            'Erroring',1052            'Indirection',1053            'Indirection',1054            'Indirection',1055            'Suspense',1056            'div',1057            'App',1058          ]),1059        ],1060      ],1061      [1062        [1063          'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1064          expectedDigest,1065        ],1066      ],1067    );1068  });10691070  it('Errors in boundaries should be sent to the client and reported on client render - Error after flushing', async () => {1071    let rejectComponent;1072    const LazyComponent = React.lazy(() => {1073      return new Promise((resolve, reject) => {1074        rejectComponent = reject;1075      });1076    });10771078    function App({isClient}) {1079      return (1080        <div>1081          <Suspense fallback={<Text text="Loading..." />}>1082            {isClient ? <Text text="Hello" /> : <LazyComponent text="Hello" />}1083          </Suspense>1084        </div>1085      );1086    }10871088    const loggedErrors = [];1089    const theError = new Error('uh oh');1090    function onError(x) {1091      loggedErrors.push(x);1092      return 'hash(' + x.message + ')';1093    }1094    const expectedDigest = onError(theError);1095    loggedErrors.length = 0;10961097    await act(() => {1098      const {pipe} = renderToPipeableStream(1099        <App />,11001101        {1102          onError,1103        },1104      );1105      pipe(writable);1106    });1107    expect(loggedErrors).toEqual([]);11081109    const errors = [];1110    // Attempt to hydrate the content.1111    ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {1112      onRecoverableError(error, errorInfo) {1113        errors.push({error, errorInfo});1114      },1115    });1116    await waitForAll([]);11171118    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);11191120    await act(() => {1121      rejectComponent(theError);1122    });11231124    expect(loggedErrors).toEqual([theError]);1125    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);11261127    // Now we can client render it instead.1128    await waitForAll([]);11291130    expectErrors(1131      errors,1132      [1133        [1134          'Switched to client rendering because the server rendering errored:\n\n' +1135            theError.message,1136          expectedDigest,1137          componentStack(['Lazy', 'Suspense', 'div', 'App']),1138        ],1139      ],1140      [1141        [1142          'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1143          expectedDigest,1144        ],1145      ],1146    );11471148    // The client rendered HTML is now in place.1149    expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);1150    expect(loggedErrors).toEqual([theError]);1151  });11521153  it('should asynchronously load the suspense boundary', async () => {1154    await act(() => {1155      const {pipe} = renderToPipeableStream(1156        <div>1157          <Suspense fallback={<Text text="Loading..." />}>1158            <AsyncText text="Hello World" />1159          </Suspense>1160        </div>,1161      );1162      pipe(writable);1163    });1164    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);1165    await act(() => {1166      resolveText('Hello World');1167    });1168    expect(getVisibleChildren(container)).toEqual(<div>Hello World</div>);1169  });11701171  it('waits for pending content to come in from the server and then hydrates it', async () => {1172    const ref = React.createRef();11731174    function App() {1175      return (1176        <div>1177          <Suspense fallback="Loading...">1178            <h1 ref={ref}>1179              <AsyncText text="Hello" />1180            </h1>1181          </Suspense>1182        </div>1183      );1184    }11851186    let bootstrapped = false;1187    window.__INIT__ = function () {1188      bootstrapped = true;1189      // Attempt to hydrate the content.1190      ReactDOMClient.hydrateRoot(container, <App />);1191    };11921193    await act(() => {1194      const {pipe} = renderToPipeableStream(<App />, {1195        bootstrapScriptContent: '__INIT__();',1196      });1197      pipe(writable);1198    });11991200    // We're still showing a fallback.1201    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);12021203    // We already bootstrapped.1204    expect(bootstrapped).toBe(true);12051206    // Attempt to hydrate the content.1207    await waitForAll([]);12081209    // We're still loading because we're waiting for the server to stream more content.1210    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);12111212    // The server now updates the content in place in the fallback.1213    await act(() => {1214      resolveText('Hello');1215    });12161217    // The final HTML is now in place.1218    expect(getVisibleChildren(container)).toEqual(1219      <div>1220        <h1>Hello</h1>1221      </div>,1222    );1223    const h1 = container.getElementsByTagName('h1')[0];12241225    // But it is not yet hydrated.1226    expect(ref.current).toBe(null);12271228    await waitForAll([]);12291230    // Now it's hydrated.1231    expect(ref.current).toBe(h1);1232  });12331234  it('handles an error on the client if the server ends up erroring', async () => {1235    const ref = React.createRef();12361237    class ErrorBoundary extends React.Component {1238      state = {error: null};1239      static getDerivedStateFromError(error) {1240        return {error};1241      }1242      render() {1243        if (this.state.error) {1244          return <b ref={ref}>{this.state.error.message}</b>;1245        }1246        return this.props.children;1247      }1248    }12491250    function App() {1251      return (1252        <ErrorBoundary>1253          <div>1254            <Suspense fallback="Loading...">1255              <span ref={ref}>1256                <AsyncText text="This Errors" />1257              </span>1258            </Suspense>1259          </div>1260        </ErrorBoundary>1261      );1262    }12631264    const loggedErrors = [];12651266    // We originally suspend the boundary and start streaming the loading state.1267    await act(() => {1268      const {pipe} = renderToPipeableStream(1269        <App />,12701271        {1272          onError(x) {1273            loggedErrors.push(x);1274          },1275        },1276      );1277      pipe(writable);1278    });12791280    // We're still showing a fallback.1281    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);12821283    expect(loggedErrors).toEqual([]);12841285    // Attempt to hydrate the content.1286    ReactDOMClient.hydrateRoot(container, <App />);1287    await waitForAll([]);12881289    // We're still loading because we're waiting for the server to stream more content.1290    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);12911292    const theError = new Error('Error Message');1293    await act(() => {1294      rejectText('This Errors', theError);1295    });12961297    expect(loggedErrors).toEqual([theError]);12981299    // The server errored, but we still haven't hydrated. We don't know if the1300    // client will succeed yet, so we still show the loading state.1301    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);1302    expect(ref.current).toBe(null);13031304    // Flush the hydration.1305    await waitForAll([]);13061307    // Hydrating should've generated an error and replaced the suspense boundary.1308    expect(getVisibleChildren(container)).toEqual(<b>Error Message</b>);13091310    const b = container.getElementsByTagName('b')[0];1311    expect(ref.current).toBe(b);1312  });13131314  // @gate enableSuspenseList1315  it('shows inserted items before pending in a SuspenseList as fallbacks while hydrating', async () => {1316    const ref = React.createRef();13171318    // These are hoisted to avoid them from rerendering.1319    const a = (1320      <Suspense fallback="Loading A">1321        <span ref={ref}>1322          <AsyncText text="A" />1323        </span>1324      </Suspense>1325    );1326    const b = (1327      <Suspense fallback="Loading B">1328        <span>1329          <Text text="B" />1330        </span>1331      </Suspense>1332    );13331334    function App({showMore}) {1335      return (1336        <div>1337          <SuspenseList revealOrder="forwards" tail="visible">1338            {a}1339            {b}1340            {showMore ? (1341              <Suspense fallback="Loading C">1342                <span>C</span>1343              </Suspense>1344            ) : null}1345          </SuspenseList>1346        </div>1347      );1348    }13491350    // We originally suspend the boundary and start streaming the loading state.1351    await act(() => {1352      const {pipe} = renderToPipeableStream(<App showMore={false} />);1353      pipe(writable);1354    });13551356    const root = ReactDOMClient.hydrateRoot(1357      container,1358      <App showMore={false} />,1359    );1360    await waitForAll([]);13611362    // We're not hydrated yet.1363    expect(ref.current).toBe(null);1364    expect(getVisibleChildren(container)).toEqual(1365      <div>1366        {'Loading A'}1367        {'Loading B'}1368      </div>,1369    );13701371    // Add more rows before we've hydrated the first two.1372    root.render(<App showMore={true} />);1373    await waitForAll([]);13741375    // We're not hydrated yet.1376    expect(ref.current).toBe(null);13771378    // We haven't resolved yet.1379    expect(getVisibleChildren(container)).toEqual(1380      <div>1381        {'Loading A'}1382        {'Loading B'}1383        {'Loading C'}1384      </div>,1385    );13861387    await act(async () => {1388      await resolveText('A');1389    });13901391    await waitForAll([]);13921393    expect(getVisibleChildren(container)).toEqual(1394      <div>1395        <span>A</span>1396        <span>B</span>1397        <span>C</span>1398      </div>,1399    );14001401    const span = container.getElementsByTagName('span')[0];1402    expect(ref.current).toBe(span);1403  });14041405  it('client renders a boundary if it does not resolve before aborting', async () => {1406    function App() {1407      return (1408        <div>1409          <Suspense fallback="Loading...">1410            <h1>1411              <AsyncText text="Hello" />1412            </h1>1413          </Suspense>1414          <main>1415            <Suspense fallback="loading...">1416              <AsyncText text="World" />1417            </Suspense>1418          </main>1419        </div>1420      );1421    }14221423    const loggedErrors = [];1424    const expectedDigest = 'Hash for Abort';1425    function onError(error) {1426      loggedErrors.push(error);1427      return expectedDigest;1428    }14291430    let controls;1431    await act(() => {1432      controls = renderToPipeableStream(<App />, {onError});1433      controls.pipe(writable);1434    });14351436    // We're still showing a fallback.14371438    const errors = [];1439    // Attempt to hydrate the content.1440    ReactDOMClient.hydrateRoot(container, <App />, {1441      onRecoverableError(error, errorInfo) {1442        errors.push({error, errorInfo});1443      },1444    });1445    await waitForAll([]);14461447    // We're still loading because we're waiting for the server to stream more content.1448    expect(getVisibleChildren(container)).toEqual(1449      <div>1450        Loading...<main>loading...</main>1451      </div>,1452    );14531454    // We abort the server response.1455    await act(() => {1456      controls.abort();1457    });14581459    // We still can't render it on the client.1460    await waitForAll([]);1461    expectErrors(1462      errors,1463      [1464        [1465          'Switched to client rendering because the server rendering aborted due to:\n\n' +1466            'The render was aborted by the server without a reason.',1467          expectedDigest,1468          // We get the stack of the task when it was aborted which is why we see `h1`1469          componentStack(['AsyncText', 'h1', 'Suspense', 'div', 'App']),1470        ],1471        [1472          'Switched to client rendering because the server rendering aborted due to:\n\n' +1473            'The render was aborted by the server without a reason.',1474          expectedDigest,1475          componentStack(['AsyncText', 'Suspense', 'main', 'div', 'App']),1476        ],1477      ],1478      [1479        [1480          'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1481          expectedDigest,1482        ],1483        [1484          'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1485          expectedDigest,1486        ],1487      ],1488    );1489    expect(getVisibleChildren(container)).toEqual(1490      <div>1491        Loading...<main>loading...</main>1492      </div>,1493    );14941495    // We now resolve it on the client.1496    await clientAct(() => {1497      resolveText('Hello');1498      resolveText('World');1499    });1500    assertLog([]);15011502    // The client rendered HTML is now in place.1503    expect(getVisibleChildren(container)).toEqual(1504      <div>1505        <h1>Hello</h1>1506        <main>World</main>1507      </div>,1508    );1509  });15101511  it('should allow for two containers to be written to the same document', async () => {1512    // We create two passthrough streams for each container to write into.1513    // Notably we don't implement a end() call for these. Because we don't want to1514    // close the underlying stream just because one of the streams is done. Instead1515    // we manually close when both are done.1516    const writableA = new Stream.Writable();1517    writableA._write = (chunk, encoding, next) => {1518      writable.write(chunk, encoding, next);1519    };1520    const writableB = new Stream.Writable();1521    writableB._write = (chunk, encoding, next) => {1522      writable.write(chunk, encoding, next);1523    };15241525    await act(() => {1526      const {pipe} = renderToPipeableStream(1527        // We use two nested boundaries to flush out coverage of an old reentrancy bug.1528        <div>1529          <Suspense fallback="Loading...">1530            <Suspense fallback={<Text text="Loading A..." />}>1531              <>1532                <Text text="This will show A: " />1533                <div>1534                  <AsyncText text="A" />1535                </div>1536              </>1537            </Suspense>1538          </Suspense>1539        </div>,1540        {1541          identifierPrefix: 'A_',1542          onShellReady() {1543            writableA.write('<div id="container-A">');1544            pipe(writableA);1545            writableA.write('</div>');1546          },1547        },1548      );1549    });15501551    await act(() => {1552      const {pipe} = renderToPipeableStream(1553        <div>1554          <Suspense fallback={<Text text="Loading B..." />}>1555            <Text text="This will show B: " />1556            <div>1557              <AsyncText text="B" />1558            </div>1559          </Suspense>1560        </div>,1561        {1562          identifierPrefix: 'B_',1563          onShellReady() {1564            writableB.write('<div id="container-B">');1565            pipe(writableB);1566            writableB.write('</div>');1567          },1568        },1569      );1570    });15711572    expect(getVisibleChildren(container)).toEqual([1573      <div id="container-A">1574        <div>Loading A...</div>1575      </div>,1576      <div id="container-B">1577        <div>Loading B...</div>1578      </div>,1579    ]);15801581    await act(() => {1582      resolveText('B');1583    });15841585    expect(getVisibleChildren(container)).toEqual([1586      <div id="container-A">1587        <div>Loading A...</div>1588      </div>,1589      <div id="container-B">1590        <div>1591          This will show B: <div>B</div>1592        </div>1593      </div>,1594    ]);15951596    await act(() => {1597      resolveText('A');1598    });15991600    // We're done writing both streams now.1601    writable.end();16021603    expect(getVisibleChildren(container)).toEqual([1604      <div id="container-A">1605        <div>1606          This will show A: <div>A</div>1607        </div>1608      </div>,1609      <div id="container-B">1610        <div>1611          This will show B: <div>B</div>1612        </div>1613      </div>,1614    ]);1615  });16161617  it('can resolve async content in esoteric parents', async () => {1618    function AsyncOption({text}) {1619      return <option>{readText(text)}</option>;1620    }16211622    function AsyncCol({className}) {1623      return <col className={readText(className)} />;1624    }16251626    function AsyncPath({id}) {1627      return <path id={readText(id)} />;1628    }16291630    function AsyncMi({id}) {1631      return <mi id={readText(id)} />;1632    }16331634    function App() {1635      return (1636        <div>1637          <select>1638            <Suspense fallback="Loading...">1639              <AsyncOption text="Hello" />1640            </Suspense>1641          </select>1642          <Suspense fallback="Loading...">1643            <table>1644              <colgroup>1645                <AsyncCol className="World" />1646              </colgroup>1647            </table>1648            <svg>1649              <g>1650                <AsyncPath id="my-path" />1651              </g>1652            </svg>1653            <math>1654              <AsyncMi id="my-mi" />1655            </math>1656          </Suspense>1657        </div>1658      );1659    }16601661    await act(() => {1662      const {pipe} = renderToPipeableStream(<App />);1663      pipe(writable);1664    });16651666    expect(getVisibleChildren(container)).toEqual(1667      <div>1668        <select>Loading...</select>Loading...1669      </div>,1670    );16711672    await act(() => {1673      resolveText('Hello');1674    });16751676    await act(() => {1677      resolveText('World');1678    });16791680    await act(() => {1681      resolveText('my-path');1682      resolveText('my-mi');1683    });16841685    expect(getVisibleChildren(container)).toEqual(1686      <div>1687        <select>1688          <option>Hello</option>1689        </select>1690        <table>1691          <colgroup>1692            <col class="World" />1693          </colgroup>1694        </table>1695        <svg>1696          <g>1697            <path id="my-path" />1698          </g>1699        </svg>1700        <math>1701          <mi id="my-mi" />1702        </math>1703      </div>,1704    );17051706    expect(container.querySelector('#my-path').namespaceURI).toBe(1707      'http://www.w3.org/2000/svg',1708    );1709    expect(container.querySelector('#my-mi').namespaceURI).toBe(1710      'http://www.w3.org/1998/Math/MathML',1711    );1712  });17131714  it('can resolve async content in table parents', async () => {1715    function AsyncTableBody({className, children}) {1716      return <tbody className={readText(className)}>{children}</tbody>;1717    }17181719    function AsyncTableRow({className, children}) {1720      return <tr className={readText(className)}>{children}</tr>;1721    }17221723    function AsyncTableCell({text}) {1724      return <td>{readText(text)}</td>;1725    }17261727    function App() {1728      return (1729        <table>1730          <Suspense1731            fallback={1732              <tbody>1733                <tr>1734                  <td>Loading...</td>1735                </tr>1736              </tbody>1737            }>1738            <AsyncTableBody className="A">1739              <AsyncTableRow className="B">1740                <AsyncTableCell text="C" />1741              </AsyncTableRow>1742            </AsyncTableBody>1743          </Suspense>1744        </table>1745      );1746    }17471748    await act(() => {1749      const {pipe} = renderToPipeableStream(<App />);1750      pipe(writable);1751    });17521753    expect(getVisibleChildren(container)).toEqual(1754      <table>1755        <tbody>1756          <tr>1757            <td>Loading...</td>1758          </tr>1759        </tbody>1760      </table>,1761    );17621763    await act(() => {1764      resolveText('A');1765    });17661767    await act(() => {1768      resolveText('B');1769    });17701771    await act(() => {1772      resolveText('C');1773    });17741775    expect(getVisibleChildren(container)).toEqual(1776      <table>1777        <tbody class="A">1778          <tr class="B">1779            <td>C</td>1780          </tr>1781        </tbody>1782      </table>,1783    );1784  });17851786  it('can stream into an SVG container', async () => {1787    function AsyncPath({id}) {1788      return <path id={readText(id)} />;1789    }17901791    function App() {1792      return (1793        <g>1794          <Suspense fallback={<text>Loading...</text>}>1795            <AsyncPath id="my-path" />1796          </Suspense>1797        </g>1798      );1799    }18001801    await act(() => {1802      const {pipe} = renderToPipeableStream(1803        <App />,18041805        {1806          namespaceURI: 'http://www.w3.org/2000/svg',1807          onShellReady() {1808            writable.write('<svg>');1809            pipe(writable);1810            writable.write('</svg>');1811          },1812        },1813      );1814    });18151816    expect(getVisibleChildren(container)).toEqual(1817      <svg>1818        <g>1819          <text>Loading...</text>1820        </g>1821      </svg>,1822    );18231824    await act(() => {1825      resolveText('my-path');1826    });18271828    expect(getVisibleChildren(container)).toEqual(1829      <svg>1830        <g>1831          <path id="my-path" />1832        </g>1833      </svg>,1834    );18351836    expect(container.querySelector('#my-path').namespaceURI).toBe(1837      'http://www.w3.org/2000/svg',1838    );1839  });18401841  function normalizeCodeLocInfo(str) {1842    return (1843      str &&1844      String(str).replace(/\n +(?:at|in) ([^\(]+) [^\n]*/g, function (m, name) {1845        return '\n    in ' + name + ' (at **)';1846      })1847    );1848  }18491850  it('should include a component stack across suspended boundaries', async () => {1851    function B() {1852      const children = [readText('Hello'), readText('World')];1853      // Intentionally trigger a key warning here.1854      return (1855        <div>1856          {children.map(function mapper(t) {1857            return <span>{t}</span>;1858          })}1859        </div>1860      );1861    }1862    function C() {1863      return (1864        <inCorrectTag>1865          <Text text="Loading" />1866        </inCorrectTag>1867      );1868    }1869    function A() {1870      return (1871        <div>1872          <Suspense fallback={<C />}>1873            <B />1874          </Suspense>1875        </div>1876      );1877    }18781879    await act(() => {1880      const {pipe} = renderToPipeableStream(<A />);1881      pipe(writable);1882    });18831884    expect(getVisibleChildren(container)).toEqual(1885      <div>1886        <incorrecttag>Loading</incorrecttag>1887      </div>,1888    );18891890    assertConsoleErrorDev([1891      '<inCorrectTag /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.' +1892        '\n' +1893        '    in inCorrectTag (at **)\n' +1894        '    in C (at **)\n' +1895        '    in A (at **)',1896    ]);18971898    await act(() => {1899      resolveText('Hello');1900      resolveText('World');1901    });19021903    assertConsoleErrorDev([1904      'Each child in a list should have a unique "key" prop.\n\nCheck the render method of `B`.' +1905        ' See https://react.dev/link/warning-keys for more information.\n' +1906        '    in span (at **)\n' +1907        '    in mapper (at **)\n' +1908        '    in Array.map (at **)\n' +1909        '    in B (at **)\n' +1910        '    in A (at **)',1911    ]);19121913    expect(getVisibleChildren(container)).toEqual(1914      <div>1915        <div>1916          <span>Hello</span>1917          <span>World</span>1918        </div>1919      </div>,1920    );1921  });19221923  // @gate !disableLegacyContext1924  it('should can suspend in a class component with legacy context', async () => {1925    class TestProvider extends React.Component {1926      static childContextTypes = {1927        test: PropTypes.string,1928      };1929      state = {ctxToSet: null};1930      static getDerivedStateFromProps(props, state) {1931        return {ctxToSet: props.ctx};1932      }1933      getChildContext() {1934        return {1935          test: this.state.ctxToSet,1936        };1937      }1938      render() {1939        return this.props.children;1940      }1941    }19421943    class TestConsumer extends React.Component {1944      static contextTypes = {1945        test: PropTypes.string,1946      };1947      render() {1948        const child = (1949          <b>1950            <Text text={this.context.test} />1951          </b>1952        );1953        if (this.props.prefix) {1954          return (1955            <>1956              {readText(this.props.prefix)}1957              {child}1958            </>1959          );1960        }1961        return child;1962      }1963    }19641965    await act(() => {1966      const {pipe} = renderToPipeableStream(1967        <TestProvider ctx="A">1968          <div>1969            <Suspense1970              fallback={1971                <>1972                  <Text text="Loading: " />1973                  <TestConsumer />1974                </>1975              }>1976              <TestProvider ctx="B">1977                <TestConsumer prefix="Hello: " />1978              </TestProvider>1979              <TestConsumer />1980            </Suspense>1981          </div>1982        </TestProvider>,1983      );1984      pipe(writable);1985    });1986    assertConsoleErrorDev([1987      'TestProvider uses the legacy childContextTypes API which will soon be removed. ' +1988        'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +1989        '    in TestProvider (at **)',1990      'TestConsumer uses the legacy contextTypes API which will soon be removed. ' +1991        'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +1992        '    in TestConsumer (at **)',1993    ]);1994    expect(getVisibleChildren(container)).toEqual(1995      <div>1996        Loading: <b>A</b>1997      </div>,1998    );1999    await act(() => {2000      resolveText('Hello: ');

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.