[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>
Hendrik Liebau committed
Jun 25, 2025 at 22:34 UTC
9b2a545b32ef19c6b67812d2c3444a709406a58b
2 files changed
+304
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+136
@@ -32,6 +32,7 @@ let webpackModuleLoading;
32
let React;
33
let ReactServer;
34
let ReactDOMServer;
35
+let ReactDOMFizzStatic;
36
let ReactServerDOMServer;
37
let ReactServerDOMStaticServer;
38
let ReactServerDOMClient;
@@ -102,6 +103,7 @@ describe('ReactFlightDOMEdge', () => {
103
);
104
React = require('react');
105
ReactDOMServer = require('react-dom/server.edge');
106
+ ReactDOMFizzStatic = require('react-dom/static.edge');
107
ReactServerDOMClient = require('react-server-dom-webpack/client');
108
use = React.use;
109
});
@@ -228,6 +230,30 @@ describe('ReactFlightDOMEdge', () => {
230
}
231
}
232
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
+
257
it('should allow an alternative module mapping to be used for SSR', async () => {
258
function ClientComponent() {
259
return <span>Client Component</span>;
@@ -1777,4 +1803,114 @@ describe('ReactFlightDOMEdge', () => {
1803
expect(error).not.toBe(null);
1804
expect(error.message).toBe(expectedMessage);
1805
});
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
+ });
1916
});
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
+168
@@ -21,6 +21,7 @@ let webpackModules;
21
let webpackModuleLoading;
22
let React;
23
let ReactDOMServer;
24
+let ReactDOMFizzStatic;
25
let ReactServer;
26
let ReactServerDOMServer;
27
let ReactServerDOMStaticServer;
@@ -70,11 +71,21 @@ describe('ReactFlightDOMNode', () => {
71
72
React = require('react');
73
ReactDOMServer = require('react-dom/server.node');
74
+ ReactDOMFizzStatic = require('react-dom/static');
75
ReactServerDOMClient = require('react-server-dom-webpack/client');
76
Stream = require('stream');
77
use = React.use;
78
});
79
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
+
89
function readResult(stream) {
90
return new Promise((resolve, reject) => {
91
let buffer = '';
@@ -93,6 +104,42 @@ describe('ReactFlightDOMNode', () => {
104
});
105
}
106
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
+
143
it('should support web streams in node', async () => {
144
function Text({children}) {
145
return <span>{children}</span>;
@@ -543,4 +590,125 @@ describe('ReactFlightDOMNode', () => {
590
const result = await readResult(ssrStream);
591
expect(result).toContain('loading...');
592
});
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
+ });
714
});