"`);
});
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);
},
},
);
});
controller.abort();
const result = await resultPromise;
const prelude = await readContent(result.prelude);
expect(prelude).toContain('Loading');
expect(errors).toEqual(['This operation was aborted']);
});
// @gate !enableHalt
it('should reject if aborting before the shell is complete and enableHalt is disabled', 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');
controller.abort(theReason);
let caughtError = null;
try {
await promise;
} catch (error) {
caughtError = error;
}
expect(caughtError).toBe(theReason);
expect(errors).toEqual(['aborted for reasons']);
});
// @gate enableHalt
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');
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);
},
},
),
);
if (gate(flags => flags.enableHalt)) {
const {prelude} = await streamPromise;
const content = await readContent(prelude);
expect(errors).toEqual(['This operation was aborted']);
expect(content).toBe('');
} else {
let caughtError = null;
try {
await streamPromise;
} catch (error) {
caughtError = error;
}
expect(caughtError.message).toBe('This operation was aborted');
expect(errors).toEqual(['This operation was aborted']);
}
});
// @gate !enableHalt
it('should reject if passing an already aborted signal and enableHalt is disabled', 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 caughtError = null;
try {
await promise;
} catch (error) {
caughtError = error;
}
expect(caughtError).toBe(theReason);
expect(errors).toEqual(['aborted for reasons']);
});
// @gate enableHalt
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';
},
});
});
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';
},
});
});
controller.abort(new Error('uh oh'));
await resultPromise;
expect(errors).toEqual(['uh oh', 'uh oh']);
});
// @gate enablePostpone
it('supports postponing in prerender and resuming later', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return ['Hello', 'World'];
}
function App() {
return (
,
);
});
// @gate enablePostpone
it('supports postponing in prerender and resuming with a prefix', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return 'World';
}
function App() {
return (
,
]);
});
// @gate enablePostpone
it('can postpone a boundary after it has already been added', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return 'Hello';
}
function App() {
return (
,
);
});
// @gate enablePostpone
it('can postpone in fallback without postponing the tree', async () => {
function Postpone() {
React.unstable_postpone();
}
const lazyText = React.lazy(async () => {
await 0; // causes the fallback to start work
return {default: 'Hello'};
});
function App() {
return (
}>{lazyText}
);
}
const prerendered = await serverAct(() =>
ReactDOMFizzStatic.prerender(),
);
// TODO: This should actually be null because we should've been able to fully
// resolve the render on the server eventually, even though the fallback postponed.
// So we should not need to resume.
expect(prerendered.postponed).not.toBe(null);
await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual(
,
]);
prerendering = false;
const errors = [];
const resumed = await serverAct(() =>
ReactDOMFizzServer.resume(
,
JSON.parse(JSON.stringify(prerendered.postponed)),
{
onError(x) {
errors.push(x.message);
},
},
),
);
expect(errors).toEqual([
'Expected the resume to render in this slot but instead it rendered . ' +
"The tree doesn't match so React will fallback to client rendering.",
'Expected the resume to render in this slot but instead it rendered . ' +
"The tree doesn't match so React will fallback to client rendering.",
]);
// TODO: Test the component stack but we don't expose it to the server yet.
await readIntoContainer(resumed);
// Client rendered
expect(getVisibleChildren(container)).toEqual([
Loading...
,
Loading...
,
]);
});
// @gate enablePostpone
it('can abort the resume', async () => {
let prerendering = true;
const infinitePromise = new Promise(() => {});
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return 'Hello';
}
function App() {
if (!prerendering) {
React.use(infinitePromise);
}
return (
);
});
// @gate enablePostpone
it('can suspend in a replayed component several layers deep', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return 'Hello';
}
let resolve;
const promise = new Promise(r => (resolve = r));
function Delay({children}) {
if (!prerendering) {
React.use(promise);
}
return children;
}
// This wrapper will cause us to do one destructive render past this.
function Outer({children}) {
return children;
}
function App() {
return (