/** * 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