Files
react/packages/react-dom/src/__tests__/ReactDOMFloat-test.js
T
Josh Story fbe8d2ab10 add warnings for non-resources rendered outside body or head (#25532)
Adds some clarifying warnings when you render a component that is almost
a resource but isn't and the element was rendered outside the main
document tree (outside of `<body>` or `<head>`
2022-12-03 11:05:28 -05:00

5264 lines
170 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
let JSDOM;
let Stream;
let Scheduler;
let React;
let ReactDOM;
let ReactDOMClient;
let ReactDOMFizzServer;
let Suspense;
let textCache;
let document;
let writable;
const CSPnonce = null;
let container;
let buffer = '';
let hasErrored = false;
let fatalError = undefined;
describe('ReactDOMFloat', () => {
beforeEach(() => {
jest.resetModules();
JSDOM = require('jsdom').JSDOM;
Scheduler = require('scheduler');
React = require('react');
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
ReactDOMFizzServer = require('react-dom/server');
Stream = require('stream');
Suspense = React.Suspense;
textCache = new Map();
// Test Environment
const jsdom = new JSDOM(
'<!DOCTYPE html><html><head></head><body><div id="container">',
{
runScripts: 'dangerously',
},
);
document = jsdom.window.document;
container = document.getElementById('container');
buffer = '';
hasErrored = false;
writable = new Stream.PassThrough();
writable.setEncoding('utf8');
writable.on('data', chunk => {
buffer += chunk;
});
writable.on('error', error => {
hasErrored = true;
fatalError = error;
});
});
function normalizeCodeLocInfo(str) {
return (
typeof str === 'string' &&
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
return '\n in ' + name + ' (at **)';
})
);
}
function componentStack(components) {
return components
.map(component => `\n in ${component} (at **)`)
.join('');
}
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.
const bufferedContent = buffer;
buffer = '';
const fakeBody = document.createElement('body');
fakeBody.innerHTML = bufferedContent;
const parent =
container.nodeName === '#document' ? container.body : container;
while (fakeBody.firstChild) {
const node = fakeBody.firstChild;
if (
node.nodeName === 'SCRIPT' &&
(CSPnonce === null || node.getAttribute('nonce') === CSPnonce)
) {
const script = document.createElement('script');
script.textContent = node.textContent;
fakeBody.removeChild(node);
parent.appendChild(script);
} else {
parent.appendChild(node);
}
}
}
async function actIntoEmptyDocument(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.
const bufferedContent = buffer;
// Test Environment
const jsdom = new JSDOM(bufferedContent, {
runScripts: 'dangerously',
});
document = jsdom.window.document;
container = document;
buffer = '';
}
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.hasAttribute('async')) ||
(node.tagName !== 'SCRIPT' &&
node.tagName !== 'TEMPLATE' &&
node.tagName !== 'template' &&
!node.hasAttribute('hidden') &&
!node.hasAttribute('aria-hidden'))
) {
const props = {};
const attributes = node.attributes;
for (let i = 0; i < attributes.length; i++) {
if (
attributes[i].name === 'id' &&
attributes[i].value.includes(':')
) {
// We assume this is a React added ID that's a non-visual implementation detail.
continue;
}
props[attributes[i].name] = attributes[i].value;
}
props.children = 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 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);
}
// @gate enableFloat
it('can render resources before singletons', async () => {
const root = ReactDOMClient.createRoot(document);
root.render(
<>
<title>foo</title>
<html>
<head>
<link rel="foo" href="foo" />
</head>
<body>hello world</body>
</html>
</>,
);
try {
expect(Scheduler).toFlushWithoutYielding();
} 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
expect(Scheduler).toFlushWithoutYielding();
throw e;
}
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<title>foo</title>
<link rel="foo" href="foo" />
</head>
<body>hello world</body>
</html>,
);
});
function renderSafelyAndExpect(root, children) {
root.render(children);
return expect(() => {
try {
expect(Scheduler).toFlushWithoutYielding();
} catch (e) {
try {
expect(Scheduler).toFlushWithoutYielding();
} catch (f) {}
}
});
}
// @gate enableFloat || !__DEV__
it('warns if you render resource-like elements above <head> or <body>', async () => {
const root = ReactDOMClient.createRoot(document);
renderSafelyAndExpect(
root,
<>
<noscript>foo</noscript>
<html>
<body>foo</body>
</html>
</>,
).toErrorDev(
[
'Cannot render <noscript> outside the main document. Try moving it into the root <head> tag.',
'Warning: validateDOMNesting(...): <noscript> cannot appear as a child of <#document>.',
],
{withoutStack: 1},
);
renderSafelyAndExpect(
root,
<html>
<template>foo</template>
<body>foo</body>
</html>,
).toErrorDev([
'Cannot render <template> outside the main document. Try moving it into the root <head> tag.',
'Warning: validateDOMNesting(...): <template> cannot appear as a child of <html>.',
]);
renderSafelyAndExpect(
root,
<html>
<body>foo</body>
<style>foo</style>
</html>,
).toErrorDev([
'Cannot render <style> outside the main document. Try moving it into the root <head> tag.',
'Warning: validateDOMNesting(...): <style> cannot appear as a child of <html>.',
]);
renderSafelyAndExpect(
root,
<>
<html>
<body>foo</body>
</html>
<link rel="stylesheet" href="foo" />
</>,
).toErrorDev(
[
'Cannot render a <link rel="stylesheet" /> outside the main document without knowing its precedence. Consider adding precedence="default" or moving it into the root <head> tag.',
'Warning: validateDOMNesting(...): <link> cannot appear as a child of <#document>.',
],
{withoutStack: 1},
);
renderSafelyAndExpect(
root,
<>
<html>
<body>foo</body>
<script href="foo" />
</html>
</>,
).toErrorDev([
'Cannot render a sync or defer <script> outside the main document without knowing its order. Try adding async="" or moving it into the root <head> tag.',
'Warning: validateDOMNesting(...): <script> cannot appear as a child of <html>.',
]);
renderSafelyAndExpect(
root,
<>
<html>
<script async={true} onLoad={() => {}} href="bar" />
<body>foo</body>
</html>
</>,
).toErrorDev([
'Cannot render a <script> with onLoad or onError listeners outside the main document. Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or somewhere in the <body>.',
]);
renderSafelyAndExpect(
root,
<>
<link rel="foo" onLoad={() => {}} href="bar" />
<html>
<body>foo</body>
</html>
</>,
).toErrorDev(
[
'Cannot render a <link> with onLoad or onError listeners outside the main document. Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or somewhere in the <body>.',
],
{withoutStack: 1},
);
});
// @gate enableFloat
it('can acquire a resource after releasing it in the same commit', async () => {
const root = ReactDOMClient.createRoot(container);
root.render(
<>
<title>foo</title>
</>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<title>foo</title>
</head>
<body>
<div id="container" />
</body>
</html>,
);
// title is keyed off children so this second resource should match the first one
root.render(
<>
{null}
<title data-new="new">foo</title>
</>,
);
expect(Scheduler).toFlushWithoutYielding();
// we don't see the attribute because the resource is the same and was not reconstructed
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<title>foo</title>
</head>
<body>
<div id="container" />
</body>
</html>,
);
});
// @gate enableFloat
it('errors if the document does not contain a head when inserting a resource', async () => {
document.head.parentNode.removeChild(document.head);
const root = ReactDOMClient.createRoot(document);
root.render(
<html>
<body>
<link rel="stylesheet" href="foo" precedence="default" />
foo
</body>
</html>,
);
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow(
'While attempting to insert a Resource, React expected the Document to contain a head element but it was not found.',
);
});
// @gate enableFloat
it('emits resources before everything else when rendering with no head', async () => {
function App() {
return (
<>
<title>foo</title>
<link rel="preload" href="foo" as="style" />
</>
);
}
await actIntoEmptyDocument(() => {
buffer = `<!DOCTYPE html><html><head>${ReactDOMFizzServer.renderToString(
<App />,
)}</head><body>foo</body></html>`;
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="preload" href="foo" as="style" />
<title>foo</title>
</head>
<body>foo</body>
</html>,
);
});
// @gate enableFloat
it('emits resources before everything else when rendering with just a head', async () => {
function App() {
return (
<head>
<title>foo</title>
<link rel="preload" href="foo" as="style" />
</head>
);
}
await actIntoEmptyDocument(() => {
buffer = `<!DOCTYPE html><html>${ReactDOMFizzServer.renderToString(
<App />,
)}<body>foo</body></html>`;
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="preload" href="foo" as="style" />
<title>foo</title>
</head>
<body>foo</body>
</html>,
);
});
// @gate enableFloat
it('does not emit closing tags in out of order position when rendering a non-void resource type', async () => {
const chunks = [];
writable.on('data', chunk => {
chunks.push(chunk);
});
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<>
<title>foo</title>
<html>
<body>bar</body>
</html>
<script async={true} src="foo" />
</>,
);
pipe(writable);
});
expect(chunks).toEqual([
'<!DOCTYPE html><html><script async="" src="foo"></script><title>foo</title><body>bar',
'</body></html>',
]);
});
describe('HostResource', () => {
// @gate enableFloat
it('warns when you update props to an invalid type', async () => {
const root = ReactDOMClient.createRoot(container);
root.render(
<div>
<link rel="stylesheet" href="foo" precedence="foo" />
<link rel="stylesheet" href="bar" precedence="foo" />
</div>,
);
expect(Scheduler).toFlushWithoutYielding();
root.render(
<div>
<link rel={() => {}} href="bar" />
<link rel="foo" href={() => {}} />
</div>,
);
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toErrorDev([
'Warning: A <link> previously rendered as a Resource with href "foo" with rel ""stylesheet"" but was updated with an invalid rel: something with type "function". When a link does not have a valid rel prop it is not represented in the DOM. If this is intentional, instead do not render the <link> anymore.',
'Warning: A <link> previously rendered as a Resource with href "bar" but was updated with an invalid href prop: something with type "function". When a link does not have a valid href prop it is not represented in the DOM. If this is intentional, instead do not render the <link> anymore.',
]);
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="foo" />
<link rel="stylesheet" href="bar" data-precedence="foo" />
<link rel="preload" as="style" href="foo" />
<link rel="preload" as="style" href="bar" />
</head>
<body>
<div id="container">
<div />
</div>
</body>
</html>,
);
});
});
describe('ReactDOM.preload', () => {
// @gate enableFloat
it('inserts a preload resource into the stream when called during server rendering', async () => {
function Component() {
ReactDOM.preload('foo', {as: 'style'});
return 'foo';
}
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<Component />
</body>
</html>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="preload" as="style" href="foo" />
</head>
<body>foo</body>
</html>,
);
});
// @gate enableFloat
it('inserts a preload resource into the document during render when called during client rendering', async () => {
function Component() {
ReactDOM.preload('foo', {as: 'style'});
return 'foo';
}
const root = ReactDOMClient.createRoot(container);
root.render(<Component />);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="preload" as="style" href="foo" />
</head>
<body>
<div id="container">foo</div>
</body>
</html>,
);
});
// @gate enableFloat
it('inserts a preload resource when called in a layout effect', async () => {
function App() {
React.useLayoutEffect(() => {
ReactDOM.preload('foo', {as: 'style'});
}, []);
return 'foobar';
}
const root = ReactDOMClient.createRoot(container);
root.render(<App />);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="preload" as="style" href="foo" />
</head>
<body>
<div id="container">foobar</div>
</body>
</html>,
);
});
// @gate enableFloat
it('inserts a preload resource when called in a passive effect', async () => {
function App() {
React.useEffect(() => {
ReactDOM.preload('foo', {as: 'style'});
}, []);
return 'foobar';
}
const root = ReactDOMClient.createRoot(container);
root.render(<App />);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="preload" as="style" href="foo" />
</head>
<body>
<div id="container">foobar</div>
</body>
</html>,
);
});
// @gate enableFloat
it('inserts a preload resource when called in module scope if a root has already been created', async () => {
// The requirement that a root be created has to do with bootstrapping the dispatcher.
// We are intentionally avoiding setting it to the default via import due to cycles and
// we are trying to avoid doing a mutable initailation in module scope.
ReactDOM.preload('foo', {as: 'style'});
ReactDOMClient.createRoot(container);
ReactDOM.preload('bar', {as: 'style'});
// We need to use global.document because preload falls back
// to the window.document global when no other documents have been used
// The way the JSDOM runtim is created for these tests the local document
// global does not point to the global.document
expect(getMeaningfulChildren(global.document)).toEqual(
<html>
<head>
<link rel="preload" as="style" href="bar" />
</head>
<body />
</html>,
);
});
// @gate enableFloat
it('supports script preloads', async () => {
function ServerApp() {
ReactDOM.preload('foo', {as: 'script', integrity: 'foo hash'});
ReactDOM.preload('bar', {
as: 'script',
crossOrigin: 'use-credentials',
integrity: 'bar hash',
});
return (
<html>
<link rel="preload" href="baz" as="script" />
<head>
<title>hi</title>
</head>
<body>foo</body>
</html>
);
}
function ClientApp() {
ReactDOM.preload('foo', {as: 'script', integrity: 'foo hash'});
ReactDOM.preload('qux', {as: 'script'});
return (
<html>
<head>
<title>hi</title>
</head>
<body>foo</body>
<link
rel="preload"
href="quux"
as="script"
crossOrigin=""
integrity="quux hash"
/>
</html>
);
}
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<ServerApp />);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="preload" as="script" href="foo" integrity="foo hash" />
<link
rel="preload"
as="script"
href="bar"
crossorigin="use-credentials"
integrity="bar hash"
/>
<link rel="preload" as="script" href="baz" />
<title>hi</title>
</head>
<body>foo</body>
</html>,
);
ReactDOMClient.hydrateRoot(document, <ClientApp />);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="preload" as="script" href="foo" integrity="foo hash" />
<link
rel="preload"
as="script"
href="bar"
crossorigin="use-credentials"
integrity="bar hash"
/>
<link rel="preload" as="script" href="baz" />
<title>hi</title>
<link rel="preload" as="script" href="qux" />
<link
rel="preload"
as="script"
href="quux"
crossorigin=""
integrity="quux hash"
/>
</head>
<body>foo</body>
</html>,
);
});
});
describe('ReactDOM.preinit as style', () => {
// @gate enableFloat
it('creates a style Resource when called during server rendering before first flush', async () => {
function Component() {
ReactDOM.preinit('foo', {as: 'style'});
return 'foo';
}
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<Component />
</body>
</html>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="default" />
</head>
<body>foo</body>
</html>,
);
});
// @gate enableFloat
it('creates a preload Resource when called during server rendering after first flush', async () => {
function BlockedOn({text, children}) {
readText(text);
return children;
}
function Component() {
ReactDOM.preinit('foo', {as: 'style', precedence: 'foo'});
return 'foo';
}
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<Suspense fallback="loading...">
<BlockedOn text="unblock">
<Component />
</BlockedOn>
</Suspense>
</body>
</html>,
);
pipe(writable);
});
await act(() => {
resolveText('unblock');
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head />
<body>
foo
<link rel="preload" as="style" href="foo" />
</body>
</html>,
);
});
// @gate enableFloat
it('inserts a style Resource into the document during render when called during client rendering', async () => {
function Component() {
ReactDOM.preinit('foo', {as: 'style', precedence: 'foo'});
return 'foo';
}
const root = ReactDOMClient.createRoot(container);
root.render(<Component />);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="foo" />
</head>
<body>
<div id="container">foo</div>
</body>
</html>,
);
});
// @gate enableFloat
it('inserts a preload resource into the document when called in an insertion effect, layout effect, or passive effect', async () => {
function App() {
React.useEffect(() => {
ReactDOM.preinit('passive', {as: 'style', precedence: 'default'});
}, []);
React.useLayoutEffect(() => {
ReactDOM.preinit('layout', {as: 'style', precedence: 'default'});
});
React.useInsertionEffect(() => {
ReactDOM.preinit('insertion', {as: 'style', precedence: 'default'});
});
return 'foobar';
}
const root = ReactDOMClient.createRoot(container);
root.render(<App />);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="preload" as="style" href="insertion" />
<link rel="preload" as="style" href="layout" />
<link rel="preload" as="style" href="passive" />
</head>
<body>
<div id="container">foobar</div>
</body>
</html>,
);
});
// @gate enableFloat
it('inserts a preload resource when called in module scope', async () => {
// The requirement that a root be created has to do with bootstrapping the dispatcher.
// We are intentionally avoiding setting it to the default via import due to cycles and
// we are trying to avoid doing a mutable initailation in module scope.
ReactDOM.preinit('foo', {as: 'style'});
ReactDOMClient.hydrateRoot(container, null);
ReactDOM.preinit('bar', {as: 'style'});
// We need to use global.document because preload falls back
// to the window.document global when no other documents have been used
// The way the JSDOM runtim is created for these tests the local document
// global does not point to the global.document
expect(getMeaningfulChildren(global.document)).toEqual(
<html>
<head>
<link rel="preload" as="style" href="bar" />
</head>
<body />
</html>,
);
});
});
describe('ReactDOM.preinit as script', () => {
// @gate enableFloat
it('can preinit a script', async () => {
function App({srcs}) {
srcs.forEach(src => ReactDOM.preinit(src, {as: 'script'}));
return (
<html>
<head>
<title>title</title>
</head>
<body>foo</body>
</html>
);
}
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<App srcs={['server', 'shared']} />,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<script src="server" async="" />
<script src="shared" async="" />
<title>title</title>
</head>
<body>foo</body>
</html>,
);
ReactDOMClient.hydrateRoot(document, <App srcs={['client', 'shared']} />);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<script src="server" async="" />
<script src="shared" async="" />
<title>title</title>
<script src="client" async="" />
</head>
<body>foo</body>
</html>,
);
});
});
describe('document encapsulation', () => {
// @gate enableFloat
it('can support styles inside portals to a shadowRoot', async () => {
const shadow = document.body.attachShadow({mode: 'open'});
const root = ReactDOMClient.createRoot(container);
root.render(
<>
<link rel="stylesheet" href="foo" precedence="default" />
{ReactDOM.createPortal(
<div>
<link
rel="stylesheet"
href="foo"
data-extra-prop="foo"
precedence="different"
/>
shadow
</div>,
shadow,
)}
container
</>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="default" />
<link rel="preload" href="foo" as="style" />
</head>
<body>
<div id="container">container</div>
</body>
</html>,
);
expect(getMeaningfulChildren(shadow)).toEqual([
<link
rel="stylesheet"
href="foo"
data-precedence="different"
data-extra-prop="foo"
/>,
<div>shadow</div>,
]);
});
// @gate enableFloat
it('can support styles inside portals to an element in shadowRoots', async () => {
const template = document.createElement('template');
template.innerHTML =
"<div><div id='shadowcontainer1'></div><div id='shadowcontainer2'></div></div>";
const shadow = document.body.attachShadow({mode: 'open'});
shadow.appendChild(template.content);
const shadowContainer1 = shadow.getElementById('shadowcontainer1');
const shadowContainer2 = shadow.getElementById('shadowcontainer2');
const root = ReactDOMClient.createRoot(container);
root.render(
<>
<link rel="stylesheet" href="foo" precedence="default" />
{ReactDOM.createPortal(
<div>
<link rel="stylesheet" href="foo" precedence="one" />
<link rel="stylesheet" href="bar" precedence="two" />1
</div>,
shadow,
)}
{ReactDOM.createPortal(
<div>
<link rel="stylesheet" href="foo" precedence="one" />
<link rel="stylesheet" href="baz" precedence="one" />2
</div>,
shadowContainer1,
)}
{ReactDOM.createPortal(
<div>
<link rel="stylesheet" href="bar" precedence="two" />
<link rel="stylesheet" href="qux" precedence="three" />3
</div>,
shadowContainer2,
)}
container
</>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="default" />
<link rel="preload" href="foo" as="style" />
<link rel="preload" href="bar" as="style" />
<link rel="preload" href="baz" as="style" />
<link rel="preload" href="qux" as="style" />
</head>
<body>
<div id="container">container</div>
</body>
</html>,
);
expect(getMeaningfulChildren(shadow)).toEqual([
<link rel="stylesheet" href="foo" data-precedence="one" />,
<link rel="stylesheet" href="baz" data-precedence="one" />,
<link rel="stylesheet" href="bar" data-precedence="two" />,
<link rel="stylesheet" href="qux" data-precedence="three" />,
<div>
<div id="shadowcontainer1">
<div>2</div>
</div>
<div id="shadowcontainer2">
<div>3</div>
</div>
</div>,
<div>1</div>,
]);
});
});
describe('head resources', () => {
// @gate enableFloat
it('supports preconnects, prefetc-dns, and arbitrary other link types', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<div>hello world</div>
</body>
<link rel="foo" href="bar" />
<link rel="preload" href="bar" />
<link rel="preload" href="bar" as="style" />
<link rel="stylesheet" href="bar" precedence="default" />
<link rel="preconnect" href="bar" />
<link rel="dns-prefetch" href="bar" />
<link rel="icon" href="bar" />
<link rel="icon" href="bar" sizes="1x1" />
<link rel="icon" href="bar" media="foo" />
<link rel="shortcut icon" href="bar" />
<link rel="apple-touch-icon" href="bar" />
</html>,
);
pipe(writable);
});
// "preconnect" and "dns-prefetch" get hoisted to the front.
// All other generic links (not styles, or typed preloads)
// get emitted after styles and other higher priority Resources
// Sizes and Media are part of generic link keys
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="preconnect" href="bar" />
<link rel="dns-prefetch" href="bar" />
<link rel="stylesheet" href="bar" data-precedence="default" />
<link rel="foo" href="bar" />
<link rel="preload" href="bar" />
<link rel="icon" href="bar" />
<link rel="icon" href="bar" sizes="1x1" />
<link rel="icon" href="bar" media="foo" />
<link rel="shortcut icon" href="bar" />
<link rel="apple-touch-icon" href="bar" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
const root = ReactDOMClient.hydrateRoot(
document,
<html>
<head />
<body>
<div>hello world</div>
</body>
<link rel="foo" href="bar" />
<link rel="preload" href="bar" />
<link rel="preload" href="bar" as="style" />
<link rel="stylesheet" href="bar" precedence="default" />
<link rel="preconnect" href="bar" />
<link rel="dns-prefetch" href="bar" />
<link rel="icon" href="bar" />
<link rel="icon" href="bar" sizes="1x1" />
<link rel="icon" href="bar" media="foo" />
<link rel="shortcut icon" href="bar" />
<link rel="apple-touch-icon" href="bar" />
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="preconnect" href="bar" />
<link rel="dns-prefetch" href="bar" />
<link rel="stylesheet" href="bar" data-precedence="default" />
<link rel="foo" href="bar" />
<link rel="preload" href="bar" />
<link rel="icon" href="bar" />
<link rel="icon" href="bar" sizes="1x1" />
<link rel="icon" href="bar" media="foo" />
<link rel="shortcut icon" href="bar" />
<link rel="apple-touch-icon" href="bar" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
root.render(
<html>
<head />
<body>
<div>hello world</div>
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="bar" data-precedence="default" />
<link rel="preload" href="bar" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
});
// @gate enableFloat
it('can render icons and apple-touch-icons as resources', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<>
<html>
<head />
<body>
<link rel="icon" href="foo" />
<div>hello world</div>
</body>
</html>
<link rel="apple-touch-icon" href="foo" />
</>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="icon" href="foo" />
<link rel="apple-touch-icon" href="foo" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
ReactDOMClient.hydrateRoot(
document,
<html>
<link rel="apple-touch-icon" href="foo" />
<head />
<body>
<link rel="icon" href="foo" />
<div>hello world</div>
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="icon" href="foo" />
<link rel="apple-touch-icon" href="foo" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
});
// @gate enableFloat
it('can hydrate the right instances for deeply nested structured metas', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<>
<html>
<head />
<body>
<div>hello world</div>
</body>
</html>
<meta property="og:foo" content="one" />
<meta property="og:foo:bar" content="bar" />
<meta property="og:foo:bar:baz" content="baz" />
<meta property="og:foo" content="two" />
<meta property="og:foo:bar" content="bar" />
<meta property="og:foo:bar:baz" content="baz" />
</>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<meta property="og:foo" content="one" />
<meta property="og:foo:bar" content="bar" />
<meta property="og:foo:bar:baz" content="baz" />
<meta property="og:foo" content="two" />
<meta property="og:foo:bar" content="bar" />
<meta property="og:foo:bar:baz" content="baz" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
const root = ReactDOMClient.hydrateRoot(
document,
<>
<html>
<head />
<body>
<div>hello world</div>
</body>
</html>
<meta property="og:foo" content="one" />
<meta property="og:foo:bar" content="bar" />
<meta property="og:foo:bar:baz" content="baz" />
<meta property="og:foo" content="two" />
<meta property="og:foo:bar" content="bar" />
<meta property="og:foo:bar:baz" content="baz" />
</>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<meta property="og:foo" content="one" />
<meta property="og:foo:bar" content="bar" />
<meta property="og:foo:bar:baz" content="baz" />
<meta property="og:foo" content="two" />
<meta property="og:foo:bar" content="bar" />
<meta property="og:foo:bar:baz" content="baz" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
root.render(
<>
<html>
<head />
<body>
<div>hello world</div>
</body>
</html>
</>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head />
<body>
<div>hello world</div>
</body>
</html>,
);
});
// @gate enableFloat
it('can insert meta tags in the expected location', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<>
<html>
<head />
<body>
<div>hello world</div>
</body>
</html>
<meta charSet="utf-8" />
<meta name="google-site-verification" content="somehash1" />
<meta name="google-site-verification" content="somehash2" />
<meta
name="description"
property="og:description"
content="my site"
/>
<meta property="og:image" content="foo" />
<meta property="og:image:width" content="100" />
<meta httpEquiv="refresh" content="dont actually" />
<meta property="og:image:height" content="100" />
<meta property="og:image" content="bar" />
<meta property="og:image:width" content="100" />
<meta itemProp="someprop" content="somevalue" />
<meta property="og:image:height" content="100" />
<meta property="og:description:foo" content="foo" />
</>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<meta charset="utf-8" />
<meta name="google-site-verification" content="somehash1" />
<meta name="google-site-verification" content="somehash2" />
<meta
name="description"
property="og:description"
content="my site"
/>
<meta property="og:image" content="foo" />
<meta property="og:image:width" content="100" />
<meta http-equiv="refresh" content="dont actually" />
<meta property="og:image:height" content="100" />
<meta property="og:image" content="bar" />
<meta property="og:image:width" content="100" />
<meta itemprop="someprop" content="somevalue" />
<meta property="og:image:height" content="100" />
<meta property="og:description:foo" content="foo" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
const root = ReactDOMClient.hydrateRoot(
document,
<html>
<head>
<meta property="og:image" content="bar" />
<meta property="og:image:width" content="100" />
<meta property="og:image:height" content="100" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<meta charset="utf-8" />
<meta name="google-site-verification" content="somehash1" />
<meta name="google-site-verification" content="somehash2" />
<meta
name="description"
property="og:description"
content="my site"
/>
<meta property="og:image" content="foo" />
<meta property="og:image:width" content="100" />
<meta http-equiv="refresh" content="dont actually" />
<meta property="og:image:height" content="100" />
<meta property="og:image" content="bar" />
<meta property="og:image:width" content="100" />
<meta itemprop="someprop" content="somevalue" />
<meta property="og:image:height" content="100" />
<meta property="og:description:foo" content="foo" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
root.render(
<html>
<head>
<meta property="og:image" content="bar" />
<meta property="og:image:width" content="100" />
<meta property="og:image:height" content="100" />
<meta property="og:description" content="my site" />
<meta
itemProp="description bar"
property="og:description:bar"
content="bar"
/>
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<meta charset="utf-8" />
<meta name="google-site-verification" content="somehash1" />
<meta name="google-site-verification" content="somehash2" />
<meta
name="description"
property="og:description"
content="my site"
/>
<meta
itemprop="description bar"
property="og:description:bar"
content="bar"
/>
<meta property="og:image" content="foo" />
<meta property="og:image:width" content="100" />
<meta http-equiv="refresh" content="dont actually" />
<meta property="og:image:height" content="100" />
<meta property="og:image" content="bar" />
<meta property="og:image:width" content="100" />
<meta itemprop="someprop" content="somevalue" />
<meta property="og:image:height" content="100" />
<meta property="og:description:foo" content="foo" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
});
// @gate enableFloat
it('can render meta tags with og properties with structured data', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<>
<html>
<head />
<body>
<div>hello world</div>
</body>
</html>
<meta property="og:image" content="foo" />
<meta property="og:image:width" content="100" />
<meta property="og:image:height" content="100" />
<meta property="og:image" content="bar" />
<meta property="og:image:width" content="100" />
<meta property="og:image:height" content="100" />
</>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<meta property="og:image" content="foo" />
<meta property="og:image:width" content="100" />
<meta property="og:image:height" content="100" />
<meta property="og:image" content="bar" />
<meta property="og:image:width" content="100" />
<meta property="og:image:height" content="100" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
const root = ReactDOMClient.hydrateRoot(
document,
<html>
<head />
<body>
<meta property="og:image" content="foo" />
<meta property="og:image:width" content="100" />
<meta property="og:image:height" content="100" />
<meta property="og:image" content="bar" />
<meta property="og:image:width" content="100" />
<meta property="og:image:height" content="100" />
<div>hello world</div>
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<meta property="og:image" content="foo" />
<meta property="og:image:width" content="100" />
<meta property="og:image:height" content="100" />
<meta property="og:image" content="bar" />
<meta property="og:image:width" content="100" />
<meta property="og:image:height" content="100" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
root.render(
<html>
<head />
<body>
<meta property="og:image" content="foo" />
<meta property="og:image:width" content="100" />
<meta property="og:image:height" content="100" />
<meta property="og:image" content="bar" />
<meta property="og:image:height" content="100" />
<div>hello world</div>
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<meta property="og:image" content="foo" />
<meta property="og:image:width" content="100" />
<meta property="og:image:height" content="100" />
<meta property="og:image" content="bar" />
<meta property="og:image:height" content="100" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
root.render(
<html>
<head />
<body>
<meta property="og:image" content="foo" />
<meta property="og:image:width" content="100" />
<meta property="og:image:height" content="100" />
<meta property="og:image:foo" content="foo" />
<meta property="og:image" content="bar" />
<meta property="og:image:height" content="100" />
<div>hello world</div>
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<meta property="og:image" content="foo" />
<meta property="og:image:foo" content="foo" />
<meta property="og:image:width" content="100" />
<meta property="og:image:height" content="100" />
<meta property="og:image" content="bar" />
<meta property="og:image:height" content="100" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
root.render(
<html>
<head />
<body>
<meta property="og:image" content="foo" />
<meta property="og:image:width" content="100" />
<meta property="og:image:width:bar" content="bar" />
<meta property="og:image:height" content="100" />
<meta property="og:image:foo" content="foo" />
<meta property="og:image" content="bar" />
<meta property="og:image:height" content="100" />
<div>hello world</div>
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<meta property="og:image" content="foo" />
<meta property="og:image:foo" content="foo" />
<meta property="og:image:height" content="100" />
<meta property="og:image:width" content="100" />
<meta property="og:image:width:bar" content="bar" />
<meta property="og:image" content="bar" />
<meta property="og:image:height" content="100" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
});
// @gate enableFloat
it('can render meta tags as resources', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<>
<html>
<head />
<body>
<div>hello world</div>
</body>
</html>
<meta name="robots" content="noindex" />
<meta httpEquiv="content-security-policy" content="foo" />
<meta itemProp="description" content="desc" />
<meta property="description" content="desc2" />
<meta charSet="utf-8" />
</>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<meta charset="utf-8" />
<meta name="robots" content="noindex" />
<meta http-equiv="content-security-policy" content="foo" />
<meta itemprop="description" content="desc" />
<meta property="description" content="desc2" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
ReactDOMClient.hydrateRoot(
document,
<html>
<head>
<meta charSet="utf-8" />
</head>
<body>
<meta name="robots" content="noindex" />
<meta httpEquiv="content-security-policy" content="foo" />
<div>hello world</div>
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<meta charset="utf-8" />
<meta name="robots" content="noindex" />
<meta http-equiv="content-security-policy" content="foo" />
<meta itemprop="description" content="desc" />
<meta property="description" content="desc2" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
});
// @gate enableFloat
it('can rendering title tags anywhere in the tree', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<>
<title>before</title>
<>
<html>
<head>
<title>in head</title>
</head>
<body>
<div>
<title>during</title>
hello world
</div>
</body>
</html>
</>
<title>after</title>
</>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<title>before</title>
<title>in head</title>
<title>during</title>
<title>after</title>
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
ReactDOMClient.hydrateRoot(
document,
<>
<title>before</title>
<>
<html>
<head>
<title>in head</title>
</head>
<body>
<div>
<title>during</title>
hello world
</div>
</body>
</html>
</>
<title>after</title>
</>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<title>before</title>
<title>in head</title>
<title>during</title>
<title>after</title>
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
});
// @gate enableFloat
it('prepends new titles on the client so newer ones override older ones, including orphaned server rendered titles', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head>
<title>server</title>
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<title>server</title>
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
ReactDOMClient.hydrateRoot(
document,
<html>
<title>html</title>
<head>
<title>head</title>
</head>
<body>
<title>body</title>
<div>hello world</div>
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<title>body</title>
<title>head</title>
<title>html</title>
<title>server</title>
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
});
// @gate enableFloat
it('keys titles on text children and only removes them when no more instances refer to that title', async () => {
const root = ReactDOMClient.createRoot(container);
root.render(
<div>
<title>{[2]}</title>hello world<title>2</title>
</div>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<title>2</title>
</head>
<body>
<div id="container">
<div>hello world</div>
</div>
</body>
</html>,
);
root.render(
<div>
{null}hello world<title>2</title>
</div>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<title>2</title>
</head>
<body>
<div id="container">
<div>hello world</div>
</div>
</body>
</html>,
);
root.render(
<div>
{null}hello world{null}
</div>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head />
<body>
<div id="container">
<div>hello world</div>
</div>
</body>
</html>,
);
});
// @gate enableFloat
it('can render a title before a singleton even if that singleton clears its contents', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<>
<title>foo</title>
<html>
<head />
<body>
<div>server</div>
</body>
</html>
,
</>,
);
pipe(writable);
});
const errors = [];
ReactDOMClient.hydrateRoot(
document,
<>
<title>foo</title>
<html>
<head />
<body>
<div>client</div>
</body>
</html>
</>,
{
onRecoverableError(err) {
errors.push(err.message);
},
},
);
try {
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toErrorDev(
[
'Warning: Text content did not match. Server: "server" Client: "client"',
'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
],
{withoutStack: 1},
);
} catch (e) {
// When gates are false this test fails on a DOMException if you don't clear the scheduler after catching.
// When gates are true this branch should not be hit
expect(Scheduler).toFlushWithoutYielding();
throw e;
}
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<title>foo</title>
</head>
<body>
<div>client</div>
</body>
</html>,
);
});
});
describe('style resources', () => {
// @gate enableFloat
it('treats link rel stylesheet elements as a style resource when it includes a precedence when server rendering', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<link rel="stylesheet" href="aresource" precedence="foo" />
<div>hello world</div>
</body>
</html>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="aresource" data-precedence="foo" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
});
// @gate enableFloat
it('treats link rel stylesheet elements as a style resource when it includes a precedence when client rendering', async () => {
const root = ReactDOMClient.createRoot(document);
root.render(
<html>
<head />
<body>
<link rel="stylesheet" href="aresource" precedence="foo" />
<div>hello world</div>
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="aresource" data-precedence="foo" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
});
// @gate enableFloat
it('treats link rel stylesheet elements as a style resource when it includes a precedence when hydrating', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<link rel="stylesheet" href="aresource" precedence="foo" />
<div>hello world</div>
</body>
</html>,
);
pipe(writable);
});
ReactDOMClient.hydrateRoot(
document,
<html>
<head />
<body>
<link rel="stylesheet" href="aresource" precedence="foo" />
<div>hello world</div>
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="aresource" data-precedence="foo" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
});
// @gate enableFloat
it('preloads stylesheets without a precedence prop when server rendering', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<link rel="stylesheet" href="notaresource" />
<div>hello world</div>
</body>
</html>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="preload" as="style" href="notaresource" />
</head>
<body>
<link rel="stylesheet" href="notaresource" />
<div>hello world</div>
</body>
</html>,
);
});
// @gate enableFloat
it('hoists style resources to the correct precedence', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<link rel="stylesheet" href="foo1" precedence="foo" />
<link rel="stylesheet" href="default1" precedence="default" />
<link rel="stylesheet" href="foo2" precedence="foo" />
<div>hello world</div>
</body>
</html>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo1" data-precedence="foo" />
<link rel="stylesheet" href="foo2" data-precedence="foo" />
<link rel="stylesheet" href="default1" data-precedence="default" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
ReactDOMClient.hydrateRoot(
document,
<html>
<head />
<body>
<link rel="stylesheet" href="bar1" precedence="bar" />
<link rel="stylesheet" href="foo3" precedence="foo" />
<link rel="stylesheet" href="default2" precedence="default" />
<div>hello world</div>
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo1" data-precedence="foo" />
<link rel="stylesheet" href="foo2" data-precedence="foo" />
<link rel="stylesheet" href="foo3" data-precedence="foo" />
<link rel="stylesheet" href="default1" data-precedence="default" />
<link rel="stylesheet" href="default2" data-precedence="default" />
<link rel="stylesheet" href="bar1" data-precedence="bar" />
<link rel="preload" as="style" href="bar1" />
<link rel="preload" as="style" href="foo3" />
<link rel="preload" as="style" href="default2" />
</head>
<body>
<div>hello world</div>
</body>
</html>,
);
});
// @gate enableFloat
it('retains styles even after the last referring Resource unmounts', async () => {
// This test is true until a future update where there is some form of garbage collection.
const root = ReactDOMClient.createRoot(document);
root.render(
<html>
<head />
<body>
hello world
<link rel="stylesheet" href="foo" precedence="foo" />
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
root.render(
<html>
<head />
<body>hello world</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="foo" />
</head>
<body>hello world</body>
</html>,
);
});
// @gate enableFloat && enableHostSingletons
it('retains styles even when a new html, head, and/body mount', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<link rel="stylesheet" href="foo" precedence="foo" />
<link rel="stylesheet" href="bar" precedence="bar" />
server
</body>
</html>,
);
pipe(writable);
});
const errors = [];
ReactDOMClient.hydrateRoot(
document,
<html>
<head>
<link rel="stylesheet" href="qux" precedence="qux" />
<link rel="stylesheet" href="foo" precedence="foo" />
</head>
<body>client</body>
</html>,
{
onRecoverableError(error) {
errors.push(error.message);
},
},
);
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toErrorDev(
[
'Warning: Text content did not match. Server: "server" Client: "client"',
'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
],
{withoutStack: 1},
);
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="foo" />
<link rel="stylesheet" href="bar" data-precedence="bar" />
<link rel="stylesheet" href="qux" data-precedence="qux" />
</head>
<body>client</body>
</html>,
);
});
// @gate enableFloat && !enableHostSingletons
it('retains styles even when a new html, head, and/body mount - without HostSingleton', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<link rel="stylesheet" href="foo" precedence="foo" />
<link rel="stylesheet" href="bar" precedence="bar" />
server
</body>
</html>,
);
pipe(writable);
});
const errors = [];
ReactDOMClient.hydrateRoot(
document,
<html>
<head>
<link rel="stylesheet" href="qux" precedence="qux" />
<link rel="stylesheet" href="foo" precedence="foo" />
</head>
<body>client</body>
</html>,
{
onRecoverableError(error) {
errors.push(error.message);
},
},
);
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toErrorDev(
[
'Warning: Text content did not match. Server: "server" Client: "client"',
'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
],
{withoutStack: 1},
);
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="qux" data-precedence="qux" />
<link rel="stylesheet" href="foo" data-precedence="foo" />
</head>
<body>client</body>
</html>,
);
});
// @gate enableFloat && enableHostSingletons
it('retains styles in head through head remounts', async () => {
const root = ReactDOMClient.createRoot(document);
root.render(
<html>
<head key={1} />
<body>
<link rel="stylesheet" href="foo" precedence="foo" />
<link rel="stylesheet" href="bar" precedence="bar" />
{null}
hello
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="foo" />
<link rel="stylesheet" href="bar" data-precedence="bar" />
</head>
<body>hello</body>
</html>,
);
root.render(
<html>
<head key={2} />
<body>
<link rel="stylesheet" href="foo" precedence="foo" />
{null}
<link rel="stylesheet" href="baz" precedence="baz" />
hello
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
// The reason we do not see preloads in the head is they are inserted synchronously
// during render and then when the new singleton mounts it resets it's content, retaining only styles
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="foo" />
<link rel="stylesheet" href="bar" data-precedence="bar" />
<link rel="stylesheet" href="baz" data-precedence="baz" />
<link rel="preload" href="baz" as="style" />
</head>
<body>hello</body>
</html>,
);
});
});
describe('script resources', () => {
// @gate enableFloat
it('treats async scripts without onLoad or onError as Resources', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<script src="foo" async={true} />
<script src="bar" async={true} onLoad={() => {}} />
<script src="baz" data-meaningful="" />
hello world
</body>
</html>,
);
pipe(writable);
});
// The plain async script is converted to a resource and emitted as part of the shell
// The async script with onLoad is preloaded in the shell but is expecting to be added
// during hydration. This is novel, the script is NOT a HostResource but it also will
// never hydrate
// The regular script is just a normal html that should hydrate with a HostComponent
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<script src="foo" async="" />
<link rel="preload" href="bar" as="script" />
</head>
<body>
<script src="baz" data-meaningful="" />
hello world
</body>
</html>,
);
ReactDOMClient.hydrateRoot(
document,
<html>
<head />
<body>
<script src="foo" async={true} />
<script src="bar" async={true} onLoad={() => {}} />
<script src="baz" data-meaningful="" />
hello world
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
// The async script with onLoad is inserted in the right place but does not cause the hydration
// to fail.
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<script src="foo" async="" />
<link rel="preload" href="bar" as="script" />
</head>
<body>
<script src="bar" async="" />
<script src="baz" data-meaningful="" />
hello world
</body>
</html>,
);
});
});
// @gate enableFloat
it('client renders a boundary if a style Resource dependency fails to load', async () => {
function BlockedOn({text, children}) {
readText(text);
return children;
}
function App() {
return (
<html>
<head />
<body>
<Suspense fallback="loading...">
<BlockedOn text="unblock">
<link rel="stylesheet" href="foo" precedence="arbitrary" />
<link rel="stylesheet" href="bar" precedence="arbitrary" />
Hello
</BlockedOn>
</Suspense>
</body>
</html>
);
}
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});
await act(() => {
resolveText('unblock');
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="arbitrary" />
<link rel="stylesheet" href="bar" data-precedence="arbitrary" />
</head>
<body>
loading...
<link rel="preload" href="foo" as="style" />
<link rel="preload" href="bar" as="style" />
</body>
</html>,
);
await act(() => {
const barLink = document.querySelector(
'link[rel="stylesheet"][href="bar"]',
);
const event = document.createEvent('Events');
event.initEvent('error', true, true);
barLink.dispatchEvent(event);
});
const boundaryTemplateInstance = document.getElementById('B:0');
const suspenseInstance = boundaryTemplateInstance.previousSibling;
expect(suspenseInstance.data).toEqual('$!');
expect(boundaryTemplateInstance.dataset.dgst).toBe(
'Resource failed to load',
);
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="arbitrary" />
<link rel="stylesheet" href="bar" data-precedence="arbitrary" />
</head>
<body>
loading...
<link rel="preload" href="foo" as="style" />
<link rel="preload" href="bar" as="style" />
</body>
</html>,
);
const errors = [];
ReactDOMClient.hydrateRoot(document, <App />, {
onRecoverableError(err, errInfo) {
errors.push(err.message);
errors.push(err.digest);
},
});
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="arbitrary" />
<link rel="stylesheet" href="bar" data-precedence="arbitrary" />
</head>
<body>
<link rel="preload" href="foo" as="style" />
<link rel="preload" href="bar" as="style" />
Hello
</body>
</html>,
);
expect(errors).toEqual([
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
'Resource failed to load',
]);
});
// @gate enableFloat
it('treats stylesheet links with a precedence as a resource', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<link rel="stylesheet" href="foo" precedence="arbitrary" />
Hello
</body>
</html>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="arbitrary" />
</head>
<body>Hello</body>
</html>,
);
ReactDOMClient.hydrateRoot(
document,
<html>
<head />
<body>Hello</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="arbitrary" />
</head>
<body>Hello</body>
</html>,
);
});
// @gate enableFloat
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}
<link rel="stylesheet" href={href} precedence={precedence} />
</>
);
}
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<AsyncTextWithResource text="foo" href="foo" precedence="one" />
<AsyncTextWithResource text="bar" href="bar" precedence="two" />
<AsyncTextWithResource text="baz" href="baz" precedence="three" />
</body>
</html>,
);
pipe(writable);
resolveText('foo');
resolveText('bar');
resolveText('baz');
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="one" />
<link rel="stylesheet" href="bar" data-precedence="two" />
<link rel="stylesheet" href="baz" data-precedence="three" />
</head>
<body>
{'foo'}
{'bar'}
{'baz'}
</body>
</html>,
);
});
// @gate enableFloat
it('hoists late stylesheets the correct precedence', async () => {
function AsyncListItemWithResource({text, href, precedence, ...rest}) {
const value = readText(text);
return (
<li>
<link
rel="stylesheet"
href={href}
precedence={precedence}
{...rest}
/>
{value}
</li>
);
}
function BlockingChildren({text, children}) {
readText(text);
return children;
}
function PresetPrecedence() {
ReactDOM.preinit('preset', {as: 'style', precedence: 'preset'});
}
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<link rel="stylesheet" href="initial" precedence="one" />
<PresetPrecedence />
<div>
<Suspense fallback="loading foo bar...">
<link rel="stylesheet" href="foo" precedence="one" />
<ul>
<li>
<AsyncText text="foo" />
</li>
<AsyncListItemWithResource
text="bar"
href="bar"
precedence="default"
data-foo="foo"
crossOrigin="anonymous"
/>
</ul>
</Suspense>
</div>
<div>
<Suspense fallback="loading bar baz qux...">
<ul>
<AsyncListItemWithResource
text="bar"
href="bar"
precedence="default"
/>
<AsyncListItemWithResource
text="baz"
href="baz"
precedence="two"
/>
<AsyncListItemWithResource
text="qux"
href="qux"
precedence="one"
/>
</ul>
</Suspense>
</div>
<div>
<Suspense fallback="loading bar baz qux...">
<BlockingChildren text="unblock">
<ul>
<AsyncListItemWithResource
text="bar"
href="bar"
precedence="default"
/>
<AsyncListItemWithResource
text="baz"
href="baz"
precedence="two"
/>
<AsyncListItemWithResource
text="qux"
href="qux"
precedence="one"
/>
</ul>
</BlockingChildren>
</Suspense>
</div>
</body>
</html>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="initial" data-precedence="one" />
<link rel="stylesheet" href="preset" data-precedence="preset" />
<link rel="preload" href="foo" as="style" />
</head>
<body>
<div>loading foo bar...</div>
<div>loading bar baz qux...</div>
<div>loading bar baz qux...</div>
</body>
</html>,
);
await act(() => {
resolveText('foo');
resolveText('bar');
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="initial" data-precedence="one" />
<link rel="stylesheet" href="foo" data-precedence="one" />
<link rel="stylesheet" href="preset" data-precedence="preset" />
<link
rel="stylesheet"
href="bar"
data-precedence="default"
data-foo="foo"
crossorigin="anonymous"
/>
<link rel="preload" href="foo" as="style" />
</head>
<body>
<div>loading foo bar...</div>
<div>loading bar baz qux...</div>
<div>loading bar baz qux...</div>
<link rel="preload" href="bar" as="style" crossorigin="anonymous" />
</body>
</html>,
);
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(
<html>
<head>
<link rel="stylesheet" href="initial" data-precedence="one" />
<link rel="stylesheet" href="foo" data-precedence="one" />
<link rel="stylesheet" href="preset" data-precedence="preset" />
<link
rel="stylesheet"
href="bar"
data-precedence="default"
data-foo="foo"
crossorigin="anonymous"
/>
<link rel="preload" href="foo" as="style" />
</head>
<body>
<div>loading foo bar...</div>
<div>loading bar baz qux...</div>
<div>loading bar baz qux...</div>
<link rel="preload" href="bar" as="style" crossorigin="anonymous" />
</body>
</html>,
);
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(
<html>
<head>
<link rel="stylesheet" href="initial" data-precedence="one" />
<link rel="stylesheet" href="foo" data-precedence="one" />
<link rel="stylesheet" href="preset" data-precedence="preset" />
<link
rel="stylesheet"
href="bar"
data-precedence="default"
data-foo="foo"
crossorigin="anonymous"
/>
<link rel="preload" href="foo" as="style" />
</head>
<body>
<div>
<ul>
<li>foo</li>
<li>bar</li>
</ul>
</div>
<div>loading bar baz qux...</div>
<div>loading bar baz qux...</div>
<link rel="preload" href="bar" as="style" crossorigin="anonymous" />
</body>
</html>,
);
await act(() => {
resolveText('baz');
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="initial" data-precedence="one" />
<link rel="stylesheet" href="foo" data-precedence="one" />
<link rel="stylesheet" href="preset" data-precedence="preset" />
<link
rel="stylesheet"
href="bar"
data-precedence="default"
data-foo="foo"
crossorigin="anonymous"
/>
<link rel="preload" href="foo" as="style" />
</head>
<body>
<div>
<ul>
<li>foo</li>
<li>bar</li>
</ul>
</div>
<div>loading bar baz qux...</div>
<div>loading bar baz qux...</div>
<link rel="preload" as="style" href="bar" crossorigin="anonymous" />
<link rel="preload" as="style" href="baz" />
</body>
</html>,
);
await act(() => {
resolveText('qux');
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="initial" data-precedence="one" />
<link rel="stylesheet" href="foo" data-precedence="one" />
<link rel="stylesheet" href="qux" data-precedence="one" />
<link rel="stylesheet" href="preset" data-precedence="preset" />
<link
rel="stylesheet"
href="bar"
data-precedence="default"
data-foo="foo"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="baz" data-precedence="two" />
<link rel="preload" href="foo" as="style" />
</head>
<body>
<div>
<ul>
<li>foo</li>
<li>bar</li>
</ul>
</div>
<div>loading bar baz qux...</div>
<div>loading bar baz qux...</div>
<link rel="preload" as="style" href="bar" crossorigin="anonymous" />
<link rel="preload" as="style" href="baz" />
<link rel="preload" as="style" href="qux" />
</body>
</html>,
);
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(
<html>
<head>
<link rel="stylesheet" href="initial" data-precedence="one" />
<link rel="stylesheet" href="foo" data-precedence="one" />
<link rel="stylesheet" href="qux" data-precedence="one" />
<link rel="stylesheet" href="preset" data-precedence="preset" />
<link
rel="stylesheet"
href="bar"
data-precedence="default"
data-foo="foo"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="baz" data-precedence="two" />
<link rel="preload" href="foo" as="style" />
</head>
<body>
<div>
<ul>
<li>foo</li>
<li>bar</li>
</ul>
</div>
<div>
<ul>
<li>bar</li>
<li>baz</li>
<li>qux</li>
</ul>
</div>
<div>loading bar baz qux...</div>
<link rel="preload" as="style" href="bar" crossorigin="anonymous" />
<link rel="preload" as="style" href="baz" />
<link rel="preload" as="style" href="qux" />
</body>
</html>,
);
await act(() => {
resolveText('unblock');
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="initial" data-precedence="one" />
<link rel="stylesheet" href="foo" data-precedence="one" />
<link rel="stylesheet" href="qux" data-precedence="one" />
<link rel="stylesheet" href="preset" data-precedence="preset" />
<link
rel="stylesheet"
href="bar"
data-precedence="default"
data-foo="foo"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="baz" data-precedence="two" />
<link rel="preload" href="foo" as="style" />
</head>
<body>
<div>
<ul>
<li>foo</li>
<li>bar</li>
</ul>
</div>
<div>
<ul>
<li>bar</li>
<li>baz</li>
<li>qux</li>
</ul>
</div>
<div>
<ul>
<li>bar</li>
<li>baz</li>
<li>qux</li>
</ul>
</div>
<link rel="preload" as="style" href="bar" crossorigin="anonymous" />
<link rel="preload" as="style" href="baz" />
<link rel="preload" as="style" href="qux" />
</body>
</html>,
);
});
// @gate enableFloat
it('normalizes style resource precedence for all boundaries inlined as part of the shell flush', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<div>
outer
<link rel="stylesheet" href="1one" precedence="one" />
<link rel="stylesheet" href="1two" precedence="two" />
<link rel="stylesheet" href="1three" precedence="three" />
<link rel="stylesheet" href="1four" precedence="four" />
<Suspense fallback={null}>
<div>
middle
<link rel="stylesheet" href="2one" precedence="one" />
<link rel="stylesheet" href="2two" precedence="two" />
<link rel="stylesheet" href="2three" precedence="three" />
<link rel="stylesheet" href="2four" precedence="four" />
<Suspense fallback={null}>
<div>
inner
<link rel="stylesheet" href="3five" precedence="five" />
<link rel="stylesheet" href="3one" precedence="one" />
<link rel="stylesheet" href="3two" precedence="two" />
<link rel="stylesheet" href="3three" precedence="three" />
<link rel="stylesheet" href="3four" precedence="four" />
</div>
</Suspense>
</div>
</Suspense>
<Suspense fallback={null}>
<div>middle</div>
<link rel="stylesheet" href="4one" precedence="one" />
<link rel="stylesheet" href="4two" precedence="two" />
<link rel="stylesheet" href="4three" precedence="three" />
<link rel="stylesheet" href="4four" precedence="four" />
</Suspense>
</div>
</body>
</html>,
);
pipe(writable);
});
// The reason the href's aren't ordered linearly is that when boundaries complete their resources
// get hoisted to the shell directly so they can flush in the head. If a boundary doesn't suspend then
// child boundaries will complete before the parent boundary and thus have their resources hoist
// early. The reason precedences are still ordered correctly between child and parent is because
// the precedence ordering is determined upon first discovernig a resource rather than on hoist and
// so it follows render order
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="1one" data-precedence="one" />
<link rel="stylesheet" href="3one" data-precedence="one" />
<link rel="stylesheet" href="2one" data-precedence="one" />
<link rel="stylesheet" href="4one" data-precedence="one" />
<link rel="stylesheet" href="1two" data-precedence="two" />
<link rel="stylesheet" href="3two" data-precedence="two" />
<link rel="stylesheet" href="2two" data-precedence="two" />
<link rel="stylesheet" href="4two" data-precedence="two" />
<link rel="stylesheet" href="1three" data-precedence="three" />
<link rel="stylesheet" href="3three" data-precedence="three" />
<link rel="stylesheet" href="2three" data-precedence="three" />
<link rel="stylesheet" href="4three" data-precedence="three" />
<link rel="stylesheet" href="1four" data-precedence="four" />
<link rel="stylesheet" href="3four" data-precedence="four" />
<link rel="stylesheet" href="2four" data-precedence="four" />
<link rel="stylesheet" href="4four" data-precedence="four" />
<link rel="stylesheet" href="3five" data-precedence="five" />
</head>
<body>
<div>
outer
<div>
middle<div>inner</div>
</div>
<div>middle</div>
</div>
</body>
</html>,
);
});
// @gate enableFloat
it('style resources are inserted according to precedence order on the client', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<div>
<link rel="stylesheet" href="foo" precedence="one" />
<link rel="stylesheet" href="bar" precedence="two" />
Hello
</div>
</body>
</html>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="one" />
<link rel="stylesheet" href="bar" data-precedence="two" />
</head>
<body>
<div>Hello</div>
</body>
</html>,
);
const root = ReactDOMClient.hydrateRoot(
document,
<html>
<head />
<body>
<div>
<link rel="stylesheet" href="foo" precedence="one" />
<link rel="stylesheet" href="bar" precedence="two" />
Hello
</div>
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="one" />
<link rel="stylesheet" href="bar" data-precedence="two" />
</head>
<body>
<div>Hello</div>
</body>
</html>,
);
root.render(
<html>
<head />
<body>
<div>Hello</div>
<link rel="stylesheet" href="baz" precedence="one" />
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="one" />
<link rel="stylesheet" href="baz" data-precedence="one" />
<link rel="stylesheet" href="bar" data-precedence="two" />
<link rel="preload" as="style" href="baz" />
</head>
<body>
<div>Hello</div>
</body>
</html>,
);
});
// @gate enableFloat
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(
<ErrorBoundary>
<link rel="stylesheet" href="foo" precedence="default" />
<div>foo</div>
<Throw />
</ErrorBoundary>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="preload" href="foo" as="style" />
</head>
<body>
<div id="container">Uh oh!</div>
</body>
</html>,
);
});
// @gate enableFloat
it('does not emit preinit stylesheets if they are invoked after the shell flushes', async () => {
function PreinitsBlockedOn({text}) {
readText(text);
ReactDOM.preinit('one', {precedence: 'one', as: 'style'});
ReactDOM.preinit('two', {precedence: 'two', as: 'style'});
return null;
}
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<div>
<link rel="stylesheet" href="foo" precedence="one" />
<link rel="stylesheet" href="bar" precedence="two" />
Hello
</div>
<div>
<Suspense fallback={'loading...'}>
<PreinitsBlockedOn text="foo" />
<AsyncText text="bar" />
</Suspense>
</div>
</body>
</html>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="one" />
<link rel="stylesheet" href="bar" data-precedence="two" />
</head>
<body>
<div>Hello</div>
<div>loading...</div>
</body>
</html>,
);
await act(() => {
resolveText('foo');
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="one" />
<link rel="stylesheet" href="bar" data-precedence="two" />
</head>
<body>
<div>Hello</div>
<div>loading...</div>
<link rel="preload" href="one" as="style" />
<link rel="preload" href="two" as="style" />
</body>
</html>,
);
await act(() => {
resolveText('bar');
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="one" />
<link rel="stylesheet" href="bar" data-precedence="two" />
</head>
<body>
<div>Hello</div>
<div>bar</div>
<link rel="preload" href="one" as="style" />
<link rel="preload" href="two" as="style" />
</body>
</html>,
);
});
// @gate enableFloat
it('will include child boundary style resources in the boundary reveal instruction', async () => {
function BlockedOn({text, children}) {
readText(text);
return children;
}
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<div>
<Suspense fallback="loading foo...">
<BlockedOn text="foo">
<div>foo</div>
<link rel="stylesheet" href="foo" precedence="default" />
<Suspense fallback="loading bar...">
<BlockedOn text="bar">
<div>bar</div>
<link rel="stylesheet" href="bar" precedence="default" />
<Suspense fallback="loading baz...">
<BlockedOn text="baz">
<div>baz</div>
<link
rel="stylesheet"
href="baz"
precedence="default"
/>
</BlockedOn>
</Suspense>
</BlockedOn>
</Suspense>
</BlockedOn>
</Suspense>
</div>
</body>
</html>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head />
<body>
<div>loading foo...</div>
</body>
</html>,
);
await act(() => {
resolveText('bar');
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head />
<body>
<div>loading foo...</div>
</body>
</html>,
);
await act(() => {
resolveText('baz');
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head />
<body>
<div>loading foo...</div>
</body>
</html>,
);
await act(() => {
resolveText('foo');
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="default" />
<link rel="stylesheet" href="bar" data-precedence="default" />
<link rel="stylesheet" href="baz" data-precedence="default" />
</head>
<body>
<div>loading foo...</div>
<link rel="preload" href="foo" as="style" />
<link rel="preload" href="bar" as="style" />
<link rel="preload" href="baz" as="style" />
</body>
</html>,
);
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(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="default" />
<link rel="stylesheet" href="bar" data-precedence="default" />
<link rel="stylesheet" href="baz" data-precedence="default" />
</head>
<body>
<div>
<div>foo</div>
<div>bar</div>
<div>baz</div>
</div>
<link rel="preload" href="foo" as="style" />
<link rel="preload" href="bar" as="style" />
<link rel="preload" href="baz" as="style" />
</body>
</html>,
);
});
// @gate enableFloat
it('will hoist resources of child boundaries emitted as part of a partial boundary to the parent boundary', async () => {
function BlockedOn({text, children}) {
readText(text);
return children;
}
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<div>
<Suspense fallback="loading...">
<div>
<BlockedOn text="foo">
<div>foo</div>
<link rel="stylesheet" href="foo" precedence="default" />
<Suspense fallback="loading bar...">
<BlockedOn text="bar">
<div>bar</div>
<link
rel="stylesheet"
href="bar"
precedence="default"
/>
<Suspense fallback="loading baz...">
<div>
<BlockedOn text="baz">
<div>baz</div>
<link
rel="stylesheet"
href="baz"
precedence="default"
/>
</BlockedOn>
</div>
</Suspense>
</BlockedOn>
</Suspense>
</BlockedOn>
<BlockedOn text="qux">
<div>qux</div>
<link rel="stylesheet" href="qux" precedence="default" />
</BlockedOn>
</div>
</Suspense>
</div>
</body>
</html>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head />
<body>
<div>loading...</div>
</body>
</html>,
);
// This will enqueue a style resource in a deep blocked boundary (loading baz...).
await act(() => {
resolveText('baz');
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head />
<body>
<div>loading...</div>
</body>
</html>,
);
// This will enqueue a style resource in the intermediate blocked boundary (loading bar...).
await act(() => {
resolveText('bar');
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head />
<body>
<div>loading...</div>
</body>
</html>,
);
// 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(
<html>
<head />
<body>
<div>loading...</div>
<link rel="preload" href="foo" as="style" />
<link rel="preload" href="bar" as="style" />
<link rel="preload" href="baz" as="style" />
</body>
</html>,
);
// 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(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="default" />
<link rel="stylesheet" href="bar" data-precedence="default" />
<link rel="stylesheet" href="baz" data-precedence="default" />
<link rel="stylesheet" href="qux" data-precedence="default" />
</head>
<body>
<div>loading...</div>
<link rel="preload" href="foo" as="style" />
<link rel="preload" href="bar" as="style" />
<link rel="preload" href="baz" as="style" />
<link rel="preload" href="qux" as="style" />
</body>
</html>,
);
// 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(
<html>
<head>
<link rel="stylesheet" href="foo" data-precedence="default" />
<link rel="stylesheet" href="bar" data-precedence="default" />
<link rel="stylesheet" href="baz" data-precedence="default" />
<link rel="stylesheet" href="qux" data-precedence="default" />
</head>
<body>
<div>
<div>
<div>foo</div>
<div>bar</div>
<div>
<div>baz</div>
</div>
<div>qux</div>
</div>
</div>
<link rel="preload" href="foo" as="style" />
<link rel="preload" href="bar" as="style" />
<link rel="preload" href="baz" as="style" />
<link rel="preload" href="qux" as="style" />
</body>
</html>,
);
});
// @gate enableFloat
it('encodes attributes consistently whether resources are flushed in shell or in late boundaries', async () => {
const originalConsoleError = console.error;
const mockError = jest.fn();
console.error = (...args) => {
mockError(...args.map(normalizeCodeLocInfo));
};
function BlockedOn({text, children}) {
readText(text);
return children;
}
function App() {
return (
<html>
<head />
<body>
<div>
<link
// This preload is explicit so it can flush with a lot of potential attrs
// We will duplicate this as a style that flushes after the shell
rel="preload"
as="style"
href="foo"
// precedence is not a special attribute for preloads so this will just flush as is
precedence="default"
// Some standard link props
crossOrigin="anonymous"
media="all"
integrity="somehash"
referrerPolicy="origin"
// data and non starndard attributes that should flush
data-foo={'"quoted"'}
nonStandardAttr="attr"
properlyformattednonstandardattr="attr"
// attributes that should be filtered out for violating certain rules
onSomething="this should be removed b/c event handler"
shouldnotincludefunctions={() => {}}
norsymbols={Symbol('foo')}
/>
<Suspense fallback={'loading...'}>
<BlockedOn text="unblock">
<link
// This preload is explicit so it can flush with a lot of potential attrs
// We will duplicate this as a style that flushes after the shell
rel="stylesheet"
href="foo"
// opt-in property to get this treated as a resource
precedence="default"
// Some standard link props
crossOrigin="anonymous"
media="all"
integrity="somehash"
referrerPolicy="origin"
// data and non starndard attributes that should flush
data-foo={'"quoted"'}
nonStandardAttr="attr"
properlyformattednonstandardattr="attr"
// attributes that should be filtered out for violating certain rules
onSomething="this should be removed b/c event handler"
shouldnotincludefunctions={() => {}}
norsymbols={Symbol('foo')}
/>
</BlockedOn>
</Suspense>
</div>
</body>
</html>
);
}
try {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link
rel="preload"
as="style"
href="foo"
precedence="default"
crossorigin="anonymous"
media="all"
integrity="somehash"
referrerpolicy="origin"
data-foo={'"quoted"'}
nonstandardattr="attr"
properlyformattednonstandardattr="attr"
/>
</head>
<body>
<div>loading...</div>
</body>
</html>,
);
if (__DEV__) {
expect(mockError).toHaveBeenCalledTimes(2);
expect(mockError).toHaveBeenCalledWith(
'Warning: React does not recognize the `%s` prop on a DOM element.' +
' If you intentionally want it to appear in the DOM as a custom attribute,' +
' spell it as lowercase `%s` instead. If you accidentally passed it from a' +
' parent component, remove it from the DOM element.%s',
'nonStandardAttr',
'nonstandardattr',
componentStack(['link', 'div', 'body', 'html', 'App']),
);
expect(mockError).toHaveBeenCalledWith(
'Warning: Invalid values for props %s on <%s> tag. Either remove them from' +
' the element, or pass a string or number value to keep them in the DOM. For' +
' details, see https://reactjs.org/link/attribute-behavior %s',
'`shouldnotincludefunctions`, `norsymbols`',
'link',
componentStack(['link', 'div', 'body', 'html', 'App']),
);
mockError.mockClear();
} else {
expect(mockError).not.toHaveBeenCalled();
}
// Now we flush the stylesheet with the boundary
await act(() => {
resolveText('unblock');
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link
rel="stylesheet"
href="foo"
data-precedence="default"
crossorigin="anonymous"
media="all"
integrity="somehash"
referrerpolicy="origin"
data-foo={'"quoted"'}
nonstandardattr="attr"
properlyformattednonstandardattr="attr"
/>
<link
rel="preload"
as="style"
href="foo"
precedence="default"
crossorigin="anonymous"
media="all"
integrity="somehash"
referrerpolicy="origin"
data-foo={'"quoted"'}
nonstandardattr="attr"
properlyformattednonstandardattr="attr"
/>
</head>
<body>
<div>loading...</div>
</body>
</html>,
);
if (__DEV__) {
// The way the test is currently set up the props that would warn have already warned
// so no new warnings appear. This is really testing the same code pathway so
// exercising that more here isn't all that useful
expect(mockError).toHaveBeenCalledTimes(0);
} else {
expect(mockError).not.toHaveBeenCalled();
}
} finally {
console.error = originalConsoleError;
}
});
// @gate enableFloat
it('boundary style resource dependencies hoist to a parent boundary when flushed inline', async () => {
function BlockedOn({text, children}) {
readText(text);
return children;
}
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<div>
<Suspense fallback="loading A...">
<BlockedOn text="unblock">
<AsyncText text="A" />
<link rel="stylesheet" href="A" precedence="A" />
<Suspense fallback="loading AA...">
<AsyncText text="AA" />
<link rel="stylesheet" href="AA" precedence="AA" />
<Suspense fallback="loading AAA...">
<AsyncText text="AAA" />
<link rel="stylesheet" href="AAA" precedence="AAA" />
<Suspense fallback="loading AAAA...">
<AsyncText text="AAAA" />
<link rel="stylesheet" href="AAAA" precedence="AAAA" />
</Suspense>
</Suspense>
</Suspense>
</BlockedOn>
</Suspense>
</div>
</body>
</html>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head />
<body>
<div>loading A...</div>
</body>
</html>,
);
await act(() => {
resolveText('unblock');
resolveText('AAAA');
resolveText('AA');
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head />
<body>
<div>loading A...</div>
<link rel="preload" as="style" href="A" />
<link rel="preload" as="style" href="AA" />
<link rel="preload" as="style" href="AAA" />
<link rel="preload" as="style" href="AAAA" />
</body>
</html>,
);
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(
<html>
<head>
<link rel="stylesheet" href="A" data-precedence="A" />
<link rel="stylesheet" href="AA" data-precedence="AA" />
</head>
<body>
<div>
{'A'}
{'AA'}
{'loading AAA...'}
</div>
<link rel="preload" as="style" href="A" />
<link rel="preload" as="style" href="AA" />
<link rel="preload" as="style" href="AAA" />
<link rel="preload" as="style" href="AAAA" />
</body>
</html>,
);
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(
<html>
<head>
<link rel="stylesheet" href="A" data-precedence="A" />
<link rel="stylesheet" href="AA" data-precedence="AA" />
<link rel="stylesheet" href="AAA" data-precedence="AAA" />
<link rel="stylesheet" href="AAAA" data-precedence="AAAA" />
</head>
<body>
<div>
{'A'}
{'AA'}
{'AAA'}
{'AAAA'}
</div>
<link rel="preload" as="style" href="A" />
<link rel="preload" as="style" href="AA" />
<link rel="preload" as="style" href="AAA" />
<link rel="preload" as="style" href="AAAA" />
</body>
</html>,
);
});
// @gate enableFloat
it('always enforces crossOrigin "anonymous" for font preloads', async () => {
const originalConsoleError = console.error;
const mockError = jest.fn();
console.error = (...args) => {
mockError(...args.map(normalizeCodeLocInfo));
};
try {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<link rel="preload" as="font" href="foo" />
<link rel="preload" as="font" href="bar" crossOrigin="foo" />
<link
rel="preload"
as="font"
href="baz"
crossOrigin="use-credentials"
/>
<link
rel="preload"
as="font"
href="qux"
crossOrigin="anonymous"
/>
</body>
</html>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="preload" as="font" href="foo" crossorigin="" />
<link rel="preload" as="font" href="bar" crossorigin="" />
<link rel="preload" as="font" href="baz" crossorigin="" />
<link rel="preload" as="font" href="qux" crossorigin="" />
</head>
<body />
</html>,
);
if (__DEV__) {
expect(mockError).toHaveBeenCalledTimes(2);
expect(mockError).toHaveBeenCalledWith(
'Warning: A %s with href "%s" did not specify the crossOrigin prop. Font preloads must always use' +
' anonymouse CORS mode. To fix add an empty string, "anonymous", or any other string' +
' value except "use-credentials" for the crossOrigin prop of all font preloads.%s',
'preload Resource (as "font")',
'foo',
componentStack(['link', 'body', 'html']),
);
expect(mockError).toHaveBeenCalledWith(
'Warning: A %s with href "%s" specified a crossOrigin value of "use-credentials". Font preloads must always use' +
' anonymouse CORS mode. To fix use an empty string, "anonymous", or any other string' +
' value except "use-credentials" for the crossOrigin prop of all font preloads.%s',
'preload Resource (as "font")',
'baz',
componentStack(['link', 'body', 'html']),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
} finally {
console.error = originalConsoleError;
}
});
describe('ReactDOM.pre* function validation', () => {
function Preloads({scenarios}) {
for (let i = 0; i < scenarios.length; i++) {
const href = scenarios[i][0];
const options = scenarios[i][1];
ReactDOM.preload(href, options);
}
}
function Preinits({scenarios}) {
for (let i = 0; i < scenarios.length; i++) {
const href = scenarios[i][0];
const options = scenarios[i][1];
ReactDOM.preinit(href, options);
}
}
async function renderOnServer(Component, scenarios) {
const originalConsoleError = console.error;
const mockError = jest.fn();
console.error = (...args) => {
mockError(...args.map(normalizeCodeLocInfo));
};
try {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head>
<Component scenarios={scenarios} />
</head>
</html>,
);
pipe(writable);
});
for (let i = 0; i < scenarios.length; i++) {
const assertion = scenarios[i][2];
assertion(mockError, i);
}
} finally {
console.error = originalConsoleError;
}
}
async function renderOnClient(Component, scenarios) {
const originalConsoleError = console.error;
const mockError = jest.fn();
console.error = (...args) => {
mockError(...args.map(normalizeCodeLocInfo));
};
try {
const root = ReactDOMClient.createRoot(document);
root.render(
<html>
<head>
<Component scenarios={scenarios} />
</head>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
for (let i = 0; i < scenarios.length; i++) {
const assertion = scenarios[i][2];
assertion(mockError, i);
}
} finally {
console.error = originalConsoleError;
}
}
[
['server', renderOnServer],
['client', renderOnClient],
].forEach(([environment, render]) => {
// @gate enableFloat
it(
'warns when an invalid href argument is provided to ReactDOM.preload on the ' +
environment,
async () => {
const expectedMessage =
'Warning: ReactDOM.preload() expected the first argument to be a string representing an href but found %s instead.%s';
const expectedStack = componentStack(['Preloads', 'head', 'html']);
function makeArgs(...substitutions) {
return [expectedMessage, ...substitutions, expectedStack];
}
await render(Preloads, [
[
'',
undefined,
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('an empty string'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
undefined,
undefined,
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('undefined'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
null,
undefined,
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('null'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
232132,
undefined,
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('something with type "number"'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
{},
undefined,
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('something with type "object"'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
]);
},
);
// @gate enableFloat
it(
'warns when an invalid href argument is provided to ReactDOM.preinit on the ' +
environment,
async () => {
const expectedMessage =
'Warning: ReactDOM.preinit() expected the first argument to be a string representing an href but found %s instead.%s';
const expectedStack = componentStack(['Preinits', 'head', 'html']);
function makeArgs(...substitutions) {
return [expectedMessage, ...substitutions, expectedStack];
}
await render(Preinits, [
[
'',
undefined,
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('an empty string'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
undefined,
undefined,
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('undefined'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
null,
undefined,
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('null'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
232132,
undefined,
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('something with type "number"'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
{},
undefined,
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('something with type "object"'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
]);
},
);
// @gate enableFloat
it(
'warns when an invalid options argument is provided to ReactDOM.preload on the ' +
environment,
async () => {
const expectedMessage =
'Warning: ReactDOM.preload() expected the second argument to be an options argument containing at least an "as" property' +
' specifying the Resource type. It found %s instead. The href for the preload call where this warning originated is "%s".%s';
const expectedStack = componentStack(['Preloads', 'head', 'html']);
function makeArgs(...substitutions) {
return [expectedMessage, ...substitutions, expectedStack];
}
await render(Preloads, [
[
'foo',
undefined,
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('undefined', 'foo'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
'foo',
null,
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('null', 'foo'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
'foo',
'bar',
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('something with type "string"', 'foo'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
'foo',
123,
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('something with type "number"', 'foo'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
]);
},
);
// @gate enableFloat
it(
'warns when an invalid options argument is provided to ReactDOM.preinit on the ' +
environment,
async () => {
const expectedMessage =
'Warning: ReactDOM.preinit() expected the second argument to be an options argument containing at least an "as" property' +
' specifying the Resource type. It found %s instead. The href for the preload call where this warning originated is "%s".%s';
const expectedStack = componentStack(['Preinits', 'head', 'html']);
function makeArgs(...substitutions) {
return [expectedMessage, ...substitutions, expectedStack];
}
await render(Preinits, [
[
'foo',
undefined,
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('undefined', 'foo'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
'foo',
null,
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('null', 'foo'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
'foo',
'bar',
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('something with type "string"', 'foo'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
'foo',
123,
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('something with type "number"', 'foo'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
]);
},
);
// @gate enableFloat
it(
'warns when an invalid "as" option is provided to ReactDOM.preload on the ' +
environment,
async () => {
const expectedMessage =
'Warning: ReactDOM.preload() expected a valid "as" type in the options (second) argument but found %s instead.' +
' Please use one of the following valid values instead: %s. The href for the preload call where this' +
' warning originated is "%s".%s';
const expectedStack = componentStack(['Preloads', 'head', 'html']);
function makeArgs(...substitutions) {
return [expectedMessage, ...substitutions, expectedStack];
}
await render(Preloads, [
[
'foo',
{},
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs(
'undefined',
'"style", "font", or "script"',
'foo',
),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
'bar',
{as: null},
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('null', '"style", "font", or "script"', 'bar'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
'baz',
{as: 123},
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs(
'something with type "number"',
'"style", "font", or "script"',
'baz',
),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
'qux',
{as: {}},
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs(
'something with type "object"',
'"style", "font", or "script"',
'qux',
),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
'quux',
{as: 'bar'},
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('"bar"', '"style", "font", or "script"', 'quux'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
]);
},
);
// @gate enableFloat
it(
'warns when an invalid "as" option is provided to ReactDOM.preinit on the ' +
environment,
async () => {
const expectedMessage =
'Warning: ReactDOM.preinit() expected the second argument to be an options argument containing at least an "as" property' +
' specifying the Resource type. It found %s instead. Currently, valid resource types for for preinit are "style"' +
' and "script". The href for the preinit call where this warning originated is "%s".%s';
const expectedStack = componentStack(['Preinits', 'head', 'html']);
function makeArgs(...substitutions) {
return [expectedMessage, ...substitutions, expectedStack];
}
await render(Preinits, [
[
'foo',
{},
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('undefined', 'foo'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
'bar',
{as: null},
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('null', 'bar'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
'baz',
{as: 123},
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('something with type "number"', 'baz'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
'qux',
{as: {}},
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('something with type "object"', 'qux'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
[
'quux',
{as: 'bar'},
(mockError, scenarioNumber) => {
if (__DEV__) {
expect(mockError.mock.calls[scenarioNumber]).toEqual(
makeArgs('"bar"', 'quux'),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
},
],
]);
},
);
});
});
describe('prop validation', () => {
// @gate enableFloat
it('warns when you change props on a resource unless you also change the href', async () => {
const root = ReactDOMClient.createRoot(container);
root.render(
<div>
<link
rel="stylesheet"
href="foo"
precedence="foo"
data-something-extra="extra"
/>
<link
rel="stylesheet"
href="bar"
precedence="bar"
data-something-extra="extra"
/>
<script src="sfoo" async={true} data-something-extra="extra" />
<script src="sbar" async={true} data-something-extra="extra" />
hello
</div>,
);
expect(Scheduler).toFlushWithoutYielding();
root.render(
<div>
<link
rel="stylesheet"
href="foo"
precedence="fu"
data-something-new="new"
/>
<link
rel="stylesheet"
href="baz"
precedence="baz"
data-something-new="new"
/>
<script src="sfoo" async={true} data-something-new="new" />
<script src="sbaz" async={true} data-something-new="new" />
hello
</div>,
);
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toErrorDev([
'Warning: A style Resource with href "foo" recieved new props with different values from the props used' +
' when this Resource was first rendered. React will only use the props provided when' +
' this resource was first rendered until a new href is provided. Unlike conventional' +
' DOM elements, Resources instances do not have a one to one correspondence with Elements' +
' in the DOM and as such, every instance of a Resource for a single Resource identifier' +
' (href) must have props that agree with each other. The differences are described below.' +
'\n data-something-extra: missing or null in latest props, "extra" in original props' +
'\n data-something-new: "new" in latest props, missing or null in original props' +
'\n precedence: "fu" in latest props, "foo" in original props',
'Warning: A script Resource with src "sfoo" recieved new props with different values from the props used' +
' when this Resource was first rendered. React will only use the props provided when' +
' this resource was first rendered until a new src is provided. Unlike conventional' +
' DOM elements, Resources instances do not have a one to one correspondence with Elements' +
' in the DOM and as such, every instance of a Resource for a single Resource identifier' +
' (src) must have props that agree with each other. The differences are described below.' +
'\n data-something-extra: missing or null in latest props, "extra" in original props' +
'\n data-something-new: "new" in latest props, missing or null in original props',
]);
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link
rel="stylesheet"
href="foo"
data-precedence="foo"
data-something-extra="extra"
/>
<link
rel="stylesheet"
href="bar"
data-precedence="bar"
data-something-extra="extra"
/>
<link
rel="stylesheet"
href="baz"
data-precedence="baz"
data-something-new="new"
/>
<link rel="preload" as="style" href="foo" />
<link rel="preload" as="style" href="bar" />
<script src="sfoo" async="" data-something-extra="extra" />
<script src="sbar" async="" data-something-extra="extra" />
<link rel="preload" as="style" href="baz" />
<script src="sbaz" async="" data-something-new="new" />
</head>
<body>
<div id="container">
<div>hello</div>
</div>
</body>
</html>,
);
});
// @gate enableFloat
it('warns when style Resource have different values for media for the same href', async () => {
const originalConsoleError = console.error;
const mockError = jest.fn();
console.error = (...args) => {
mockError(...args.map(normalizeCodeLocInfo));
};
try {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head>
<link
rel="stylesheet"
href="foo"
precedence="foo"
media="all"
/>
<link rel="stylesheet" href="foo" precedence="foo" />
<link rel="stylesheet" href="bar" precedence="bar" />
<link
rel="stylesheet"
href="bar"
precedence="bar"
media="all"
/>
<link
rel="stylesheet"
href="baz"
precedence="baz"
media="some"
/>
<link
rel="stylesheet"
href="baz"
precedence="baz"
media="all"
/>
</head>
</html>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link
rel="stylesheet"
href="foo"
data-precedence="foo"
media="all"
/>
<link rel="stylesheet" href="bar" data-precedence="bar" />
<link
rel="stylesheet"
href="baz"
data-precedence="baz"
media="some"
/>
</head>
<body />
</html>,
);
if (__DEV__) {
expect(mockError).toHaveBeenCalledTimes(3);
expect(mockError).toHaveBeenCalledWith(
'Warning: A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
' that were provided the first time they are encountered so any differences will be ignored. Please' +
' update Resources that share an %s to have props that agree. The differences are described below.%s%s',
'style Resource',
'href',
'foo',
'an earlier instance of this Resource',
'href',
'\n media: missing or null in latest props, "all" in original props',
componentStack(['link', 'head', 'html']),
);
expect(mockError).toHaveBeenCalledWith(
'Warning: A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
' that were provided the first time they are encountered so any differences will be ignored. Please' +
' update Resources that share an %s to have props that agree. The differences are described below.%s%s',
'style Resource',
'href',
'bar',
'an earlier instance of this Resource',
'href',
'\n media: "all" in latest props, missing or null in original props',
componentStack(['link', 'head', 'html']),
);
expect(mockError).toHaveBeenCalledWith(
'Warning: A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
' that were provided the first time they are encountered so any differences will be ignored. Please' +
' update Resources that share an %s to have props that agree. The differences are described below.%s%s',
'style Resource',
'href',
'baz',
'an earlier instance of this Resource',
'href',
'\n media: "all" in latest props, "some" in original props',
componentStack(['link', 'head', 'html']),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
} finally {
console.error = originalConsoleError;
}
});
// @gate enableFloat
it('warns when style Resource props differ or are added for the same href', async () => {
const originalConsoleError = console.error;
const mockError = jest.fn();
console.error = (...args) => {
mockError(...args.map(normalizeCodeLocInfo));
};
try {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head>
<link
rel="stylesheet"
href="foo"
precedence="foo"
data-foo="an original value"
/>
<link rel="stylesheet" href="foo" precedence="foo" />
<link rel="stylesheet" href="foo" precedence="foonew" />
<link rel="stylesheet" href="bar" precedence="bar" />
<link
rel="stylesheet"
href="bar"
precedence="bar"
data-foo="a new value"
/>
<link
rel="stylesheet"
href="baz"
precedence="baz"
data-foo="an original value"
/>
<link
rel="stylesheet"
href="baz"
precedence="baz"
data-foo="a new value"
/>
</head>
</html>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link
rel="stylesheet"
href="foo"
data-precedence="foo"
data-foo="an original value"
/>
<link rel="stylesheet" href="bar" data-precedence="bar" />
<link
rel="stylesheet"
href="baz"
data-precedence="baz"
data-foo="an original value"
/>
</head>
<body />
</html>,
);
if (__DEV__) {
expect(mockError).toHaveBeenCalledTimes(3);
expect(mockError).toHaveBeenCalledWith(
'Warning: A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
' that were provided the first time they are encountered so any differences will be ignored. Please' +
' update Resources that share an %s to have props that agree. The differences are described below.%s%s',
'style Resource',
'href',
'foo',
'an earlier instance of this Resource',
'href',
'\n precedence: "foonew" in latest props, "foo" in original props',
componentStack(['link', 'head', 'html']),
);
expect(mockError).toHaveBeenCalledWith(
'Warning: A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
' that were provided the first time they are encountered so any differences will be ignored. Please' +
' update Resources that share an %s to have props that agree. The differences are described below.%s%s',
'style Resource',
'href',
'bar',
'an earlier instance of this Resource',
'href',
'\n data-foo: "a new value" in latest props, missing or null in original props',
componentStack(['link', 'head', 'html']),
);
expect(mockError).toHaveBeenCalledWith(
'Warning: A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
' that were provided the first time they are encountered so any differences will be ignored. Please' +
' update Resources that share an %s to have props that agree. The differences are described below.%s%s',
'style Resource',
'href',
'baz',
'an earlier instance of this Resource',
'href',
'\n data-foo: "a new value" in latest props, "an original value" in original props',
componentStack(['link', 'head', 'html']),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
} finally {
console.error = originalConsoleError;
}
});
// @gate enableFloat
it('warns when style Resource includes any combination of onLoad, onError, or disabled props', async () => {
const originalConsoleError = console.error;
const mockError = jest.fn();
console.error = (...args) => {
mockError(...args.map(normalizeCodeLocInfo));
};
try {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head>
<link
rel="stylesheet"
href="foo"
precedence="foo"
onLoad={() => {}}
onError={() => {}}
/>
<link
rel="stylesheet"
href="bar"
precedence="bar"
onLoad={() => {}}
/>
<link
rel="stylesheet"
href="baz"
precedence="baz"
onError={() => {}}
/>
<link
rel="stylesheet"
href="qux"
precedence="qux"
disabled={true}
/>
</head>
<body />
</html>,
);
pipe(writable);
});
// precedence is removed from the stylesheets because it is considered a reserved prop for
// stylesheets to opt into resource semantics.
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="preload" as="style" href="foo" />
<link rel="preload" as="style" href="bar" />
<link rel="preload" as="style" href="baz" />
<link rel="preload" as="style" href="qux" />
<link rel="stylesheet" href="foo" />
<link rel="stylesheet" href="bar" />
<link rel="stylesheet" href="baz" />
<link rel="stylesheet" href="qux" disabled="" />
</head>
<body />
</html>,
);
if (__DEV__) {
expect(mockError).toHaveBeenCalledTimes(4);
expect(mockError).toHaveBeenCalledWith(
'Warning: A link (rel="stylesheet") element with href "%s" has the precedence prop but also included the %s.' +
' When using %s React will opt out of Resource behavior. If you meant for this' +
' element to be treated as a Resource remove the %s. Otherwise remove the precedence prop.%s',
'foo',
'onLoad and onError props',
'onLoad, onError, or disabled',
'onLoad and onError props',
componentStack(['link', 'head', 'html']),
);
expect(mockError).toHaveBeenCalledWith(
'Warning: A link (rel="stylesheet") element with href "%s" has the precedence prop but also included the %s.' +
' When using %s React will opt out of Resource behavior. If you meant for this' +
' element to be treated as a Resource remove the %s. Otherwise remove the precedence prop.%s',
'bar',
'onLoad prop',
'onLoad, onError, or disabled',
'onLoad prop',
componentStack(['link', 'head', 'html']),
);
expect(mockError).toHaveBeenCalledWith(
'Warning: A link (rel="stylesheet") element with href "%s" has the precedence prop but also included the %s.' +
' When using %s React will opt out of Resource behavior. If you meant for this' +
' element to be treated as a Resource remove the %s. Otherwise remove the precedence prop.%s',
'baz',
'onError prop',
'onLoad, onError, or disabled',
'onError prop',
componentStack(['link', 'head', 'html']),
);
expect(mockError).toHaveBeenCalledWith(
'Warning: A link (rel="stylesheet") element with href "%s" has the precedence prop but also included the %s.' +
' When using %s React will opt out of Resource behavior. If you meant for this' +
' element to be treated as a Resource remove the %s. Otherwise remove the precedence prop.%s',
'qux',
'disabled prop',
'onLoad, onError, or disabled',
'disabled prop',
componentStack(['link', 'head', 'html']),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
} finally {
console.error = originalConsoleError;
}
});
// @gate enableFloat
it('warns when script Resources have new or different values for props', async () => {
const originalConsoleError = console.error;
const mockError = jest.fn();
console.error = (...args) => {
mockError(...args.map(normalizeCodeLocInfo));
};
try {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head>
<script src="foo" async={true} data-foo="a current value" />
<script src="foo" async={true} data-foo="a new value" />
</head>
</html>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<script src="foo" async="" data-foo="a current value" />
</head>
<body />
</html>,
);
if (__DEV__) {
expect(mockError).toHaveBeenCalledTimes(1);
expect(mockError).toHaveBeenCalledWith(
'Warning: A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
' that were provided the first time they are encountered so any differences will be ignored. Please' +
' update Resources that share an %s to have props that agree. The differences are described below.%s%s',
'script Resource',
'src',
'foo',
'an earlier instance of this Resource',
'src',
'\n data-foo: "a new value" in latest props, "a current value" in original props',
componentStack(['script', 'head', 'html']),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
} finally {
console.error = originalConsoleError;
}
});
// @gate enableFloat
it('warns when preload Resources have new or different values for props', async () => {
const originalConsoleError = console.error;
const mockError = jest.fn();
console.error = (...args) => {
mockError(...args.map(normalizeCodeLocInfo));
};
try {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head>
<link
rel="preload"
as="style"
href="foo"
data-foo="a current value"
/>
<link
rel="preload"
as="style"
href="foo"
data-foo="a new value"
/>
<link
rel="preload"
as="style"
href="bar"
data-bar="a current value"
/>
<link
rel="preload"
as="font"
href="bar"
data-bar="a current value"
crossOrigin=""
/>
</head>
</html>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link
rel="preload"
as="style"
href="foo"
data-foo="a current value"
/>
<link
rel="preload"
as="style"
href="bar"
data-bar="a current value"
/>
</head>
<body />
</html>,
);
if (__DEV__) {
expect(mockError).toHaveBeenCalledTimes(2);
expect(mockError).toHaveBeenCalledWith(
'Warning: A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
' that were provided the first time they are encountered so any differences will be ignored. Please' +
' update Resources that share an %s to have props that agree. The differences are described below.%s%s',
'preload Resource (as "style")',
'href',
'foo',
'an earlier instance of this Resource',
'href',
'\n data-foo: "a new value" in latest props, "a current value" in original props',
componentStack(['link', 'head', 'html']),
);
expect(mockError).toHaveBeenCalledWith(
'Warning: A %s is using the same href "%s" as a %s. This is always an error and React will only keep the first preload' +
' for any given href, discarding subsequent instances. To fix, find where you are using this href in link' +
' tags or in calls to ReactDOM.preload() or ReactDOM.preinit() and either make the Resource types agree or' +
' update the hrefs to be distinct for different Resource types.%s',
'preload Resource (as "font")',
'bar',
'preload Resource (as "style")',
componentStack(['link', 'head', 'html']),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
} finally {
console.error = originalConsoleError;
}
});
// @gate enableFloat
it('warns when an existing preload Resource has certain specific different props from a style Resource of the same href', async () => {
const originalConsoleError = console.error;
const mockError = jest.fn();
console.error = (...args) => {
mockError(...args.map(normalizeCodeLocInfo));
};
try {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head>
<link
rel="preload"
as="style"
href="foo"
crossOrigin="preload value"
/>
<link
rel="stylesheet"
href="foo"
precedence="foo"
crossOrigin="style value"
/>
</head>
</html>,
);
pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link
rel="stylesheet"
href="foo"
data-precedence="foo"
crossorigin="style value"
/>
</head>
<body />
</html>,
);
if (__DEV__) {
expect(mockError).toHaveBeenCalledTimes(1);
expect(mockError).toHaveBeenCalledWith(
'Warning: A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
' that were provided the first time they are encountered so any differences will be ignored. Please' +
' update Resources that share an %s to have props that agree. The differences are described below.%s%s',
'style Resource',
'href',
'foo',
'a preload Resource (as "style") with the same href',
'href',
'\n crossOrigin: "style value" in latest props, "preload value" in original props',
componentStack(['link', 'head', 'html']),
);
} else {
expect(mockError).not.toHaveBeenCalled();
}
} finally {
console.error = originalConsoleError;
}
});
});
describe('escaping', () => {
// @gate enableFloat
it('escapes hrefs when selecting matching elements in the document when rendering Resources', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<link rel="preload" href="preload" as="style" />
<link rel="stylesheet" href="style" precedence="style" />
<link rel="stylesheet" href="with\slashes" precedence="style" />
<link rel="preload" href={'with\nnewline'} as="style" />
<div id="container" />
</body>
</html>,
);
pipe(writable);
});
container = document.getElementById('container');
const root = ReactDOMClient.createRoot(container);
root.render(
<div>
<link rel="preload" href={'preload"][rel="preload'} as="style" />
<link
rel="stylesheet"
href={'style"][rel="stylesheet'}
precedence="style"
/>
<link rel="stylesheet" href={'with\\slashes'} precedence="style" />
<link rel="preload" href={'with\nnewline'} as="style" />
foo
</div>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="style" data-precedence="style" />
<link
rel="stylesheet"
href="with\slashes"
data-precedence="style"
/>
<link
rel="stylesheet"
href={'style"][rel="stylesheet'}
data-precedence="style"
/>
<link rel="preload" as="style" href="preload" />
<link rel="preload" href={'with\nnewline'} as="style" />
<link rel="preload" href={'preload"][rel="preload'} as="style" />
<link rel="preload" href={'style"][rel="stylesheet'} as="style" />
</head>
<body>
<div id="container">
<div>foo</div>
</div>
</body>
</html>,
);
});
// @gate enableFloat
it('escapes hrefs when selecting matching elements in the document when using preload and preinit', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<link rel="preload" href="preload" as="style" />
<link rel="stylesheet" href="style" precedence="style" />
<link rel="stylesheet" href="with\slashes" precedence="style" />
<link rel="preload" href={'with\nnewline'} as="style" />
<div id="container" />
</body>
</html>,
);
pipe(writable);
});
function App() {
ReactDOM.preload('preload"][rel="preload', {as: 'style'});
ReactDOM.preinit('style"][rel="stylesheet', {
as: 'style',
precedence: 'style',
});
ReactDOM.preinit('with\\slashes', {
as: 'style',
precedence: 'style',
});
ReactDOM.preload('with\nnewline', {as: 'style'});
return <div>foo</div>;
}
container = document.getElementById('container');
const root = ReactDOMClient.createRoot(container);
root.render(<App />);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="stylesheet" href="style" data-precedence="style" />
<link
rel="stylesheet"
href="with\slashes"
data-precedence="style"
/>
<link
rel="stylesheet"
href={'style"][rel="stylesheet'}
data-precedence="style"
/>
<link rel="preload" as="style" href="preload" />
<link rel="preload" href={'with\nnewline'} as="style" />
<link rel="preload" href={'preload"][rel="preload'} as="style" />
</head>
<body>
<div id="container">
<div>foo</div>
</div>
</body>
</html>,
);
});
});
});