Files
react/packages/react-server-dom-fb/src/__tests__/ReactDOMServerFB-test.internal.js
T
Josh Story 49eba01930 [Fizz][Float] Refactor Resources (#27400)
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.
2023-09-26 09:59:39 -07:00

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]);
});
});