@samitouri / QOS-React / commits / e5708b3ea9

[Tests][Fizz] Better HTML parsing behavior for Fizz tests (#26570)

In anticipation of making Fiber use the document global for dispatching Float methods that arrive from Flight I needed to update some tests that commonly recreated the JSDOM instance after importing react. This change updates a few tests to only create JSDOM once per test, before importing react-dom/client. Additionally the current act implementation for server streaming did not adequately model streaming semantics so I rewrite the act implementation in a way that better mirrors how a browser would parse incoming HTML. The new act implementation does the following 1. the first time it processes meaningful streamed content it figures out whether it is rendering into the existing document container or if it needs to reset the document. this is based on whether the streamed content contains tags `<html>` or `<body>` etc... 2. Once the streaming container is set it will typically continue to stream into that container for future calls to act. The exception is if the streaming container is the `<head>` in which case it will switch to streaming into the body once it receives a `<body>` tag. This means for tests that render something like a `<div>...</div>` it will naturally stream into the default `<div id="container">...` and for tests that render a full document the HTML will parse like a real browser would (with some very minor edge case differences) I also refactored the way we move nodes from buffered content into the document and execute any scripts we find. Previously we were using window.eval and I switched this to just setting the external script content as script text. Additionally the nonce logic is reworked to be a bit simpler.

Josh Story committed Apr 20, 2023 at 14:27 UTC e5708b3ea9190c1285c9081ff338e46be9ff39bc
3 files changed +759 -559
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+461 -374
@@ -10,7 +10,7 @@
10
11 'use strict';
12 import {
13 - replaceScriptsAndMove,
13 + insertNodesAndExecuteScripts,
14 mergeOptions,
15 stripExternalRuntimeInNodes,
16 withLoadingReadyState,
@@ -29,8 +29,6 @@ let useSyncExternalStoreWithSelector;
29 let use;
30 let PropTypes;
31 let textCache;
32 -let window;
33 -let document;
32 let writable;
33 let CSPnonce = null;
34 let container;
@@ -43,20 +41,32 @@ let waitForAll;
41 let assertLog;
42 let waitForPaint;
43 let clientAct;
46 -
47 -function resetJSDOM(markup) {
48 - // Test Environment
49 - const jsdom = new JSDOM(markup, {
50 - runScripts: 'dangerously',
51 - });
52 - window = jsdom.window;
53 - document = jsdom.window.document;
54 -}
44 +let streamingContainer;
45
46 describe('ReactDOMFizzServer', () => {
47 beforeEach(() => {
48 jest.resetModules();
49 JSDOM = require('jsdom').JSDOM;
50 +
51 + const jsdom = new JSDOM(
52 + '<!DOCTYPE html><html><head></head><body><div id="container">',
53 + {
54 + runScripts: 'dangerously',
55 + },
56 + );
57 + // We mock matchMedia. for simplicity it only matches 'all' or '' and misses everything else
58 + Object.defineProperty(jsdom.window, 'matchMedia', {
59 + writable: true,
60 + value: jest.fn().mockImplementation(query => ({
61 + matches: query === 'all' || query === '',
62 + media: query,
63 + })),
64 + });
65 + streamingContainer = null;
66 + global.window = jsdom.window;
67 + global.document = jsdom.window.document;
68 + container = document.getElementById('container');
69 +
70 Scheduler = require('scheduler');
71 React = require('react');
72 ReactDOMClient = require('react-dom/client');
@@ -93,9 +103,6 @@ describe('ReactDOMFizzServer', () => {
103
104 textCache = new Map();
105
96 - resetJSDOM('<!DOCTYPE html><html><head></head><body><div id="container">');
97 - container = document.getElementById('container');
98 -
106 buffer = '';
107 hasErrored = false;
108
@@ -140,6 +147,9 @@ describe('ReactDOMFizzServer', () => {
147 .join('');
148 }
149
150 + const bodyStartMatch = /<body(?:>| .*?>)/;
151 + const headStartMatch = /<head(?:>| .*?>)/;
152 +
153 async function act(callback) {
154 await callback();
155 // Await one turn around the event loop.
@@ -153,40 +163,123 @@ describe('ReactDOMFizzServer', () => {
163 // JSDOM doesn't support stream HTML parser so we need to give it a proper fragment.
164 // We also want to execute any scripts that are embedded.
165 // We assume that we have now received a proper fragment of HTML.
156 - const bufferedContent = buffer;
166 + let bufferedContent = buffer;
167 buffer = '';
158 - const fakeBody = document.createElement('body');
159 - fakeBody.innerHTML = bufferedContent;
160 - const parent =
161 - container.nodeName === '#document' ? container.body : container;
168
163 - await withLoadingReadyState(async () => {
164 - while (fakeBody.firstChild) {
165 - const node = fakeBody.firstChild;
166 - await replaceScriptsAndMove(window, CSPnonce, node, parent);
167 - }
168 - }, document);
169 - }
170 -
171 - async function actIntoEmptyDocument(callback) {
172 - await callback();
173 - // Await one turn around the event loop.
174 - // This assumes that we'll flush everything we have so far.
175 - await new Promise(resolve => {
176 - setImmediate(resolve);
177 - });
178 - if (hasErrored) {
179 - throw fatalError;
169 + if (!bufferedContent) {
170 + return;
171 }
181 - // JSDOM doesn't support stream HTML parser so we need to give it a proper fragment.
182 - // We also want to execute any scripts that are embedded.
183 - // We assume that we have now received a proper fragment of HTML.
184 - const bufferedContent = buffer;
185 - resetJSDOM(bufferedContent);
186 - container = document;
187 - buffer = '';
172 +
173 await withLoadingReadyState(async () => {
189 - await replaceScriptsAndMove(window, CSPnonce, document.documentElement);
174 + const bodyMatch = bufferedContent.match(bodyStartMatch);
175 + const headMatch = bufferedContent.match(headStartMatch);
176 +
177 + if (streamingContainer === null) {
178 + // This is the first streamed content. We decide here where to insert it. If we get <html>, <head>, or <body>
179 + // we abandon the pre-built document and start from scratch. If we get anything else we assume it goes into the
180 + // container. This is not really production behavior because you can't correctly stream into a deep div effectively
181 + // but it's pragmatic for tests.
182 +
183 + if (
184 + bufferedContent.startsWith('<head>') ||
185 + bufferedContent.startsWith('<head ') ||
186 + bufferedContent.startsWith('<body>') ||
187 + bufferedContent.startsWith('<body ')
188 + ) {
189 + // wrap in doctype to normalize the parsing process
190 + bufferedContent = '<!DOCTYPE html><html>' + bufferedContent;
191 + } else if (
192 + bufferedContent.startsWith('<html>') ||
193 + bufferedContent.startsWith('<html ')
194 + ) {
195 + throw new Error(
196 + 'Recieved <html> without a <!DOCTYPE html> which is almost certainly a bug in React',
197 + );
198 + }
199 +
200 + if (bufferedContent.startsWith('<!DOCTYPE html>')) {
201 + // we can just use the whole document
202 + const tempDom = new JSDOM(bufferedContent);
203 +
204 + // Wipe existing head and body content
205 + document.head.innerHTML = '';
206 + document.body.innerHTML = '';
207 +
208 + // Copy the <html> attributes over
209 + const tempHtmlNode = tempDom.window.document.documentElement;
210 + for (let i = 0; i < tempHtmlNode.attributes.length; i++) {
211 + const attr = tempHtmlNode.attributes[i];
212 + document.documentElement.setAttribute(attr.name, attr.value);
213 + }
214 +
215 + if (headMatch) {
216 + // We parsed a head open tag. we need to copy head attributes and insert future
217 + // content into <head>
218 + streamingContainer = document.head;
219 + const tempHeadNode = tempDom.window.document.head;
220 + for (let i = 0; i < tempHeadNode.attributes.length; i++) {
221 + const attr = tempHeadNode.attributes[i];
222 + document.head.setAttribute(attr.name, attr.value);
223 + }
224 + const source = document.createElement('head');
225 + source.innerHTML = tempHeadNode.innerHTML;
226 + await insertNodesAndExecuteScripts(source, document.head, CSPnonce);
227 + }
228 +
229 + if (bodyMatch) {
230 + // We parsed a body open tag. we need to copy head attributes and insert future
231 + // content into <body>
232 + streamingContainer = document.body;
233 + const tempBodyNode = tempDom.window.document.body;
234 + for (let i = 0; i < tempBodyNode.attributes.length; i++) {
235 + const attr = tempBodyNode.attributes[i];
236 + document.body.setAttribute(attr.name, attr.value);
237 + }
238 + const source = document.createElement('body');
239 + source.innerHTML = tempBodyNode.innerHTML;
240 + await insertNodesAndExecuteScripts(source, document.body, CSPnonce);
241 + }
242 +
243 + if (!headMatch && !bodyMatch) {
244 + throw new Error('expected <head> or <body> after <html>');
245 + }
246 + } else {
247 + // we assume we are streaming into the default container'
248 + streamingContainer = container;
249 + const div = document.createElement('div');
250 + div.innerHTML = bufferedContent;
251 + await insertNodesAndExecuteScripts(div, container, CSPnonce);
252 + }
253 + } else if (streamingContainer === document.head) {
254 + bufferedContent = '<!DOCTYPE html><html><head>' + bufferedContent;
255 + const tempDom = new JSDOM(bufferedContent);
256 +
257 + const tempHeadNode = tempDom.window.document.head;
258 + const source = document.createElement('head');
259 + source.innerHTML = tempHeadNode.innerHTML;
260 + await insertNodesAndExecuteScripts(source, document.head, CSPnonce);
261 +
262 + if (bodyMatch) {
263 + streamingContainer = document.body;
264 +
265 + const tempBodyNode = tempDom.window.document.body;
266 + for (let i = 0; i < tempBodyNode.attributes.length; i++) {
267 + const attr = tempBodyNode.attributes[i];
268 + document.body.setAttribute(attr.name, attr.value);
269 + }
270 + const bodySource = document.createElement('body');
271 + bodySource.innerHTML = tempBodyNode.innerHTML;
272 + await insertNodesAndExecuteScripts(
273 + bodySource,
274 + document.body,
275 + CSPnonce,
276 + );
277 + }
278 + } else {
279 + const div = document.createElement('div');
280 + div.innerHTML = bufferedContent;
281 + await insertNodesAndExecuteScripts(div, streamingContainer, CSPnonce);
282 + }
283 }, document);
284 }
285
@@ -3467,7 +3560,7 @@ describe('ReactDOMFizzServer', () => {
3560 });
3561
3562 it('accepts an integrity property for bootstrapScripts and bootstrapModules', async () => {
3470 - await actIntoEmptyDocument(() => {
3563 + await act(() => {
3564 const {pipe} = renderToPipeableStream(
3565 <html>
3566 <head />
@@ -3584,7 +3677,7 @@ describe('ReactDOMFizzServer', () => {
3677
3678 // @gate enableFizzExternalRuntime
3679 it('supports option to load runtime as an external script', async () => {
3587 - await actIntoEmptyDocument(() => {
3680 + await act(() => {
3681 const {pipe} = renderToPipeableStream(
3682 <html>
3683 <head />
@@ -3631,7 +3724,7 @@ describe('ReactDOMFizzServer', () => {
3724 </div>
3725 );
3726 }
3634 - await actIntoEmptyDocument(() => {
3727 + await act(() => {
3728 const {pipe} = renderToPipeableStream(<App />);
3729 pipe(writable);
3730 });
@@ -3644,7 +3737,7 @@ describe('ReactDOMFizzServer', () => {
3737 });
3738
3739 it('does not send the external runtime for static pages', async () => {
3647 - await actIntoEmptyDocument(() => {
3740 + await act(() => {
3741 const {pipe} = renderToPipeableStream(
3742 <html>
3743 <head />
@@ -4446,7 +4539,7 @@ describe('ReactDOMFizzServer', () => {
4539 </body>
4540 );
4541 }
4449 - await actIntoEmptyDocument(() => {
4542 + await act(() => {
4543 const {pipe} = renderToPipeableStream(
4544 <html data-html="html">
4545 <AsyncNoOutput />
@@ -4456,17 +4549,13 @@ describe('ReactDOMFizzServer', () => {
4549 );
4550 pipe(writable);
4551 });
4459 - await actIntoEmptyDocument(() => {
4552 + await act(() => {
4553 resolveText('body');
4554 });
4462 - await actIntoEmptyDocument(() => {
4555 + await act(() => {
4556 resolveText('nooutput');
4557 });
4465 - // We need to use actIntoEmptyDocument because act assumes that buffered
4466 - // content should be fake streamed into the body which is normally true
4467 - // but in this test the entire shell was delayed and we need the initial
4468 - // construction to be done to get the parsing right
4469 - await actIntoEmptyDocument(() => {
4558 + await act(() => {
4559 resolveText('head');
4560 });
4561 expect(getVisibleChildren(document)).toEqual(
@@ -4487,7 +4576,7 @@ describe('ReactDOMFizzServer', () => {
4576 chunks.push(chunk);
4577 });
4578
4490 - await actIntoEmptyDocument(() => {
4579 + await act(() => {
4580 const {pipe} = renderToPipeableStream(
4581 <html>
4582 <head />
@@ -4953,23 +5042,21 @@ describe('ReactDOMFizzServer', () => {
5042 });
5043
5044 describe('title children', () => {
4956 - function prepareJSDOMForTitle() {
4957 - resetJSDOM('<!DOCTYPE html><html><head>\u0000');
4958 - container = document.getElementsByTagName('head')[0];
4959 - }
4960 -
5045 it('should accept a single string child', async () => {
5046 // a Single string child
5047 function App() {
4964 - return <title>hello</title>;
5048 + return (
5049 + <head>
5050 + <title>hello</title>
5051 + </head>
5052 + );
5053 }
5054
4967 - prepareJSDOMForTitle();
5055 await act(() => {
5056 const {pipe} = renderToPipeableStream(<App />);
5057 pipe(writable);
5058 });
4972 - expect(getVisibleChildren(container)).toEqual(<title>hello</title>);
5059 + expect(getVisibleChildren(document.head)).toEqual(<title>hello</title>);
5060
5061 const errors = [];
5062 ReactDOMClient.hydrateRoot(container, <App />, {
@@ -4979,21 +5066,24 @@ describe('ReactDOMFizzServer', () => {
5066 });
5067 await waitForAll([]);
5068 expect(errors).toEqual([]);
4982 - expect(getVisibleChildren(container)).toEqual(<title>hello</title>);
5069 + expect(getVisibleChildren(document.head)).toEqual(<title>hello</title>);
5070 });
5071
5072 it('should accept children array of length 1 containing a string', async () => {
5073 // a Single string child
5074 function App() {
4988 - return <title>{['hello']}</title>;
5075 + return (
5076 + <head>
5077 + <title>{['hello']}</title>
5078 + </head>
5079 + );
5080 }
5081
4991 - prepareJSDOMForTitle();
5082 await act(() => {
5083 const {pipe} = renderToPipeableStream(<App />);
5084 pipe(writable);
5085 });
4996 - expect(getVisibleChildren(container)).toEqual(<title>hello</title>);
5086 + expect(getVisibleChildren(document.head)).toEqual(<title>hello</title>);
5087
5088 const errors = [];
5089 ReactDOMClient.hydrateRoot(container, <App />, {
@@ -5003,16 +5093,18 @@ describe('ReactDOMFizzServer', () => {
5093 });
5094 await waitForAll([]);
5095 expect(errors).toEqual([]);
5006 - expect(getVisibleChildren(container)).toEqual(<title>hello</title>);
5096 + expect(getVisibleChildren(document.head)).toEqual(<title>hello</title>);
5097 });
5098
5099 it('should warn in dev when given an array of length 2 or more', async () => {
5100 function App() {
5011 - return <title>{['hello1', 'hello2']}</title>;
5101 + return (
5102 + <head>
5103 + <title>{['hello1', 'hello2']}</title>
5104 + </head>
5105 + );
5106 }
5107
5014 - prepareJSDOMForTitle();
5015 -
5108 await expect(async () => {
5109 await act(() => {
5110 const {pipe} = renderToPipeableStream(<App />);
@@ -5023,15 +5115,15 @@ describe('ReactDOMFizzServer', () => {
5115 ]);
5116
5117 if (gate(flags => flags.enableFloat)) {
5026 - expect(getVisibleChildren(container)).toEqual(<title />);
5118 + expect(getVisibleChildren(document.head)).toEqual(<title />);
5119 } else {
5028 - expect(getVisibleChildren(container)).toEqual(
5120 + expect(getVisibleChildren(document.head)).toEqual(
5121 <title>{'hello1<!-- -->hello2'}</title>,
5122 );
5123 }
5124
5125 const errors = [];
5034 - ReactDOMClient.hydrateRoot(container, <App />, {
5126 + ReactDOMClient.hydrateRoot(document.head, <App />, {
5127 onRecoverableError(error) {
5128 errors.push(error.message);
5129 },
@@ -5040,7 +5132,7 @@ describe('ReactDOMFizzServer', () => {
5132 if (gate(flags => flags.enableFloat)) {
5133 expect(errors).toEqual([]);
5134 // with float, the title doesn't render on the client or on the server
5043 - expect(getVisibleChildren(container)).toEqual(<title />);
5135 + expect(getVisibleChildren(document.head)).toEqual(<title />);
5136 } else {
5137 expect(errors).toEqual(
5138 [
@@ -5051,7 +5143,7 @@ describe('ReactDOMFizzServer', () => {
5143 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
5144 ].filter(Boolean),
5145 );
5054 - expect(getVisibleChildren(container)).toEqual(
5146 + expect(getVisibleChildren(document.head)).toEqual(
5147 <title>{['hello1', 'hello2']}</title>,
5148 );
5149 }
@@ -5064,16 +5156,14 @@ describe('ReactDOMFizzServer', () => {
5156
5157 function App() {
5158 return (
5067 - <>
5159 + <head>
5160 <title>
5161 <IndirectTitle />
5162 </title>
5071 - </>
5163 + </head>
5164 );
5165 }
5166
5075 - prepareJSDOMForTitle();
5076 -
5167 if (gate(flags => flags.enableFloat)) {
5168 await expect(async () => {
5169 await act(() => {
@@ -5096,15 +5186,15 @@ describe('ReactDOMFizzServer', () => {
5186
5187 if (gate(flags => flags.enableFloat)) {
5188 // object titles are toStringed when float is on
5099 - expect(getVisibleChildren(container)).toEqual(
5189 + expect(getVisibleChildren(document.head)).toEqual(
5190 <title>{'[object Object]'}</title>,
5191 );
5192 } else {
5103 - expect(getVisibleChildren(container)).toEqual(<title>hello</title>);
5193 + expect(getVisibleChildren(document.head)).toEqual(<title>hello</title>);
5194 }
5195
5196 const errors = [];
5107 - ReactDOMClient.hydrateRoot(container, <App />, {
5197 + ReactDOMClient.hydrateRoot(document.head, <App />, {
5198 onRecoverableError(error) {
5199 errors.push(error.message);
5200 },
@@ -5113,344 +5203,341 @@ describe('ReactDOMFizzServer', () => {
5203 expect(errors).toEqual([]);
5204 if (gate(flags => flags.enableFloat)) {
5205 // object titles are toStringed when float is on
5116 - expect(getVisibleChildren(container)).toEqual(
5206 + expect(getVisibleChildren(document.head)).toEqual(
5207 <title>{'[object Object]'}</title>,
5208 );
5209 } else {
5120 - expect(getVisibleChildren(container)).toEqual(<title>hello</title>);
5210 + expect(getVisibleChildren(document.head)).toEqual(<title>hello</title>);
5211 }
5212 });
5213 + });
5214
5124 - // @gate enableUseHook
5125 - it('basic use(promise)', async () => {
5126 - const promiseA = Promise.resolve('A');
5127 - const promiseB = Promise.resolve('B');
5128 - const promiseC = Promise.resolve('C');
5215 + // @gate enableUseHook
5216 + it('basic use(promise)', async () => {
5217 + const promiseA = Promise.resolve('A');
5218 + const promiseB = Promise.resolve('B');
5219 + const promiseC = Promise.resolve('C');
5220
5130 - function Async() {
5131 - return use(promiseA) + use(promiseB) + use(promiseC);
5132 - }
5221 + function Async() {
5222 + return use(promiseA) + use(promiseB) + use(promiseC);
5223 + }
5224
5134 - function App() {
5135 - return (
5136 - <Suspense fallback="Loading...">
5137 - <Async />
5138 - </Suspense>
5139 - );
5140 - }
5225 + function App() {
5226 + return (
5227 + <Suspense fallback="Loading...">
5228 + <Async />
5229 + </Suspense>
5230 + );
5231 + }
5232
5142 - await act(() => {
5143 - const {pipe} = renderToPipeableStream(<App />);
5144 - pipe(writable);
5145 - });
5233 + await act(() => {
5234 + const {pipe} = renderToPipeableStream(<App />);
5235 + pipe(writable);
5236 + });
5237
5147 - // TODO: The `act` implementation in this file doesn't unwrap microtasks
5148 - // automatically. We can't use the same `act` we use for Fiber tests
5149 - // because that relies on the mock Scheduler. Doesn't affect any public
5150 - // API but we might want to fix this for our own internal tests.
5151 - //
5152 - // For now, wait for each promise in sequence.
5153 - await act(async () => {
5154 - await promiseA;
5155 - });
5156 - await act(async () => {
5157 - await promiseB;
5158 - });
5159 - await act(async () => {
5160 - await promiseC;
5161 - });
5238 + // TODO: The `act` implementation in this file doesn't unwrap microtasks
5239 + // automatically. We can't use the same `act` we use for Fiber tests
5240 + // because that relies on the mock Scheduler. Doesn't affect any public
5241 + // API but we might want to fix this for our own internal tests.
5242 + //
5243 + // For now, wait for each promise in sequence.
5244 + await act(async () => {
5245 + await promiseA;
5246 + });
5247 + await act(async () => {
5248 + await promiseB;
5249 + });
5250 + await act(async () => {
5251 + await promiseC;
5252 + });
5253
5163 - expect(getVisibleChildren(container)).toEqual('ABC');
5254 + expect(getVisibleChildren(container)).toEqual('ABC');
5255
5165 - ReactDOMClient.hydrateRoot(container, <App />);
5166 - await waitForAll([]);
5167 - expect(getVisibleChildren(container)).toEqual('ABC');
5168 - });
5256 + ReactDOMClient.hydrateRoot(container, <App />);
5257 + await waitForAll([]);
5258 + expect(getVisibleChildren(container)).toEqual('ABC');
5259 + });
5260
5170 - // @gate enableUseHook
5171 - it('basic use(context)', async () => {
5172 - const ContextA = React.createContext('default');
5173 - const ContextB = React.createContext('B');
5174 - const ServerContext = React.createServerContext(
5175 - 'ServerContext',
5176 - 'default',
5261 + // @gate enableUseHook
5262 + it('basic use(context)', async () => {
5263 + const ContextA = React.createContext('default');
5264 + const ContextB = React.createContext('B');
5265 + const ServerContext = React.createServerContext('ServerContext', 'default');
5266 + function Client() {
5267 + return use(ContextA) + use(ContextB);
5268 + }
5269 + function ServerComponent() {
5270 + return use(ServerContext);
5271 + }
5272 + function Server() {
5273 + return (
5274 + <ServerContext.Provider value="C">
5275 + <ServerComponent />
5276 + </ServerContext.Provider>
5277 );
5178 - function Client() {
5179 - return use(ContextA) + use(ContextB);
5180 - }
5181 - function ServerComponent() {
5182 - return use(ServerContext);
5183 - }
5184 - function Server() {
5185 - return (
5186 - <ServerContext.Provider value="C">
5187 - <ServerComponent />
5188 - </ServerContext.Provider>
5189 - );
5190 - }
5191 - function App() {
5192 - return (
5193 - <>
5194 - <ContextA.Provider value="A">
5195 - <Client />
5196 - </ContextA.Provider>
5197 - <Server />
5198 - </>
5199 - );
5200 - }
5201 -
5202 - await act(() => {
5203 - const {pipe} = renderToPipeableStream(<App />);
5204 - pipe(writable);
5205 - });
5206 - expect(getVisibleChildren(container)).toEqual(['AB', 'C']);
5278 + }
5279 + function App() {
5280 + return (
5281 + <>
5282 + <ContextA.Provider value="A">
5283 + <Client />
5284 + </ContextA.Provider>
5285 + <Server />
5286 + </>
5287 + );
5288 + }
5289
5208 - // Hydration uses a different renderer runtime (Fiber instead of Fizz).
5209 - // We reset _currentRenderer here to not trigger a warning about multiple
5210 - // renderers concurrently using these contexts
5211 - ContextA._currentRenderer = null;
5212 - ServerContext._currentRenderer = null;
5213 - ReactDOMClient.hydrateRoot(container, <App />);
5214 - await waitForAll([]);
5215 - expect(getVisibleChildren(container)).toEqual(['AB', 'C']);
5290 + await act(() => {
5291 + const {pipe} = renderToPipeableStream(<App />);
5292 + pipe(writable);
5293 });
5294 + expect(getVisibleChildren(container)).toEqual(['AB', 'C']);
5295
5218 - // @gate enableUseHook
5219 - it('use(promise) in multiple components', async () => {
5220 - const promiseA = Promise.resolve('A');
5221 - const promiseB = Promise.resolve('B');
5222 - const promiseC = Promise.resolve('C');
5223 - const promiseD = Promise.resolve('D');
5224 -
5225 - function Child({prefix}) {
5226 - return prefix + use(promiseC) + use(promiseD);
5227 - }
5296 + // Hydration uses a different renderer runtime (Fiber instead of Fizz).
5297 + // We reset _currentRenderer here to not trigger a warning about multiple
5298 + // renderers concurrently using these contexts
5299 + ContextA._currentRenderer = null;
5300 + ServerContext._currentRenderer = null;
5301 + ReactDOMClient.hydrateRoot(container, <App />);
5302 + await waitForAll([]);
5303 + expect(getVisibleChildren(container)).toEqual(['AB', 'C']);
5304 + });
5305
5229 - function Parent() {
5230 - return <Child prefix={use(promiseA) + use(promiseB)} />;
5231 - }
5306 + // @gate enableUseHook
5307 + it('use(promise) in multiple components', async () => {
5308 + const promiseA = Promise.resolve('A');
5309 + const promiseB = Promise.resolve('B');
5310 + const promiseC = Promise.resolve('C');
5311 + const promiseD = Promise.resolve('D');
5312
5233 - function App() {
5234 - return (
5235 - <Suspense fallback="Loading...">
5236 - <Parent />
5237 - </Suspense>
5238 - );
5239 - }
5313 + function Child({prefix}) {
5314 + return prefix + use(promiseC) + use(promiseD);
5315 + }
5316
5241 - await act(() => {
5242 - const {pipe} = renderToPipeableStream(<App />);
5243 - pipe(writable);
5244 - });
5317 + function Parent() {
5318 + return <Child prefix={use(promiseA) + use(promiseB)} />;
5319 + }
5320
5246 - // TODO: The `act` implementation in this file doesn't unwrap microtasks
5247 - // automatically. We can't use the same `act` we use for Fiber tests
5248 - // because that relies on the mock Scheduler. Doesn't affect any public
5249 - // API but we might want to fix this for our own internal tests.
5250 - //
5251 - // For now, wait for each promise in sequence.
5252 - await act(async () => {
5253 - await promiseA;
5254 - });
5255 - await act(async () => {
5256 - await promiseB;
5257 - });
5258 - await act(async () => {
5259 - await promiseC;
5260 - });
5261 - await act(async () => {
5262 - await promiseD;
5263 - });
5321 + function App() {
5322 + return (
5323 + <Suspense fallback="Loading...">
5324 + <Parent />
5325 + </Suspense>
5326 + );
5327 + }
5328
5265 - expect(getVisibleChildren(container)).toEqual('ABCD');
5329 + await act(() => {
5330 + const {pipe} = renderToPipeableStream(<App />);
5331 + pipe(writable);
5332 + });
5333
5267 - ReactDOMClient.hydrateRoot(container, <App />);
5268 - await waitForAll([]);
5269 - expect(getVisibleChildren(container)).toEqual('ABCD');
5334 + // TODO: The `act` implementation in this file doesn't unwrap microtasks
5335 + // automatically. We can't use the same `act` we use for Fiber tests
5336 + // because that relies on the mock Scheduler. Doesn't affect any public
5337 + // API but we might want to fix this for our own internal tests.
5338 + //
5339 + // For now, wait for each promise in sequence.
5340 + await act(async () => {
5341 + await promiseA;
5342 + });
5343 + await act(async () => {
5344 + await promiseB;
5345 + });
5346 + await act(async () => {
5347 + await promiseC;
5348 + });
5349 + await act(async () => {
5350 + await promiseD;
5351 });
5352
5272 - // @gate enableUseHook
5273 - it('using a rejected promise will throw', async () => {
5274 - const promiseA = Promise.resolve('A');
5275 - const promiseB = Promise.reject(new Error('Oops!'));
5276 - const promiseC = Promise.resolve('C');
5353 + expect(getVisibleChildren(container)).toEqual('ABCD');
5354
5278 - // Jest/Node will raise an unhandled rejected error unless we await this. It
5279 - // works fine in the browser, though.
5280 - await expect(promiseB).rejects.toThrow('Oops!');
5355 + ReactDOMClient.hydrateRoot(container, <App />);
5356 + await waitForAll([]);
5357 + expect(getVisibleChildren(container)).toEqual('ABCD');
5358 + });
5359
5282 - function Async() {
5283 - return use(promiseA) + use(promiseB) + use(promiseC);
5284 - }
5360 + // @gate enableUseHook
5361 + it('using a rejected promise will throw', async () => {
5362 + const promiseA = Promise.resolve('A');
5363 + const promiseB = Promise.reject(new Error('Oops!'));
5364 + const promiseC = Promise.resolve('C');
5365
5286 - class ErrorBoundary extends React.Component {
5287 - state = {error: null};
5288 - static getDerivedStateFromError(error) {
5289 - return {error};
5290 - }
5291 - render() {
5292 - if (this.state.error) {
5293 - return this.state.error.message;
5294 - }
5295 - return this.props.children;
5366 + // Jest/Node will raise an unhandled rejected error unless we await this. It
5367 + // works fine in the browser, though.
5368 + await expect(promiseB).rejects.toThrow('Oops!');
5369 +
5370 + function Async() {
5371 + return use(promiseA) + use(promiseB) + use(promiseC);
5372 + }
5373 +
5374 + class ErrorBoundary extends React.Component {
5375 + state = {error: null};
5376 + static getDerivedStateFromError(error) {
5377 + return {error};
5378 + }
5379 + render() {
5380 + if (this.state.error) {
5381 + return this.state.error.message;
5382 }
5383 + return this.props.children;
5384 }
5385 + }
5386
5299 - function App() {
5300 - return (
5301 - <Suspense fallback="Loading...">
5302 - <ErrorBoundary>
5303 - <Async />
5304 - </ErrorBoundary>
5305 - </Suspense>
5306 - );
5307 - }
5387 + function App() {
5388 + return (
5389 + <Suspense fallback="Loading...">
5390 + <ErrorBoundary>
5391 + <Async />
5392 + </ErrorBoundary>
5393 + </Suspense>
5394 + );
5395 + }
5396
5309 - const reportedServerErrors = [];
5310 - await act(() => {
5311 - const {pipe} = renderToPipeableStream(<App />, {
5312 - onError(error) {
5313 - reportedServerErrors.push(error);
5314 - },
5315 - });
5316 - pipe(writable);
5397 + const reportedServerErrors = [];
5398 + await act(() => {
5399 + const {pipe} = renderToPipeableStream(<App />, {
5400 + onError(error) {
5401 + reportedServerErrors.push(error);
5402 + },
5403 });
5404 + pipe(writable);
5405 + });
5406
5319 - // TODO: The `act` implementation in this file doesn't unwrap microtasks
5320 - // automatically. We can't use the same `act` we use for Fiber tests
5321 - // because that relies on the mock Scheduler. Doesn't affect any public
5322 - // API but we might want to fix this for our own internal tests.
5323 - //
5324 - // For now, wait for each promise in sequence.
5325 - await act(async () => {
5326 - await promiseA;
5327 - });
5328 - await act(async () => {
5329 - await expect(promiseB).rejects.toThrow('Oops!');
5330 - });
5331 - await act(async () => {
5332 - await promiseC;
5333 - });
5407 + // TODO: The `act` implementation in this file doesn't unwrap microtasks
5408 + // automatically. We can't use the same `act` we use for Fiber tests
5409 + // because that relies on the mock Scheduler. Doesn't affect any public
5410 + // API but we might want to fix this for our own internal tests.
5411 + //
5412 + // For now, wait for each promise in sequence.
5413 + await act(async () => {
5414 + await promiseA;
5415 + });
5416 + await act(async () => {
5417 + await expect(promiseB).rejects.toThrow('Oops!');
5418 + });
5419 + await act(async () => {
5420 + await promiseC;
5421 + });
5422
5335 - expect(getVisibleChildren(container)).toEqual('Loading...');
5336 - expect(reportedServerErrors.length).toBe(1);
5337 - expect(reportedServerErrors[0].message).toBe('Oops!');
5423 + expect(getVisibleChildren(container)).toEqual('Loading...');
5424 + expect(reportedServerErrors.length).toBe(1);
5425 + expect(reportedServerErrors[0].message).toBe('Oops!');
5426
5339 - const reportedClientErrors = [];
5340 - ReactDOMClient.hydrateRoot(container, <App />, {
5341 - onRecoverableError(error) {
5342 - reportedClientErrors.push(error);
5343 - },
5344 - });
5345 - await waitForAll([]);
5346 - expect(getVisibleChildren(container)).toEqual('Oops!');
5347 - expect(reportedClientErrors.length).toBe(1);
5348 - if (__DEV__) {
5349 - expect(reportedClientErrors[0].message).toBe('Oops!');
5350 - } else {
5351 - expect(reportedClientErrors[0].message).toBe(
5352 - 'The server could not finish this Suspense boundary, likely due to ' +
5353 - 'an error during server rendering. Switched to client rendering.',
5354 - );
5355 - }
5427 + const reportedClientErrors = [];
5428 + ReactDOMClient.hydrateRoot(container, <App />, {
5429 + onRecoverableError(error) {
5430 + reportedClientErrors.push(error);
5431 + },
5432 });
5433 + await waitForAll([]);
5434 + expect(getVisibleChildren(container)).toEqual('Oops!');
5435 + expect(reportedClientErrors.length).toBe(1);
5436 + if (__DEV__) {
5437 + expect(reportedClientErrors[0].message).toBe('Oops!');
5438 + } else {
5439 + expect(reportedClientErrors[0].message).toBe(
5440 + 'The server could not finish this Suspense boundary, likely due to ' +
5441 + 'an error during server rendering. Switched to client rendering.',
5442 + );
5443 + }
5444 + });
5445
5358 - // @gate enableUseHook
5359 - it("use a promise that's already been instrumented and resolved", async () => {
5360 - const thenable = {
5361 - status: 'fulfilled',
5362 - value: 'Hi',
5363 - then() {},
5364 - };
5365 -
5366 - // This will never suspend because the thenable already resolved
5367 - function App() {
5368 - return use(thenable);
5369 - }
5446 + // @gate enableUseHook
5447 + it("use a promise that's already been instrumented and resolved", async () => {
5448 + const thenable = {
5449 + status: 'fulfilled',
5450 + value: 'Hi',
5451 + then() {},
5452 + };
5453
5371 - await act(() => {
5372 - const {pipe} = renderToPipeableStream(<App />);
5373 - pipe(writable);
5374 - });
5375 - expect(getVisibleChildren(container)).toEqual('Hi');
5454 + // This will never suspend because the thenable already resolved
5455 + function App() {
5456 + return use(thenable);
5457 + }
5458
5377 - ReactDOMClient.hydrateRoot(container, <App />);
5378 - await waitForAll([]);
5379 - expect(getVisibleChildren(container)).toEqual('Hi');
5459 + await act(() => {
5460 + const {pipe} = renderToPipeableStream(<App />);
5461 + pipe(writable);
5462 });
5463 + expect(getVisibleChildren(container)).toEqual('Hi');
5464
5382 - // @gate enableUseHook
5383 - it('unwraps thenable that fulfills synchronously without suspending', async () => {
5384 - function App() {
5385 - const thenable = {
5386 - then(resolve) {
5387 - // This thenable immediately resolves, synchronously, without waiting
5388 - // a microtask.
5389 - resolve('Hi');
5390 - },
5391 - };
5392 - try {
5393 - return <Text text={use(thenable)} />;
5394 - } catch {
5395 - throw new Error(
5396 - '`use` should not suspend because the thenable resolved synchronously.',
5397 - );
5398 - }
5465 + ReactDOMClient.hydrateRoot(container, <App />);
5466 + await waitForAll([]);
5467 + expect(getVisibleChildren(container)).toEqual('Hi');
5468 + });
5469 +
5470 + // @gate enableUseHook
5471 + it('unwraps thenable that fulfills synchronously without suspending', async () => {
5472 + function App() {
5473 + const thenable = {
5474 + then(resolve) {
5475 + // This thenable immediately resolves, synchronously, without waiting
5476 + // a microtask.
5477 + resolve('Hi');
5478 + },
5479 + };
5480 + try {
5481 + return <Text text={use(thenable)} />;
5482 + } catch {
5483 + throw new Error(
5484 + '`use` should not suspend because the thenable resolved synchronously.',
5485 + );
5486 }
5400 - // Because the thenable resolves synchronously, we should be able to finish
5401 - // rendering synchronously, with no fallback.
5402 - await act(() => {
5403 - const {pipe} = renderToPipeableStream(<App />);
5404 - pipe(writable);
5405 - });
5406 - expect(getVisibleChildren(container)).toEqual('Hi');
5487 + }
5488 + // Because the thenable resolves synchronously, we should be able to finish
5489 + // rendering synchronously, with no fallback.
5490 + await act(() => {
5491 + const {pipe} = renderToPipeableStream(<App />);
5492 + pipe(writable);
5493 });
5494 + expect(getVisibleChildren(container)).toEqual('Hi');
5495 + });
5496
5409 - it('promise as node', async () => {
5410 - const promise = Promise.resolve('Hi');
5411 - await act(async () => {
5412 - const {pipe} = renderToPipeableStream(promise);
5413 - pipe(writable);
5414 - });
5415 -
5416 - // TODO: The `act` implementation in this file doesn't unwrap microtasks
5417 - // automatically. We can't use the same `act` we use for Fiber tests
5418 - // because that relies on the mock Scheduler. Doesn't affect any public
5419 - // API but we might want to fix this for our own internal tests.
5420 - await act(async () => {
5421 - await promise;
5422 - });
5423 -
5424 - expect(getVisibleChildren(container)).toEqual('Hi');
5497 + it('promise as node', async () => {
5498 + const promise = Promise.resolve('Hi');
5499 + await act(async () => {
5500 + const {pipe} = renderToPipeableStream(promise);
5501 + pipe(writable);
5502 });
5503
5427 - it('context as node', async () => {
5428 - const Context = React.createContext('Hi');
5429 - await act(async () => {
5430 - const {pipe} = renderToPipeableStream(Context);
5431 - pipe(writable);
5432 - });
5433 - expect(getVisibleChildren(container)).toEqual('Hi');
5504 + // TODO: The `act` implementation in this file doesn't unwrap microtasks
5505 + // automatically. We can't use the same `act` we use for Fiber tests
5506 + // because that relies on the mock Scheduler. Doesn't affect any public
5507 + // API but we might want to fix this for our own internal tests.
5508 + await act(async () => {
5509 + await promise;
5510 });
5511
5436 - it('recursive Usable as node', async () => {
5437 - const Context = React.createContext('Hi');
5438 - const promiseForContext = Promise.resolve(Context);
5439 - await act(async () => {
5440 - const {pipe} = renderToPipeableStream(promiseForContext);
5441 - pipe(writable);
5442 - });
5512 + expect(getVisibleChildren(container)).toEqual('Hi');
5513 + });
5514
5444 - // TODO: The `act` implementation in this file doesn't unwrap microtasks
5445 - // automatically. We can't use the same `act` we use for Fiber tests
5446 - // because that relies on the mock Scheduler. Doesn't affect any public
5447 - // API but we might want to fix this for our own internal tests.
5448 - await act(async () => {
5449 - await promiseForContext;
5450 - });
5515 + it('context as node', async () => {
5516 + const Context = React.createContext('Hi');
5517 + await act(async () => {
5518 + const {pipe} = renderToPipeableStream(Context);
5519 + pipe(writable);
5520 + });
5521 + expect(getVisibleChildren(container)).toEqual('Hi');
5522 + });
5523
5452 - expect(getVisibleChildren(container)).toEqual('Hi');
5524 + it('recursive Usable as node', async () => {
5525 + const Context = React.createContext('Hi');
5526 + const promiseForContext = Promise.resolve(Context);
5527 + await act(async () => {
5528 + const {pipe} = renderToPipeableStream(promiseForContext);
5529 + pipe(writable);
5530 });
5531 +
5532 + // TODO: The `act` implementation in this file doesn't unwrap microtasks
5533 + // automatically. We can't use the same `act` we use for Fiber tests
5534 + // because that relies on the mock Scheduler. Doesn't affect any public
5535 + // API but we might want to fix this for our own internal tests.
5536 + await act(async () => {
5537 + await promiseForContext;
5538 + });
5539 +
5540 + expect(getVisibleChildren(container)).toEqual('Hi');
5541 });
5542
5543 describe('useEffectEvent', () => {
@@ -5555,7 +5642,7 @@ describe('ReactDOMFizzServer', () => {
5642 });
5643
5644 it('can render scripts with simple children', async () => {
5558 - await actIntoEmptyDocument(async () => {
5645 + await act(async () => {
5646 const {pipe} = renderToPipeableStream(
5647 <html>
5648 <body>
@@ -5583,7 +5670,7 @@ describe('ReactDOMFizzServer', () => {
5670 };
5671
5672 try {
5586 - await actIntoEmptyDocument(async () => {
5673 + await act(async () => {
5674 const {pipe} = renderToPipeableStream(
5675 <html>
5676 <body>
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+212 -131
@@ -10,7 +10,7 @@
10
11 'use strict';
12 import {
13 - replaceScriptsAndMove,
13 + insertNodesAndExecuteScripts,
14 mergeOptions,
15 withLoadingReadyState,
16 } from '../test-utils/FizzTestUtils';
@@ -24,8 +24,6 @@ let ReactDOMFizzServer;
24 let Suspense;
25 let textCache;
26 let loadCache;
27 -let window;
28 -let document;
27 let writable;
28 const CSPnonce = null;
29 let container;
@@ -38,28 +36,32 @@ let waitForThrow;
36 let assertLog;
37 let Scheduler;
38 let clientAct;
41 -
42 -function resetJSDOM(markup) {
43 - // Test Environment
44 - const jsdom = new JSDOM(markup, {
45 - runScripts: 'dangerously',
46 - });
47 - // We mock matchMedia. for simplicity it only matches 'all' or '' and misses everything else
48 - Object.defineProperty(jsdom.window, 'matchMedia', {
49 - writable: true,
50 - value: jest.fn().mockImplementation(query => ({
51 - matches: query === 'all' || query === '',
52 - media: query,
53 - })),
54 - });
55 - window = jsdom.window;
56 - document = jsdom.window.document;
57 -}
39 +let streamingContainer;
40
41 describe('ReactDOMFloat', () => {
42 beforeEach(() => {
43 jest.resetModules();
44 JSDOM = require('jsdom').JSDOM;
45 +
46 + const jsdom = new JSDOM(
47 + '<!DOCTYPE html><html><head></head><body><div id="container">',
48 + {
49 + runScripts: 'dangerously',
50 + },
51 + );
52 + // We mock matchMedia. for simplicity it only matches 'all' or '' and misses everything else
53 + Object.defineProperty(jsdom.window, 'matchMedia', {
54 + writable: true,
55 + value: jest.fn().mockImplementation(query => ({
56 + matches: query === 'all' || query === '',
57 + media: query,
58 + })),
59 + });
60 + streamingContainer = null;
61 + global.window = jsdom.window;
62 + global.document = jsdom.window.document;
63 + container = document.getElementById('container');
64 +
65 React = require('react');
66 ReactDOM = require('react-dom');
67 ReactDOMClient = require('react-dom/client');
@@ -77,9 +79,6 @@ describe('ReactDOMFloat', () => {
79 textCache = new Map();
80 loadCache = new Set();
81
80 - resetJSDOM('<!DOCTYPE html><html><head></head><body><div id="container">');
81 - container = document.getElementById('container');
82 -
82 buffer = '';
83 hasErrored = false;
84
@@ -100,6 +99,9 @@ describe('ReactDOMFloat', () => {
99 }
100 });
101
102 + const bodyStartMatch = /<body(?:>| .*?>)/;
103 + const headStartMatch = /<head(?:>| .*?>)/;
104 +
105 async function act(callback) {
106 await callback();
107 // Await one turn around the event loop.
@@ -113,44 +115,123 @@ describe('ReactDOMFloat', () => {
115 // JSDOM doesn't support stream HTML parser so we need to give it a proper fragment.
116 // We also want to execute any scripts that are embedded.
117 // We assume that we have now received a proper fragment of HTML.
116 - const bufferedContent = buffer;
118 + let bufferedContent = buffer;
119 buffer = '';
118 - const fakeBody = document.createElement('body');
119 - fakeBody.innerHTML = bufferedContent;
120 - const parent =
121 - container.nodeName === '#document' ? container.body : container;
122 - await withLoadingReadyState(async () => {
123 - while (fakeBody.firstChild) {
124 - const node = fakeBody.firstChild;
125 - await replaceScriptsAndMove(
126 - document.defaultView,
127 - CSPnonce,
128 - node,
129 - parent,
130 - );
131 - }
132 - }, document);
133 - }
120
135 - async function actIntoEmptyDocument(callback) {
136 - await callback();
137 - // Await one turn around the event loop.
138 - // This assumes that we'll flush everything we have so far.
139 - await new Promise(resolve => {
140 - setImmediate(resolve);
141 - });
142 - if (hasErrored) {
143 - throw fatalError;
121 + if (!bufferedContent) {
122 + return;
123 }
145 - // JSDOM doesn't support stream HTML parser so we need to give it a proper fragment.
146 - // We also want to execute any scripts that are embedded.
147 - // We assume that we have now received a proper fragment of HTML.
148 - const bufferedContent = buffer;
149 - resetJSDOM(bufferedContent);
150 - container = document;
151 - buffer = '';
124 +
125 await withLoadingReadyState(async () => {
153 - await replaceScriptsAndMove(window, null, document.documentElement);
126 + const bodyMatch = bufferedContent.match(bodyStartMatch);
127 + const headMatch = bufferedContent.match(headStartMatch);
128 +
129 + if (streamingContainer === null) {
130 + // This is the first streamed content. We decide here where to insert it. If we get <html>, <head>, or <body>
131 + // we abandon the pre-built document and start from scratch. If we get anything else we assume it goes into the
132 + // container. This is not really production behavior because you can't correctly stream into a deep div effectively
133 + // but it's pragmatic for tests.
134 +
135 + if (
136 + bufferedContent.startsWith('<head>') ||
137 + bufferedContent.startsWith('<head ') ||
138 + bufferedContent.startsWith('<body>') ||
139 + bufferedContent.startsWith('<body ')
140 + ) {
141 + // wrap in doctype to normalize the parsing process
142 + bufferedContent = '<!DOCTYPE html><html>' + bufferedContent;
143 + } else if (
144 + bufferedContent.startsWith('<html>') ||
145 + bufferedContent.startsWith('<html ')
146 + ) {
147 + throw new Error(
148 + 'Recieved <html> without a <!DOCTYPE html> which is almost certainly a bug in React',
149 + );
150 + }
151 +
152 + if (bufferedContent.startsWith('<!DOCTYPE html>')) {
153 + // we can just use the whole document
154 + const tempDom = new JSDOM(bufferedContent);
155 +
156 + // Wipe existing head and body content
157 + document.head.innerHTML = '';
158 + document.body.innerHTML = '';
159 +
160 + // Copy the <html> attributes over
161 + const tempHtmlNode = tempDom.window.document.documentElement;
162 + for (let i = 0; i < tempHtmlNode.attributes.length; i++) {
163 + const attr = tempHtmlNode.attributes[i];
164 + document.documentElement.setAttribute(attr.name, attr.value);
165 + }
166 +
167 + if (headMatch) {
168 + // We parsed a head open tag. we need to copy head attributes and insert future
169 + // content into <head>
170 + streamingContainer = document.head;
171 + const tempHeadNode = tempDom.window.document.head;
172 + for (let i = 0; i < tempHeadNode.attributes.length; i++) {
173 + const attr = tempHeadNode.attributes[i];
174 + document.head.setAttribute(attr.name, attr.value);
175 + }
176 + const source = document.createElement('head');
177 + source.innerHTML = tempHeadNode.innerHTML;
178 + await insertNodesAndExecuteScripts(source, document.head, CSPnonce);
179 + }
180 +
181 + if (bodyMatch) {
182 + // We parsed a body open tag. we need to copy head attributes and insert future
183 + // content into <body>
184 + streamingContainer = document.body;
185 + const tempBodyNode = tempDom.window.document.body;
186 + for (let i = 0; i < tempBodyNode.attributes.length; i++) {
187 + const attr = tempBodyNode.attributes[i];
188 + document.body.setAttribute(attr.name, attr.value);
189 + }
190 + const source = document.createElement('body');
191 + source.innerHTML = tempBodyNode.innerHTML;
192 + await insertNodesAndExecuteScripts(source, document.body, CSPnonce);
193 + }
194 +
195 + if (!headMatch && !bodyMatch) {
196 + throw new Error('expected <head> or <body> after <html>');
197 + }
198 + } else {
199 + // we assume we are streaming into the default container'
200 + streamingContainer = container;
201 + const div = document.createElement('div');
202 + div.innerHTML = bufferedContent;
203 + await insertNodesAndExecuteScripts(div, container, CSPnonce);
204 + }
205 + } else if (streamingContainer === document.head) {
206 + bufferedContent = '<!DOCTYPE html><html><head>' + bufferedContent;
207 + const tempDom = new JSDOM(bufferedContent);
208 +
209 + const tempHeadNode = tempDom.window.document.head;
210 + const source = document.createElement('head');
211 + source.innerHTML = tempHeadNode.innerHTML;
212 + await insertNodesAndExecuteScripts(source, document.head, CSPnonce);
213 +
214 + if (bodyMatch) {
215 + streamingContainer = document.body;
216 +
217 + const tempBodyNode = tempDom.window.document.body;
218 + for (let i = 0; i < tempBodyNode.attributes.length; i++) {
219 + const attr = tempBodyNode.attributes[i];
220 + document.body.setAttribute(attr.name, attr.value);
221 + }
222 + const bodySource = document.createElement('body');
223 + bodySource.innerHTML = tempBodyNode.innerHTML;
224 + await insertNodesAndExecuteScripts(
225 + bodySource,
226 + document.body,
227 + CSPnonce,
228 + );
229 + }
230 + } else {
231 + const div = document.createElement('div');
232 + div.innerHTML = bufferedContent;
233 + await insertNodesAndExecuteScripts(div, streamingContainer, CSPnonce);
234 + }
235 }, document);
236 }
237
@@ -350,7 +431,7 @@ describe('ReactDOMFloat', () => {
431
432 // @gate enableFloat
433 it('can hydrate non Resources in head when Resources are also inserted there', async () => {
353 - await actIntoEmptyDocument(() => {
434 + await act(() => {
435 const {pipe} = renderToPipeableStream(
436 <html>
437 <head>
@@ -375,7 +456,7 @@ describe('ReactDOMFloat', () => {
456 <meta property="foo" content="bar" />
457 <title>foo</title>
458 <link rel="foo" href="bar" />
378 - <noscript>&lt;link rel="icon" href="icon"/&gt;</noscript>
459 + <noscript>&lt;link rel="icon" href="icon"&gt;</noscript>
460 <base target="foo" href="bar" />
461 </head>
462 <body>foo</body>
@@ -406,7 +487,7 @@ describe('ReactDOMFloat', () => {
487 <meta property="foo" content="bar" />
488 <title>foo</title>
489 <link rel="foo" href="bar" />
409 - <noscript>&lt;link rel="icon" href="icon"/&gt;</noscript>
490 + <noscript>&lt;link rel="icon" href="icon"&gt;</noscript>
491 <base target="foo" href="bar" />
492 <script async="" src="foo" />
493 </head>
@@ -598,7 +679,7 @@ describe('ReactDOMFloat', () => {
679 );
680 }
681
601 - await actIntoEmptyDocument(() => {
682 + await act(() => {
683 buffer = `<!DOCTYPE html><html><head>${ReactDOMFizzServer.renderToString(
684 <App />,
685 )}</head><body>foo</body></html>`;
@@ -625,7 +706,7 @@ describe('ReactDOMFloat', () => {
706 );
707 }
708
628 - await actIntoEmptyDocument(() => {
709 + await act(() => {
710 buffer = `<!DOCTYPE html><html>${ReactDOMFizzServer.renderToString(
711 <App />,
712 )}<body>foo</body></html>`;
@@ -649,7 +730,7 @@ describe('ReactDOMFloat', () => {
730 chunks.push(chunk);
731 });
732
652 - await actIntoEmptyDocument(() => {
733 + await act(() => {
734 const {pipe} = renderToPipeableStream(
735 <>
736 <title>foo</title>
@@ -681,7 +762,7 @@ describe('ReactDOMFloat', () => {
762 );
763 }
764
684 - await actIntoEmptyDocument(() => {
765 + await act(() => {
766 const {pipe} = renderToPipeableStream(
767 <html>
768 <head />
@@ -703,7 +784,7 @@ describe('ReactDOMFloat', () => {
784
785 // @gate enableFloat
786 it('can avoid inserting a late stylesheet if it already rendered on the client', async () => {
706 - await actIntoEmptyDocument(() => {
787 + await act(() => {
788 renderToPipeableStream(
789 <html>
790 <body>
@@ -829,7 +910,7 @@ body {
910 background-color: red;
911 }`;
912
832 - await actIntoEmptyDocument(() => {
913 + await act(() => {
914 renderToPipeableStream(
915 <html>
916 <body>
@@ -1125,7 +1206,7 @@ body {
1206 </html>
1207 );
1208 }
1128 - await actIntoEmptyDocument(() => {
1209 + await act(() => {
1210 const {pipe} = renderToPipeableStream(<App />);
1211 pipe(writable);
1212 });
@@ -1211,7 +1292,7 @@ body {
1292
1293 // @gate enableFloat
1294 it('treats stylesheet links with a precedence as a resource', async () => {
1214 - await actIntoEmptyDocument(() => {
1295 + await act(() => {
1296 const {pipe} = renderToPipeableStream(
1297 <html>
1298 <head />
@@ -1264,7 +1345,7 @@ body {
1345 );
1346 }
1347
1267 - await actIntoEmptyDocument(() => {
1348 + await act(() => {
1349 const {pipe} = renderToPipeableStream(
1350 <html>
1351 <head />
@@ -1302,7 +1383,7 @@ body {
1383 function PresetPrecedence() {
1384 ReactDOM.preinit('preset', {as: 'style', precedence: 'preset'});
1385 }
1305 - await actIntoEmptyDocument(() => {
1386 + await act(() => {
1387 const {pipe} = renderToPipeableStream(
1388 <html>
1389 <head />
@@ -1584,7 +1665,7 @@ body {
1665
1666 // @gate enableFloat
1667 it('normalizes stylesheet resource precedence for all boundaries inlined as part of the shell flush', async () => {
1587 - await actIntoEmptyDocument(() => {
1668 + await act(() => {
1669 const {pipe} = renderToPipeableStream(
1670 <html>
1671 <head />
@@ -1668,7 +1749,7 @@ body {
1749
1750 // @gate enableFloat
1751 it('stylesheet resources are inserted according to precedence order on the client', async () => {
1671 - await actIntoEmptyDocument(() => {
1752 + await act(() => {
1753 const {pipe} = renderToPipeableStream(
1754 <html>
1755 <head />
@@ -1791,7 +1872,7 @@ body {
1872
1873 // @gate enableFloat
1874 it('will include child boundary stylesheet resources in the boundary reveal instruction', async () => {
1794 - await actIntoEmptyDocument(() => {
1875 + await act(() => {
1876 const {pipe} = renderToPipeableStream(
1877 <html>
1878 <head />
@@ -1910,7 +1991,7 @@ body {
1991
1992 // @gate enableFloat
1993 it('will hoist resources of child boundaries emitted as part of a partial boundary to the parent boundary', async () => {
1913 - await actIntoEmptyDocument(() => {
1994 + await act(() => {
1995 const {pipe} = renderToPipeableStream(
1996 <html>
1997 <head />
@@ -2132,7 +2213,7 @@ body {
2213 );
2214 }
2215 await expect(async () => {
2135 - await actIntoEmptyDocument(() => {
2216 + await act(() => {
2217 const {pipe} = renderToPipeableStream(<App />);
2218 pipe(writable);
2219 });
@@ -2218,7 +2299,7 @@ body {
2299
2300 // @gate enableFloat
2301 it('boundary stylesheet resource dependencies hoist to a parent boundary when flushed inline', async () => {
2221 - await actIntoEmptyDocument(() => {
2302 + await act(() => {
2303 const {pipe} = renderToPipeableStream(
2304 <html>
2305 <head />
@@ -2353,7 +2434,7 @@ body {
2434 </html>
2435 );
2436 }
2356 - await actIntoEmptyDocument(() => {
2437 + await act(() => {
2438 const {pipe} = renderToPipeableStream(<App />);
2439 pipe(writable);
2440 });
@@ -2417,7 +2498,7 @@ body {
2498 </html>
2499 );
2500 }
2420 - await actIntoEmptyDocument(() => {
2501 + await act(() => {
2502 renderToPipeableStream(<App />).pipe(writable);
2503 });
2504
@@ -2573,7 +2654,7 @@ body {
2654 );
2655 }
2656
2576 - await actIntoEmptyDocument(() => {
2657 + await act(() => {
2658 renderToPipeableStream(<App />).pipe(writable);
2659 });
2660
@@ -2593,7 +2674,7 @@ body {
2674 <link rel="stylesheet" href="stylesheet" />
2675 <script src="sync rendered" data-meaningful="" />
2676 <style>{'body { background-color: red; }'}</style>
2596 - <noscript>&lt;meta name="noscript" content="noscript"/&gt;</noscript>
2677 + <noscript>&lt;meta name="noscript" content="noscript"&gt;</noscript>
2678 <link rel="foo" href="foo" />
2679 </head>
2680 <body>
@@ -2659,7 +2740,7 @@ body {
2740 <script src="sync rendered" data-meaningful="" />
2741 <style>{'body { background-color: red; }'}</style>
2742 <script src="async rendered" async="" />
2662 - <noscript>&lt;meta name="noscript" content="noscript"/&gt;</noscript>
2743 + <noscript>&lt;meta name="noscript" content="noscript"&gt;</noscript>
2744 <link rel="foo" href="foo" />
2745 <style>{'body { background-color: blue; }'}</style>
2746 <div />
@@ -2713,7 +2794,7 @@ body {
2794 });
2795
2796 it('does not preload nomodule scripts', async () => {
2716 - await actIntoEmptyDocument(() => {
2797 + await act(() => {
2798 renderToPipeableStream(
2799 <html>
2800 <body>
@@ -2839,7 +2920,7 @@ body {
2920 });
2921
2922 it('assumes stylesheets that load in the shell loaded already', async () => {
2842 - await actIntoEmptyDocument(() => {
2923 + await act(() => {
2924 renderToPipeableStream(
2925 <html>
2926 <body>
@@ -3321,7 +3402,7 @@ body {
3402 }
3403
3404 await expect(async () => {
3324 - await actIntoEmptyDocument(() => {
3405 + await act(() => {
3406 renderToPipeableStream(<App url="foo" />).pipe(writable);
3407 });
3408 }).toErrorDev([
@@ -3390,7 +3471,7 @@ body {
3471 }
3472
3473 await expect(async () => {
3393 - await actIntoEmptyDocument(() => {
3474 + await act(() => {
3475 renderToPipeableStream(<App url="foo" />).pipe(writable);
3476 });
3477 }).toErrorDev(
@@ -3469,7 +3550,7 @@ body {
3550 return <div>hello</div>;
3551 }
3552
3472 - await actIntoEmptyDocument(() => {
3553 + await act(() => {
3554 const {pipe} = renderToPipeableStream(<App />);
3555 pipe(writable);
3556 });
@@ -3555,7 +3636,7 @@ body {
3636 );
3637 }
3638
3558 - await actIntoEmptyDocument(() => {
3639 + await act(() => {
3640 const {pipe} = renderToPipeableStream(<App />);
3641 pipe(writable);
3642 });
@@ -3620,7 +3701,7 @@ body {
3701 }
3702
3703 await expect(async () => {
3623 - await actIntoEmptyDocument(() => {
3704 + await act(() => {
3705 renderToPipeableStream(
3706 <html>
3707 <body>
@@ -3645,7 +3726,7 @@ body {
3726 }
3727
3728 await expect(async () => {
3648 - await actIntoEmptyDocument(() => {
3729 + await act(() => {
3730 renderToPipeableStream(
3731 <html>
3732 <body>
@@ -3689,7 +3770,7 @@ body {
3770 return <div>hello</div>;
3771 }
3772
3692 - await actIntoEmptyDocument(() => {
3773 + await act(() => {
3774 const {pipe} = renderToPipeableStream(<App />);
3775 pipe(writable);
3776 });
@@ -3799,7 +3880,7 @@ body {
3880 return <div>hello</div>;
3881 }
3882
3802 - await actIntoEmptyDocument(() => {
3883 + await act(() => {
3884 const {pipe} = renderToPipeableStream(<App />);
3885 pipe(writable);
3886 });
@@ -3916,7 +3997,7 @@ body {
3997 }
3998
3999 await expect(async () => {
3919 - await actIntoEmptyDocument(() => {
4000 + await act(() => {
4001 renderToPipeableStream(
4002 <html>
4003 <body>
@@ -3952,7 +4033,7 @@ body {
4033 }
4034
4035 await expect(async () => {
3955 - await actIntoEmptyDocument(() => {
4036 + await act(() => {
4037 renderToPipeableStream(
4038 <html>
4039 <body>
@@ -3977,7 +4058,7 @@ body {
4058 }
4059
4060 await expect(async () => {
3980 - await actIntoEmptyDocument(() => {
4061 + await act(() => {
4062 renderToPipeableStream(
4063 <html>
4064 <body>
@@ -4007,7 +4088,7 @@ body {
4088 }
4089
4090 await expect(async () => {
4010 - await actIntoEmptyDocument(() => {
4091 + await act(() => {
4092 renderToPipeableStream(
4093 <html>
4094 <body>
@@ -4025,7 +4106,7 @@ body {
4106 describe('Stylesheet Resources', () => {
4107 // @gate enableFloat
4108 it('treats link rel stylesheet elements as a stylesheet resource when it includes a precedence when server rendering', async () => {
4028 - await actIntoEmptyDocument(() => {
4109 + await act(() => {
4110 const {pipe} = renderToPipeableStream(
4111 <html>
4112 <head />
@@ -4078,7 +4159,7 @@ body {
4159
4160 // @gate enableFloat
4161 it('treats link rel stylesheet elements as a stylesheet resource when it includes a precedence when hydrating', async () => {
4081 - await actIntoEmptyDocument(() => {
4162 + await act(() => {
4163 const {pipe} = renderToPipeableStream(
4164 <html>
4165 <head />
@@ -4116,7 +4197,7 @@ body {
4197
4198 // @gate enableFloat
4199 it('preloads stylesheets without a precedence prop when server rendering', async () => {
4119 - await actIntoEmptyDocument(() => {
4200 + await act(() => {
4201 const {pipe} = renderToPipeableStream(
4202 <html>
4203 <head />
@@ -4144,7 +4225,7 @@ body {
4225
4226 // @gate enableFloat
4227 it('hoists stylesheet resources to the correct precedence', async () => {
4147 - await actIntoEmptyDocument(() => {
4228 + await act(() => {
4229 const {pipe} = renderToPipeableStream(
4230 <html>
4231 <head />
@@ -4239,7 +4320,7 @@ body {
4320
4321 // @gate enableFloat && enableHostSingletons && enableClientRenderFallbackOnTextMismatch
4322 it('retains styles even when a new html, head, and/body mount', async () => {
4242 - await actIntoEmptyDocument(() => {
4323 + await act(() => {
4324 const {pipe} = renderToPipeableStream(
4325 <html>
4326 <head />
@@ -4291,7 +4372,7 @@ body {
4372
4373 // @gate enableFloat && !enableHostSingletons
4374 it('retains styles even when a new html, head, and/body mount - without HostSingleton', async () => {
4294 - await actIntoEmptyDocument(() => {
4375 + await act(() => {
4376 const {pipe} = renderToPipeableStream(
4377 <html>
4378 <head />
@@ -4521,7 +4602,7 @@ body {
4602 </html>
4603 );
4604 }
4524 - await actIntoEmptyDocument(() => {
4605 + await act(() => {
4606 const {pipe} = renderToPipeableStream(<App />);
4607 pipe(writable);
4608 });
@@ -4575,7 +4656,7 @@ body {
4656
4657 // @gate enableFloat
4658 it('escapes hrefs when selecting matching elements in the document when using preload and preinit', async () => {
4578 - await actIntoEmptyDocument(() => {
4659 + await act(() => {
4660 const {pipe} = renderToPipeableStream(
4661 <html>
4662 <head />
@@ -4638,7 +4719,7 @@ body {
4719
4720 // @gate enableFloat
4721 it('does not create stylesheet resources when inside an <svg> context', async () => {
4641 - await actIntoEmptyDocument(() => {
4722 + await act(() => {
4723 const {pipe} = renderToPipeableStream(
4724 <html>
4725 <body>
@@ -4699,7 +4780,7 @@ body {
4780
4781 // @gate enableFloat
4782 it('does not create stylesheet resources when inside a <noscript> context', async () => {
4702 - await actIntoEmptyDocument(() => {
4783 + await act(() => {
4784 const {pipe} = renderToPipeableStream(
4785 <html>
4786 <body>
@@ -4716,7 +4797,7 @@ body {
4797 <head />
4798 <body>
4799 <noscript>
4719 - &lt;link rel="stylesheet" href="foo" precedence="default"/&gt;
4800 + &lt;link rel="stylesheet" href="foo" precedence="default"&gt;
4801 </noscript>
4802 </body>
4803 </html>,
@@ -4742,7 +4823,7 @@ body {
4823 // @gate enableFloat
4824 it('warns if you provide a `precedence` prop with other props that invalidate the creation of a stylesheet resource', async () => {
4825 await expect(async () => {
4745 - await actIntoEmptyDocument(() => {
4826 + await act(() => {
4827 renderToPipeableStream(
4828 <html>
4829 <body>
@@ -4826,7 +4907,7 @@ body {
4907 );
4908 }
4909 await expect(async () => {
4829 - await actIntoEmptyDocument(() => {
4910 + await act(() => {
4911 const {pipe} = renderToPipeableStream(<App />);
4912 pipe(writable);
4913 });
@@ -4854,7 +4935,7 @@ body {
4935 );
4936 }
4937 await expect(async () => {
4857 - await actIntoEmptyDocument(() => {
4938 + await act(() => {
4939 const {pipe} = renderToPipeableStream(<App />);
4940 pipe(writable);
4941 });
@@ -4865,7 +4946,7 @@ body {
4946
4947 // @gate enableFloat
4948 it('will not block displaying a Suspense boundary on a stylesheet with media that does not match', async () => {
4868 - await actIntoEmptyDocument(() => {
4949 + await act(() => {
4950 renderToPipeableStream(
4951 <html>
4952 <body>
@@ -4986,7 +5067,7 @@ body {
5067 body {
5068 background-color: red;
5069 }`;
4989 - await actIntoEmptyDocument(() => {
5070 + await act(() => {
5071 renderToPipeableStream(
5072 <html>
5073 <body>
@@ -5024,7 +5105,7 @@ background-color: blue;
5105 body {
5106 background-color: green;
5107 }`;
5027 - await actIntoEmptyDocument(() => {
5108 + await act(() => {
5109 renderToPipeableStream(
5110 <html>
5111 <body>
@@ -5129,7 +5210,7 @@ background-color: green;
5210 // @gate enableFloat
5211 it('can emit styles early when a partial boundary flushes', async () => {
5212 const css = 'body { background-color: red; }';
5132 - await actIntoEmptyDocument(() => {
5213 + await act(() => {
5214 renderToPipeableStream(
5215 <html>
5216 <body>
@@ -5197,7 +5278,7 @@ background-color: green;
5278 });
5279
5280 it('can hoist styles flushed early even when no other style dependencies are flushed on completion', async () => {
5200 - await actIntoEmptyDocument(() => {
5281 + await act(() => {
5282 renderToPipeableStream(
5283 <html>
5284 <body>
@@ -5261,7 +5342,7 @@ background-color: green;
5342 });
5343
5344 it('can emit multiple style rules into a single style tag for a given precedence', async () => {
5264 - await actIntoEmptyDocument(() => {
5345 + await act(() => {
5346 renderToPipeableStream(
5347 <html>
5348 <body>
@@ -5440,7 +5521,7 @@ background-color: green;
5521
5522 it('warns if you render a <style> with an href with a space on the server', async () => {
5523 await expect(async () => {
5443 - await actIntoEmptyDocument(() => {
5524 + await act(() => {
5525 renderToPipeableStream(
5526 <html>
5527 <body>
@@ -5460,7 +5541,7 @@ background-color: green;
5541 describe('Script Resources', () => {
5542 // @gate enableFloat
5543 it('treats async scripts without onLoad or onError as Resources', async () => {
5463 - await actIntoEmptyDocument(() => {
5544 + await act(() => {
5545 const {pipe} = renderToPipeableStream(
5546 <html>
5547 <head />
@@ -5526,7 +5607,7 @@ background-color: green;
5607
5608 // @gate enableFloat
5609 it('does not create script resources when inside an <svg> context', async () => {
5529 - await actIntoEmptyDocument(() => {
5610 + await act(() => {
5611 const {pipe} = renderToPipeableStream(
5612 <html>
5613 <body>
@@ -5587,7 +5668,7 @@ background-color: green;
5668
5669 // @gate enableFloat
5670 it('does not create script resources when inside a <noscript> context', async () => {
5590 - await actIntoEmptyDocument(() => {
5671 + await act(() => {
5672 const {pipe} = renderToPipeableStream(
5673 <html>
5674 <body>
@@ -5646,7 +5727,7 @@ background-color: green;
5727 );
5728 }
5729 await expect(async () => {
5649 - await actIntoEmptyDocument(() => {
5730 + await act(() => {
5731 const {pipe} = renderToPipeableStream(<App />);
5732 pipe(writable);
5733 });
@@ -5679,7 +5760,7 @@ background-color: green;
5760 );
5761 }
5762 await expect(async () => {
5682 - await actIntoEmptyDocument(() => {
5763 + await act(() => {
5764 const {pipe} = renderToPipeableStream(<App />);
5765 pipe(writable);
5766 });
@@ -5692,7 +5773,7 @@ background-color: green;
5773 describe('Hoistables', () => {
5774 // @gate enableFloat
5775 it('can hoist meta tags on the server and hydrate them on the client', async () => {
5695 - await actIntoEmptyDocument(() => {
5776 + await act(() => {
5777 const {pipe} = renderToPipeableStream(
5778 <html>
5779 <body>
@@ -5768,7 +5849,7 @@ background-color: green;
5849
5850 // @gate enableFloat
5851 it('can hoist link (non-stylesheet) tags on the server and hydrate them on the client', async () => {
5771 - await actIntoEmptyDocument(() => {
5852 + await act(() => {
5853 const {pipe} = renderToPipeableStream(
5854 <html>
5855 <body>
@@ -5844,7 +5925,7 @@ background-color: green;
5925
5926 // @gate enableFloat
5927 it('can hoist title tags on the server and hydrate them on the client', async () => {
5847 - await actIntoEmptyDocument(() => {
5928 + await act(() => {
5929 const {pipe} = renderToPipeableStream(
5930 <html>
5931 <body>
@@ -5920,7 +6001,7 @@ background-color: green;
6001
6002 // @gate enableFloat
6003 it('prioritizes ordering for certain hoistables over others when rendering on the server', async () => {
5923 - await actIntoEmptyDocument(() => {
6004 + await act(() => {
6005 const {pipe} = renderToPipeableStream(
6006 <html>
6007 <body>
@@ -5962,7 +6043,7 @@ background-color: green;
6043 let content = '';
6044 writable.on('data', chunk => (content += chunk));
6045
5965 - await actIntoEmptyDocument(() => {
6046 + await act(() => {
6047 const {pipe} = renderToPipeableStream(
6048 <html>
6049 <body>
@@ -6010,7 +6091,7 @@ background-color: green;
6091
6092 // @gate enableFloat
6093 it('supports rendering hoistables outside of <html> scope', async () => {
6013 - await actIntoEmptyDocument(() => {
6094 + await act(() => {
6095 const {pipe} = renderToPipeableStream(
6096 <>
6097 <meta name="before" />
@@ -6098,7 +6179,7 @@ background-color: green;
6179 </html>
6180 );
6181 }
6101 - await actIntoEmptyDocument(() => {
6182 + await act(() => {
6183 renderToPipeableStream(<App />).pipe(writable);
6184 });
6185
@@ -6188,7 +6269,7 @@ background-color: green;
6269
6270 // @gate enableFloat
6271 it('does not hoist inside an <svg> context', async () => {
6191 - await actIntoEmptyDocument(() => {
6272 + await act(() => {
6273 const {pipe} = renderToPipeableStream(
6274 <html>
6275 <body>
@@ -6222,7 +6303,7 @@ background-color: green;
6303
6304 // @gate enableFloat
6305 it('does not hoist inside noscript context', async () => {
6225 - await actIntoEmptyDocument(() => {
6306 + await act(() => {
6307 const {pipe} = renderToPipeableStream(
6308 <html>
6309 <body>
@@ -6249,7 +6330,7 @@ background-color: green;
6330
6331 // @gate enableFloat && enableHostSingletons && (enableClientRenderFallbackOnTextMismatch || !__DEV__)
6332 it('can render a title before a singleton even if that singleton clears its contents', async () => {
6252 - await actIntoEmptyDocument(() => {
6333 + await act(() => {
6334 const {pipe} = renderToPipeableStream(
6335 <>
6336 <title>foo</title>
packages/react-dom/src/test-utils/FizzTestUtils.js
+86 -54
@@ -70,66 +70,98 @@ async function getRollupResult(scriptSrc: string): Promise<string | null> {
70 }
71 }
72
73 -// Utility function to process received HTML nodes and execute
74 -// embedded scripts by:
75 -// 1. Matching nonce attributes and moving node into an existing
76 -// parent container (if passed)
77 -// 2. Resolving scripts with sources
78 -// 3. Moving data attribute nodes to the body
79 -async function replaceScriptsAndMove(
80 - window: any,
73 +async function insertNodesAndExecuteScripts(
74 + source: Document | Element,
75 + target: Node,
76 CSPnonce: string | null,
82 - node: Node,
83 - parent: Node | null,
77 ) {
85 - if (
86 - node.nodeType === 1 &&
87 - (node.nodeName === 'SCRIPT' || node.nodeName === 'script')
88 - ) {
89 - // $FlowFixMe[incompatible-cast]
90 - const element = (node: HTMLElement);
91 - const script = window.document.createElement('SCRIPT');
92 - const scriptSrc = element.getAttribute('src');
93 - if (scriptSrc) {
94 - const rollupOutput = await getRollupResult(scriptSrc);
95 - if (rollupOutput) {
96 - // Manually call eval(...) here, since changing the HTML text content
97 - // may interfere with hydration
98 - window.eval(rollupOutput);
99 - }
100 - for (let i = 0; i < element.attributes.length; i++) {
101 - const attr = element.attributes.item(i);
102 - script.setAttribute(attr.name, attr.value);
78 + const ownerDocument = target.ownerDocument || target;
79 +
80 + // We need to remove the script content for any scripts that would not run based on CSP
81 + // We restore the script content after moving the nodes into the target
82 + const badNonceScriptNodes: Map<Element, string> = new Map();
83 + if (CSPnonce) {
84 + const scripts = source.querySelectorAll('script');
85 + for (let i = 0; i < scripts.length; i++) {
86 + const script = scripts[i];
87 + if (
88 + !script.hasAttribute('src') &&
89 + script.getAttribute('nonce') !== CSPnonce
90 + ) {
91 + badNonceScriptNodes.set(script, script.textContent);
92 + script.textContent = '';
93 }
104 - } else if (element === null || element.getAttribute('nonce') === CSPnonce) {
105 - script.textContent = node.textContent;
94 }
107 - if (parent) {
108 - element.parentNode?.removeChild(element);
109 - parent.appendChild(script);
110 - } else {
111 - element.parentNode?.replaceChild(script, element);
95 + }
96 + let lastChild = null;
97 + while (source.firstChild) {
98 + const node = source.firstChild;
99 + if (lastChild === node) {
100 + throw new Error('Infinite loop.');
101 }
113 - } else if (
114 - node.nodeType === 1 &&
115 - // $FlowFixMe[prop-missing]
116 - node.dataset != null &&
117 - (node.dataset.rxi != null ||
118 - node.dataset.rri != null ||
119 - node.dataset.rci != null ||
120 - node.dataset.rsi != null)
121 - ) {
122 - // External runtime assumes that instruction data nodes are eventually
123 - // appended to the body
124 - window.document.body.appendChild(node);
125 - } else {
126 - for (let i = 0; i < node.childNodes.length; i++) {
127 - const inner = node.childNodes[i];
128 - await replaceScriptsAndMove(window, CSPnonce, inner, null);
102 + lastChild = node;
103 +
104 + if (node.nodeType === 1) {
105 + const element: Element = (node: any);
106 + if (
107 + // $FlowFixMe[prop-missing]
108 + element.dataset != null &&
109 + (element.dataset.rxi != null ||
110 + element.dataset.rri != null ||
111 + element.dataset.rci != null ||
112 + element.dataset.rsi != null)
113 + ) {
114 + // Fizz external runtime instructions are expected to be in the body.
115 + // When we have renderIntoContainer and renderDocument this will be
116 + // more enforceable. At the moment you can misconfigure your stream and end up
117 + // with instructions that are deep in the document
118 + (ownerDocument.body: any).appendChild(element);
119 + } else {
120 + target.appendChild(element);
121 +
122 + if (element.nodeName === 'SCRIPT') {
123 + await executeScript(element);
124 + } else {
125 + const scripts = element.querySelectorAll('script');
126 + for (let i = 0; i < scripts.length; i++) {
127 + const script = scripts[i];
128 + await executeScript(script);
129 + }
130 + }
131 + }
132 + } else {
133 + target.appendChild(node);
134 }
130 - if (parent != null) {
131 - parent.appendChild(node);
135 + }
136 +
137 + // restore the textContent now that we have finished attempting to execute scripts
138 + badNonceScriptNodes.forEach((scriptContent, script) => {
139 + script.textContent = scriptContent;
140 + });
141 +}
142 +
143 +async function executeScript(script: Element) {
144 + const ownerDocument = script.ownerDocument;
145 + if (script.parentNode == null) {
146 + throw new Error(
147 + 'executeScript expects to be called on script nodes that are currently in a document',
148 + );
149 + }
150 + const parent = script.parentNode;
151 + const scriptSrc = script.getAttribute('src');
152 + if (scriptSrc) {
153 + const rollupOutput = await getRollupResult(scriptSrc);
154 + if (rollupOutput) {
155 + const transientScript = ownerDocument.createElement('script');
156 + transientScript.textContent = rollupOutput;
157 + parent.appendChild(transientScript);
158 + parent.removeChild(transientScript);
159 }
160 + } else {
161 + const newScript = ownerDocument.createElement('script');
162 + newScript.textContent = script.textContent;
163 + parent.insertBefore(newScript, script);
164 + parent.removeChild(script);
165 }
166 }
167
@@ -191,7 +223,7 @@ async function withLoadingReadyState<T>(
223 }
224
225 export {
194 - replaceScriptsAndMove,
226 + insertNodesAndExecuteScripts,
227 mergeOptions,
228 stripExternalRuntimeInNodes,
229 withLoadingReadyState,