Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/PageCollector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from '../node_modules/chrome-devtools-frontend/mcp/mcp.js';

import {FakeIssuesManager} from './DevtoolsUtils.js';
import {features} from './features.js';
import {logger} from './logger.js';
import type {
CDPSession,
Expand Down Expand Up @@ -231,6 +232,9 @@ export class ConsoleCollector extends PageCollector<

override addPage(page: Page): void {
super.addPage(page);
if (!features.issues) {
return;
}
if (!this.#subscribedPages.has(page)) {
const subscriber = new PageIssueSubscriber(page);
this.#subscribedPages.set(page, subscriber);
Expand Down
16 changes: 16 additions & 0 deletions src/features.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
let issuesEnabled = false;

export const features = {
get issues() {
return issuesEnabled;
},
};

export function setIssuesEnabled(value: boolean) {
issuesEnabled = value;
}
5 changes: 4 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import './polyfill.js';
import type {Channel} from './browser.js';
import {ensureBrowserConnected, ensureBrowserLaunched} from './browser.js';
import {parseArguments} from './cli.js';
import {features} from './features.js';
import {loadIssueDescriptions} from './issue-descriptions.js';
import {logger, saveLogsToFile} from './logger.js';
import {McpContext} from './McpContext.js';
Expand Down Expand Up @@ -190,7 +191,9 @@ for (const tool of tools) {
registerTool(tool);
}

await loadIssueDescriptions();
if (features.issues) {
await loadIssueDescriptions();
}
const transport = new StdioServerTransport();
await server.connect(transport);
logger('Chrome DevTools MCP Server connected');
Expand Down
7 changes: 6 additions & 1 deletion src/tools/console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
* SPDX-License-Identifier: Apache-2.0
*/

import {features} from '../features.js';
import {zod} from '../third_party/index.js';
import type {ConsoleMessageType} from '../third_party/index.js';

import {ToolCategory} from './categories.js';
import {defineTool} from './ToolDefinition.js';
type ConsoleResponseType = ConsoleMessageType | 'issue';

const FILTERABLE_MESSAGE_TYPES: readonly [
const FILTERABLE_MESSAGE_TYPES: [
ConsoleResponseType,
...ConsoleResponseType[],
] = [
Expand All @@ -37,6 +38,10 @@ const FILTERABLE_MESSAGE_TYPES: readonly [
'issue',
];

if (features.issues) {
FILTERABLE_MESSAGE_TYPES.push('issue');
}

export const listConsoleMessages = defineTool({
name: 'list_console_messages',
description:
Expand Down
8 changes: 7 additions & 1 deletion tests/PageCollector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import assert from 'node:assert';
import {beforeEach, describe, it} from 'node:test';
import {afterEach, beforeEach, describe, it} from 'node:test';

import type {
Browser,
Expand All @@ -17,6 +17,7 @@ import type {
import sinon from 'sinon';

import {AggregatedIssue} from '../node_modules/chrome-devtools-frontend/mcp/mcp.js';
import {setIssuesEnabled} from '../src/features.js';
import type {ListenerMap} from '../src/PageCollector.js';
import {
ConsoleCollector,
Expand Down Expand Up @@ -357,6 +358,11 @@ describe('ConsoleCollector', () => {
},
},
};
setIssuesEnabled(true);
});

afterEach(() => {
setIssuesEnabled(false);
});

it('emits issues on page', async () => {
Expand Down
115 changes: 62 additions & 53 deletions tests/tools/console.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
import assert from 'node:assert';
import {before, describe, it} from 'node:test';
import {afterEach, before, beforeEach, describe, it} from 'node:test';

import {setIssuesEnabled} from '../../src/features.js';
import {loadIssueDescriptions} from '../../src/issue-descriptions.js';
import {
getConsoleMessage,
Expand Down Expand Up @@ -41,79 +42,87 @@ describe('console', () => {
});
});

it('lists issues', async () => {
it('work with primitive unhandled errors', async () => {
await withBrowser(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 page.setContent('<script>throw undefined;</script>');
await listConsoleMessages.handler({params: {}}, response, context);
const formattedResponse = await response.handle('test', context);
const textContent = formattedResponse[0] as {text: string};
assert.ok(
textContent.text.includes(
`msgid=1 [issue] An element doesn't have an autocomplete attribute (count: 1)`,
),
textContent.text.includes('msgid=1 [error] undefined (0 args)'),
);
});
});

it('lists issues after a page reload', async () => {
await withBrowser(async (response, context) => {
const page = await context.newPage();
const issuePromise = new Promise<void>(resolve => {
page.once('issue', () => {
resolve();
describe('issues', () => {
beforeEach(() => {
setIssuesEnabled(true);
});
afterEach(() => {
setIssuesEnabled(false);
});
it('lists issues', async () => {
await withBrowser(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);
{
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 = formattedResponse[0] as {text: string};
assert.ok(
textContent.text.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 = formattedResponse[0] as {text: string};
assert.ok(
textContent.text.includes(
`msgid=2 [issue] An element doesn't have an autocomplete attribute (count: 1)`,
),
);
}
});
});

it('work with primitive unhandled errors', async () => {
await withBrowser(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 = formattedResponse[0] as {text: string};
assert.ok(
textContent.text.includes('msgid=1 [error] undefined (0 args)'),
);
it('lists issues after a page reload', async () => {
await withBrowser(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 = formattedResponse[0] as {text: string};
assert.ok(
textContent.text.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 = formattedResponse[0] as {text: string};
assert.ok(
textContent.text.includes(
`msgid=2 [issue] An element doesn't have an autocomplete attribute (count: 1)`,
),
);
}
});
});
});
});
Expand Down
Loading