Skip to content

Commit 7e9699b

Browse files
committed
[Flight] Add a failing test for aborted hanging promise stacks
1 parent dce1f6c commit 7e9699b

File tree

1 file changed

+171
-4
lines changed

1 file changed

+171
-4
lines changed

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

Lines changed: 171 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
import {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate';
1414

15+
const path = require('path');
16+
1517
global.ReadableStream =
1618
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
1719

@@ -88,12 +90,25 @@ describe('ReactFlightDOMNode', () => {
8890
);
8991
}
9092

91-
function normalizeCodeLocInfo(str) {
93+
const repoRoot = path.resolve(__dirname, '../../../../');
94+
95+
function normalizeCodeLocInfo(str, {preserveLocation = false} = {}) {
9296
return (
9397
str &&
94-
str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
95-
return ' in ' + name + (/\d/.test(m) ? ' (at **)' : '');
96-
})
98+
str.replace(
99+
/^ +(?:at|in) ([\S]+) ([^\n]*)/gm,
100+
function (m, name, location) {
101+
return (
102+
' in ' +
103+
name +
104+
(/\d/.test(m)
105+
? preserveLocation
106+
? ' ' + location.replace(repoRoot, '')
107+
: ' (at **)'
108+
: '')
109+
);
110+
},
111+
)
97112
);
98113
}
99114

@@ -896,6 +911,158 @@ describe('ReactFlightDOMNode', () => {
896911
}
897912
});
898913

914+
// @gate enableHalt && enableAsyncDebugInfo
915+
it('includes deeper location for aborted hanging promises', async () => {
916+
function createHangingPromise(signal) {
917+
return new Promise((resolve, reject) => {
918+
signal.addEventListener('abort', () => reject(signal.reason));
919+
});
920+
}
921+
922+
async function Component({promise}) {
923+
await promise;
924+
return null;
925+
}
926+
927+
function App({promise}) {
928+
return ReactServer.createElement(
929+
'html',
930+
null,
931+
ReactServer.createElement(
932+
'body',
933+
null,
934+
ReactServer.createElement(
935+
ReactServer.Suspense,
936+
{fallback: 'Loading...'},
937+
ReactServer.createElement(Component, {promise}),
938+
),
939+
),
940+
);
941+
}
942+
943+
function ClientRoot({response}) {
944+
return use(response);
945+
}
946+
947+
// This test relies on tasks resolving exactly as they would in a real
948+
// environment, which is not the case when using fake timers and serverAct.
949+
jest.useRealTimers();
950+
951+
try {
952+
const serverRenderAbortController = new AbortController();
953+
const serverCleanupAbortController = new AbortController();
954+
const promise = createHangingPromise(serverCleanupAbortController.signal);
955+
const errors = [];
956+
957+
// destructure trick to avoid the act scope from awaiting the returned value
958+
const {prelude} = await new Promise(resolve => {
959+
let result;
960+
961+
setImmediate(() => {
962+
result = ReactServerDOMStaticServer.unstable_prerender(
963+
ReactServer.createElement(App, {promise}),
964+
webpackMap,
965+
{
966+
signal: serverRenderAbortController.signal,
967+
onError(error) {
968+
errors.push(error);
969+
},
970+
filterStackFrame,
971+
},
972+
);
973+
974+
serverRenderAbortController.signal.addEventListener('abort', () => {
975+
serverCleanupAbortController.abort();
976+
});
977+
});
978+
979+
setImmediate(() => {
980+
serverRenderAbortController.abort();
981+
resolve(result);
982+
});
983+
});
984+
985+
expect(errors).toEqual([]);
986+
987+
const prerenderResponse = ReactServerDOMClient.createFromReadableStream(
988+
await createBufferedUnclosingStream(prelude),
989+
{
990+
serverConsumerManifest: {
991+
moduleMap: null,
992+
moduleLoading: null,
993+
},
994+
},
995+
);
996+
997+
let componentStack;
998+
let ownerStack;
999+
1000+
const clientAbortController = new AbortController();
1001+
1002+
const fizzPrerenderStream = await new Promise(resolve => {
1003+
let result;
1004+
1005+
setImmediate(() => {
1006+
result = ReactDOMFizzStatic.prerender(
1007+
React.createElement(ClientRoot, {response: prerenderResponse}),
1008+
{
1009+
signal: clientAbortController.signal,
1010+
onError(error, errorInfo) {
1011+
componentStack = errorInfo.componentStack;
1012+
ownerStack = React.captureOwnerStack
1013+
? React.captureOwnerStack()
1014+
: null;
1015+
},
1016+
},
1017+
);
1018+
});
1019+
1020+
setImmediate(() => {
1021+
clientAbortController.abort();
1022+
resolve(result);
1023+
});
1024+
});
1025+
1026+
const prerenderHTML = await readWebResult(fizzPrerenderStream.prelude);
1027+
1028+
expect(prerenderHTML).toContain('Loading...');
1029+
1030+
if (__DEV__) {
1031+
expect(normalizeCodeLocInfo(componentStack, {preserveLocation: true}))
1032+
.toMatchInlineSnapshot(`
1033+
"
1034+
in Component (file:///packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js:923:7)
1035+
in Suspense
1036+
in body
1037+
in html
1038+
in App (file:///packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js:937:25)
1039+
in ClientRoot (/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js:943:54)"
1040+
`);
1041+
} else {
1042+
expect(normalizeCodeLocInfo(componentStack)).toMatchInlineSnapshot(`
1043+
"
1044+
in Suspense
1045+
in body
1046+
in html
1047+
in ClientRoot (at **)"
1048+
`);
1049+
}
1050+
1051+
if (__DEV__) {
1052+
expect(normalizeCodeLocInfo(ownerStack, {preserveLocation: true}))
1053+
.toMatchInlineSnapshot(`
1054+
"
1055+
in Component (file:///packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js:923:7)
1056+
in App (file:///packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js:937:25)"
1057+
`);
1058+
} else {
1059+
expect(ownerStack).toBeNull();
1060+
}
1061+
} finally {
1062+
jest.useFakeTimers();
1063+
}
1064+
});
1065+
8991066
// @gate experimental
9001067
// @gate enableHalt
9011068
it('can handle an empty prelude when prerendering', async () => {

0 commit comments

Comments
 (0)