diff --git a/src/DevtoolsUtils.ts b/src/DevtoolsUtils.ts index 81f559b2d..c1f6efcb4 100644 --- a/src/DevtoolsUtils.ts +++ b/src/DevtoolsUtils.ts @@ -286,6 +286,23 @@ export class SymbolizedError { let cause: SymbolizedError | undefined; if (opts.resolvedCauseForTesting) { cause = opts.resolvedCauseForTesting; + } else if (opts.details.exception) { + try { + const causeRemoteObj = await SymbolizedError.#lookupCause( + opts.devTools, + opts.details.exception, + opts.targetId, + ); + if (causeRemoteObj) { + cause = await SymbolizedError.fromError({ + devTools: opts.devTools, + error: causeRemoteObj, + targetId: opts.targetId, + }); + } + } catch { + // Ignore + } } return new SymbolizedError(message, stackTrace, cause); } @@ -355,6 +372,30 @@ export class SymbolizedError { ); } + static async #lookupCause( + devTools: TargetUniverse | undefined, + error: Protocol.Runtime.RemoteObject, + targetId: string, + ): Promise { + if (!devTools || (error.type !== 'object' && error.subtype !== 'error')) { + return null; + } + + const targetManager = devTools.universe.context.get(DevTools.TargetManager); + const target = targetId + ? targetManager.targetById(targetId) || devTools.target + : devTools.target; + + const properties = await target.runtimeAgent().invoke_getProperties({ + objectId: error.objectId as DevTools.Protocol.Runtime.RemoteObjectId, + }); + if (properties.getError()) { + return null; + } + + return properties.result.find(prop => prop.name === 'cause')?.value ?? null; + } + static createForTesting( message: string, stackTrace?: DevTools.StackTrace.StackTrace.StackTrace, diff --git a/tests/tools/console.test.js.snapshot b/tests/tools/console.test.js.snapshot index 9574c9257..c7c99410f 100644 --- a/tests/tools/console.test.js.snapshot +++ b/tests/tools/console.test.js.snapshot @@ -1,3 +1,25 @@ +exports[`console > get_console_message > applies source maps to stack traces of Error object (with cause) console.log arguments 1`] = ` +# test response +ID: 1 +Message: log> foo failed JSHandle@error +### Arguments +Arg #0: foo failed +Arg #1: Error: bar failed +at foo (main.js:10:11) +at Iife (main.js:16:5) +at (main.js:14:1) +Caused by: Error: b00m! +at bar (main.js:3:9) +at foo (main.js:8:5) +at Iife (main.js:16:5) +at (main.js:14:1) +Note: line and column numbers use 1-based indexing +### Stack trace +at Iife (main.js:18:13) +at (main.js:14:1) +Note: line and column numbers use 1-based indexing +`; + exports[`console > get_console_message > applies source maps to stack traces of Error object console.log arguments 1`] = ` # test response ID: 1 @@ -42,6 +64,25 @@ at (main.js:10:1) Note: line and column numbers use 1-based indexing `; +exports[`console > get_console_message > applies source maps to stack traces of uncaught exceptions with cause 1`] = ` +# test response +ID: 1 +Message: error> Uncaught Error: foo failed +### Stack trace +at Iife (main.js:18:11) +at (main.js:14:1) +Caused by: Error: bar failed +at foo (main.js:10:11) +at Iife (main.js:16:5) +at (main.js:14:1) +Caused by: Error: b00m! +at bar (main.js:3:9) +at foo (main.js:8:5) +at Iife (main.js:16:5) +at (main.js:14:1) +Note: line and column numbers use 1-based indexing +`; + exports[`console > get_console_message > issues type > gets issue details with node id parsing 1`] = ` # test response ID: 1 diff --git a/tests/tools/console.test.ts b/tests/tools/console.test.ts index 7d12c4363..79eae104c 100644 --- a/tests/tools/console.test.ts +++ b/tests/tools/console.test.ts @@ -315,5 +315,63 @@ describe('console', () => { t.assert.snapshot?.(rawText); }); }); + + it('applies source maps to stack traces of uncaught exceptions with cause', async t => { + server.addRoute('/main.min.js', (_req, res) => { + res.setHeader('Content-Type', 'text/javascript'); + res.statusCode = 200; + res.end(`function r(){throw new Error("b00m!")}function o(){try{r()}catch(r){throw new Error("bar failed",{cause:r})}}(function r(){try{o()}catch(r){throw new Error("foo failed",{cause:r})}})(); + //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJiYXIiLCJFcnJvciIsImZvbyIsImUiLCJjYXVzZSIsIklpZmUiXSwic291cmNlcyI6WyIuL21haW4uanMiXSwic291cmNlc0NvbnRlbnQiOlsiXG5mdW5jdGlvbiBiYXIoKSB7XG4gIHRocm93IG5ldyBFcnJvcignYjAwbSEnKTtcbn1cblxuZnVuY3Rpb24gZm9vKCkge1xuICB0cnkge1xuICAgIGJhcigpO1xuICB9IGNhdGNoIChlKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKCdiYXIgZmFpbGVkJywgeyBjYXVzZTogZSB9KTtcbiAgfVxufVxuXG4oZnVuY3Rpb24gSWlmZSgpIHtcbiAgdHJ5IHtcbiAgICBmb28oKTtcbiAgfSBjYXRjaCAoZSkge1xuICAgIHRocm93IG5ldyBFcnJvcignZm9vIGZhaWxlZCcsIHsgY2F1c2U6IGUgfSk7XG4gIH1cbn0pKCk7XG5cbiJdLCJtYXBwaW5ncyI6IkFBQ0EsU0FBU0EsSUFDUCxNQUFNLElBQUlDLE1BQU0sUUFDbEIsQ0FFQSxTQUFTQyxJQUNQLElBQ0VGLEdBQ0YsQ0FBRSxNQUFPRyxHQUNQLE1BQU0sSUFBSUYsTUFBTSxhQUFjLENBQUVHLE1BQU9ELEdBQ3pDLENBQ0YsRUFFQSxTQUFVRSxJQUNSLElBQ0VILEdBQ0YsQ0FBRSxNQUFPQyxHQUNQLE1BQU0sSUFBSUYsTUFBTSxhQUFjLENBQUVHLE1BQU9ELEdBQ3pDLENBQ0QsRUFORCIsImlnbm9yZUxpc3QiOltdfQ== + `); + }); + server.addHtmlRoute( + '/index.html', + ``, + ); + + await withMcpContext(async (response, context) => { + const page = await context.newPage(); + await page.goto(server.getRoute('/index.html')); + + await getConsoleMessage.handler( + {params: {msgid: 1}}, + response, + context, + ); + const formattedResponse = await response.handle('test', context); + const rawText = getTextContent(formattedResponse.content[0]); + + t.assert.snapshot?.(rawText); + }); + }); + + it('applies source maps to stack traces of Error object (with cause) console.log arguments', async t => { + server.addRoute('/main.min.js', (_req, res) => { + res.setHeader('Content-Type', 'text/javascript'); + res.statusCode = 200; + res.end(`function o(){throw new Error("b00m!")}function r(){try{o()}catch(o){throw new Error("bar failed",{cause:o})}}(function o(){try{r()}catch(o){console.log("foo failed",o)}})(); + //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJiYXIiLCJFcnJvciIsImZvbyIsImUiLCJjYXVzZSIsIklpZmUiLCJjb25zb2xlIiwibG9nIl0sInNvdXJjZXMiOlsiLi9tYWluLmpzIl0sInNvdXJjZXNDb250ZW50IjpbIlxuZnVuY3Rpb24gYmFyKCkge1xuICB0aHJvdyBuZXcgRXJyb3IoJ2IwMG0hJyk7XG59XG5cbmZ1bmN0aW9uIGZvbygpIHtcbiAgdHJ5IHtcbiAgICBiYXIoKTtcbiAgfSBjYXRjaCAoZSkge1xuICAgIHRocm93IG5ldyBFcnJvcignYmFyIGZhaWxlZCcsIHsgY2F1c2U6IGUgfSk7XG4gIH1cbn1cblxuKGZ1bmN0aW9uIElpZmUoKSB7XG4gIHRyeSB7XG4gICAgZm9vKCk7XG4gIH0gY2F0Y2ggKGUpIHtcbiAgICBjb25zb2xlLmxvZygnZm9vIGZhaWxlZCcsIGUpO1xuICB9XG59KSgpO1xuXG4iXSwibWFwcGluZ3MiOiJBQUNBLFNBQVNBLElBQ1AsTUFBTSxJQUFJQyxNQUFNLFFBQ2xCLENBRUEsU0FBU0MsSUFDUCxJQUNFRixHQUNGLENBQUUsTUFBT0csR0FDUCxNQUFNLElBQUlGLE1BQU0sYUFBYyxDQUFFRyxNQUFPRCxHQUN6QyxDQUNGLEVBRUEsU0FBVUUsSUFDUixJQUNFSCxHQUNGLENBQUUsTUFBT0MsR0FDUEcsUUFBUUMsSUFBSSxhQUFjSixFQUM1QixDQUNELEVBTkQiLCJpZ25vcmVMaXN0IjpbXX0= + `); + }); + server.addHtmlRoute( + '/index.html', + ``, + ); + + await withMcpContext(async (response, context) => { + const page = await context.newPage(); + await page.goto(server.getRoute('/index.html')); + + await getConsoleMessage.handler( + {params: {msgid: 1}}, + response, + context, + ); + const formattedResponse = await response.handle('test', context); + const rawText = getTextContent(formattedResponse.content[0]); + + t.assert.snapshot?.(rawText); + }); + }); }); });