mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Refactors Resources to have a more compact and memory efficient struture. Resources generally are just an Array of chunks. A resource is flushed when it's chunks is length zero. A resource does not have any other state. Stylesheets and Style tags are different and have been modeled as a unit as a StyleQueue. This object stores the style rules to flush as part of style tags using precedence as well as all the stylesheets associated with the precedence. Stylesheets still need to track state because it affects how we issue boundary completion instructions. Additionally stylesheets encode chunks lazily because we may never write them as html if they are discovered late. The preload props transfer is now maximally compact (only stores the props we would ever actually adopt) and only stores props for stylesheets and scripts because other preloads have no resource counterpart to adopt props into. The ResumableState maps that track which keys have been observed are being overloaded. Previously if a key was found it meant that a resource already exists (either in this render or in a prior prerender). Now we discriminate between null and object values. If map value is null we can assume the resource exists but if it is an object that represents a prior preload for that resource and the resource must still be constructed.
217 lines
5.4 KiB
JavaScript
217 lines
5.4 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @emails react-core
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
let React;
|
|
let ReactDOMServer;
|
|
let Suspense;
|
|
|
|
describe('ReactDOMServerFB', () => {
|
|
beforeEach(() => {
|
|
jest.resetModules();
|
|
React = require('react');
|
|
ReactDOMServer = require('../ReactDOMServerFB');
|
|
Suspense = React.Suspense;
|
|
});
|
|
|
|
const theError = new Error('This is an error');
|
|
function Throw() {
|
|
throw theError;
|
|
}
|
|
const theInfinitePromise = new Promise(() => {});
|
|
function InfiniteSuspend() {
|
|
throw theInfinitePromise;
|
|
}
|
|
|
|
function readResult(stream) {
|
|
let result = '';
|
|
while (!ReactDOMServer.hasFinished(stream)) {
|
|
result += ReactDOMServer.renderNextChunk(stream);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
it('should be able to render basic HTML', async () => {
|
|
const stream = ReactDOMServer.renderToStream(<div>hello world</div>, {
|
|
onError(x) {
|
|
console.error(x);
|
|
},
|
|
});
|
|
const result = readResult(stream);
|
|
expect(result).toMatchInlineSnapshot(`"<div>hello world</div>"`);
|
|
});
|
|
|
|
it('should emit bootstrap script src at the end', () => {
|
|
const stream = ReactDOMServer.renderToStream(<div>hello world</div>, {
|
|
bootstrapScriptContent: 'INIT();',
|
|
bootstrapScripts: ['init.js'],
|
|
bootstrapModules: ['init.mjs'],
|
|
onError(x) {
|
|
console.error(x);
|
|
},
|
|
});
|
|
const result = readResult(stream);
|
|
expect(result).toMatchInlineSnapshot(
|
|
`"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
|
|
);
|
|
});
|
|
|
|
it('emits all HTML as one unit if we wait until the end to start', async () => {
|
|
let hasLoaded = false;
|
|
let resolve;
|
|
const promise = new Promise(r => (resolve = r));
|
|
function Wait() {
|
|
if (!hasLoaded) {
|
|
throw promise;
|
|
}
|
|
return 'Done';
|
|
}
|
|
const stream = ReactDOMServer.renderToStream(
|
|
<div>
|
|
<Suspense fallback="Loading">
|
|
<Wait />
|
|
</Suspense>
|
|
</div>,
|
|
{
|
|
onError(x) {
|
|
console.error(x);
|
|
},
|
|
},
|
|
);
|
|
await jest.runAllTimers();
|
|
// Resolve the loading.
|
|
hasLoaded = true;
|
|
await resolve();
|
|
|
|
await jest.runAllTimers();
|
|
|
|
const result = readResult(stream);
|
|
expect(result).toMatchInlineSnapshot(`"<div><!--$-->Done<!--/$--></div>"`);
|
|
});
|
|
|
|
it('should throw an error when an error is thrown at the root', () => {
|
|
const reportedErrors = [];
|
|
const stream = ReactDOMServer.renderToStream(
|
|
<div>
|
|
<Throw />
|
|
</div>,
|
|
{
|
|
onError(x) {
|
|
reportedErrors.push(x);
|
|
},
|
|
},
|
|
);
|
|
|
|
let caughtError = null;
|
|
let result = '';
|
|
try {
|
|
result = readResult(stream);
|
|
} catch (x) {
|
|
caughtError = x;
|
|
}
|
|
expect(caughtError).toBe(theError);
|
|
expect(result).toBe('');
|
|
expect(reportedErrors).toEqual([theError]);
|
|
});
|
|
|
|
it('should throw an error when an error is thrown inside a fallback', () => {
|
|
const reportedErrors = [];
|
|
const stream = ReactDOMServer.renderToStream(
|
|
<div>
|
|
<Suspense fallback={<Throw />}>
|
|
<InfiniteSuspend />
|
|
</Suspense>
|
|
</div>,
|
|
{
|
|
onError(x) {
|
|
reportedErrors.push(x);
|
|
},
|
|
},
|
|
);
|
|
|
|
let caughtError = null;
|
|
let result = '';
|
|
try {
|
|
result = readResult(stream);
|
|
} catch (x) {
|
|
caughtError = x;
|
|
}
|
|
expect(caughtError).toBe(theError);
|
|
expect(result).toBe('');
|
|
expect(reportedErrors).toEqual([theError]);
|
|
});
|
|
|
|
it('should not throw an error when an error is thrown inside suspense boundary', async () => {
|
|
const reportedErrors = [];
|
|
const stream = ReactDOMServer.renderToStream(
|
|
<div>
|
|
<Suspense fallback={<div>Loading</div>}>
|
|
<Throw />
|
|
</Suspense>
|
|
</div>,
|
|
{
|
|
onError(x) {
|
|
reportedErrors.push(x);
|
|
},
|
|
},
|
|
);
|
|
|
|
const result = readResult(stream);
|
|
expect(result).toContain('Loading');
|
|
expect(reportedErrors).toEqual([theError]);
|
|
});
|
|
|
|
it('should be able to complete by aborting even if the promise never resolves', () => {
|
|
const errors = [];
|
|
const stream = ReactDOMServer.renderToStream(
|
|
<div>
|
|
<Suspense fallback={<div>Loading</div>}>
|
|
<InfiniteSuspend />
|
|
</Suspense>
|
|
</div>,
|
|
{
|
|
onError(x) {
|
|
errors.push(x.message);
|
|
},
|
|
},
|
|
);
|
|
|
|
const partial = ReactDOMServer.renderNextChunk(stream);
|
|
expect(partial).toContain('Loading');
|
|
|
|
ReactDOMServer.abortStream(stream);
|
|
|
|
const remaining = readResult(stream);
|
|
expect(remaining).toEqual('');
|
|
|
|
expect(errors).toEqual([
|
|
'The render was aborted by the server without a reason.',
|
|
]);
|
|
});
|
|
|
|
it('should allow setting an abort reason', () => {
|
|
const errors = [];
|
|
const stream = ReactDOMServer.renderToStream(
|
|
<div>
|
|
<Suspense fallback={<div>Loading</div>}>
|
|
<InfiniteSuspend />
|
|
</Suspense>
|
|
</div>,
|
|
{
|
|
onError(error) {
|
|
errors.push(error);
|
|
},
|
|
},
|
|
);
|
|
ReactDOMServer.abortStream(stream, theError);
|
|
expect(errors).toEqual([theError]);
|
|
});
|
|
});
|