/**
* 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(
'
',
{
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(
<>
foo
hello world
>,
);
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(
foo
hello world
,
);
});
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 or ', async () => {
const root = ReactDOMClient.createRoot(document);
renderSafelyAndExpect(
root,
<>
foo
>,
).toErrorDev(
[
'Cannot render