@samitouri / QOS-React / commits / ac2c1a5a58

[Flight] Ensure blocked debug info is handled properly (#34524)

This PR ensures that server components are reliably included in the DevTools component tree, even if debug info is received delayed, e.g. when using a debug channel. The fix consists of three parts: - We must not unset the debug chunk before all debug info entries are resolved. - We must ensure that the "RSC Stream" IO debug info entry is pushed last, after all other entries were resolved. - We need to transfer the debug info from blocked element chunks onto the lazy node and the element. Ideally, we wouldn't even create a lazy node for blocked elements that are at the root of the JSON payload, because that would basically wrap a lazy in a lazy. This optimization that ensures that everything around the blocked element can proceed is only needed for nested elements. However, we also need it for resolving deduped references in blocked root elements, unless we adapt that logic, which would be a bigger lift. When reloading the Flight fixture, the component tree is now displayed deterministically. Previously, it would sometimes omit synchronous server components. <img width="306" height="565" alt="complete" src="https://github.com/user-attachments/assets/db61aa10-1816-43e6-9903-0e585190cdf1" /> --------- Co-authored-by: Sebastian Markbage <sebastian@calyptus.eu>

Hendrik Liebau committed Sep 25, 2025 at 15:13 UTC ac2c1a5a5840d5e043d1d7a12a356f226e285c02
5 files changed +341 -113
packages/react-client/src/ReactFlightClient.js
+149 -63
@@ -499,10 +499,44 @@ function createErrorChunk<T>(
499 return new ReactPromise(ERRORED, null, error);
500 }
501
502 +function moveDebugInfoFromChunkToInnerValue<T>(
503 + chunk: InitializedChunk<T>,
504 + value: T,
505 +): void {
506 + // Remove the debug info from the initialized chunk, and add it to the inner
507 + // value instead. This can be a React element, an array, or an uninitialized
508 + // Lazy.
509 + const resolvedValue = resolveLazy(value);
510 + if (
511 + typeof resolvedValue === 'object' &&
512 + resolvedValue !== null &&
513 + (isArray(resolvedValue) ||
514 + typeof resolvedValue[ASYNC_ITERATOR] === 'function' ||
515 + resolvedValue.$$typeof === REACT_ELEMENT_TYPE ||
516 + resolvedValue.$$typeof === REACT_LAZY_TYPE)
517 + ) {
518 + const debugInfo = chunk._debugInfo.splice(0);
519 + if (isArray(resolvedValue._debugInfo)) {
520 + // $FlowFixMe[method-unbinding]
521 + resolvedValue._debugInfo.unshift.apply(
522 + resolvedValue._debugInfo,
523 + debugInfo,
524 + );
525 + } else {
526 + Object.defineProperty((resolvedValue: any), '_debugInfo', {
527 + configurable: false,
528 + enumerable: false,
529 + writable: true,
530 + value: debugInfo,
531 + });
532 + }
533 + }
534 +}
535 +
536 function wakeChunk<T>(
537 listeners: Array<InitializationReference | (T => mixed)>,
538 value: T,
505 - chunk: SomeChunk<T>,
539 + chunk: InitializedChunk<T>,
540 ): void {
541 for (let i = 0; i < listeners.length; i++) {
542 const listener = listeners[i];
@@ -512,6 +546,10 @@ function wakeChunk<T>(
546 fulfillReference(listener, value, chunk);
547 }
548 }
549 +
550 + if (__DEV__) {
551 + moveDebugInfoFromChunkToInnerValue(chunk, value);
552 + }
553 }
554
555 function rejectChunk(
@@ -649,7 +687,6 @@ function triggerErrorOnChunk<T>(
687 }
688 try {
689 initializeDebugChunk(response, chunk);
652 - chunk._debugChunk = null;
690 if (initializingHandler !== null) {
691 if (initializingHandler.errored) {
692 // Ignore error parsing debug info, we'll report the original error instead.
@@ -932,9 +969,9 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
969 }
970
971 if (__DEV__) {
935 - // Lazily initialize any debug info and block the initializing chunk on any unresolved entries.
972 + // Initialize any debug info and block the initializing chunk on any
973 + // unresolved entries.
974 initializeDebugChunk(response, chunk);
937 - chunk._debugChunk = null;
975 }
976
977 try {
@@ -946,7 +983,14 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
983 if (resolveListeners !== null) {
984 cyclicChunk.value = null;
985 cyclicChunk.reason = null;
949 - wakeChunk(resolveListeners, value, cyclicChunk);
986 + for (let i = 0; i < resolveListeners.length; i++) {
987 + const listener = resolveListeners[i];
988 + if (typeof listener === 'function') {
989 + listener(value);
990 + } else {
991 + fulfillReference(listener, value, cyclicChunk);
992 + }
993 + }
994 }
995 if (initializingHandler !== null) {
996 if (initializingHandler.errored) {
@@ -963,6 +1007,10 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
1007 const initializedChunk: InitializedChunk<T> = (chunk: any);
1008 initializedChunk.status = INITIALIZED;
1009 initializedChunk.value = value;
1010 +
1011 + if (__DEV__) {
1012 + moveDebugInfoFromChunkToInnerValue(initializedChunk, value);
1013 + }
1014 } catch (error) {
1015 const erroredChunk: ErroredChunk<T> = (chunk: any);
1016 erroredChunk.status = ERRORED;
@@ -1079,7 +1127,7 @@ function getTaskName(type: mixed): string {
1127 function initializeElement(
1128 response: Response,
1129 element: any,
1082 - lazyType: null | LazyComponent<
1130 + lazyNode: null | LazyComponent<
1131 React$Element<any>,
1132 SomeChunk<React$Element<any>>,
1133 >,
@@ -1151,15 +1199,33 @@ function initializeElement(
1199 initializeFakeStack(response, owner);
1200 }
1201
1154 - // In case the JSX runtime has validated the lazy type as a static child, we
1155 - // need to transfer this information to the element.
1156 - if (
1157 - lazyType &&
1158 - lazyType._store &&
1159 - lazyType._store.validated &&
1160 - !element._store.validated
1161 - ) {
1162 - element._store.validated = lazyType._store.validated;
1202 + if (lazyNode !== null) {
1203 + // In case the JSX runtime has validated the lazy type as a static child, we
1204 + // need to transfer this information to the element.
1205 + if (
1206 + lazyNode._store &&
1207 + lazyNode._store.validated &&
1208 + !element._store.validated
1209 + ) {
1210 + element._store.validated = lazyNode._store.validated;
1211 + }
1212 +
1213 + // If the lazy node is initialized, we move its debug info to the inner
1214 + // value.
1215 + if (lazyNode._payload.status === INITIALIZED && lazyNode._debugInfo) {
1216 + const debugInfo = lazyNode._debugInfo.splice(0);
1217 + if (element._debugInfo) {
1218 + // $FlowFixMe[method-unbinding]
1219 + element._debugInfo.unshift.apply(element._debugInfo, debugInfo);
1220 + } else {
1221 + Object.defineProperty(element, '_debugInfo', {
1222 + configurable: false,
1223 + enumerable: false,
1224 + writable: true,
1225 + value: debugInfo,
1226 + });
1227 + }
1228 + }
1229 }
1230
1231 // TODO: We should be freezing the element but currently, we might write into
@@ -1279,13 +1345,13 @@ function createElement(
1345 createBlockedChunk(response);
1346 handler.value = element;
1347 handler.chunk = blockedChunk;
1282 - const lazyType = createLazyChunkWrapper(blockedChunk, validated);
1348 + const lazyNode = createLazyChunkWrapper(blockedChunk, validated);
1349 if (__DEV__) {
1350 // After we have initialized any blocked references, initialize stack etc.
1285 - const init = initializeElement.bind(null, response, element, lazyType);
1351 + const init = initializeElement.bind(null, response, element, lazyNode);
1352 blockedChunk.then(init, init);
1353 }
1288 - return lazyType;
1354 + return lazyNode;
1355 }
1356 }
1357 if (__DEV__) {
@@ -1466,7 +1532,7 @@ function fulfillReference(
1532 const element: any = handler.value;
1533 switch (key) {
1534 case '3':
1469 - transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
1535 + transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
1536 element.props = mappedValue;
1537 break;
1538 case '4':
@@ -1482,11 +1548,11 @@ function fulfillReference(
1548 }
1549 break;
1550 default:
1485 - transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
1551 + transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
1552 break;
1553 }
1554 } else if (__DEV__ && !reference.isDebug) {
1489 - transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
1555 + transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
1556 }
1557
1558 handler.deps--;
@@ -1808,47 +1874,34 @@ function loadServerReference<A: Iterable<any>, T>(
1874 return (null: any);
1875 }
1876
1877 +function resolveLazy(value: any): mixed {
1878 + while (
1879 + typeof value === 'object' &&
1880 + value !== null &&
1881 + value.$$typeof === REACT_LAZY_TYPE
1882 + ) {
1883 + const payload: SomeChunk<any> = value._payload;
1884 + if (payload.status === INITIALIZED) {
1885 + value = payload.value;
1886 + continue;
1887 + }
1888 + break;
1889 + }
1890 +
1891 + return value;
1892 +}
1893 +
1894 function transferReferencedDebugInfo(
1895 parentChunk: null | SomeChunk<any>,
1896 referencedChunk: SomeChunk<any>,
1814 - referencedValue: mixed,
1897 ): void {
1898 if (__DEV__) {
1817 - const referencedDebugInfo = referencedChunk._debugInfo;
1818 - // If we have a direct reference to an object that was rendered by a synchronous
1819 - // server component, it might have some debug info about how it was rendered.
1820 - // We forward this to the underlying object. This might be a React Element or
1821 - // an Array fragment.
1822 - // If this was a string / number return value we lose the debug info. We choose
1823 - // that tradeoff to allow sync server components to return plain values and not
1824 - // use them as React Nodes necessarily. We could otherwise wrap them in a Lazy.
1825 - if (
1826 - typeof referencedValue === 'object' &&
1827 - referencedValue !== null &&
1828 - (isArray(referencedValue) ||
1829 - typeof referencedValue[ASYNC_ITERATOR] === 'function' ||
1830 - referencedValue.$$typeof === REACT_ELEMENT_TYPE)
1831 - ) {
1832 - // We should maybe use a unique symbol for arrays but this is a React owned array.
1833 - // $FlowFixMe[prop-missing]: This should be added to elements.
1834 - const existingDebugInfo: ?ReactDebugInfo =
1835 - (referencedValue._debugInfo: any);
1836 - if (existingDebugInfo == null) {
1837 - Object.defineProperty((referencedValue: any), '_debugInfo', {
1838 - configurable: false,
1839 - enumerable: false,
1840 - writable: true,
1841 - value: referencedDebugInfo.slice(0), // Clone so that pushing later isn't going into the original
1842 - });
1843 - } else {
1844 - // $FlowFixMe[method-unbinding]
1845 - existingDebugInfo.push.apply(existingDebugInfo, referencedDebugInfo);
1846 - }
1847 - }
1848 - // We also add the debug info to the initializing chunk since the resolution of that promise is
1849 - // also blocked by the referenced debug info. By adding it to both we can track it even if the array/element
1850 - // is extracted, or if the root is rendered as is.
1899 + // We add the debug info to the initializing chunk since the resolution of
1900 + // that promise is also blocked by the referenced debug info. By adding it
1901 + // to both we can track it even if the array/element/lazy is extracted, or
1902 + // if the root is rendered as is.
1903 if (parentChunk !== null) {
1904 + const referencedDebugInfo = referencedChunk._debugInfo;
1905 const parentDebugInfo = parentChunk._debugInfo;
1906 for (let i = 0; i < referencedDebugInfo.length; ++i) {
1907 const debugInfoEntry = referencedDebugInfo[i];
@@ -1999,7 +2052,7 @@ function getOutlinedModel<T>(
2052 // If we're resolving the "owner" or "stack" slot of an Element array, we don't call
2053 // transferReferencedDebugInfo because this reference is to a debug chunk.
2054 } else {
2002 - transferReferencedDebugInfo(initializingChunk, chunk, chunkValue);
2055 + transferReferencedDebugInfo(initializingChunk, chunk);
2056 }
2057 return chunkValue;
2058 case PENDING:
@@ -2709,14 +2762,47 @@ function incrementChunkDebugInfo(
2762 }
2763 }
2764
2765 +function addDebugInfo(chunk: SomeChunk<any>, debugInfo: ReactDebugInfo): void {
2766 + const value = resolveLazy(chunk.value);
2767 + if (
2768 + typeof value === 'object' &&
2769 + value !== null &&
2770 + (isArray(value) ||
2771 + typeof value[ASYNC_ITERATOR] === 'function' ||
2772 + value.$$typeof === REACT_ELEMENT_TYPE ||
2773 + value.$$typeof === REACT_LAZY_TYPE)
2774 + ) {
2775 + if (isArray(value._debugInfo)) {
2776 + // $FlowFixMe[method-unbinding]
2777 + value._debugInfo.push.apply(value._debugInfo, debugInfo);
2778 + } else {
2779 + Object.defineProperty((value: any), '_debugInfo', {
2780 + configurable: false,
2781 + enumerable: false,
2782 + writable: true,
2783 + value: debugInfo,
2784 + });
2785 + }
2786 + } else {
2787 + // $FlowFixMe[method-unbinding]
2788 + chunk._debugInfo.push.apply(chunk._debugInfo, debugInfo);
2789 + }
2790 +}
2791 +
2792 function resolveChunkDebugInfo(
2793 streamState: StreamState,
2794 chunk: SomeChunk<any>,
2795 ): void {
2796 if (__DEV__ && enableAsyncDebugInfo) {
2717 - // Push the currently resolving chunk's debug info representing the stream on the Promise
2718 - // that was waiting on the stream.
2719 - chunk._debugInfo.push({awaited: streamState._debugInfo});
2797 + // Add the currently resolving chunk's debug info representing the stream
2798 + // to the Promise that was waiting on the stream, or its underlying value.
2799 + const debugInfo: ReactDebugInfo = [{awaited: streamState._debugInfo}];
2800 + if (chunk.status === PENDING || chunk.status === BLOCKED) {
2801 + const boundAddDebugInfo = addDebugInfo.bind(null, chunk, debugInfo);
2802 + chunk.then(boundAddDebugInfo, boundAddDebugInfo);
2803 + } else {
2804 + addDebugInfo(chunk, debugInfo);
2805 + }
2806 }
2807 }
2808
@@ -2909,7 +2995,8 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
2995 const resolveListeners = chunk.value;
2996
2997 if (__DEV__) {
2912 - // Lazily initialize any debug info and block the initializing chunk on any unresolved entries.
2998 + // Initialize any debug info and block the initializing chunk on any
2999 + // unresolved entries.
3000 if (chunk._debugChunk != null) {
3001 const prevHandler = initializingHandler;
3002 const prevChunk = initializingChunk;
@@ -2923,7 +3010,6 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
3010 }
3011 try {
3012 initializeDebugChunk(response, chunk);
2926 - chunk._debugChunk = null;
3013 if (initializingHandler !== null) {
3014 if (initializingHandler.errored) {
3015 // Ignore error parsing debug info, we'll report the original error instead.
@@ -2947,7 +3033,7 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
3033 resolvedChunk.value = stream;
3034 resolvedChunk.reason = controller;
3035 if (resolveListeners !== null) {
2950 - wakeChunk(resolveListeners, chunk.value, chunk);
3036 + wakeChunk(resolveListeners, chunk.value, (chunk: any));
3037 }
3038 }
3039
packages/react-client/src/__tests__/ReactFlight-test.js
+13 -42
@@ -327,8 +327,8 @@ describe('ReactFlight', () => {
327 const transport = ReactNoopFlightServer.render(root);
328
329 await act(async () => {
330 - const promise = ReactNoopFlightClient.read(transport);
331 - expect(getDebugInfo(promise)).toEqual(
330 + const result = await ReactNoopFlightClient.read(transport);
331 + expect(getDebugInfo(result)).toEqual(
332 __DEV__
333 ? [
334 {time: 12},
@@ -346,7 +346,7 @@ describe('ReactFlight', () => {
346 ]
347 : undefined,
348 );
349 - ReactNoop.render(await promise);
349 + ReactNoop.render(result);
350 });
351
352 expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb Smith</span>);
@@ -1378,9 +1378,7 @@ describe('ReactFlight', () => {
1378 environmentName: 'Server',
1379 },
1380 ],
1381 - findSourceMapURLCalls: [
1382 - [__filename, 'Server'],
1383 - [__filename, 'Server'],
1381 + findSourceMapURLCalls: expect.arrayContaining([
1382 // TODO: What should we request here? The outer (<anonymous>) or the inner (inspected-page.html)?
1383 ['inspected-page.html:29:11), <anonymous>', 'Server'],
1384 [
@@ -1389,8 +1387,7 @@ describe('ReactFlight', () => {
1387 ],
1388 ['file:///testing.js', 'Server'],
1389 ['', 'Server'],
1392 - [__filename, 'Server'],
1393 - ],
1390 + ]),
1391 });
1392 } else {
1393 expect(errors.map(getErrorForJestMatcher)).toEqual([
@@ -2785,8 +2782,8 @@ describe('ReactFlight', () => {
2782 );
2783
2784 await act(async () => {
2788 - const promise = ReactNoopFlightClient.read(transport);
2789 - expect(getDebugInfo(promise)).toEqual(
2785 + const result = await ReactNoopFlightClient.read(transport);
2786 + expect(getDebugInfo(result)).toEqual(
2787 __DEV__
2788 ? [
2789 {time: gate(flags => flags.enableAsyncDebugInfo) ? 22 : 20},
@@ -2803,11 +2800,10 @@ describe('ReactFlight', () => {
2800 ]
2801 : undefined,
2802 );
2806 - const result = await promise;
2803
2804 const thirdPartyChildren = await result.props.children[1];
2805 // We expect the debug info to be transferred from the inner stream to the outer.
2810 - expect(getDebugInfo(thirdPartyChildren[0])).toEqual(
2806 + expect(getDebugInfo(await thirdPartyChildren[0])).toEqual(
2807 __DEV__
2808 ? [
2809 {time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22}, // Clamped to the start
@@ -2910,8 +2906,8 @@ describe('ReactFlight', () => {
2906 );
2907
2908 await act(async () => {
2913 - const promise = ReactNoopFlightClient.read(transport);
2914 - expect(getDebugInfo(promise)).toEqual(
2909 + const result = await ReactNoopFlightClient.read(transport);
2910 + expect(getDebugInfo(result)).toEqual(
2911 __DEV__
2912 ? [
2913 {time: 16},
@@ -2924,17 +2920,10 @@ describe('ReactFlight', () => {
2920 transport: expect.arrayContaining([]),
2921 },
2922 },
2927 - {
2928 - time: 16,
2929 - },
2930 - {
2931 - time: 16,
2932 - },
2923 {time: 31},
2924 ]
2925 : undefined,
2926 );
2937 - const result = await promise;
2927 const thirdPartyFragment = await result.props.children;
2928 expect(getDebugInfo(thirdPartyFragment)).toEqual(
2929 __DEV__
@@ -2949,15 +2938,7 @@ describe('ReactFlight', () => {
2938 children: {},
2939 },
2940 },
2952 - {
2953 - time: 33,
2954 - },
2955 - {
2956 - time: 33,
2957 - },
2958 - {
2959 - time: 33,
2960 - },
2941 + {time: 33},
2942 ]
2943 : undefined,
2944 );
@@ -3013,8 +2994,8 @@ describe('ReactFlight', () => {
2994 );
2995
2996 await act(async () => {
3016 - const promise = ReactNoopFlightClient.read(transport);
3017 - expect(getDebugInfo(promise)).toEqual(
2997 + const result = await ReactNoopFlightClient.read(transport);
2998 + expect(getDebugInfo(result)).toEqual(
2999 __DEV__
3000 ? [
3001 {time: 16},
@@ -3040,7 +3021,6 @@ describe('ReactFlight', () => {
3021 ]
3022 : undefined,
3023 );
3043 - const result = await promise;
3024 ReactNoop.render(result);
3025 });
3026
@@ -3891,15 +3871,6 @@ describe('ReactFlight', () => {
3871 {
3872 time: 13,
3873 },
3894 - {
3895 - time: 14,
3896 - },
3897 - {
3898 - time: 15,
3899 - },
3900 - {
3901 - time: 16,
3902 - },
3874 ]);
3875 } else {
3876 expect(root._debugInfo).toBe(undefined);
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+147
@@ -27,6 +27,7 @@ let webpackMap;
27 let webpackServerMap;
28 let act;
29 let serverAct;
30 +let getDebugInfo;
31 let React;
32 let ReactDOM;
33 let ReactDOMClient;
@@ -48,6 +49,10 @@ describe('ReactFlightDOMBrowser', () => {
49 ReactServerScheduler = require('scheduler');
50 patchMessageChannel(ReactServerScheduler);
51 serverAct = require('internal-test-utils').serverAct;
52 + getDebugInfo = require('internal-test-utils').getDebugInfo.bind(null, {
53 + ignoreProps: true,
54 + useFixedTime: true,
55 + });
56
57 // Simulate the condition resolution
58
@@ -1767,6 +1772,9 @@ describe('ReactFlightDOMBrowser', () => {
1772 webpackMap,
1773 ),
1774 );
1775 +
1776 + // Snapshot updates change this formatting, so we let prettier ignore it.
1777 + // prettier-ignore
1778 const response =
1779 await ReactServerDOMClient.createFromReadableStream(stream);
1780
@@ -2906,4 +2914,143 @@ describe('ReactFlightDOMBrowser', () => {
2914 '<div><span>Hi</span><span>Sebbie</span></div>',
2915 );
2916 });
2917 +
2918 + it('should fully resolve debug info when transported through a (slow) debug channel', async () => {
2919 + function Paragraph({children}) {
2920 + return ReactServer.createElement('p', null, children);
2921 + }
2922 +
2923 + let debugReadableStreamController;
2924 +
2925 + const debugReadableStream = new ReadableStream({
2926 + start(controller) {
2927 + debugReadableStreamController = controller;
2928 + },
2929 + });
2930 +
2931 + const stream = await serverAct(() =>
2932 + ReactServerDOMServer.renderToReadableStream(
2933 + {
2934 + root: ReactServer.createElement(
2935 + ReactServer.Fragment,
2936 + null,
2937 + ReactServer.createElement(Paragraph, null, 'foo'),
2938 + ReactServer.createElement(Paragraph, null, 'bar'),
2939 + ),
2940 + },
2941 + webpackMap,
2942 + {
2943 + debugChannel: {
2944 + writable: new WritableStream({
2945 + write(chunk) {
2946 + debugReadableStreamController.enqueue(chunk);
2947 + },
2948 + close() {
2949 + debugReadableStreamController.close();
2950 + },
2951 + }),
2952 + },
2953 + },
2954 + ),
2955 + );
2956 +
2957 + function ClientRoot({response}) {
2958 + const {root} = use(response);
2959 + return root;
2960 + }
2961 +
2962 + const [slowDebugStream1, slowDebugStream2] =
2963 + createDelayedStream(debugReadableStream).tee();
2964 +
2965 + const response = ReactServerDOMClient.createFromReadableStream(stream, {
2966 + debugChannel: {readable: slowDebugStream1},
2967 + });
2968 +
2969 + const container = document.createElement('div');
2970 + const clientRoot = ReactDOMClient.createRoot(container);
2971 +
2972 + await act(() => {
2973 + clientRoot.render(<ClientRoot response={response} />);
2974 + });
2975 +
2976 + if (__DEV__) {
2977 + const debugStreamReader = slowDebugStream2.getReader();
2978 + while (true) {
2979 + const {done} = await debugStreamReader.read();
2980 + if (done) {
2981 + break;
2982 + }
2983 + // Allow the client to process each debug chunk as it arrives.
2984 + await act(() => {});
2985 + }
2986 + }
2987 +
2988 + expect(container.innerHTML).toBe('<p>foo</p><p>bar</p>');
2989 +
2990 + if (
2991 + __DEV__ &&
2992 + gate(
2993 + flags =>
2994 + flags.enableComponentPerformanceTrack && flags.enableAsyncDebugInfo,
2995 + )
2996 + ) {
2997 + const result = await response;
2998 + const firstParagraph = result.root[0];
2999 +
3000 + expect(getDebugInfo(firstParagraph)).toMatchInlineSnapshot(`
3001 + [
3002 + {
3003 + "time": 0,
3004 + },
3005 + {
3006 + "env": "Server",
3007 + "key": null,
3008 + "name": "Paragraph",
3009 + "props": {},
3010 + "stack": [
3011 + [
3012 + "",
3013 + "/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js",
3014 + 2937,
3015 + 27,
3016 + 2931,
3017 + 34,
3018 + ],
3019 + [
3020 + "serverAct",
3021 + "/packages/internal-test-utils/internalAct.js",
3022 + 270,
3023 + 19,
3024 + 231,
3025 + 1,
3026 + ],
3027 + [
3028 + "Object.<anonymous>",
3029 + "/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js",
3030 + 2931,
3031 + 18,
3032 + 2918,
3033 + 89,
3034 + ],
3035 + ],
3036 + },
3037 + {
3038 + "time": 0,
3039 + },
3040 + {
3041 + "awaited": {
3042 + "byteSize": 0,
3043 + "end": 0,
3044 + "name": "RSC stream",
3045 + "owner": null,
3046 + "start": 0,
3047 + "value": {
3048 + "value": "stream",
3049 + },
3050 + },
3051 + },
3052 + ]
3053 + `);
3054 + }
3055 + });
3056 });
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+12 -4
@@ -1240,7 +1240,7 @@ describe('ReactFlightDOMEdge', () => {
1240 env: 'Server',
1241 });
1242 if (gate(flags => flags.enableAsyncDebugInfo)) {
1243 - expect(lazyWrapper._debugInfo).toEqual([
1243 + expect(greeting._debugInfo).toEqual([
1244 {time: 12},
1245 greetInfo,
1246 {time: 13},
@@ -1259,7 +1259,7 @@ describe('ReactFlightDOMEdge', () => {
1259 }
1260 // The owner that created the span was the outer server component.
1261 // We expect the debug info to be referentially equal to the owner.
1262 - expect(greeting._owner).toBe(lazyWrapper._debugInfo[1]);
1262 + expect(greeting._owner).toBe(greeting._debugInfo[1]);
1263 } else {
1264 expect(lazyWrapper._debugInfo).toBe(undefined);
1265 expect(greeting._owner).toBe(undefined);
@@ -1930,11 +1930,19 @@ describe('ReactFlightDOMEdge', () => {
1930
1931 if (__DEV__) {
1932 expect(normalizeCodeLocInfo(componentStack)).toBe(
1933 - '\n in Component\n in Suspense\n in body\n in html\n in ClientRoot (at **)',
1933 + '\n in Component\n' +
1934 + ' in Suspense\n' +
1935 + ' in body\n' +
1936 + ' in html\n' +
1937 + ' in App (at **)\n' +
1938 + ' in ClientRoot (at **)',
1939 );
1940 } else {
1941 expect(normalizeCodeLocInfo(componentStack)).toBe(
1937 - '\n in Suspense\n in body\n in html\n in ClientRoot (at **)',
1942 + '\n in Suspense\n' +
1943 + ' in body\n' +
1944 + ' in html\n' +
1945 + ' in ClientRoot (at **)',
1946 );
1947 }
1948
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
+20 -4
@@ -722,11 +722,19 @@ describe('ReactFlightDOMNode', () => {
722
723 if (__DEV__) {
724 expect(normalizeCodeLocInfo(componentStack)).toBe(
725 - '\n in Component (at **)\n in Suspense\n in body\n in html\n in ClientRoot (at **)',
725 + '\n in Component (at **)\n' +
726 + ' in Suspense\n' +
727 + ' in body\n' +
728 + ' in html\n' +
729 + ' in App (at **)\n' +
730 + ' in ClientRoot (at **)',
731 );
732 } else {
733 expect(normalizeCodeLocInfo(componentStack)).toBe(
729 - '\n in Suspense\n in body\n in html\n in ClientRoot (at **)',
734 + '\n in Suspense\n' +
735 + ' in body\n' +
736 + ' in html\n' +
737 + ' in ClientRoot (at **)',
738 );
739 }
740
@@ -861,11 +869,19 @@ describe('ReactFlightDOMNode', () => {
869
870 if (__DEV__) {
871 expect(normalizeCodeLocInfo(componentStack)).toBe(
864 - '\n in Component (at **)\n in Suspense\n in body\n in html\n in ClientRoot (at **)',
872 + '\n in Component (at **)\n' +
873 + ' in Suspense\n' +
874 + ' in body\n' +
875 + ' in html\n' +
876 + ' in App (at **)\n' +
877 + ' in ClientRoot (at **)',
878 );
879 } else {
880 expect(normalizeCodeLocInfo(componentStack)).toBe(
868 - '\n in Suspense\n in body\n in html\n in ClientRoot (at **)',
881 + '\n in Suspense\n' +
882 + ' in body\n' +
883 + ' in html\n' +
884 + ' in ClientRoot (at **)',
885 );
886 }
887