Skip to content

Commit 4beef64

Browse files
committed
[Flight] Add a test for aborted hanging promise stacks
1 parent f78b234 commit 4beef64

File tree

1 file changed

+164
-4
lines changed

1 file changed

+164
-4
lines changed

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

Lines changed: 164 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,151 @@ describe('ReactFlightDOMNode', () => {
896911
}
897912
});
898913

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

0 commit comments

Comments
 (0)