@samitouri / QOS-React-1 / commits / 41b3e9a670

[Fizz] Push a stalled use() to the ownerStack/debugTask (#35226)

Sebastian "Sebbie" Silbermann committed Jan 19, 2026 at 09:10 UTC 41b3e9a67004eb42631a9ff4504c56d22e2f97f0
4 files changed +527 -27
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
+188 -7
@@ -108,6 +108,28 @@ describe('ReactFlightDOMNode', () => {
108 );
109 }
110
111 + /**
112 + * Removes all stackframes not pointing into this file
113 + */
114 + function ignoreListStack(str) {
115 + if (!str) {
116 + return str;
117 + }
118 +
119 + let ignoreListedStack = '';
120 + const lines = str.split('\n');
121 +
122 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
123 + for (const line of lines) {
124 + if (line.indexOf(__filename) === -1) {
125 + } else {
126 + ignoreListedStack += '\n' + line.replace(__dirname, '.');
127 + }
128 + }
129 +
130 + return ignoreListedStack;
131 + }
132 +
133 function readResult(stream) {
134 return new Promise((resolve, reject) => {
135 let buffer = '';
@@ -784,6 +806,165 @@ describe('ReactFlightDOMNode', () => {
806 }
807 });
808
809 + // @gate enableHalt
810 + it('includes source locations in component and owner stacks for halted Client components', async () => {
811 + function SharedComponent({p1, p2, p3}) {
812 + use(p1);
813 + use(p2);
814 + use(p3);
815 + return <div>Hello, Dave!</div>;
816 + }
817 + const ClientComponentOnTheServer = clientExports(SharedComponent);
818 + const ClientComponentOnTheClient = clientExports(
819 + SharedComponent,
820 + 123,
821 + 'path/to/chunk.js',
822 + );
823 +
824 + let resolvePendingPromise;
825 + function ServerComponent() {
826 + const p1 = Promise.resolve();
827 + const p2 = new Promise(resolve => {
828 + resolvePendingPromise = value => {
829 + p2.status = 'fulfilled';
830 + p2.value = value;
831 + resolve(value);
832 + };
833 + });
834 + const p3 = new Promise(() => {});
835 + return ReactServer.createElement(ClientComponentOnTheClient, {
836 + p1: p1,
837 + p2: p2,
838 + p3: p3,
839 + });
840 + }
841 +
842 + function App() {
843 + return ReactServer.createElement(
844 + 'html',
845 + null,
846 + ReactServer.createElement(
847 + 'body',
848 + null,
849 + ReactServer.createElement(
850 + ReactServer.Suspense,
851 + {fallback: 'Loading...'},
852 + ReactServer.createElement(ServerComponent, null),
853 + ),
854 + ),
855 + );
856 + }
857 +
858 + const errors = [];
859 + const rscStream = await serverAct(() =>
860 + ReactServerDOMServer.renderToPipeableStream(
861 + ReactServer.createElement(App, null),
862 + webpackMap,
863 + ),
864 + );
865 +
866 + const readable = new Stream.PassThrough(streamOptions);
867 + rscStream.pipe(readable);
868 +
869 + function ClientRoot({response}) {
870 + return use(response);
871 + }
872 +
873 + const serverConsumerManifest = {
874 + moduleMap: {
875 + [webpackMap[ClientComponentOnTheClient.$$id].id]: {
876 + '*': webpackMap[ClientComponentOnTheServer.$$id],
877 + },
878 + },
879 + moduleLoading: webpackModuleLoading,
880 + };
881 +
882 + expect(errors).toEqual([]);
883 +
884 + function ClientRoot({response}) {
885 + return use(response);
886 + }
887 +
888 + const response = ReactServerDOMClient.createFromNodeStream(
889 + readable,
890 + serverConsumerManifest,
891 + );
892 +
893 + let componentStack;
894 + let ownerStack;
895 +
896 + const clientAbortController = new AbortController();
897 +
898 + const fizzPrerenderStreamResult = ReactDOMFizzStatic.prerender(
899 + React.createElement(ClientRoot, {response}),
900 + {
901 + signal: clientAbortController.signal,
902 + onError(error, errorInfo) {
903 + componentStack = errorInfo.componentStack;
904 + ownerStack = React.captureOwnerStack
905 + ? React.captureOwnerStack()
906 + : null;
907 + },
908 + },
909 + );
910 +
911 + resolvePendingPromise('custom-instrum-resolve');
912 + await serverAct(
913 + async () =>
914 + new Promise(resolve => {
915 + setImmediate(() => {
916 + clientAbortController.abort();
917 + resolve();
918 + });
919 + }),
920 + );
921 +
922 + const fizzPrerenderStream = await fizzPrerenderStreamResult;
923 + const prerenderHTML = await readWebResult(fizzPrerenderStream.prelude);
924 +
925 + expect(prerenderHTML).toContain('Loading...');
926 +
927 + if (__DEV__) {
928 + expect(normalizeCodeLocInfo(componentStack)).toBe(
929 + '\n' +
930 + ' in SharedComponent (at **)\n' +
931 + ' in ServerComponent' +
932 + (gate(flags => flags.enableAsyncDebugInfo) ? ' (at **)' : '') +
933 + '\n' +
934 + ' in Suspense\n' +
935 + ' in body\n' +
936 + ' in html\n' +
937 + ' in App (at **)\n' +
938 + ' in ClientRoot (at **)',
939 + );
940 + } else {
941 + expect(normalizeCodeLocInfo(componentStack)).toBe(
942 + '\n' +
943 + ' in SharedComponent (at **)\n' +
944 + ' in Suspense\n' +
945 + ' in body\n' +
946 + ' in html\n' +
947 + ' in ClientRoot (at **)',
948 + );
949 + }
950 +
951 + if (__DEV__) {
952 + expect(ignoreListStack(ownerStack)).toBe(
953 + // eslint-disable-next-line react-internal/safe-string-coercion
954 + '' +
955 + // The concrete location may change as this test is updated.
956 + // Just make sure they still point at React.use(p2)
957 + (gate(flags => flags.enableAsyncDebugInfo)
958 + ? '\n at SharedComponent (./ReactFlightDOMNode-test.js:813:7)'
959 + : '') +
960 + '\n at ServerComponent (file://./ReactFlightDOMNode-test.js:835:26)' +
961 + '\n at App (file://./ReactFlightDOMNode-test.js:852:25)',
962 + );
963 + } else {
964 + expect(ownerStack).toBeNull();
965 + }
966 + });
967 +
968 // @gate enableHalt
969 it('includes deeper location for aborted stacks', async () => {
970 async function getData() {
@@ -1364,12 +1545,12 @@ describe('ReactFlightDOMNode', () => {
1545 '\n' +
1546 ' in Dynamic' +
1547 (gate(flags => flags.enableAsyncDebugInfo)
1367 - ? ' (file://ReactFlightDOMNode-test.js:1238:27)\n'
1548 + ? ' (file://ReactFlightDOMNode-test.js:1419:27)\n'
1549 : '\n') +
1550 ' in body\n' +
1551 ' in html\n' +
1371 - ' in App (file://ReactFlightDOMNode-test.js:1251:25)\n' +
1372 - ' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1552 + ' in App (file://ReactFlightDOMNode-test.js:1432:25)\n' +
1553 + ' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
1554 );
1555 } else {
1556 expect(
@@ -1378,7 +1559,7 @@ describe('ReactFlightDOMNode', () => {
1559 '\n' +
1560 ' in body\n' +
1561 ' in html\n' +
1381 - ' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1562 + ' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
1563 );
1564 }
1565
@@ -1388,8 +1569,8 @@ describe('ReactFlightDOMNode', () => {
1569 normalizeCodeLocInfo(ownerStack, {preserveLocation: true}),
1570 ).toBe(
1571 '\n' +
1391 - ' in Dynamic (file://ReactFlightDOMNode-test.js:1238:27)\n' +
1392 - ' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1572 + ' in Dynamic (file://ReactFlightDOMNode-test.js:1419:27)\n' +
1573 + ' in App (file://ReactFlightDOMNode-test.js:1432:25)',
1574 );
1575 } else {
1576 expect(
@@ -1397,7 +1578,7 @@ describe('ReactFlightDOMNode', () => {
1578 ).toBe(
1579 '' +
1580 '\n' +
1400 - ' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1581 + ' in App (file://ReactFlightDOMNode-test.js:1432:25)',
1582 );
1583 }
1584 } else {
packages/react-server/src/ReactFizzServer.js
+100 -7
@@ -190,7 +190,14 @@ import assign from 'shared/assign';
190 import noop from 'shared/noop';
191 import getComponentNameFromType from 'shared/getComponentNameFromType';
192 import isArray from 'shared/isArray';
193 -import {SuspenseException, getSuspendedThenable} from './ReactFizzThenable';
193 +import {
194 + SuspenseException,
195 + getSuspendedThenable,
196 + ensureSuspendableThenableStateDEV,
197 + getSuspendedCallSiteStackDEV,
198 + getSuspendedCallSiteDebugTaskDEV,
199 + setCaptureSuspendedCallSiteDEV,
200 +} from './ReactFizzThenable';
201
202 // Linked list representing the identity of a component given the component/tag name and key.
203 // The name might be minified but we assume that it's going to be the same generated name. Typically
@@ -355,6 +362,7 @@ const OPEN = 11;
362 const ABORTING = 12;
363 const CLOSING = 13;
364 const CLOSED = 14;
365 +const STALLED_DEV = 15;
366
367 export opaque type Request = {
368 destination: null | Destination,
@@ -363,7 +371,7 @@ export opaque type Request = {
371 +renderState: RenderState,
372 +rootFormatContext: FormatContext,
373 +progressiveChunkSize: number,
366 - status: 10 | 11 | 12 | 13 | 14,
374 + status: 10 | 11 | 12 | 13 | 14 | 15,
375 fatalError: mixed,
376 nextSegmentId: number,
377 allPendingTasks: number, // when it reaches zero, we can close the connection.
@@ -1023,6 +1031,89 @@ function pushHaltedAwaitOnComponentStack(
1031 }
1032 }
1033
1034 +// performWork + retryTask without mutation
1035 +function rerenderStalledTask(request: Request, task: Task): void {
1036 + const prevStatus = request.status;
1037 + request.status = STALLED_DEV;
1038 +
1039 + const prevContext = getActiveContext();
1040 + const prevDispatcher = ReactSharedInternals.H;
1041 + ReactSharedInternals.H = HooksDispatcher;
1042 + const prevAsyncDispatcher = ReactSharedInternals.A;
1043 + ReactSharedInternals.A = DefaultAsyncDispatcher;
1044 +
1045 + const prevRequest = currentRequest;
1046 + currentRequest = request;
1047 +
1048 + const prevGetCurrentStackImpl = ReactSharedInternals.getCurrentStack;
1049 + ReactSharedInternals.getCurrentStack = getCurrentStackInDEV;
1050 +
1051 + const prevResumableState = currentResumableState;
1052 + setCurrentResumableState(request.resumableState);
1053 + switchContext(task.context);
1054 + const prevTaskInDEV = currentTaskInDEV;
1055 + setCurrentTaskInDEV(task);
1056 + try {
1057 + retryNode(request, task);
1058 + } catch (x) {
1059 + // Suspended again.
1060 + resetHooksState();
1061 + } finally {
1062 + setCurrentTaskInDEV(prevTaskInDEV);
1063 + setCurrentResumableState(prevResumableState);
1064 +
1065 + ReactSharedInternals.H = prevDispatcher;
1066 + ReactSharedInternals.A = prevAsyncDispatcher;
1067 +
1068 + ReactSharedInternals.getCurrentStack = prevGetCurrentStackImpl;
1069 + if (prevDispatcher === HooksDispatcher) {
1070 + // This means that we were in a reentrant work loop. This could happen
1071 + // in a renderer that supports synchronous work like renderToString,
1072 + // when it's called from within another renderer.
1073 + // Normally we don't bother switching the contexts to their root/default
1074 + // values when leaving because we'll likely need the same or similar
1075 + // context again. However, when we're inside a synchronous loop like this
1076 + // we'll to restore the context to what it was before returning.
1077 + switchContext(prevContext);
1078 + }
1079 + currentRequest = prevRequest;
1080 + request.status = prevStatus;
1081 + }
1082 +}
1083 +
1084 +function pushSuspendedCallSiteOnComponentStack(
1085 + request: Request,
1086 + task: Task,
1087 +): void {
1088 + setCaptureSuspendedCallSiteDEV(true);
1089 + const restoreThenableState = ensureSuspendableThenableStateDEV(
1090 + // refined at the callsite
1091 + ((task.thenableState: any): ThenableState),
1092 + );
1093 + try {
1094 + rerenderStalledTask(request, task);
1095 + } finally {
1096 + restoreThenableState();
1097 + setCaptureSuspendedCallSiteDEV(false);
1098 + }
1099 +
1100 + const suspendCallSiteStack = getSuspendedCallSiteStackDEV();
1101 + const suspendCallSiteDebugTask = getSuspendedCallSiteDebugTaskDEV();
1102 +
1103 + if (suspendCallSiteStack !== null) {
1104 + const ownerStack = task.componentStack;
1105 + task.componentStack = {
1106 + // The owner of the suspended call site would be the owner of this task.
1107 + // We need the task itself otherwise we'd miss a frame.
1108 + owner: ownerStack,
1109 + parent: suspendCallSiteStack.parent,
1110 + stack: suspendCallSiteStack.stack,
1111 + type: suspendCallSiteStack.type,
1112 + };
1113 + }
1114 + task.debugTask = suspendCallSiteDebugTask;
1115 +}
1116 +
1117 function pushServerComponentStack(
1118 task: Task,
1119 debugInfo: void | null | ReactDebugInfo,
@@ -2723,7 +2814,12 @@ function renderLazyComponent(
2814 const init = lazyComponent._init;
2815 Component = init(payload);
2816 }
2726 - if (request.status === ABORTING) {
2817 + if (
2818 + request.status === ABORTING &&
2819 + // We're going to discard this render anyway.
2820 + // We just need to reach the point where we suspended in dev.
2821 + (!__DEV__ || request.status !== STALLED_DEV)
2822 + ) {
2823 // eslint-disable-next-line no-throw-literal
2824 throw null;
2825 }
@@ -4535,12 +4631,9 @@ function abortTask(task: Task, request: Request, error: mixed): void {
4631 debugInfo = node._debugInfo;
4632 }
4633 pushHaltedAwaitOnComponentStack(task, debugInfo);
4538 - /*
4634 if (task.thenableState !== null) {
4540 - // TODO: If we were stalled inside use() of a Client Component then we should
4541 - // rerender to get the stack trace from the use() call.
4635 + pushSuspendedCallSiteOnComponentStack(request, task);
4636 }
4543 - */
4637 }
4638 }
4639
packages/react-server/src/ReactFizzThenable.js
+133 -4
@@ -7,20 +7,19 @@
7 * @flow
8 */
9
10 -// Corresponds to ReactFiberWakeable and ReactFlightWakeable modules. Generally,
10 +// Corresponds to ReactFiberThenable and ReactFlightThenable modules. Generally,
11 // changes to one module should be reflected in the others.
12
13 -// TODO: Rename this module and the corresponding Fiber one to "Thenable"
14 -// instead of "Wakeable". Or some other more appropriate name.
15 -
13 import type {
14 Thenable,
15 PendingThenable,
16 FulfilledThenable,
17 RejectedThenable,
18 } from 'shared/ReactTypes';
19 +import type {ComponentStackNode} from './ReactFizzComponentStack';
20
21 import noop from 'shared/noop';
22 +import {currentTaskInDEV} from './ReactFizzCurrentTask';
23
24 export opaque type ThenableState = Array<Thenable<any>>;
25
@@ -126,6 +125,9 @@ export function trackUsedThenable<T>(
125 // get captured by the work loop, log a warning, because that means
126 // something in userspace must have caught it.
127 suspendedThenable = thenable;
128 + if (__DEV__ && shouldCaptureSuspendedCallSite) {
129 + captureSuspendedCallSite();
130 + }
131 throw SuspenseException;
132 }
133 }
@@ -163,3 +165,130 @@ export function getSuspendedThenable(): Thenable<mixed> {
165 suspendedThenable = null;
166 return thenable;
167 }
168 +
169 +let shouldCaptureSuspendedCallSite: boolean = false;
170 +export function setCaptureSuspendedCallSiteDEV(capture: boolean): void {
171 + if (!__DEV__) {
172 + // eslint-disable-next-line react-internal/prod-error-codes
173 + throw new Error(
174 + 'setCaptureSuspendedCallSiteDEV was called in a production environment. ' +
175 + 'This is a bug in React.',
176 + );
177 + }
178 + shouldCaptureSuspendedCallSite = capture;
179 +}
180 +
181 +// DEV-only
182 +let suspendedCallSiteStack: ComponentStackNode | null = null;
183 +let suspendedCallSiteDebugTask: ConsoleTask | null = null;
184 +function captureSuspendedCallSite(): void {
185 + // This is currently only used when aborting in Fizz.
186 + // You can only abort the render in Fizz and Flight.
187 + // In Fiber we only track suspended use via DevTools.
188 + // In Flight, we track suspended use via async debug info.
189 + const currentTask = currentTaskInDEV;
190 + if (currentTask === null) {
191 + // eslint-disable-next-line react-internal/prod-error-codes -- not a prod error
192 + throw new Error(
193 + 'Expected to have a current task when tracking a suspend call site. ' +
194 + 'This is a bug in React.',
195 + );
196 + }
197 + const currentComponentStack = currentTask.componentStack;
198 + if (currentComponentStack === null) {
199 + // eslint-disable-next-line react-internal/prod-error-codes -- not a prod error
200 + throw new Error(
201 + 'Expected to have a component stack on the current task when ' +
202 + 'tracking a suspended call site. This is a bug in React.',
203 + );
204 + }
205 + suspendedCallSiteStack = {
206 + parent: currentComponentStack.parent,
207 + type: currentComponentStack.type,
208 + owner: currentComponentStack.owner,
209 + stack: Error('react-stack-top-frame'),
210 + };
211 + // TODO: If this is used in error handlers, the ConsoleTask stack
212 + // will just be this debugTask + the stack of the abort() call which usually means
213 + // it's just this debugTask.
214 + // Ideally we'd be able to reconstruct the owner ConsoleTask as well.
215 + // The stack of the debugTask would not point to the suspend location anyway.
216 + // The focus is really on callsite which should be used in captureOwnerStack().
217 + suspendedCallSiteDebugTask = currentTask.debugTask;
218 +}
219 +export function getSuspendedCallSiteStackDEV(): ComponentStackNode | null {
220 + if (__DEV__) {
221 + if (suspendedCallSiteStack === null) {
222 + return null;
223 + }
224 + const callSite = suspendedCallSiteStack;
225 + suspendedCallSiteStack = null;
226 + return callSite;
227 + } else {
228 + // eslint-disable-next-line react-internal/prod-error-codes
229 + throw new Error(
230 + 'getSuspendedCallSiteDEV was called in a production environment. ' +
231 + 'This is a bug in React.',
232 + );
233 + }
234 +}
235 +
236 +export function getSuspendedCallSiteDebugTaskDEV(): ConsoleTask | null {
237 + if (__DEV__) {
238 + if (suspendedCallSiteDebugTask === null) {
239 + return null;
240 + }
241 + const debugTask = suspendedCallSiteDebugTask;
242 + suspendedCallSiteDebugTask = null;
243 + return debugTask;
244 + } else {
245 + // eslint-disable-next-line react-internal/prod-error-codes
246 + throw new Error(
247 + 'getSuspendedCallSiteDebugTaskDEV was called in a production environment. ' +
248 + 'This is a bug in React.',
249 + );
250 + }
251 +}
252 +
253 +export function ensureSuspendableThenableStateDEV(
254 + thenableState: ThenableState,
255 +): () => void {
256 + if (__DEV__) {
257 + const lastThenable = thenableState[thenableState.length - 1];
258 + // Reset the last thenable back to pending.
259 + switch (lastThenable.status) {
260 + case 'fulfilled':
261 + const previousThenableValue = lastThenable.value;
262 + // $FlowIgnore[method-unbinding] We rebind .then immediately.
263 + const previousThenableThen = lastThenable.then.bind(lastThenable);
264 + delete lastThenable.value;
265 + delete (lastThenable: any).status;
266 + // We'll call .then again if we resuspend. Since we potentially corrupted
267 + // the internal state of unknown classes, we need to diffuse the potential
268 + // crash by replacing the .then method with a noop.
269 + // $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
270 + lastThenable.then = noop;
271 + return () => {
272 + // $FlowFixMe[cannot-write] Custom userspace Thenables may not be but native Promises are.
273 + lastThenable.then = previousThenableThen;
274 + lastThenable.value = previousThenableValue;
275 + lastThenable.status = 'fulfilled';
276 + };
277 + case 'rejected':
278 + const previousThenableReason = lastThenable.reason;
279 + delete lastThenable.reason;
280 + delete (lastThenable: any).status;
281 + return () => {
282 + lastThenable.reason = previousThenableReason;
283 + lastThenable.status = 'rejected';
284 + };
285 + }
286 + return noop;
287 + } else {
288 + // eslint-disable-next-line react-internal/prod-error-codes
289 + throw new Error(
290 + 'ensureSuspendableThenableStateDEV was called in a production environment. ' +
291 + 'This is a bug in React.',
292 + );
293 + }
294 +}
packages/react-server/src/__tests__/ReactServer-test.js
+106 -9
@@ -9,6 +9,7 @@
9 */
10
11 'use strict';
12 +import {AsyncLocalStorage} from 'node:async_hooks';
13
14 let act;
15 let React;
@@ -27,10 +28,43 @@ function normalizeCodeLocInfo(str) {
28 );
29 }
30
31 +/**
32 + * Removes all stackframes not pointing into this file
33 + */
34 +function ignoreListStack(str) {
35 + if (!str) {
36 + return str;
37 + }
38 +
39 + let ignoreListedStack = '';
40 + const lines = str.split('\n');
41 +
42 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
43 + for (const line of lines) {
44 + if (line.indexOf(__filename) === -1) {
45 + } else {
46 + ignoreListedStack += '\n' + line.replace(__dirname, '.');
47 + }
48 + }
49 +
50 + return ignoreListedStack;
51 +}
52 +
53 +const currentTask = new AsyncLocalStorage({defaultValue: null});
54 +
55 describe('ReactServer', () => {
56 beforeEach(() => {
57 jest.resetModules();
58
59 + console.createTask = jest.fn(taskName => {
60 + return {
61 + run: taskFn => {
62 + const parentTask = currentTask.getStore() || '';
63 + return currentTask.run(parentTask + '\n' + taskName, taskFn);
64 + },
65 + };
66 + });
67 +
68 act = require('internal-test-utils').act;
69 React = require('react');
70 ReactNoopServer = require('react-noop-renderer/server');
@@ -49,29 +83,67 @@ describe('ReactServer', () => {
83 });
84
85 it('has Owner Stacks in DEV when aborted', async () => {
52 - function Component({promise}) {
53 - React.use(promise);
86 + const Context = React.createContext(null);
87 +
88 + function Component({p1, p2, p3}) {
89 + const context = React.use(Context);
90 + if (context === null) {
91 + throw new Error('Missing context');
92 + }
93 + React.use(p1);
94 + React.use(p2);
95 + React.use(p3);
96 return <div>Hello, Dave!</div>;
97 }
56 - function App({promise}) {
57 - return <Component promise={promise} />;
98 + function Indirection({p1, p2, p3}) {
99 + return (
100 + <div>
101 + <Component p1={p1} p2={p2} p3={p3} />
102 + </div>
103 + );
104 + }
105 + function App({p1, p2, p3}) {
106 + return (
107 + <section>
108 + <div>
109 + <Indirection p1={p1} p2={p2} p3={p3} />
110 + </div>
111 + </section>
112 + );
113 }
114
115 let caughtError;
116 let componentStack;
117 let ownerStack;
118 + let task;
119 + const resolvedPromise = Promise.resolve('one');
120 + resolvedPromise.status = 'fulfilled';
121 + resolvedPromise.value = 'one';
122 + let resolvePendingPromise;
123 + const pendingPromise = new Promise(resolve => {
124 + resolvePendingPromise = value => {
125 + pendingPromise.status = 'fulfilled';
126 + pendingPromise.value = value;
127 + resolve(value);
128 + };
129 + });
130 + const hangingPromise = new Promise(() => {});
131 const result = ReactNoopServer.render(
64 - <App promise={new Promise(() => {})} />,
132 + <Context value="provided">
133 + <App p1={resolvedPromise} p2={pendingPromise} p3={hangingPromise} />
134 + </Context>,
135 {
136 onError: (error, errorInfo) => {
137 caughtError = error;
138 componentStack = errorInfo.componentStack;
139 ownerStack = __DEV__ ? React.captureOwnerStack() : null;
140 + task = currentTask.getStore();
141 },
142 },
143 );
144
145 await act(async () => {
146 + resolvePendingPromise('two');
147 result.abort();
148 });
149 expect(caughtError).toEqual(
@@ -80,10 +152,35 @@ describe('ReactServer', () => {
152 }),
153 );
154 expect(normalizeCodeLocInfo(componentStack)).toEqual(
83 - '\n in Component (at **)' + '\n in App (at **)',
84 - );
85 - expect(normalizeCodeLocInfo(ownerStack)).toEqual(
86 - __DEV__ ? '\n in App (at **)' : null,
155 + '\n in Component (at **)' +
156 + '\n in div' +
157 + '\n in Indirection (at **)' +
158 + '\n in div' +
159 + '\n in section' +
160 + '\n in App (at **)',
161 );
162 + if (__DEV__) {
163 + // The concrete location may change as this test is updated.
164 + // Just make sure they still point at the same code
165 + if (gate(flags => flags.enableAsyncDebugInfo)) {
166 + expect(ignoreListStack(ownerStack)).toEqual(
167 + '' +
168 + // Pointing at React.use(p2)
169 + '\n at Component (./ReactServer-test.js:94:13)' +
170 + '\n at Indirection (./ReactServer-test.js:101:44)' +
171 + '\n at App (./ReactServer-test.js:109:46)',
172 + );
173 + } else {
174 + expect(ignoreListStack(ownerStack)).toEqual(
175 + '' +
176 + '\n at Indirection (./ReactServer-test.js:101:44)' +
177 + '\n at App (./ReactServer-test.js:109:46)',
178 + );
179 + }
180 + expect(task).toEqual('\n<Component>');
181 + } else {
182 + expect(ownerStack).toBeNull();
183 + expect(task).toEqual(undefined);
184 + }
185 });
186 });