|
12 | 12 |
|
13 | 13 | import {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate';
|
14 | 14 |
|
| 15 | +const path = require('path'); |
| 16 | + |
15 | 17 | global.ReadableStream =
|
16 | 18 | require('web-streams-polyfill/ponyfill/es6').ReadableStream;
|
17 | 19 |
|
@@ -88,12 +90,25 @@ describe('ReactFlightDOMNode', () => {
|
88 | 90 | );
|
89 | 91 | }
|
90 | 92 |
|
91 |
| - function normalizeCodeLocInfo(str) { |
| 93 | + const repoRoot = path.resolve(__dirname, '../../../../'); |
| 94 | + |
| 95 | + function normalizeCodeLocInfo(str, {preserveLocation = false} = {}) { |
92 | 96 | return (
|
93 | 97 | 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 | + ) |
97 | 112 | );
|
98 | 113 | }
|
99 | 114 |
|
@@ -896,6 +911,158 @@ describe('ReactFlightDOMNode', () => {
|
896 | 911 | }
|
897 | 912 | });
|
898 | 913 |
|
| 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 | + |
899 | 1066 | // @gate experimental
|
900 | 1067 | // @gate enableHalt
|
901 | 1068 | it('can handle an empty prelude when prerendering', async () => {
|
|
0 commit comments