@@ -41,6 +41,18 @@ function formatV8Stack(stack) {
41
41
return v8StyleStack ;
42
42
}
43
43
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
+
44
56
function normalizeComponentInfo ( debugInfo ) {
45
57
if ( Array . isArray ( debugInfo . stack ) ) {
46
58
const { debugTask, debugStack, ...copy } = debugInfo ;
@@ -1177,6 +1189,118 @@ describe('ReactFlight', () => {
1177
1189
} ) ;
1178
1190
} ) ;
1179
1191
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
+
1180
1304
it ( 'should include server components in warning stacks' , async ( ) => {
1181
1305
function Component ( ) {
1182
1306
// Trigger key warning
0 commit comments