mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-11-04 15:00:11 +00:00
fix(console): include the reason of unhandled promise rejections (#3148)
Closes #2959 Unhandled promise rejections were listed as just "Uncaught (in promise)". `SymbolizedError.#getMessage` only appended the exception text when `details.text` was exactly "Uncaught". Rejections with a primitive reason (`Promise.reject('reason-B')`) also had no `description` to read from. Since every rejection had the same text, `list_console_messages` grouped different rejections into one line with "[3 times]". Changes: - Treat "Uncaught (in promise)" like "Uncaught": append the reason from the exception. - For primitive exceptions with no `description`, use `unserializableValue` or `value`. `throw undefined` still prints "Uncaught " as before, so the existing primitive test is unchanged. Tests: - Unit: an Error reason and a string reason produce "Uncaught (in promise) Error: reason-A" and "Uncaught (in promise) reason-B". - Real browser (console.test.ts): the three rejections from the issue are listed as three separate messages with their reasons and no "[3 times]". On main this test shows "Uncaught (in promise) (0 args) [3 times]".
This commit is contained in:
1 parent
8ddce8fc01
commit
f7cdf732e3
3 files changed
+100
-7
No files matched your search
@@ -337,9 +337,17 @@ export class SymbolizedError {
|
||||
// For Runtime.exceptionThrown with a present exception object, `details.text` will be "Uncaught" and
|
||||
// we have to manually parse out the error text from the exception description.
|
||||
// In the case of Runtime.getExceptionDetails, `details.text` has the Error.message.
|
||||
if (details.text === 'Uncaught' && details.exception) {
|
||||
// Unhandled promise rejections work the same way, with
|
||||
// "Uncaught (in promise)" as `details.text` and the rejection reason as
|
||||
// the exception.
|
||||
if (
|
||||
(details.text === 'Uncaught' ||
|
||||
details.text === 'Uncaught (in promise)') &&
|
||||
details.exception
|
||||
) {
|
||||
return (
|
||||
'Uncaught ' +
|
||||
details.text +
|
||||
' ' +
|
||||
SymbolizedError.#getMessageFromException(details.exception)
|
||||
);
|
||||
}
|
||||
@@ -347,7 +355,14 @@ export class SymbolizedError {
|
||||
}
|
||||
|
||||
static #getMessageFromException(error: RemoteObjectLike): string {
|
||||
const messageWithRest = error.description?.split('\n at ', 2) ?? [];
|
||||
if (error.description === undefined) {
|
||||
// Primitive values, e.g. `Promise.reject('reason')`, have no description.
|
||||
if (error.unserializableValue !== undefined) {
|
||||
return error.unserializableValue;
|
||||
}
|
||||
return error.value === undefined ? '' : String(error.value);
|
||||
}
|
||||
const messageWithRest = error.description.split('\n at ', 2);
|
||||
return messageWithRest[0] ?? '';
|
||||
}
|
||||
|
||||
|
||||
@@ -232,6 +232,39 @@ describe('SymbolizedError', () => {
|
||||
assert.strictEqual(error.cause, undefined);
|
||||
});
|
||||
|
||||
it('includes the reason of an unhandled promise rejection', async () => {
|
||||
const error = await SymbolizedError.fromDetails({
|
||||
details: exceptionDetails({
|
||||
text: 'Uncaught (in promise)',
|
||||
exception: {
|
||||
type: 'object',
|
||||
subtype: 'error',
|
||||
className: 'Error',
|
||||
description:
|
||||
'Error: reason-A\n at http://example.com/app.js:4:16',
|
||||
},
|
||||
}),
|
||||
targetId: '',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'Uncaught (in promise) Error: reason-A',
|
||||
);
|
||||
});
|
||||
|
||||
it('includes a primitive rejection reason', async () => {
|
||||
const error = await SymbolizedError.fromDetails({
|
||||
details: exceptionDetails({
|
||||
text: 'Uncaught (in promise)',
|
||||
exception: {type: 'string', value: 'reason-B'},
|
||||
}),
|
||||
targetId: '',
|
||||
});
|
||||
|
||||
assert.strictEqual(error.message, 'Uncaught (in promise) reason-B');
|
||||
});
|
||||
|
||||
it('keeps the full description when it has no stack frames', async () => {
|
||||
const error = await SymbolizedError.fromDetails({
|
||||
details: exceptionDetails({
|
||||
@@ -268,7 +301,7 @@ describe('SymbolizedError', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('uses an empty exception message when the exception only has a value', async () => {
|
||||
it('uses the value of a thrown primitive as the exception message', async () => {
|
||||
// A thrown primitive (e.g. `throw 'oops'`) has no description.
|
||||
const error = await SymbolizedError.fromDetails({
|
||||
details: exceptionDetails({
|
||||
@@ -280,7 +313,7 @@ describe('SymbolizedError', () => {
|
||||
targetId: '',
|
||||
});
|
||||
|
||||
assert.strictEqual(error.message, 'Uncaught ');
|
||||
assert.strictEqual(error.message, 'Uncaught oops');
|
||||
});
|
||||
|
||||
it('uses the details text verbatim when there is no exception object', async () => {
|
||||
@@ -402,7 +435,7 @@ describe('SymbolizedError', () => {
|
||||
assert.strictEqual(error.message, 'TypeError: nope');
|
||||
});
|
||||
|
||||
it('falls back to an empty message without a description', async () => {
|
||||
it('uses the value of a primitive error without a description', async () => {
|
||||
const error = await SymbolizedError.fromError({
|
||||
error: {
|
||||
type: 'string',
|
||||
@@ -411,7 +444,7 @@ describe('SymbolizedError', () => {
|
||||
targetId: '',
|
||||
});
|
||||
|
||||
assert.strictEqual(error.message, '');
|
||||
assert.strictEqual(error.message, 'oops');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -255,6 +255,51 @@ describe('console', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('lists the reason of each unhandled promise rejection', async () => {
|
||||
await withMcpContext(async (_response, context, args) => {
|
||||
const page = context.getSelectedMcpPage();
|
||||
await page.pptrPage.setContent(`<script>
|
||||
Promise.reject(new Error('reason-A'));
|
||||
Promise.reject('reason-B');
|
||||
(async () => { throw new TypeError('reason-C'); })();
|
||||
</script>`);
|
||||
// Rejections are reported once the microtask queue drains, after
|
||||
// setContent resolves, so poll until the collector has all three.
|
||||
let textContent = '';
|
||||
await waitExecutionFor(async () => {
|
||||
const pollResponse = new McpResponse(args);
|
||||
pollResponse.setPage(page);
|
||||
await listConsoleMessages(args).handler(
|
||||
{params: {}, page},
|
||||
pollResponse,
|
||||
context,
|
||||
);
|
||||
const formattedResponse = await pollResponse.handle(context);
|
||||
textContent = getTextContent(formattedResponse.content[0]);
|
||||
assert.ok(textContent.includes('msgid=3 '), textContent);
|
||||
}, 5000);
|
||||
assert.ok(
|
||||
textContent.includes(
|
||||
'msgid=1 [error] Uncaught (in promise) Error: reason-A (0 args)',
|
||||
),
|
||||
textContent,
|
||||
);
|
||||
assert.ok(
|
||||
textContent.includes(
|
||||
'msgid=2 [error] Uncaught (in promise) reason-B (0 args)',
|
||||
),
|
||||
textContent,
|
||||
);
|
||||
assert.ok(
|
||||
textContent.includes(
|
||||
'msgid=3 [error] Uncaught (in promise) TypeError: reason-C (0 args)',
|
||||
),
|
||||
textContent,
|
||||
);
|
||||
assert.ok(!textContent.includes('times]'), textContent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('issues', () => {
|
||||
it('lists issues', async () => {
|
||||
await withMcpContext(async (response, context, args) => {
|
||||
|
||||
Reference in new issue
Block a user