@samitouri / QOS-React / commits / 9b2a545b32

[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 });