10
11
'use strict';
12
import {
13
- replaceScriptsAndMove,
13
+ insertNodesAndExecuteScripts,
14
mergeOptions,
15
stripExternalRuntimeInNodes,
16
withLoadingReadyState,
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;
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');
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
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.
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
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 />
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 />
3724
</div>
3725
);
3726
}
3634
- await actIntoEmptyDocument(() => {
3727
+ await act(() => {
3728
const {pipe} = renderToPipeableStream(<App />);
3729
pipe(writable);
3730
});
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 />
4539
</body>
4540
);
4541
}
4449
- await actIntoEmptyDocument(() => {
4542
+ await act(() => {
4543
const {pipe} = renderToPipeableStream(
4544
<html data-html="html">
4545
<AsyncNoOutput />
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(
4576
chunks.push(chunk);
4577
});
4578
4490
- await actIntoEmptyDocument(() => {
4579
+ await act(() => {
4580
const {pipe} = renderToPipeableStream(
4581
<html>
4582
<head />
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 />, {
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 />, {
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 />);
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
},
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
[
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
}
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(() => {
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
},
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', () => {
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>
5670
};
5671
5672
try {
5586
- await actIntoEmptyDocument(async () => {
5673
+ await act(async () => {
5674
const {pipe} = renderToPipeableStream(
5675
<html>
5676
<body>