/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @emails react-core * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment */ 'use strict'; import { insertNodesAndExecuteScripts, mergeOptions, } from '../test-utils/FizzTestUtils'; let JSDOM; let Stream; let React; let ReactDOM; let ReactDOMClient; let ReactDOMFizzServer; let Suspense; let SuspenseList; let textCache; let loadCache; let writable; let CSPnonce = null; let container; let buffer = ''; let hasErrored = false; let fatalError = undefined; let renderOptions; let waitForAll; let assertLog; let Scheduler; let clientAct; let streamingContainer; let assertConsoleErrorDev; describe('ReactDOMFloat', () => { beforeEach(() => { jest.resetModules(); JSDOM = require('jsdom').JSDOM; const jsdom = new JSDOM( '
', { runScripts: 'dangerously', }, ); // We mock matchMedia. for simplicity it only matches 'all' or '' and misses everything else Object.defineProperty(jsdom.window, 'matchMedia', { writable: true, value: jest.fn().mockImplementation(query => ({ matches: query === 'all' || query === '', media: query, })), }); streamingContainer = null; global.window = jsdom.window; global.document = global.window.document; global.navigator = global.window.navigator; global.Node = global.window.Node; global.addEventListener = global.window.addEventListener; global.MutationObserver = global.window.MutationObserver; // The Fizz runtime assumes requestAnimationFrame exists so we need to polyfill it. global.requestAnimationFrame = global.window.requestAnimationFrame = cb => setTimeout(cb); container = document.getElementById('container'); CSPnonce = null; React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); ReactDOMFizzServer = require('react-dom/server'); Stream = require('stream'); Suspense = React.Suspense; SuspenseList = React.unstable_SuspenseList; Scheduler = require('scheduler/unstable_mock'); const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; assertLog = InternalTestUtils.assertLog; clientAct = InternalTestUtils.act; assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; textCache = new Map(); loadCache = new Set(); buffer = ''; hasErrored = false; writable = new Stream.PassThrough(); writable.setEncoding('utf8'); writable.on('data', chunk => { buffer += chunk; }); writable.on('error', error => { hasErrored = true; fatalError = error; }); renderOptions = {}; if (gate(flags => flags.shouldUseFizzExternalRuntime)) { renderOptions.unstable_externalRuntimeSrc = 'react-dom/unstable_server-external-runtime'; } }); const bodyStartMatch = /| .*?>)/; const headStartMatch = /| .*?>)/; async function act(callback) { await callback(); // Await one turn around the event loop. // This assumes that we'll flush everything we have so far. await new Promise(resolve => { setImmediate(resolve); }); if (hasErrored) { throw fatalError; } // JSDOM doesn't support stream HTML parser so we need to give it a proper fragment. // We also want to execute any scripts that are embedded. // We assume that we have now received a proper fragment of HTML. let bufferedContent = buffer; buffer = ''; if (!bufferedContent) { jest.runAllTimers(); return; } const bodyMatch = bufferedContent.match(bodyStartMatch); const headMatch = bufferedContent.match(headStartMatch); if (streamingContainer === null) { // This is the first streamed content. We decide here where to insert it. If we get , , or // we abandon the pre-built document and start from scratch. If we get anything else we assume it goes into the // container. This is not really production behavior because you can't correctly stream into a deep div effectively // but it's pragmatic for tests. if ( bufferedContent.startsWith('') || bufferedContent.startsWith('') || bufferedContent.startsWith('') || bufferedContent.startsWith(' without a which is almost certainly a bug in React', ); } if (bufferedContent.startsWith('')) { // we can just use the whole document const tempDom = new JSDOM(bufferedContent); // Wipe existing head and body content document.head.innerHTML = ''; document.body.innerHTML = ''; // Copy the attributes over const tempHtmlNode = tempDom.window.document.documentElement; for (let i = 0; i < tempHtmlNode.attributes.length; i++) { const attr = tempHtmlNode.attributes[i]; document.documentElement.setAttribute(attr.name, attr.value); } if (headMatch) { // We parsed a head open tag. we need to copy head attributes and insert future // content into streamingContainer = document.head; const tempHeadNode = tempDom.window.document.head; for (let i = 0; i < tempHeadNode.attributes.length; i++) { const attr = tempHeadNode.attributes[i]; document.head.setAttribute(attr.name, attr.value); } const source = document.createElement('head'); source.innerHTML = tempHeadNode.innerHTML; await insertNodesAndExecuteScripts(source, document.head, CSPnonce); } if (bodyMatch) { // We parsed a body open tag. we need to copy head attributes and insert future // content into streamingContainer = document.body; const tempBodyNode = tempDom.window.document.body; for (let i = 0; i < tempBodyNode.attributes.length; i++) { const attr = tempBodyNode.attributes[i]; document.body.setAttribute(attr.name, attr.value); } const source = document.createElement('body'); source.innerHTML = tempBodyNode.innerHTML; await insertNodesAndExecuteScripts(source, document.body, CSPnonce); } if (!headMatch && !bodyMatch) { throw new Error('expected or after '); } } else { // we assume we are streaming into the default container' streamingContainer = container; const div = document.createElement('div'); div.innerHTML = bufferedContent; await insertNodesAndExecuteScripts(div, container, CSPnonce); } } else if (streamingContainer === document.head) { bufferedContent = '' + bufferedContent; const tempDom = new JSDOM(bufferedContent); const tempHeadNode = tempDom.window.document.head; const source = document.createElement('head'); source.innerHTML = tempHeadNode.innerHTML; await insertNodesAndExecuteScripts(source, document.head, CSPnonce); if (bodyMatch) { streamingContainer = document.body; const tempBodyNode = tempDom.window.document.body; for (let i = 0; i < tempBodyNode.attributes.length; i++) { const attr = tempBodyNode.attributes[i]; document.body.setAttribute(attr.name, attr.value); } const bodySource = document.createElement('body'); bodySource.innerHTML = tempBodyNode.innerHTML; await insertNodesAndExecuteScripts(bodySource, document.body, CSPnonce); } } else { const div = document.createElement('div'); div.innerHTML = bufferedContent; await insertNodesAndExecuteScripts(div, streamingContainer, CSPnonce); } await 0; // Let throttled boundaries reveal jest.runAllTimers(); } function getMeaningfulChildren(element) { const children = []; let node = element.firstChild; while (node) { if (node.nodeType === 1) { if ( // some tags are ambiguous and might be hidden because they look like non-meaningful children // so we have a global override where if this data attribute is included we also include the node node.hasAttribute('data-meaningful') || (node.tagName === 'SCRIPT' && node.hasAttribute('src') && node.getAttribute('src') !== renderOptions.unstable_externalRuntimeSrc && node.hasAttribute('async')) || (node.tagName !== 'SCRIPT' && node.tagName !== 'TEMPLATE' && node.tagName !== 'template' && !node.hasAttribute('hidden') && !node.hasAttribute('aria-hidden') && // Ignore the render blocking expect (node.getAttribute('rel') !== 'expect' || node.getAttribute('blocking') !== 'render')) ) { const props = {}; const attributes = node.attributes; for (let i = 0; i < attributes.length; i++) { if ( attributes[i].name === 'id' && attributes[i].value.includes(':') ) { // We assume this is a React added ID that's a non-visual implementation detail. continue; } props[attributes[i].name] = attributes[i].value; } props.children = getMeaningfulChildren(node); children.push(React.createElement(node.tagName.toLowerCase(), props)); } } else if (node.nodeType === 3) { children.push(node.data); } node = node.nextSibling; } return children.length === 0 ? undefined : children.length === 1 ? children[0] : children; } function BlockedOn({value, children}) { readText(value); return children; } function resolveText(text) { const record = textCache.get(text); if (record === undefined) { const newRecord = { status: 'resolved', value: text, }; textCache.set(text, newRecord); } else if (record.status === 'pending') { const thenable = record.value; record.status = 'resolved'; record.value = text; thenable.pings.forEach(t => t()); } } function readText(text) { const record = textCache.get(text); if (record !== undefined) { switch (record.status) { case 'pending': throw record.value; case 'rejected': throw record.value; case 'resolved': return record.value; } } else { const thenable = { pings: [], then(resolve) { if (newRecord.status === 'pending') { thenable.pings.push(resolve); } else { Promise.resolve().then(() => resolve(newRecord.value)); } }, }; const newRecord = { status: 'pending', value: thenable, }; textCache.set(text, newRecord); throw thenable; } } function AsyncText({text}) { return readText(text); } function renderToPipeableStream(jsx, options) { // Merge options with renderOptions, which may contain featureFlag specific behavior return ReactDOMFizzServer.renderToPipeableStream( jsx, mergeOptions(options, renderOptions), ); } function loadPreloads(hrefs) { const event = new window.Event('load'); const nodes = document.querySelectorAll('link[rel="preload"]'); resolveLoadables(hrefs, nodes, event, href => Scheduler.log('load preload: ' + href), ); } function errorPreloads(hrefs) { const event = new window.Event('error'); const nodes = document.querySelectorAll('link[rel="preload"]'); resolveLoadables(hrefs, nodes, event, href => Scheduler.log('error preload: ' + href), ); } function loadStylesheets(hrefs) { loadStylesheetsFrom(document, hrefs); } function loadStylesheetsFrom(root, hrefs) { const event = new window.Event('load'); const nodes = root.querySelectorAll('link[rel="stylesheet"]'); resolveLoadables(hrefs, nodes, event, href => { Scheduler.log('load stylesheet: ' + href); }); } function errorStylesheets(hrefs) { const event = new window.Event('error'); const nodes = document.querySelectorAll('link[rel="stylesheet"]'); resolveLoadables(hrefs, nodes, event, href => { Scheduler.log('error stylesheet: ' + href); }); } function resolveLoadables(hrefs, nodes, event, onLoad) { const hrefSet = hrefs ? new Set(hrefs) : null; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (loadCache.has(node)) { continue; } const href = node.getAttribute('href'); if (!hrefSet || hrefSet.has(href)) { loadCache.add(node); onLoad(href); node.dispatchEvent(event); } } } it('can render resources before singletons', async () => { const root = ReactDOMClient.createRoot(document); root.render( <> foo hello world , ); try { await waitForAll([]); } catch (e) { // for DOMExceptions that happen when expecting this test to fail we need // to clear the scheduler first otherwise the expected failure will fail await waitForAll([]); throw e; } expect(getMeaningfulChildren(document)).toEqual( foo hello world , ); }); it('can hydrate non Resources in head when Resources are also inserted there', async () => { await act(() => { const {pipe} = renderToPipeableStream( {}} /> foo ' + (gate(flags => flags.shouldUseFizzExternalRuntime) ? '' : '') + (gate(flags => flags.enableFizzBlockingRender) ? '' : '') + 'foo' + 'bar' + (gate(flags => flags.enableFizzBlockingRender) ? '' : ''), '', ]); }); it('dedupes if the external runtime is explicitly loaded using preinit', async () => { const unstable_externalRuntimeSrc = 'src-of-external-runtime'; function App() { ReactDOM.preinit(unstable_externalRuntimeSrc, {as: 'script'}); return (
Loading...}>
); } await act(() => { const {pipe} = renderToPipeableStream( , { unstable_externalRuntimeSrc, }, ); pipe(writable); }); expect( Array.from(document.querySelectorAll('script[async]')).map( n => n.outerHTML, ), ).toEqual(['']); }); it('can send style insertion implementation independent of boundary commpletion instruction implementation', async () => { await act(() => { renderToPipeableStream( foo bar , ).pipe(writable); }); expect(getMeaningfulChildren(document)).toEqual( {'loading foo...'} {'loading bar...'} , ); await act(() => { resolveText('foo'); }); expect(getMeaningfulChildren(document)).toEqual( foo {'loading bar...'} , ); await act(() => { resolveText('bar'); }); expect(getMeaningfulChildren(document)).toEqual( foo {'loading bar...'} , ); }); it('can avoid inserting a late stylesheet if it already rendered on the client', async () => { await act(() => { renderToPipeableStream( foo bar , ).pipe(writable); }); expect(getMeaningfulChildren(document)).toEqual( {'loading foo...'} {'loading bar...'} , ); ReactDOMClient.hydrateRoot( document, foo bar , ); await waitForAll([]); loadPreloads(); await assertLog(['load preload: foo']); expect(getMeaningfulChildren(document)).toEqual( {'loading foo...'} {'loading bar...'} , ); await act(() => { resolveText('bar'); }); await act(() => { loadStylesheets(); }); await assertLog(['load stylesheet: foo', 'load stylesheet: bar']); expect(getMeaningfulChildren(document)).toEqual( {'loading foo...'} {'bar'} , ); await act(() => { resolveText('foo'); }); await act(() => { loadStylesheets(); }); await assertLog([]); expect(getMeaningfulChildren(document)).toEqual( {'foo'} {'bar'} , ); }); it('can hoist and , ).pipe(writable); }); expect(getMeaningfulChildren(document)).toEqual( , ); await act(() => { resolveText('block'); }); expect(getMeaningfulChildren(document)).toEqual( , ); await act(() => { resolveText('block2'); }); expect(getMeaningfulChildren(document)).toEqual( , ); await act(() => { resolveText('block again'); }); expect(getMeaningfulChildren(document)).toEqual( , ); ReactDOMClient.hydrateRoot( document, , ); await waitForAll([]); await act(() => { loadPreloads(); loadStylesheets(); }); await assertLog([ 'load preload: one4', 'load preload: three4', 'load preload: seven1', 'load preload: one2', 'load preload: two2', 'load preload: five1', 'load preload: three3', 'load preload: four3', 'load stylesheet: one1', 'load stylesheet: one2', 'load stylesheet: one4', 'load stylesheet: two2', 'load stylesheet: three1', 'load stylesheet: three3', 'load stylesheet: three4', 'load stylesheet: four3', 'load stylesheet: five1', 'load stylesheet: seven1', ]); expect(getMeaningfulChildren(document)).toEqual( , ); }); it('client renders a boundary if a style Resource dependency fails to load', async () => { function App() { return ( Hello ); } await act(() => { const {pipe} = renderToPipeableStream(); pipe(writable); }); expect(getMeaningfulChildren(document)).toEqual( loading... , ); await act(() => { resolveText('unblock'); }); expect(getMeaningfulChildren(document)).toEqual( loading... , ); errorStylesheets(['bar']); assertLog(['error stylesheet: bar']); await waitForAll([]); const boundaryTemplateInstance = document.getElementById('B:0'); const suspenseInstance = boundaryTemplateInstance.previousSibling; expect(suspenseInstance.data).toEqual('$!'); expect(boundaryTemplateInstance.dataset.dgst).toBe('CSS failed to load'); expect(getMeaningfulChildren(document)).toEqual( loading... , ); const errors = []; ReactDOMClient.hydrateRoot(document, , { onRecoverableError(err, errInfo) { errors.push(err.message); errors.push(err.digest); }, }); await waitForAll([]); // When binding a stylesheet that was SSR'd in a boundary reveal there is a loadingState promise // We need to use that promise to resolve the suspended commit because we don't know if the load or error // events have already fired. This requires the load to be awaited for the commit to have a chance to flush // We could change this by tracking the loadingState's fulfilled status directly on the loadingState similar // to thenables however this slightly increases the fizz runtime code size. await clientAct(() => loadStylesheets()); assertLog(['load stylesheet: foo']); expect(getMeaningfulChildren(document)).toEqual( Hello , ); expect(errors).toEqual([ 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.', 'CSS failed to load', ]); }); it('treats stylesheet links with a precedence as a resource', async () => { await act(() => { const {pipe} = renderToPipeableStream( Hello , ); pipe(writable); }); expect(getMeaningfulChildren(document)).toEqual( Hello , ); ReactDOMClient.hydrateRoot( document, Hello , ); await waitForAll([]); expect(getMeaningfulChildren(document)).toEqual( Hello , ); }); it('inserts text separators following text when followed by an element that is converted to a resource and thus removed from the html inline', async () => { // If you render many of these as siblings the values get emitted as a single text with no separator sometimes // because the link gets elided as a resource function AsyncTextWithResource({text, href, precedence}) { const value = readText(text); return ( <> {value} ); } await act(() => { const {pipe} = renderToPipeableStream( , ); pipe(writable); resolveText('foo'); resolveText('bar'); resolveText('baz'); }); expect(getMeaningfulChildren(document)).toEqual( {'foo'} {'bar'} {'baz'} , ); }); it('hoists late stylesheets the correct precedence', async () => { function PresetPrecedence() { ReactDOM.preinit('preset', {as: 'style', precedence: 'preset'}); } await act(() => { const {pipe} = renderToPipeableStream(
foo
bar
bar
baz
qux
bar
baz
qux
, ); pipe(writable); }); expect(getMeaningfulChildren(document)).toEqual(
loading foo bar...
loading bar baz qux...
loading bar baz qux...
, ); await act(() => { resolveText('foo'); resolveText('bar'); }); expect(getMeaningfulChildren(document)).toEqual(
loading foo bar...
loading bar baz qux...
loading bar baz qux...
, ); await act(() => { const link = document.querySelector('link[rel="stylesheet"][href="foo"]'); const event = document.createEvent('Events'); event.initEvent('load', true, true); link.dispatchEvent(event); }); expect(getMeaningfulChildren(document)).toEqual(
loading foo bar...
loading bar baz qux...
loading bar baz qux...
, ); await act(() => { const link = document.querySelector('link[rel="stylesheet"][href="bar"]'); const event = document.createEvent('Events'); event.initEvent('load', true, true); link.dispatchEvent(event); }); expect(getMeaningfulChildren(document)).toEqual(
foo
bar
loading bar baz qux...
loading bar baz qux...
, ); await act(() => { resolveText('baz'); }); expect(getMeaningfulChildren(document)).toEqual(
foo
bar
loading bar baz qux...
loading bar baz qux...
, ); await act(() => { resolveText('qux'); }); expect(getMeaningfulChildren(document)).toEqual(
foo
bar
loading bar baz qux...
loading bar baz qux...
, ); await act(() => { const bazlink = document.querySelector( 'link[rel="stylesheet"][href="baz"]', ); const quxlink = document.querySelector( 'link[rel="stylesheet"][href="qux"]', ); const presetLink = document.querySelector( 'link[rel="stylesheet"][href="preset"]', ); const event = document.createEvent('Events'); event.initEvent('load', true, true); bazlink.dispatchEvent(event); quxlink.dispatchEvent(event); presetLink.dispatchEvent(event); }); expect(getMeaningfulChildren(document)).toEqual(
foo
bar
bar
baz
qux
loading bar baz qux...
, ); await act(() => { resolveText('unblock'); }); expect(getMeaningfulChildren(document)).toEqual(
foo
bar
bar
baz
qux
bar
baz
qux
, ); }); it('normalizes stylesheet resource precedence for all boundaries inlined as part of the shell flush', async () => { await act(() => { const {pipe} = renderToPipeableStream(
outer
middle
inner
middle
, ); pipe(writable); }); expect(getMeaningfulChildren(document)).toEqual(
outer
middle
inner
middle
, ); }); it('stylesheet resources are inserted according to precedence order on the client', async () => { await act(() => { const {pipe} = renderToPipeableStream(
Hello
, ); pipe(writable); }); expect(getMeaningfulChildren(document)).toEqual(
Hello
, ); const root = ReactDOMClient.hydrateRoot( document,
Hello
, ); await waitForAll([]); expect(getMeaningfulChildren(document)).toEqual(
Hello
, ); root.render(
Goodbye
, ); await waitForAll([]); await act(() => { loadPreloads(); loadStylesheets(); }); await assertLog([ 'load preload: baz', 'load stylesheet: foo', 'load stylesheet: baz', 'load stylesheet: bar', ]); expect(getMeaningfulChildren(document)).toEqual(
Goodbye
, ); }); it('inserts preloads in render phase eagerly', async () => { function Throw() { throw new Error('Uh oh!'); } class ErrorBoundary extends React.Component { state = {hasError: false, error: null}; static getDerivedStateFromError(error) { return { hasError: true, error, }; } render() { if (this.state.hasError) { return this.state.error.message; } return this.props.children; } } const root = ReactDOMClient.createRoot(container); root.render(
foo
, ); await waitForAll([]); expect(getMeaningfulChildren(document)).toEqual(
Uh oh!
, ); }); it('will include child boundary stylesheet resources in the boundary reveal instruction', async () => { await act(() => { const {pipe} = renderToPipeableStream(
foo
bar
baz
, ); pipe(writable); }); expect(getMeaningfulChildren(document)).toEqual(
loading foo...
, ); await act(() => { resolveText('bar'); }); expect(getMeaningfulChildren(document)).toEqual(
loading foo...
, ); await act(() => { resolveText('baz'); }); expect(getMeaningfulChildren(document)).toEqual(
loading foo...
, ); await act(() => { resolveText('foo'); }); expect(getMeaningfulChildren(document)).toEqual(
loading foo...
, ); await act(() => { const event = document.createEvent('Events'); event.initEvent('load', true, true); Array.from(document.querySelectorAll('link[rel="stylesheet"]')).forEach( el => { el.dispatchEvent(event); }, ); }); expect(getMeaningfulChildren(document)).toEqual(
foo
bar
baz
, ); }); it('will hoist resources of child boundaries emitted as part of a partial boundary to the parent boundary', async () => { await act(() => { const {pipe} = renderToPipeableStream(
foo
bar
baz
qux
, ); pipe(writable); }); expect(getMeaningfulChildren(document)).toEqual(
loading...
, ); // This will enqueue a stylesheet resource in a deep blocked boundary (loading baz...). await act(() => { resolveText('baz'); }); expect(getMeaningfulChildren(document)).toEqual(
loading...
, ); // This will enqueue a stylesheet resource in the intermediate blocked boundary (loading bar...). await act(() => { resolveText('bar'); }); expect(getMeaningfulChildren(document)).toEqual(
loading...
, ); // This will complete a segment in the top level boundary that is still blocked on another segment. // It will flush the completed segment however the inner boundaries should not emit their style dependencies // because they are not going to be revealed yet. instead their dependencies are hoisted to the blocked // boundary (top level). await act(() => { resolveText('foo'); }); expect(getMeaningfulChildren(document)).toEqual(
loading...
, ); // This resolves the last blocked segment on the top level boundary so we see all dependencies of the // nested boundaries emitted at this level await act(() => { resolveText('qux'); }); expect(getMeaningfulChildren(document)).toEqual(
loading...
, ); // We load all stylesheets and confirm the content is revealed await act(() => { const event = document.createEvent('Events'); event.initEvent('load', true, true); Array.from(document.querySelectorAll('link[rel="stylesheet"]')).forEach( el => { el.dispatchEvent(event); }, ); }); expect(getMeaningfulChildren(document)).toEqual(
foo
bar
baz
qux
, ); }); it('encodes attributes consistently whether resources are flushed in shell or in late boundaries', async () => { function App() { return (
{}} norsymbols={Symbol('foo')} /> {}} norsymbols={Symbol('foo')} />
); } await act(() => { const {pipe} = renderToPipeableStream(); pipe(writable); }); expect(getMeaningfulChildren(document)).toEqual(
loading...
, ); assertConsoleErrorDev([ 'React does not recognize the `nonStandardAttr` prop on a DOM element. ' + 'If you intentionally want it to appear in the DOM as a custom attribute, ' + 'spell it as lowercase `nonstandardattr` instead. If you accidentally passed it from a ' + 'parent component, remove it from the DOM element.\n' + ' in link (at **)\n' + ' in App (at **)', 'Invalid values for props `shouldnotincludefunctions`, `norsymbols` on tag. ' + 'Either remove them from the element, or pass a string or number value to keep them in the DOM. ' + 'For details, see https://react.dev/link/attribute-behavior \n' + ' in link (at **)\n' + ' in App (at **)', ]); // Now we flush the stylesheet with the boundary await act(() => { resolveText('unblock'); }); expect(getMeaningfulChildren(document)).toEqual(
loading...
, ); }); it('boundary stylesheet resource dependencies hoist to a parent boundary when flushed inline', async () => { await act(() => { const {pipe} = renderToPipeableStream(
, ); pipe(writable); }); expect(getMeaningfulChildren(document)).toEqual(
loading A...
, ); await act(() => { resolveText('unblock'); resolveText('AAAA'); resolveText('AA'); }); expect(getMeaningfulChildren(document)).toEqual(
loading A...
, ); await act(() => { resolveText('A'); }); await act(() => { document.querySelectorAll('link[rel="stylesheet"]').forEach(l => { const event = document.createEvent('Events'); event.initEvent('load', true, true); l.dispatchEvent(event); }); }); expect(getMeaningfulChildren(document)).toEqual(
{'A'} {'AA'} {'loading AAA...'}
, ); await act(() => { resolveText('AAA'); }); await act(() => { document.querySelectorAll('link[rel="stylesheet"]').forEach(l => { const event = document.createEvent('Events'); event.initEvent('load', true, true); l.dispatchEvent(event); }); }); expect(getMeaningfulChildren(document)).toEqual(
{'A'} {'AA'} {'AAA'} {'AAAA'}
, ); }); it('always enforces crossOrigin "anonymous" for font preloads', async () => { function App() { ReactDOM.preload('foo', {as: 'font', type: 'font/woff2'}); ReactDOM.preload('bar', {as: 'font', crossOrigin: 'foo'}); ReactDOM.preload('baz', {as: 'font', crossOrigin: 'use-credentials'}); ReactDOM.preload('qux', {as: 'font', crossOrigin: 'anonymous'}); return ( ); } await act(() => { const {pipe} = renderToPipeableStream(); pipe(writable); }); expect(getMeaningfulChildren(document)).toEqual( , ); }); it('does not hoist anything with an itemprop prop', async () => { function App() { return ( title