chromedevtools--chrome-devtools-mcp
a3a00210a3
This is the stack trace of the console message itself. If the argument is an Error object or an "Error.stack" like string we don't do anything special (yet). The stack trace is source mapped if source maps are available. The function matches the target ID of the console message to the `SDK.Target` of DevTools. The only oddity is that DevTools works in a lazy manor: It creates stack traces upfront and as source maps come in, stack traces get updated. This does not work for the MCP server. Instead, we wait for script parsed events and source maps to attach before creating the stack trace. Since this could take potentially a while, we guard it with a time out.
262 行
9.8 KiB
TypeScript
262 行
9.8 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import assert from 'node:assert';
|
|
import {before, describe, it} from 'node:test';
|
|
|
|
import {loadIssueDescriptions} from '../../src/issue-descriptions.js';
|
|
import {McpResponse} from '../../src/McpResponse.js';
|
|
import {DevTools} from '../../src/third_party/index.js';
|
|
import {
|
|
getConsoleMessage,
|
|
listConsoleMessages,
|
|
} from '../../src/tools/console.js';
|
|
import {serverHooks} from '../server.js';
|
|
import {getTextContent, withMcpContext} from '../utils.js';
|
|
|
|
describe('console', () => {
|
|
before(async () => {
|
|
await loadIssueDescriptions();
|
|
});
|
|
describe('list_console_messages', () => {
|
|
it('list messages', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
await listConsoleMessages.handler({params: {}}, response, context);
|
|
assert.ok(response.includeConsoleData);
|
|
});
|
|
});
|
|
|
|
it('lists error messages', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = await context.newPage();
|
|
await page.setContent(
|
|
'<script>console.error("This is an error")</script>',
|
|
);
|
|
await listConsoleMessages.handler({params: {}}, response, context);
|
|
const formattedResponse = await response.handle('test', context);
|
|
const textContent = getTextContent(formattedResponse.content[0]);
|
|
assert.ok(textContent.includes('msgid=1 [error] This is an error'));
|
|
});
|
|
});
|
|
|
|
it('work with primitive unhandled errors', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = await context.newPage();
|
|
await page.setContent('<script>throw undefined;</script>');
|
|
await listConsoleMessages.handler({params: {}}, response, context);
|
|
const formattedResponse = await response.handle('test', context);
|
|
const textContent = getTextContent(formattedResponse.content[0]);
|
|
assert.ok(textContent.includes('msgid=1 [error] undefined (0 args)'));
|
|
});
|
|
});
|
|
|
|
describe('issues', () => {
|
|
it('lists issues', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = await context.newPage();
|
|
const issuePromise = new Promise<void>(resolve => {
|
|
page.once('issue', () => {
|
|
resolve();
|
|
});
|
|
});
|
|
await page.setContent('<input type="text" name="username" />');
|
|
await issuePromise;
|
|
await listConsoleMessages.handler({params: {}}, response, context);
|
|
const formattedResponse = await response.handle('test', context);
|
|
const textContent = getTextContent(formattedResponse.content[0]);
|
|
assert.ok(
|
|
textContent.includes(
|
|
`msgid=1 [issue] An element doesn't have an autocomplete attribute (count: 1)`,
|
|
),
|
|
);
|
|
});
|
|
});
|
|
|
|
it('lists issues after a page reload', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = await context.newPage();
|
|
const issuePromise = new Promise<void>(resolve => {
|
|
page.once('issue', () => {
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
await page.setContent('<input type="text" name="username" />');
|
|
await issuePromise;
|
|
await listConsoleMessages.handler({params: {}}, response, context);
|
|
{
|
|
const formattedResponse = await response.handle('test', context);
|
|
const textContent = getTextContent(formattedResponse.content[0]);
|
|
assert.ok(
|
|
textContent.includes(
|
|
`msgid=1 [issue] An element doesn't have an autocomplete attribute (count: 1)`,
|
|
),
|
|
);
|
|
}
|
|
|
|
const anotherIssuePromise = new Promise<void>(resolve => {
|
|
page.once('issue', () => {
|
|
resolve();
|
|
});
|
|
});
|
|
await page.reload();
|
|
await page.setContent('<input type="text" name="username" />');
|
|
await anotherIssuePromise;
|
|
{
|
|
const formattedResponse = await response.handle('test', context);
|
|
const textContent = getTextContent(formattedResponse.content[0]);
|
|
assert.ok(
|
|
textContent.includes(
|
|
`msgid=2 [issue] An element doesn't have an autocomplete attribute (count: 1)`,
|
|
),
|
|
);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('get_console_message', () => {
|
|
const server = serverHooks();
|
|
|
|
it('gets a specific console message', async () => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = await context.newPage();
|
|
await page.setContent(
|
|
'<script>console.error("This is an error")</script>',
|
|
);
|
|
// The list is needed to populate the console messages in the context.
|
|
await listConsoleMessages.handler({params: {}}, response, context);
|
|
await getConsoleMessage.handler(
|
|
{params: {msgid: 1}},
|
|
response,
|
|
context,
|
|
);
|
|
const formattedResponse = await response.handle('test', context);
|
|
const textContent = getTextContent(formattedResponse.content[0]);
|
|
assert.ok(
|
|
textContent.includes('msgid=1 [error] This is an error'),
|
|
'Should contain console message body',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('issues type', () => {
|
|
it('gets issue details with node id parsing', async t => {
|
|
await withMcpContext(async (response, context) => {
|
|
const page = await context.newPage();
|
|
const issuePromise = new Promise<void>(resolve => {
|
|
page.once('issue', () => {
|
|
resolve();
|
|
});
|
|
});
|
|
await page.setContent('<input type="text" name="username" />');
|
|
await context.createTextSnapshot();
|
|
await issuePromise;
|
|
await listConsoleMessages.handler({params: {}}, response, context);
|
|
const response2 = new McpResponse();
|
|
await getConsoleMessage.handler(
|
|
{params: {msgid: 1}},
|
|
response2,
|
|
context,
|
|
);
|
|
const formattedResponse = await response2.handle('test', context);
|
|
t.assert.snapshot?.(getTextContent(formattedResponse.content[0]));
|
|
});
|
|
});
|
|
it('gets issue details with request id parsing', async t => {
|
|
server.addRoute('/data.json', (_req, res) => {
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.statusCode = 200;
|
|
res.end(JSON.stringify({data: 'test data'}));
|
|
});
|
|
|
|
await withMcpContext(async (response, context) => {
|
|
const page = await context.newPage();
|
|
const issuePromise = new Promise<void>(resolve => {
|
|
page.once('issue', () => {
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
const url = server.getRoute('/data.json');
|
|
await page.setContent(`
|
|
<script>
|
|
fetch('${url}', {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Custom-Header': 'MyValue'
|
|
}
|
|
});
|
|
</script>
|
|
`);
|
|
await context.createTextSnapshot();
|
|
await issuePromise;
|
|
const messages = context.getConsoleData();
|
|
let issueMsg;
|
|
for (const message of messages) {
|
|
if (message instanceof DevTools.AggregatedIssue) {
|
|
issueMsg = message;
|
|
break;
|
|
}
|
|
}
|
|
assert.ok(issueMsg);
|
|
const id = context.getConsoleMessageStableId(issueMsg);
|
|
assert.ok(id);
|
|
await listConsoleMessages.handler(
|
|
{params: {types: ['issue']}},
|
|
response,
|
|
context,
|
|
);
|
|
const response2 = new McpResponse();
|
|
await getConsoleMessage.handler(
|
|
{params: {msgid: id}},
|
|
response2,
|
|
context,
|
|
);
|
|
const formattedResponse = await response2.handle('test', context);
|
|
const rawText = getTextContent(formattedResponse.content[0]);
|
|
const sanitizedText = rawText
|
|
.replaceAll(/ID: \d+/g, 'ID: <ID>')
|
|
.replaceAll(/reqid=\d+/g, 'reqid=<reqid>')
|
|
.replaceAll(/localhost:\d+/g, 'hostname:port');
|
|
t.assert.snapshot?.(sanitizedText);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('applies source maps to stack traces of console messages', async t => {
|
|
server.addRoute('/main.min.js', (_req, res) => {
|
|
res.setHeader('Content-Type', 'text/javascript');
|
|
res.statusCode = 200;
|
|
res.end(`function n(){console.warn("hello world")}function o(){n()}(function n(){o()})();
|
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJiYXIiLCJjb25zb2xlIiwid2FybiIsImZvbyIsIklpZmUiXSwic291cmNlcyI6WyIuL21haW4uanMiXSwic291cmNlc0NvbnRlbnQiOlsiXG5mdW5jdGlvbiBiYXIoKSB7XG4gIGNvbnNvbGUud2FybignaGVsbG8gd29ybGQnKTtcbn1cblxuZnVuY3Rpb24gZm9vKCkge1xuICBiYXIoKTtcbn1cblxuKGZ1bmN0aW9uIElpZmUoKSB7XG4gIGZvbygpO1xufSkoKTtcblxuIl0sIm1hcHBpbmdzIjoiQUFDQSxTQUFTQSxJQUNQQyxRQUFRQyxLQUFLLGNBQ2YsQ0FFQSxTQUFTQyxJQUNQSCxHQUNGLEVBRUEsU0FBVUksSUFDUkQsR0FDRCxFQUZEIiwiaWdub3JlTGlzdCI6W119
|
|
`);
|
|
});
|
|
server.addHtmlRoute(
|
|
'/index.html',
|
|
`<script src="${server.getRoute('/main.min.js')}"></script>`,
|
|
);
|
|
|
|
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);
|
|
});
|
|
});
|
|
});
|
|
});
|