Skip to content

Commit 9b2a545

Browse files
unstubbableeps1lon
andauthored
[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 <[email protected]>
1 parent bb6c9d5 commit 9b2a545

File tree

2 files changed

+304
-0
lines changed

2 files changed

+304
-0
lines changed

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ let webpackModuleLoading;
3232
let React;
3333
let ReactServer;
3434
let ReactDOMServer;
35+
let ReactDOMFizzStatic;
3536
let ReactServerDOMServer;
3637
let ReactServerDOMStaticServer;
3738
let ReactServerDOMClient;
@@ -102,6 +103,7 @@ describe('ReactFlightDOMEdge', () => {
102103
);
103104
React = require('react');
104105
ReactDOMServer = require('react-dom/server.edge');
106+
ReactDOMFizzStatic = require('react-dom/static.edge');
105107
ReactServerDOMClient = require('react-server-dom-webpack/client');
106108
use = React.use;
107109
});
@@ -228,6 +230,30 @@ describe('ReactFlightDOMEdge', () => {
228230
}
229231
}
230232

233+
async function createBufferedUnclosingStream(
234+
prelude: ReadableStream<Uint8Array>,
235+
): ReadableStream<Uint8Array> {
236+
const chunks: Array<Uint8Array> = [];
237+
const reader = prelude.getReader();
238+
while (true) {
239+
const {done, value} = await reader.read();
240+
if (done) {
241+
break;
242+
} else {
243+
chunks.push(value);
244+
}
245+
}
246+
247+
let i = 0;
248+
return new ReadableStream({
249+
async pull(controller) {
250+
if (i < chunks.length) {
251+
controller.enqueue(chunks[i++]);
252+
}
253+
},
254+
});
255+
}
256+
231257
it('should allow an alternative module mapping to be used for SSR', async () => {
232258
function ClientComponent() {
233259
return <span>Client Component</span>;
@@ -1777,4 +1803,114 @@ describe('ReactFlightDOMEdge', () => {
17771803
expect(error).not.toBe(null);
17781804
expect(error.message).toBe(expectedMessage);
17791805
});
1806+
1807+
// @gate enableHalt
1808+
it('does not include source locations in component stacks for halted components', async () => {
1809+
// We only support adding source locations for halted components in the Node.js builds.
1810+
1811+
async function Component() {
1812+
await new Promise(() => {});
1813+
return null;
1814+
}
1815+
1816+
function App() {
1817+
return ReactServer.createElement(
1818+
'html',
1819+
null,
1820+
ReactServer.createElement(
1821+
'body',
1822+
null,
1823+
ReactServer.createElement(
1824+
ReactServer.Suspense,
1825+
{fallback: 'Loading...'},
1826+
ReactServer.createElement(Component, null),
1827+
),
1828+
),
1829+
);
1830+
}
1831+
1832+
const serverAbortController = new AbortController();
1833+
const errors = [];
1834+
const prerenderResult = ReactServerDOMStaticServer.unstable_prerender(
1835+
ReactServer.createElement(App, null),
1836+
webpackMap,
1837+
{
1838+
signal: serverAbortController.signal,
1839+
onError(err) {
1840+
errors.push(err);
1841+
},
1842+
},
1843+
);
1844+
1845+
await new Promise(resolve => {
1846+
setImmediate(() => {
1847+
serverAbortController.abort();
1848+
resolve();
1849+
});
1850+
});
1851+
1852+
const {prelude} = await prerenderResult;
1853+
1854+
expect(errors).toEqual([]);
1855+
1856+
function ClientRoot({response}) {
1857+
return use(response);
1858+
}
1859+
1860+
const prerenderResponse = ReactServerDOMClient.createFromReadableStream(
1861+
await createBufferedUnclosingStream(prelude),
1862+
{
1863+
serverConsumerManifest: {
1864+
moduleMap: null,
1865+
moduleLoading: null,
1866+
},
1867+
},
1868+
);
1869+
1870+
let componentStack;
1871+
let ownerStack;
1872+
1873+
const clientAbortController = new AbortController();
1874+
1875+
const fizzPrerenderStreamResult = ReactDOMFizzStatic.prerender(
1876+
React.createElement(ClientRoot, {response: prerenderResponse}),
1877+
{
1878+
signal: clientAbortController.signal,
1879+
onError(error, errorInfo) {
1880+
componentStack = errorInfo.componentStack;
1881+
ownerStack = React.captureOwnerStack
1882+
? React.captureOwnerStack()
1883+
: null;
1884+
},
1885+
},
1886+
);
1887+
1888+
await new Promise(resolve => {
1889+
setImmediate(() => {
1890+
clientAbortController.abort();
1891+
resolve();
1892+
});
1893+
});
1894+
1895+
const fizzPrerenderStream = await fizzPrerenderStreamResult;
1896+
const prerenderHTML = await readResult(fizzPrerenderStream.prelude);
1897+
1898+
expect(prerenderHTML).toContain('Loading...');
1899+
1900+
if (__DEV__) {
1901+
expect(normalizeCodeLocInfo(componentStack)).toBe(
1902+
'\n in Component\n in Suspense\n in body\n in html\n in ClientRoot (at **)',
1903+
);
1904+
} else {
1905+
expect(normalizeCodeLocInfo(componentStack)).toBe(
1906+
'\n in Suspense\n in body\n in html\n in ClientRoot (at **)',
1907+
);
1908+
}
1909+
1910+
if (__DEV__) {
1911+
expect(normalizeCodeLocInfo(ownerStack)).toBe('\n in App (at **)');
1912+
} else {
1913+
expect(ownerStack).toBeNull();
1914+
}
1915+
});
17801916
});

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ let webpackModules;
2121
let webpackModuleLoading;
2222
let React;
2323
let ReactDOMServer;
24+
let ReactDOMFizzStatic;
2425
let ReactServer;
2526
let ReactServerDOMServer;
2627
let ReactServerDOMStaticServer;
@@ -70,11 +71,21 @@ describe('ReactFlightDOMNode', () => {
7071

7172
React = require('react');
7273
ReactDOMServer = require('react-dom/server.node');
74+
ReactDOMFizzStatic = require('react-dom/static');
7375
ReactServerDOMClient = require('react-server-dom-webpack/client');
7476
Stream = require('stream');
7577
use = React.use;
7678
});
7779

80+
function normalizeCodeLocInfo(str) {
81+
return (
82+
str &&
83+
str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
84+
return ' in ' + name + (/\d/.test(m) ? ' (at **)' : '');
85+
})
86+
);
87+
}
88+
7889
function readResult(stream) {
7990
return new Promise((resolve, reject) => {
8091
let buffer = '';
@@ -93,6 +104,42 @@ describe('ReactFlightDOMNode', () => {
93104
});
94105
}
95106

107+
async function readWebResult(webStream: ReadableStream<Uint8Array>) {
108+
const reader = webStream.getReader();
109+
let result = '';
110+
while (true) {
111+
const {done, value} = await reader.read();
112+
if (done) {
113+
return result;
114+
}
115+
result += Buffer.from(value).toString('utf8');
116+
}
117+
}
118+
119+
async function createBufferedUnclosingStream(
120+
prelude: ReadableStream<Uint8Array>,
121+
): ReadableStream<Uint8Array> {
122+
const chunks: Array<Uint8Array> = [];
123+
const reader = prelude.getReader();
124+
while (true) {
125+
const {done, value} = await reader.read();
126+
if (done) {
127+
break;
128+
} else {
129+
chunks.push(value);
130+
}
131+
}
132+
133+
let i = 0;
134+
return new ReadableStream({
135+
async pull(controller) {
136+
if (i < chunks.length) {
137+
controller.enqueue(chunks[i++]);
138+
}
139+
},
140+
});
141+
}
142+
96143
it('should support web streams in node', async () => {
97144
function Text({children}) {
98145
return <span>{children}</span>;
@@ -543,4 +590,125 @@ describe('ReactFlightDOMNode', () => {
543590
const result = await readResult(ssrStream);
544591
expect(result).toContain('loading...');
545592
});
593+
594+
// @gate enableHalt && enableAsyncDebugInfo
595+
it('includes source locations in component and owner stacks for halted components', async () => {
596+
async function Component() {
597+
await new Promise(() => {});
598+
return null;
599+
}
600+
601+
function App() {
602+
return ReactServer.createElement(
603+
'html',
604+
null,
605+
ReactServer.createElement(
606+
'body',
607+
null,
608+
ReactServer.createElement(
609+
ReactServer.Suspense,
610+
{fallback: 'Loading...'},
611+
ReactServer.createElement(Component, null),
612+
),
613+
),
614+
);
615+
}
616+
617+
const errors = [];
618+
const serverAbortController = new AbortController();
619+
const {pendingResult} = await serverAct(async () => {
620+
// destructure trick to avoid the act scope from awaiting the returned value
621+
return {
622+
pendingResult: ReactServerDOMStaticServer.unstable_prerender(
623+
ReactServer.createElement(App, null),
624+
webpackMap,
625+
{
626+
signal: serverAbortController.signal,
627+
onError(error) {
628+
errors.push(error);
629+
},
630+
},
631+
),
632+
};
633+
});
634+
635+
await await serverAct(
636+
async () =>
637+
new Promise(resolve => {
638+
setImmediate(() => {
639+
serverAbortController.abort();
640+
resolve();
641+
});
642+
}),
643+
);
644+
645+
const {prelude} = await pendingResult;
646+
647+
expect(errors).toEqual([]);
648+
649+
function ClientRoot({response}) {
650+
return use(response);
651+
}
652+
653+
const prerenderResponse = ReactServerDOMClient.createFromReadableStream(
654+
await createBufferedUnclosingStream(prelude),
655+
{
656+
serverConsumerManifest: {
657+
moduleMap: null,
658+
moduleLoading: null,
659+
},
660+
},
661+
);
662+
663+
let componentStack;
664+
let ownerStack;
665+
666+
const clientAbortController = new AbortController();
667+
668+
const fizzPrerenderStreamResult = ReactDOMFizzStatic.prerender(
669+
React.createElement(ClientRoot, {response: prerenderResponse}),
670+
{
671+
signal: clientAbortController.signal,
672+
onError(error, errorInfo) {
673+
componentStack = errorInfo.componentStack;
674+
ownerStack = React.captureOwnerStack
675+
? React.captureOwnerStack()
676+
: null;
677+
},
678+
},
679+
);
680+
681+
await await serverAct(
682+
async () =>
683+
new Promise(resolve => {
684+
setImmediate(() => {
685+
clientAbortController.abort();
686+
resolve();
687+
});
688+
}),
689+
);
690+
691+
const fizzPrerenderStream = await fizzPrerenderStreamResult;
692+
const prerenderHTML = await readWebResult(fizzPrerenderStream.prelude);
693+
694+
expect(prerenderHTML).toContain('Loading...');
695+
696+
if (__DEV__) {
697+
expect(normalizeCodeLocInfo(componentStack)).toBe(
698+
'\n in Component (at **)\n in Suspense\n in body\n in html\n in ClientRoot (at **)',
699+
);
700+
} else {
701+
expect(normalizeCodeLocInfo(componentStack)).toBe(
702+
'\n in Suspense\n in body\n in html\n in ClientRoot (at **)',
703+
);
704+
}
705+
706+
if (__DEV__) {
707+
expect(normalizeCodeLocInfo(ownerStack)).toBe(
708+
'\n in Component (at **)\n in App (at **)',
709+
);
710+
} else {
711+
expect(ownerStack).toBeNull();
712+
}
713+
});
546714
});

0 commit comments

Comments
 (0)