"`);
});
it('should reject the promise when an error is thrown at the root', async () => {
const reportedErrors = [];
let caughtError = null;
try {
await serverAct(() =>
ReactDOMFizzStatic.prerender(
,
{
onError(x) {
reportedErrors.push(x);
},
},
),
);
} catch (error) {
caughtError = error;
}
expect(caughtError).toBe(theError);
expect(reportedErrors).toEqual([theError]);
});
it('should reject the promise when an error is thrown inside a fallback', async () => {
const reportedErrors = [];
let caughtError = null;
try {
await serverAct(() =>
ReactDOMFizzStatic.prerender(
}>
,
{
onError(x) {
reportedErrors.push(x);
},
},
),
);
} catch (error) {
caughtError = error;
}
expect(caughtError).toBe(theError);
expect(reportedErrors).toEqual([theError]);
});
it('should not error the stream when an error is thrown inside suspense boundary', async () => {
const reportedErrors = [];
const result = await serverAct(() =>
ReactDOMFizzStatic.prerender(
Loading
}>
,
{
onError(x) {
reportedErrors.push(x);
},
},
),
);
const prelude = await readContent(result.prelude);
expect(prelude).toContain('Loading');
expect(reportedErrors).toEqual([theError]);
});
it('should be able to complete by aborting even if the promise never resolves', async () => {
const errors = [];
const controller = new AbortController();
let resultPromise;
await serverAct(() => {
resultPromise = ReactDOMFizzStatic.prerender(
Loading
}>
,
{
signal: controller.signal,
onError(x) {
errors.push(x.message);
},
},
);
});
await serverAct(() => {
controller.abort();
});
const result = await resultPromise;
const prelude = await readContent(result.prelude);
expect(prelude).toContain('Loading');
expect(errors).toEqual(['This operation was aborted']);
});
it('should resolve an empty prelude if aborting before the shell is complete', async () => {
const errors = [];
const controller = new AbortController();
const promise = serverAct(() =>
ReactDOMFizzStatic.prerender(
,
{
signal: controller.signal,
onError(x) {
errors.push(x.message);
},
},
),
);
await jest.runAllTimers();
const theReason = new Error('aborted for reasons');
await serverAct(() => {
controller.abort(theReason);
});
let rejected = false;
let prelude;
try {
({prelude} = await promise);
} catch (error) {
rejected = true;
}
expect(rejected).toBe(false);
expect(errors).toEqual(['aborted for reasons']);
const content = await readContent(prelude);
expect(content).toBe('');
});
it('should be able to abort before something suspends', async () => {
const errors = [];
const controller = new AbortController();
function App() {
controller.abort();
return (
Loading}>
);
}
const streamPromise = serverAct(() =>
ReactDOMFizzStatic.prerender(
,
{
signal: controller.signal,
onError(x) {
errors.push(x.message);
},
},
),
);
const {prelude} = await streamPromise;
const content = await readContent(prelude);
expect(errors).toEqual(['This operation was aborted']);
expect(content).toBe('');
});
it('reports the abort reason if a task suspends after aborting a prerender', async () => {
const promise = new Promise(() => {});
const errors = [];
const controller = new AbortController();
function App() {
controller.abort(new Error('abort reason'));
React.use(promise);
return null;
}
const result = await serverAct(() =>
ReactDOMFizzStatic.prerender(, {
signal: controller.signal,
onError(error) {
errors.push(error.message);
},
}),
);
expect(errors).toEqual(['abort reason']);
expect(await readContent(result.prelude)).toBe('');
});
it('should resolve an empty prelude if passing an already aborted signal', async () => {
const errors = [];
const controller = new AbortController();
const theReason = new Error('aborted for reasons');
controller.abort(theReason);
const promise = serverAct(() =>
ReactDOMFizzStatic.prerender(
Loading
}>
,
{
signal: controller.signal,
onError(x) {
errors.push(x.message);
},
},
),
);
// Technically we could still continue rendering the shell but currently the
// semantics mean that we also abort any pending CPU work.
let didThrow = false;
let prelude;
try {
({prelude} = await promise);
} catch (error) {
didThrow = true;
}
expect(didThrow).toBe(false);
expect(errors).toEqual(['aborted for reasons']);
const content = await readContent(prelude);
expect(content).toBe('');
});
it('supports custom abort reasons with a string', async () => {
const promise = new Promise(r => {});
function Wait() {
throw promise;
}
function App() {
return (
);
}
const errors = [];
const controller = new AbortController();
let resultPromise;
await serverAct(() => {
resultPromise = ReactDOMFizzStatic.prerender(, {
signal: controller.signal,
onError(x) {
errors.push(x);
return 'a digest';
},
});
});
await serverAct(() => {
controller.abort('foobar');
});
await resultPromise;
expect(errors).toEqual(['foobar', 'foobar']);
});
it('supports custom abort reasons with an Error', async () => {
const promise = new Promise(r => {});
function Wait() {
throw promise;
}
function App() {
return (
);
}
const errors = [];
const controller = new AbortController();
let resultPromise;
await serverAct(() => {
resultPromise = ReactDOMFizzStatic.prerender(, {
signal: controller.signal,
onError(x) {
errors.push(x.message);
return 'a digest';
},
});
});
await serverAct(() => {
controller.abort(new Error('uh oh'));
});
await resultPromise;
expect(errors).toEqual(['uh oh', 'uh oh']);
});
it('uses a rejection reason when an abort listener rejects pending work before the abort finishes', async () => {
let reject;
const rejectedPromise = new Promise((resolve, rejectPromise) => {
reject = rejectPromise;
});
const haltedPromise = new Promise(() => {});
function RejectedWait() {
React.use(rejectedPromise);
return null;
}
function HaltedWait() {
React.use(haltedPromise);
return null;
}
const errors = [];
const controller = new AbortController();
let resultPromise;
await serverAct(() => {
resultPromise = ReactDOMFizzStatic.prerender(
<>
>,
{
signal: controller.signal,
onError(error) {
errors.push(error.message);
},
},
);
});
controller.signal.addEventListener('abort', () => {
reject(new Error('rejected during abort'));
});
await serverAct(() => {
controller.abort(new Error('abort reason'));
});
await resultPromise;
expect(errors).toEqual(['rejected during abort', 'abort reason']);
});
it('logs an error if onHeaders throws but continues the prerender', async () => {
const errors = [];
function onError(error) {
errors.push(error.message);
}
function onHeaders(x) {
throw new Error('bad onHeaders');
}
const prerendered = await serverAct(() =>
ReactDOMFizzStatic.prerender(
,
);
expect(errors).toEqual(['boom']);
});
it('will render fallback Document when erroring a boundary above the body', async () => {
let isPrerendering = true;
const promise = new Promise(() => {});
function Boom() {
if (isPrerendering) {
React.use(promise);
}
throw new Error('Boom!');
}
function App() {
return (
hello error
}>
hello world
);
}
const controller = new AbortController();
let pendingResult;
const errors = [];
await serverAct(() => {
pendingResult = ReactDOMFizzStatic.prerender(, {
signal: controller.signal,
onError: e => {
errors.push(e.message);
},
});
});
await serverAct(() => {
controller.abort();
});
const prerendered = await pendingResult;
expect(errors).toEqual(['This operation was aborted']);
const content = await readContent(prerendered.prelude);
expect(content).toBe('');
isPrerendering = false;
const postponedState = JSON.stringify(prerendered.postponed);
const resumeErrors = [];
const dynamic = await serverAct(() =>
ReactDOMFizzServer.resume(, JSON.parse(postponedState), {
onError: e => {
resumeErrors.push(e.message);
},
}),
);
expect(resumeErrors).toEqual(['Boom!']);
await readIntoNewDocument(dynamic);
expect(getVisibleChildren(document)).toEqual(
hello error
,
);
});
it('reveals a resumed boundary even when the shell outlined a completed boundary', async () => {
// Regression test for a segment-id collision between the prerendered shell
// and the resume. The prelude flush outlines a large *completed* boundary
// into the shell, advancing request.nextSegmentId past the value
// getPostponedState snapshotted before the flush. If that snapshot isn't
// finalized after the flush, the resume re-allocates ids the shell already
// used; in the served document $RC (getElementById, first match) then
// reveals the wrong element and the resumed boundary stays on its fallback.
// Asserts on the rendered output rather than the segment ids.
let prerendering = true;
const shellText = 'a'.repeat(800); // > 500 bytes => eligible for outlining
const resumeText = 'b'.repeat(800);
// Completes during the prerender; large enough that the prelude flush
// outlines it into the shell.
function ShellBoundary() {
return
{shellText}
;
}
// Suspends during the prerender so its boundary becomes a hole the resume
// fills. On resume it renders a nested large boundary that itself outlines,
// so the resume allocates fresh segment ids from the postponed seed.
function Hole() {
if (prerendering) {
return React.use(theInfinitePromise);
}
return (
{resumeText}
);
}
function App() {
return (
);
}
const controller = new AbortController();
let pendingResult;
await serverAct(() => {
pendingResult = ReactDOMFizzStatic.prerender(, {
signal: controller.signal,
progressiveChunkSize: 100, // force the completed boundary to outline
onError() {},
});
});
await serverAct(() => controller.abort());
const prerendered = await pendingResult;
expect(prerendered.postponed).not.toBe(null);
const shellHTML = await readContent(prerendered.prelude);
prerendering = false;
const resumed = await serverAct(() =>
ReactDOMFizzServer.resume(
,
JSON.parse(JSON.stringify(prerendered.postponed)),
{onError() {}},
),
);
const resumeHTML = await readContent(resumed);
// Run the shell and the resume as one served document so the completion
// instructions ($RC) execute against both together, the way a browser does.
const temp = document.createElement('div');
temp.innerHTML = shellHTML + resumeHTML;
await insertNodesAndExecuteScripts(temp, container, null);
jest.runAllTimers();
// Both boundaries reveal their own content. Without the fix the resumed
// boundary reuses the shell's ids, so its $RC resolves to the shell's
// element and it stays on its "LoadingC" fallback.
expect(getVisibleChildren(container)).toEqual(
{shellText}
{resumeText}
,
);
});
it('can omit a preamble with an empty shell if no preamble is ready when prerendering finishes', async () => {
const errors = [];
let resolveA;
const promiseA = new Promise(r => (resolveA = r));
let resolveB;
const promiseB = new Promise(r => (resolveB = r));
async function ComponentA() {
await promiseA;
return (
);
}
async function ComponentB() {
await promiseB;
return 'Hello';
}
function App() {
return (
);
}
const controller = new AbortController();
let pendingResult;
await serverAct(async () => {
pendingResult = ReactDOMFizzStatic.prerender(, {
signal: controller.signal,
onError(x) {
errors.push(x.message);
},
});
});
await serverAct(() => {
controller.abort();
});
const prerendered = await pendingResult;
const postponedState = JSON.stringify(prerendered.postponed);
const content = await readContent(prerendered.prelude);
expect(content).toBe('');
await resolveA();
expect(prerendered.postponed).not.toBe(null);
const controller2 = new AbortController();
await serverAct(async () => {
pendingResult = ReactDOMFizzStatic.resumeAndPrerender(
,
JSON.parse(postponedState),
{
signal: controller2.signal,
onError(x) {
errors.push(x.message);
},
},
);
});
await serverAct(() => {
controller2.abort();
});
const prerendered2 = await pendingResult;
const postponedState2 = JSON.stringify(prerendered2.postponed);
await readIntoNewDocument(prerendered2.prelude);
expect(getVisibleChildren(document)).toEqual(
Loading B
,
);
await resolveB();
const dynamic = await serverAct(() =>
ReactDOMFizzServer.resume(, JSON.parse(postponedState2)),
);
await readIntoCurrentDocument(dynamic);
expect(getVisibleChildren(document)).toEqual(
Hello
,
);
});
// @gate enableSuspenseList
it('can resume a partially prerendered SuspenseList', async () => {
const errors = [];
let resolveA;
const promiseA = new Promise(r => (resolveA = r));
let resolveB;
const promiseB = new Promise(r => (resolveB = r));
async function ComponentA() {
await promiseA;
return 'A';
}
async function ComponentB() {
await promiseB;
return 'B';
}
function App() {
return (