Skip to content

Commit 68ab934

Browse files
committed
Current behavior for exotic frames
1 parent b943feb commit 68ab934

File tree

1 file changed

+124
-0
lines changed

1 file changed

+124
-0
lines changed

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ function formatV8Stack(stack) {
4141
return v8StyleStack;
4242
}
4343

44+
// If we just use the original Error prototype, Jest will only display the error message if assertions fail.
45+
// But we usually want to also assert on our expando properties or even the stack.
46+
// By hiding the fact from Jest that this is an error, it will show all enumerable properties on mismatch.
47+
function getErrorForJestMatcher(error) {
48+
return {
49+
...error,
50+
// non-enumerable properties that are still relevant for testing
51+
message: error.message,
52+
stack: error.stack,
53+
};
54+
}
55+
4456
function normalizeComponentInfo(debugInfo) {
4557
if (Array.isArray(debugInfo.stack)) {
4658
const {debugTask, debugStack, ...copy} = debugInfo;
@@ -1177,6 +1189,118 @@ describe('ReactFlight', () => {
11771189
});
11781190
});
11791191

1192+
it('should handle exotic stack frames', async () => {
1193+
function ServerComponent() {
1194+
const error = new Error('This is an error');
1195+
const originalStackLines = error.stack.split('\n');
1196+
// Fake a stack
1197+
error.stack = [
1198+
originalStackLines[0],
1199+
// original
1200+
// ' at ServerComponentError (file://~/react/packages/react-client/src/__tests__/ReactFlight-test.js:1166:19)',
1201+
// nested eval (https://github.com/ChromeDevTools/devtools-frontend/blob/831be28facb4e85de5ee8c1acc4d98dfeda7a73b/test/unittests/front_end/panels/console/ErrorStackParser_test.ts#L198)
1202+
' at eval (eval at testFunction (inspected-page.html:29:11), <anonymous>:1:10)',
1203+
// parens may be added by Webpack when bundle layers are used. They're also valid in directory names.
1204+
' at ServerComponentError (file://~/(some)(really)(exotic-directory)/ReactFlight-test.js:1166:19)',
1205+
// anon function (https://github.com/ChromeDevTools/devtools-frontend/blob/831be28facb4e85de5ee8c1acc4d98dfeda7a73b/test/unittests/front_end/panels/console/ErrorStackParser_test.ts#L115C9-L115C35)
1206+
' at file:///testing.js:42:3',
1207+
// async anon function (https://github.com/ChromeDevTools/devtools-frontend/blob/831be28facb4e85de5ee8c1acc4d98dfeda7a73b/test/unittests/front_end/panels/console/ErrorStackParser_test.ts#L130C9-L130C41)
1208+
' at async file:///testing.js:42:3',
1209+
...originalStackLines.slice(2),
1210+
].join('\n');
1211+
throw error;
1212+
}
1213+
1214+
const findSourceMapURL = jest.fn();
1215+
const errors = [];
1216+
class MyErrorBoundary extends React.Component {
1217+
state = {error: null};
1218+
static getDerivedStateFromError(error) {
1219+
return {error};
1220+
}
1221+
componentDidCatch(error, componentInfo) {
1222+
errors.push(error);
1223+
}
1224+
render() {
1225+
if (this.state.error) {
1226+
return null;
1227+
}
1228+
return this.props.children;
1229+
}
1230+
}
1231+
const ClientErrorBoundary = clientReference(MyErrorBoundary);
1232+
1233+
function App() {
1234+
return (
1235+
<ClientErrorBoundary>
1236+
<ServerComponent />
1237+
</ClientErrorBoundary>
1238+
);
1239+
}
1240+
1241+
const transport = ReactNoopFlightServer.render(<App />, {
1242+
onError(x) {
1243+
if (__DEV__) {
1244+
return 'a dev digest';
1245+
}
1246+
if (x instanceof Error) {
1247+
return `digest("${x.message}")`;
1248+
} else if (Array.isArray(x)) {
1249+
return `digest([])`;
1250+
} else if (typeof x === 'object' && x !== null) {
1251+
return `digest({})`;
1252+
}
1253+
return `digest(${String(x)})`;
1254+
},
1255+
});
1256+
1257+
await act(() => {
1258+
startTransition(() => {
1259+
ReactNoop.render(
1260+
ReactNoopFlightClient.read(transport, {findSourceMapURL}),
1261+
);
1262+
});
1263+
});
1264+
1265+
if (__DEV__) {
1266+
expect({
1267+
errors: errors.map(getErrorForJestMatcher),
1268+
findSourceMapURLCalls: findSourceMapURL.mock.calls,
1269+
}).toEqual({
1270+
errors: [
1271+
{
1272+
message: 'This is an error',
1273+
stack: expect.stringContaining(
1274+
'Error: This is an error\n' +
1275+
' at (anonymous) (file:///testing.js:42:3)\n' +
1276+
' at (anonymous) (file:///testing.js:42:3)\n',
1277+
),
1278+
digest: 'a dev digest',
1279+
environmentName: 'Server',
1280+
},
1281+
],
1282+
findSourceMapURLCalls: expect.arrayContaining([
1283+
['file:///testing.js'],
1284+
['file:///testing.js'],
1285+
]),
1286+
});
1287+
} else {
1288+
expect(errors.map(getErrorForJestMatcher)).toEqual([
1289+
{
1290+
message:
1291+
'An error occurred in the Server Components render. The specific message is omitted in production' +
1292+
' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
1293+
' may provide additional details about the nature of the error.',
1294+
stack:
1295+
'Error: An error occurred in the Server Components render. The specific message is omitted in production' +
1296+
' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
1297+
' may provide additional details about the nature of the error.',
1298+
digest: 'digest("This is an error")',
1299+
},
1300+
]);
1301+
}
1302+
});
1303+
11801304
it('should include server components in warning stacks', async () => {
11811305
function Component() {
11821306
// Trigger key warning

0 commit comments

Comments
 (0)