[Flight] Add tests for component and owner stacks of halted components (#33644)

This PR adds tests for the Node.js and Edge builds to verify that
component stacks and owner stacks of halted components appear as
expected, now that recent enhancements for those have been implemented
(the latest one being #33634).

---------

Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com>
This commit is contained in:
Hendrik Liebau 2025-06-25 22:34:35 +02:00 committed by GitHub
parent bb6c9d521e
commit 9b2a545b32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 304 additions and 0 deletions

View File

@ -32,6 +32,7 @@ let webpackModuleLoading;
let React;
let ReactServer;
let ReactDOMServer;
let ReactDOMFizzStatic;
let ReactServerDOMServer;
let ReactServerDOMStaticServer;
let ReactServerDOMClient;
@ -102,6 +103,7 @@ describe('ReactFlightDOMEdge', () => {
);
React = require('react');
ReactDOMServer = require('react-dom/server.edge');
ReactDOMFizzStatic = require('react-dom/static.edge');
ReactServerDOMClient = require('react-server-dom-webpack/client');
use = React.use;
});
@ -228,6 +230,30 @@ describe('ReactFlightDOMEdge', () => {
}
}
async function createBufferedUnclosingStream(
prelude: ReadableStream<Uint8Array>,
): ReadableStream<Uint8Array> {
const chunks: Array<Uint8Array> = [];
const reader = prelude.getReader();
while (true) {
const {done, value} = await reader.read();
if (done) {
break;
} else {
chunks.push(value);
}
}
let i = 0;
return new ReadableStream({
async pull(controller) {
if (i < chunks.length) {
controller.enqueue(chunks[i++]);
}
},
});
}
it('should allow an alternative module mapping to be used for SSR', async () => {
function ClientComponent() {
return <span>Client Component</span>;
@ -1777,4 +1803,114 @@ describe('ReactFlightDOMEdge', () => {
expect(error).not.toBe(null);
expect(error.message).toBe(expectedMessage);
});
// @gate enableHalt
it('does not include source locations in component stacks for halted components', async () => {
// We only support adding source locations for halted components in the Node.js builds.
async function Component() {
await new Promise(() => {});
return null;
}
function App() {
return ReactServer.createElement(
'html',
null,
ReactServer.createElement(
'body',
null,
ReactServer.createElement(
ReactServer.Suspense,
{fallback: 'Loading...'},
ReactServer.createElement(Component, null),
),
),
);
}
const serverAbortController = new AbortController();
const errors = [];
const prerenderResult = ReactServerDOMStaticServer.unstable_prerender(
ReactServer.createElement(App, null),
webpackMap,
{
signal: serverAbortController.signal,
onError(err) {
errors.push(err);
},
},
);
await new Promise(resolve => {
setImmediate(() => {
serverAbortController.abort();
resolve();
});
});
const {prelude} = await prerenderResult;
expect(errors).toEqual([]);
function ClientRoot({response}) {
return use(response);
}
const prerenderResponse = ReactServerDOMClient.createFromReadableStream(
await createBufferedUnclosingStream(prelude),
{
serverConsumerManifest: {
moduleMap: null,
moduleLoading: null,
},
},
);
let componentStack;
let ownerStack;
const clientAbortController = new AbortController();
const fizzPrerenderStreamResult = ReactDOMFizzStatic.prerender(
React.createElement(ClientRoot, {response: prerenderResponse}),
{
signal: clientAbortController.signal,
onError(error, errorInfo) {
componentStack = errorInfo.componentStack;
ownerStack = React.captureOwnerStack
? React.captureOwnerStack()
: null;
},
},
);
await new Promise(resolve => {
setImmediate(() => {
clientAbortController.abort();
resolve();
});
});
const fizzPrerenderStream = await fizzPrerenderStreamResult;
const prerenderHTML = await readResult(fizzPrerenderStream.prelude);
expect(prerenderHTML).toContain('Loading...');
if (__DEV__) {
expect(normalizeCodeLocInfo(componentStack)).toBe(
'\n in Component\n in Suspense\n in body\n in html\n in ClientRoot (at **)',
);
} else {
expect(normalizeCodeLocInfo(componentStack)).toBe(
'\n in Suspense\n in body\n in html\n in ClientRoot (at **)',
);
}
if (__DEV__) {
expect(normalizeCodeLocInfo(ownerStack)).toBe('\n in App (at **)');
} else {
expect(ownerStack).toBeNull();
}
});
});

View File

@ -21,6 +21,7 @@ let webpackModules;
let webpackModuleLoading;
let React;
let ReactDOMServer;
let ReactDOMFizzStatic;
let ReactServer;
let ReactServerDOMServer;
let ReactServerDOMStaticServer;
@ -70,11 +71,21 @@ describe('ReactFlightDOMNode', () => {
React = require('react');
ReactDOMServer = require('react-dom/server.node');
ReactDOMFizzStatic = require('react-dom/static');
ReactServerDOMClient = require('react-server-dom-webpack/client');
Stream = require('stream');
use = React.use;
});
function normalizeCodeLocInfo(str) {
return (
str &&
str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
return ' in ' + name + (/\d/.test(m) ? ' (at **)' : '');
})
);
}
function readResult(stream) {
return new Promise((resolve, reject) => {
let buffer = '';
@ -93,6 +104,42 @@ describe('ReactFlightDOMNode', () => {
});
}
async function readWebResult(webStream: ReadableStream<Uint8Array>) {
const reader = webStream.getReader();
let result = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
return result;
}
result += Buffer.from(value).toString('utf8');
}
}
async function createBufferedUnclosingStream(
prelude: ReadableStream<Uint8Array>,
): ReadableStream<Uint8Array> {
const chunks: Array<Uint8Array> = [];
const reader = prelude.getReader();
while (true) {
const {done, value} = await reader.read();
if (done) {
break;
} else {
chunks.push(value);
}
}
let i = 0;
return new ReadableStream({
async pull(controller) {
if (i < chunks.length) {
controller.enqueue(chunks[i++]);
}
},
});
}
it('should support web streams in node', async () => {
function Text({children}) {
return <span>{children}</span>;
@ -543,4 +590,125 @@ describe('ReactFlightDOMNode', () => {
const result = await readResult(ssrStream);
expect(result).toContain('loading...');
});
// @gate enableHalt && enableAsyncDebugInfo
it('includes source locations in component and owner stacks for halted components', async () => {
async function Component() {
await new Promise(() => {});
return null;
}
function App() {
return ReactServer.createElement(
'html',
null,
ReactServer.createElement(
'body',
null,
ReactServer.createElement(
ReactServer.Suspense,
{fallback: 'Loading...'},
ReactServer.createElement(Component, null),
),
),
);
}
const errors = [];
const serverAbortController = new AbortController();
const {pendingResult} = await serverAct(async () => {
// destructure trick to avoid the act scope from awaiting the returned value
return {
pendingResult: ReactServerDOMStaticServer.unstable_prerender(
ReactServer.createElement(App, null),
webpackMap,
{
signal: serverAbortController.signal,
onError(error) {
errors.push(error);
},
},
),
};
});
await await serverAct(
async () =>
new Promise(resolve => {
setImmediate(() => {
serverAbortController.abort();
resolve();
});
}),
);
const {prelude} = await pendingResult;
expect(errors).toEqual([]);
function ClientRoot({response}) {
return use(response);
}
const prerenderResponse = ReactServerDOMClient.createFromReadableStream(
await createBufferedUnclosingStream(prelude),
{
serverConsumerManifest: {
moduleMap: null,
moduleLoading: null,
},
},
);
let componentStack;
let ownerStack;
const clientAbortController = new AbortController();
const fizzPrerenderStreamResult = ReactDOMFizzStatic.prerender(
React.createElement(ClientRoot, {response: prerenderResponse}),
{
signal: clientAbortController.signal,
onError(error, errorInfo) {
componentStack = errorInfo.componentStack;
ownerStack = React.captureOwnerStack
? React.captureOwnerStack()
: null;
},
},
);
await await serverAct(
async () =>
new Promise(resolve => {
setImmediate(() => {
clientAbortController.abort();
resolve();
});
}),
);
const fizzPrerenderStream = await fizzPrerenderStreamResult;
const prerenderHTML = await readWebResult(fizzPrerenderStream.prelude);
expect(prerenderHTML).toContain('Loading...');
if (__DEV__) {
expect(normalizeCodeLocInfo(componentStack)).toBe(
'\n in Component (at **)\n in Suspense\n in body\n in html\n in ClientRoot (at **)',
);
} else {
expect(normalizeCodeLocInfo(componentStack)).toBe(
'\n in Suspense\n in body\n in html\n in ClientRoot (at **)',
);
}
if (__DEV__) {
expect(normalizeCodeLocInfo(ownerStack)).toBe(
'\n in Component (at **)\n in App (at **)',
);
} else {
expect(ownerStack).toBeNull();
}
});
});