From 2e7bbf630082c80390bda9b6efe75f7261852458 Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Fri, 21 Nov 2025 08:25:24 +0000 Subject: [PATCH 1/7] Update get_console_message with Issues details --- src/McpContext.ts | 7 ++- src/McpResponse.ts | 63 ++++++++++++------- src/formatters/consoleFormatter.ts | 48 +++++++++++++- src/issue-descriptions.ts | 5 ++ .../consoleFormatter.test.js.snapshot | 9 +++ tests/formatters/consoleFormatter.test.ts | 35 +++++++++++ tests/tools/console.test.ts | 32 ++++++++++ 7 files changed, 176 insertions(+), 23 deletions(-) diff --git a/src/McpContext.ts b/src/McpContext.ts index 22d54c94..7536fe78 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -199,8 +199,13 @@ export class McpContext implements Context { this.logger('no cdpBackendNodeId'); return; } + if (this.#textSnapshot === null) + throw new Error( + "The snapshot is not defined, can't resolve backendNodeId: " + + cdpBackendNodeId, + ); // TODO: index by backendNodeId instead. - const queue = [this.#textSnapshot?.root]; + const queue = [this.#textSnapshot.root]; while (queue.length) { const current = queue.pop()!; if (current.backendNodeId === cdpBackendNodeId) { diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 3fb83986..0d1ce6e4 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -256,6 +256,12 @@ export class McpResponse implements Response { }), ), }; + } else if (message instanceof AggregatedIssue) { + const result = mapIssuesMessage(message); + consoleData = { + consoleMessageStableId, + ...result, + }; } else { consoleData = { consoleMessageStableId, @@ -309,29 +315,10 @@ export class McpResponse implements Response { }; } if (item instanceof AggregatedIssue) { - const count = item.getAggregatedIssuesCount(); - const filename = item.getDescription()?.file; - const rawMarkdown = filename - ? getIssueDescription(filename) - : null; - if (!rawMarkdown) { - logger(`no markdown ${filename} found for issue:` + item.code); - return null; - } - const markdownAst = Marked.Marked.lexer(rawMarkdown); - const title = - MarkdownIssueDescription.findTitleFromMarkdownAst(markdownAst); - if (!title) { - logger('cannot read issue title from ' + filename); - return null; - } + const message = mapIssuesMessage(item); return { consoleMessageStableId, - type: 'issue', - item, - message: title, - count, - args: [], + ...message, }; } return { @@ -586,3 +573,37 @@ Call ${handleDialog.name} to handle it before continuing.`); this.#textResponseLines = []; } } + +function mapIssuesMessage( + message: AggregatedIssue, +): Omit | null { + const count = message.getAggregatedIssuesCount(); + const markdownDescription = message.getDescription(); + const filename = markdownDescription?.file; + if (!markdownDescription) { + logger(`no description found for issue:` + message.code); + return null; + } + const rawMarkdown = filename ? getIssueDescription(filename) : null; + if (!rawMarkdown) { + logger(`no markdown ${filename} found for issue:` + message.code); + return null; + } + const processedMarkdown = MarkdownIssueDescription.substitutePlaceholders( + rawMarkdown, + markdownDescription.substitutions, + ); + const markdownAst = Marked.Marked.lexer(processedMarkdown); + const title = MarkdownIssueDescription.findTitleFromMarkdownAst(markdownAst); + if (!title) { + logger('cannot read issue title from ' + filename); + return null; + } + return { + type: 'issue', + item: message, + message: title, + count, + args: [], + }; +} diff --git a/src/formatters/consoleFormatter.ts b/src/formatters/consoleFormatter.ts index e4eca9b1..67e149f0 100644 --- a/src/formatters/consoleFormatter.ts +++ b/src/formatters/consoleFormatter.ts @@ -4,6 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { + AggregatedIssue, + MarkdownIssueDescription, +} from '../../node_modules/chrome-devtools-frontend/mcp/mcp.js'; +import {ISSUE_UTILS} from '../issue-descriptions.js'; + export interface ConsoleMessageData { consoleMessageStableId: number; type?: string; @@ -34,12 +40,19 @@ function getArgs(msg: ConsoleMessageData) { // The verbose format for a console message, including all details. export function formatConsoleEventVerbose(msg: ConsoleMessageData): string { + if (msg.item instanceof AggregatedIssue) { + const result = [ + `ID: ${msg.consoleMessageStableId}`, + `Message: ${msg.type}> ${formatIssue(msg.item)}`, + ]; + return result.join('\n'); + } + const result = [ `ID: ${msg.consoleMessageStableId}`, `Message: ${msg.type}> ${msg.message}`, formatArgs(msg), ].filter(line => !!line); - return result.join('\n'); } @@ -62,3 +75,36 @@ function formatArgs(consoleData: ConsoleMessageData): string { return result.join('\n'); } + +export function formatIssue(issue: AggregatedIssue): string { + const markdownDescription = issue.getDescription(); + const filename = markdownDescription?.file; + const rawMarkdown = filename + ? ISSUE_UTILS.getIssueDescription(filename) + : null; + if (!markdownDescription || !rawMarkdown) { + throw new Error('Error parsing issue description ' + issue.code()); + } + let processedMarkdown = MarkdownIssueDescription.substitutePlaceholders( + rawMarkdown, + markdownDescription.substitutions, + ); + + processedMarkdown = processedMarkdown.trim(); + // Remove heading in order not to conflict with the result response markdown + if (processedMarkdown.startsWith('# ')) { + processedMarkdown = processedMarkdown.substring(2).trimStart(); + } + + const result: string[] = [processedMarkdown]; + const links = markdownDescription.links; + + if (links.length > 0) { + result.push('Learn more:'); + for (const link of links) { + result.push(`[${link.linkTitle}](${link.link})`); + } + } + + return result.join('\n'); +} diff --git a/src/issue-descriptions.ts b/src/issue-descriptions.ts index 21485e4b..c13e405e 100644 --- a/src/issue-descriptions.ts +++ b/src/issue-descriptions.ts @@ -47,3 +47,8 @@ export async function loadIssueDescriptions(): Promise { export function getIssueDescription(fileName: string): string | null { return issueDescriptions[fileName] ?? null; } + +export const ISSUE_UTILS = { + loadIssueDescriptions, + getIssueDescription, +}; diff --git a/tests/formatters/consoleFormatter.test.js.snapshot b/tests/formatters/consoleFormatter.test.js.snapshot index 7e6d5d65..e18461c7 100644 --- a/tests/formatters/consoleFormatter.test.js.snapshot +++ b/tests/formatters/consoleFormatter.test.js.snapshot @@ -34,3 +34,12 @@ Message: log> Processing file: ### Arguments Arg #0: file.txt `; + +exports[`consoleFormatter > formats a console.log message with issue type 1`] = ` +Mock Issue Title + +This is a mock issue description with a http://example.com/issue-detail. +Learn more: +[Learn more](http://example.com/learnmore) +[Learn more 2](http://example.com/another-learnmore) +`; diff --git a/tests/formatters/consoleFormatter.test.ts b/tests/formatters/consoleFormatter.test.ts index d5e71ba4..902307b4 100644 --- a/tests/formatters/consoleFormatter.test.ts +++ b/tests/formatters/consoleFormatter.test.ts @@ -6,11 +6,16 @@ import {describe, it} from 'node:test'; +import sinon from 'sinon'; + +import type {AggregatedIssue} from '../../node_modules/chrome-devtools-frontend/mcp/mcp.js'; import type {ConsoleMessageData} from '../../src/formatters/consoleFormatter.js'; import { formatConsoleEventShort, formatConsoleEventVerbose, + formatIssue, } from '../../src/formatters/consoleFormatter.js'; +import {ISSUE_UTILS} from '../../src/issue-descriptions.js'; describe('consoleFormatter', () => { describe('formatConsoleEventShort', () => { @@ -92,4 +97,34 @@ describe('consoleFormatter', () => { t.assert.snapshot?.(result); }); }); + + it('formats a console.log message with issue type', t => { + class MockAggregatedIssue { + getDescription() { + return { + file: 'mock-issue.md', + substitutions: new Map([ + ['PLACEHOLDER_URL', 'http://example.com/issue-detail'], + ]), + links: [ + {link: 'http://example.com/learnmore', linkTitle: 'Learn more'}, + {link: 'http://example.com/another-learnmore', linkTitle: 'Learn more 2'}, + ], + }; + } + } + const mockAggregatedIssue = new MockAggregatedIssue(); + const getIssueDescriptionStub = sinon.stub( + ISSUE_UTILS, + 'getIssueDescription', + ); + + getIssueDescriptionStub + .withArgs('mock-issue.md') + .returns( + '# Mock Issue Title\n\nThis is a mock issue description with a {PLACEHOLDER_URL}.', + ); + const result = formatIssue(mockAggregatedIssue as AggregatedIssue); + t.assert.snapshot?.(result); + }); }); diff --git a/tests/tools/console.test.ts b/tests/tools/console.test.ts index c7f6a97d..61ccd59e 100644 --- a/tests/tools/console.test.ts +++ b/tests/tools/console.test.ts @@ -129,6 +129,12 @@ describe('console', () => { }); describe('get_console_message', () => { + beforeEach(() => { + setIssuesEnabled(true); + }); + afterEach(() => { + setIssuesEnabled(false); + }); it('gets a specific console message', async () => { await withBrowser(async (response, context) => { const page = await context.newPage(); @@ -150,5 +156,31 @@ describe('console', () => { ); }); }); + + it('lists issues', async () => { + await withBrowser(async (response, context) => { + const page = await context.newPage(); + const issuePromise = new Promise(resolve => { + page.once('issue', () => { + resolve(); + }); + }); + await page.setContent(''); + await issuePromise; + await listConsoleMessages.handler({params: {}}, response, context); + await getConsoleMessage.handler( + {params: {msgid: 1}}, + response, + context, + ); + const formattedResponse = await response.handle('test', context); + const textContent = formattedResponse[0] as {text: string}; + assert.ok( + textContent.text.includes( + "Message: issue> An element doesn't have an autocomplete attribute", + ), + ); + }); + }); }); }); From 596a3001adf9cf1ea6d97d2305ac2e96812d82cf Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Mon, 24 Nov 2025 08:34:04 +0000 Subject: [PATCH 2/7] Fix comments and add tests --- src/DevtoolsUtils.ts | 48 ++++++++ src/McpResponse.ts | 47 +------- src/formatters/consoleFormatter.ts | 44 +++---- tests/DevtoolsUtils.test.ts | 108 +++++++++++++++++- .../consoleFormatter.test.js.snapshot | 5 +- tests/formatters/consoleFormatter.test.ts | 51 ++++----- tests/tools/console.test.ts | 51 +++++---- 7 files changed, 232 insertions(+), 122 deletions(-) diff --git a/src/DevtoolsUtils.ts b/src/DevtoolsUtils.ts index 997cb86a..eb7ad365 100644 --- a/src/DevtoolsUtils.ts +++ b/src/DevtoolsUtils.ts @@ -6,11 +6,17 @@ import { type Issue, + type AggregatedIssue, type IssuesManagerEventTypes, + MarkdownIssueDescription, + Marked, Common, I18n, } from '../node_modules/chrome-devtools-frontend/mcp/mcp.js'; +import {ISSUE_UTILS} from './issue-descriptions.js'; +import {logger} from './logger.js'; + export function extractUrlLikeFromDevToolsTitle( title: string, ): string | undefined { @@ -69,6 +75,48 @@ export class FakeIssuesManager extends Common.ObjectWrapper } } +export function mapIssueToMessageObject(issue: AggregatedIssue) { + const count = issue.getAggregatedIssuesCount(); + const markdownDescription = issue.getDescription(); + const filename = markdownDescription?.file; + if (!markdownDescription) { + logger(`no description found for issue:` + issue.code); + return null; + } + const rawMarkdown = filename + ? ISSUE_UTILS.getIssueDescription(filename) + : null; + if (!rawMarkdown) { + logger(`no markdown ${filename} found for issue:` + issue.code); + return null; + } + let processedMarkdown: string; + let title: string | null; + + try { + processedMarkdown = MarkdownIssueDescription.substitutePlaceholders( + rawMarkdown, + markdownDescription.substitutions, + ); + const markdownAst = Marked.Marked.lexer(processedMarkdown); + title = MarkdownIssueDescription.findTitleFromMarkdownAst(markdownAst); + } catch { + logger('error parsing markdown for issue ' + issue.code()); + return null; + } + if (!title) { + logger('cannot read issue title from ' + filename); + return null; + } + return { + type: 'issue', + item: issue, + message: title, + count, + description: processedMarkdown, + }; +} + I18n.DevToolsLocale.DevToolsLocale.instance({ create: true, data: { diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 0d1ce6e4..fcda81e1 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -4,12 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - AggregatedIssue, - Marked, - MarkdownIssueDescription, -} from '../node_modules/chrome-devtools-frontend/mcp/mcp.js'; +import {AggregatedIssue} from '../node_modules/chrome-devtools-frontend/mcp/mcp.js'; +import {mapIssueToMessageObject} from './DevtoolsUtils.js'; import type {ConsoleMessageData} from './formatters/consoleFormatter.js'; import { formatConsoleEventShort, @@ -23,8 +20,6 @@ import { getStatusFromRequest, } from './formatters/networkFormatter.js'; import {formatSnapshotNode} from './formatters/snapshotFormatter.js'; -import {getIssueDescription} from './issue-descriptions.js'; -import {logger} from './logger.js'; import type {McpContext} from './McpContext.js'; import type { ConsoleMessage, @@ -257,7 +252,7 @@ export class McpResponse implements Response { ), }; } else if (message instanceof AggregatedIssue) { - const result = mapIssuesMessage(message); + const result = mapIssueToMessageObject(message); consoleData = { consoleMessageStableId, ...result, @@ -315,7 +310,7 @@ export class McpResponse implements Response { }; } if (item instanceof AggregatedIssue) { - const message = mapIssuesMessage(item); + const message = mapIssueToMessageObject(item); return { consoleMessageStableId, ...message, @@ -573,37 +568,3 @@ Call ${handleDialog.name} to handle it before continuing.`); this.#textResponseLines = []; } } - -function mapIssuesMessage( - message: AggregatedIssue, -): Omit | null { - const count = message.getAggregatedIssuesCount(); - const markdownDescription = message.getDescription(); - const filename = markdownDescription?.file; - if (!markdownDescription) { - logger(`no description found for issue:` + message.code); - return null; - } - const rawMarkdown = filename ? getIssueDescription(filename) : null; - if (!rawMarkdown) { - logger(`no markdown ${filename} found for issue:` + message.code); - return null; - } - const processedMarkdown = MarkdownIssueDescription.substitutePlaceholders( - rawMarkdown, - markdownDescription.substitutions, - ); - const markdownAst = Marked.Marked.lexer(processedMarkdown); - const title = MarkdownIssueDescription.findTitleFromMarkdownAst(markdownAst); - if (!title) { - logger('cannot read issue title from ' + filename); - return null; - } - return { - type: 'issue', - item: message, - message: title, - count, - args: [], - }; -} diff --git a/src/formatters/consoleFormatter.ts b/src/formatters/consoleFormatter.ts index 67e149f0..2360171c 100644 --- a/src/formatters/consoleFormatter.ts +++ b/src/formatters/consoleFormatter.ts @@ -4,11 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - AggregatedIssue, - MarkdownIssueDescription, -} from '../../node_modules/chrome-devtools-frontend/mcp/mcp.js'; -import {ISSUE_UTILS} from '../issue-descriptions.js'; +import {AggregatedIssue} from '../../node_modules/chrome-devtools-frontend/mcp/mcp.js'; export interface ConsoleMessageData { consoleMessageStableId: number; @@ -16,6 +12,7 @@ export interface ConsoleMessageData { item?: unknown; message?: string; count?: number; + description?: string; args?: string[]; } @@ -43,7 +40,7 @@ export function formatConsoleEventVerbose(msg: ConsoleMessageData): string { if (msg.item instanceof AggregatedIssue) { const result = [ `ID: ${msg.consoleMessageStableId}`, - `Message: ${msg.type}> ${formatIssue(msg.item)}`, + `Message: ${msg.type}> ${formatIssue(msg.item, msg.description)}`, ]; return result.join('\n'); } @@ -76,35 +73,28 @@ function formatArgs(consoleData: ConsoleMessageData): string { return result.join('\n'); } -export function formatIssue(issue: AggregatedIssue): string { - const markdownDescription = issue.getDescription(); - const filename = markdownDescription?.file; - const rawMarkdown = filename - ? ISSUE_UTILS.getIssueDescription(filename) - : null; - if (!markdownDescription || !rawMarkdown) { - throw new Error('Error parsing issue description ' + issue.code()); - } - let processedMarkdown = MarkdownIssueDescription.substitutePlaceholders( - rawMarkdown, - markdownDescription.substitutions, - ); - - processedMarkdown = processedMarkdown.trim(); - // Remove heading in order not to conflict with the result response markdown - if (processedMarkdown.startsWith('# ')) { +export function formatIssue( + issue: AggregatedIssue, + description?: string, +): string { + const result: string[] = []; + + let processedMarkdown = description?.trim(); + // Remove heading in order not to conflict with the whole console message response markdown + if (processedMarkdown?.startsWith('# ')) { processedMarkdown = processedMarkdown.substring(2).trimStart(); } + if (processedMarkdown) result.push(processedMarkdown); - const result: string[] = [processedMarkdown]; - const links = markdownDescription.links; - - if (links.length > 0) { + const links = issue.getDescription()?.links; + if (links && links.length > 0) { result.push('Learn more:'); for (const link of links) { result.push(`[${link.linkTitle}](${link.link})`); } } + if (result.length === 0) + return 'No details provided for the issue ' + issue.code(); return result.join('\n'); } diff --git a/tests/DevtoolsUtils.test.ts b/tests/DevtoolsUtils.test.ts index 40ecf2c3..fd1569d5 100644 --- a/tests/DevtoolsUtils.test.ts +++ b/tests/DevtoolsUtils.test.ts @@ -5,12 +5,17 @@ */ import assert from 'node:assert'; -import {describe, it} from 'node:test'; +import {afterEach, describe, it} from 'node:test'; +import sinon from 'sinon'; + +import {AggregatedIssue} from '../node_modules/chrome-devtools-frontend/mcp/mcp.js'; import { extractUrlLikeFromDevToolsTitle, urlsEqual, + mapIssueToMessageObject, } from '../src/DevtoolsUtils.js'; +import {ISSUE_UTILS} from '../src/issue-descriptions.js'; describe('extractUrlFromDevToolsTitle', () => { it('deals with no trailing /', () => { @@ -70,3 +75,104 @@ describe('urlsEqual', () => { ); }); }); + +describe('mapIssueToMessageObject', () => { + const mockDescription = { + file: 'mock-issue.md', + substitutions: new Map([['PLACEHOLDER_VALUE', 'substitution value']]), + links: [ + {link: 'http://example.com/learnmore', linkTitle: 'Learn more'}, + { + link: 'http://example.com/another-learnmore', + linkTitle: 'Learn more 2', + }, + ], + }; + + afterEach(() => { + sinon.restore(); + }); + + it('maps aggregated issue with substituted description', () => { + const mockAggregatedIssue = sinon.createStubInstance(AggregatedIssue); + mockAggregatedIssue.getDescription.returns(mockDescription); + mockAggregatedIssue.getAggregatedIssuesCount.returns(1); + + const getIssueDescriptionStub = sinon.stub( + ISSUE_UTILS, + 'getIssueDescription', + ); + + getIssueDescriptionStub + .withArgs('mock-issue.md') + .returns( + '# Mock Issue Title\n\nThis is a mock issue description with a {PLACEHOLDER_VALUE}.', + ); + + const result = mapIssueToMessageObject(mockAggregatedIssue); + const expected = { + type: 'issue', + item: mockAggregatedIssue, + message: 'Mock Issue Title', + count: 1, + description: + '# Mock Issue Title\n\nThis is a mock issue description with a substitution value.', + }; + assert.deepStrictEqual(result, expected); + }); + + it('returns null for the issue with no description', () => { + const mockAggregatedIssue = sinon.createStubInstance(AggregatedIssue); + mockAggregatedIssue.getDescription.returns(null); + + const result = mapIssueToMessageObject(mockAggregatedIssue); + assert.equal(result, null); + }); + + it('returns null if there is no desciption file', () => { + const mockAggregatedIssue = sinon.createStubInstance(AggregatedIssue); + mockAggregatedIssue.getDescription.returns(mockDescription); + mockAggregatedIssue.getAggregatedIssuesCount.returns(1); + + const getIssueDescriptionStub = sinon.stub( + ISSUE_UTILS, + 'getIssueDescription', + ); + + getIssueDescriptionStub.withArgs('mock-issue.md').returns(null); + const result = mapIssueToMessageObject(mockAggregatedIssue); + assert.equal(result, null); + }); + + it("returns null if can't parse the title", () => { + const mockAggregatedIssue = sinon.createStubInstance(AggregatedIssue); + mockAggregatedIssue.getDescription.returns(mockDescription); + mockAggregatedIssue.getAggregatedIssuesCount.returns(1); + + const getIssueDescriptionStub = sinon.stub( + ISSUE_UTILS, + 'getIssueDescription', + ); + + getIssueDescriptionStub + .withArgs('mock-issue.md') + .returns('No title test {PLACEHOLDER_VALUE}'); + assert.deepStrictEqual(mapIssueToMessageObject(mockAggregatedIssue), null); + }); + + it('returns null if devtools utill function throws an error', () => { + const mockAggregatedIssue = sinon.createStubInstance(AggregatedIssue); + mockAggregatedIssue.getDescription.returns(mockDescription); + mockAggregatedIssue.getAggregatedIssuesCount.returns(1); + + const getIssueDescriptionStub = sinon.stub( + ISSUE_UTILS, + 'getIssueDescription', + ); + // An error will be thrown if placeholder doesn't start from PLACEHOLDER_ + getIssueDescriptionStub + .withArgs('mock-issue.md') + .returns('No title test {WRONG_PLACEHOLDER}'); + assert.deepStrictEqual(mapIssueToMessageObject(mockAggregatedIssue), null); + }); +}); diff --git a/tests/formatters/consoleFormatter.test.js.snapshot b/tests/formatters/consoleFormatter.test.js.snapshot index e18461c7..d6803572 100644 --- a/tests/formatters/consoleFormatter.test.js.snapshot +++ b/tests/formatters/consoleFormatter.test.js.snapshot @@ -36,9 +36,10 @@ Arg #0: file.txt `; exports[`consoleFormatter > formats a console.log message with issue type 1`] = ` -Mock Issue Title +ID: 5 +Message: issue> Mock Issue Title -This is a mock issue description with a http://example.com/issue-detail. +This is a mock issue description Learn more: [Learn more](http://example.com/learnmore) [Learn more 2](http://example.com/another-learnmore) diff --git a/tests/formatters/consoleFormatter.test.ts b/tests/formatters/consoleFormatter.test.ts index 902307b4..eabe2a44 100644 --- a/tests/formatters/consoleFormatter.test.ts +++ b/tests/formatters/consoleFormatter.test.ts @@ -8,14 +8,12 @@ import {describe, it} from 'node:test'; import sinon from 'sinon'; -import type {AggregatedIssue} from '../../node_modules/chrome-devtools-frontend/mcp/mcp.js'; +import {AggregatedIssue} from '../../node_modules/chrome-devtools-frontend/mcp/mcp.js'; import type {ConsoleMessageData} from '../../src/formatters/consoleFormatter.js'; import { formatConsoleEventShort, formatConsoleEventVerbose, - formatIssue, } from '../../src/formatters/consoleFormatter.js'; -import {ISSUE_UTILS} from '../../src/issue-descriptions.js'; describe('consoleFormatter', () => { describe('formatConsoleEventShort', () => { @@ -99,32 +97,29 @@ describe('consoleFormatter', () => { }); it('formats a console.log message with issue type', t => { - class MockAggregatedIssue { - getDescription() { - return { - file: 'mock-issue.md', - substitutions: new Map([ - ['PLACEHOLDER_URL', 'http://example.com/issue-detail'], - ]), - links: [ - {link: 'http://example.com/learnmore', linkTitle: 'Learn more'}, - {link: 'http://example.com/another-learnmore', linkTitle: 'Learn more 2'}, - ], - }; - } - } - const mockAggregatedIssue = new MockAggregatedIssue(); - const getIssueDescriptionStub = sinon.stub( - ISSUE_UTILS, - 'getIssueDescription', - ); + const mockAggregatedIssue = sinon.createStubInstance(AggregatedIssue); + const mockDescription = { + file: 'mock.md', + links: [ + {link: 'http://example.com/learnmore', linkTitle: 'Learn more'}, + { + link: 'http://example.com/another-learnmore', + linkTitle: 'Learn more 2', + }, + ], + }; + mockAggregatedIssue.getDescription.returns(mockDescription); + const mockDescriptionFileContent = + '# Mock Issue Title\n\nThis is a mock issue description'; - getIssueDescriptionStub - .withArgs('mock-issue.md') - .returns( - '# Mock Issue Title\n\nThis is a mock issue description with a {PLACEHOLDER_URL}.', - ); - const result = formatIssue(mockAggregatedIssue as AggregatedIssue); + const message: ConsoleMessageData = { + consoleMessageStableId: 5, + type: 'issue', + description: mockDescriptionFileContent, + item: mockAggregatedIssue, + }; + + const result = formatConsoleEventVerbose(message); t.assert.snapshot?.(result); }); }); diff --git a/tests/tools/console.test.ts b/tests/tools/console.test.ts index 61ccd59e..1c8cea85 100644 --- a/tests/tools/console.test.ts +++ b/tests/tools/console.test.ts @@ -157,29 +157,38 @@ describe('console', () => { }); }); - it('lists issues', async () => { - await withBrowser(async (response, context) => { - const page = await context.newPage(); - const issuePromise = new Promise(resolve => { - page.once('issue', () => { - resolve(); + describe('issues type', () => { + beforeEach(() => { + setIssuesEnabled(true); + }); + afterEach(() => { + setIssuesEnabled(false); + }); + + it('lists issues', async () => { + await withBrowser(async (response, context) => { + const page = await context.newPage(); + const issuePromise = new Promise(resolve => { + page.once('issue', () => { + resolve(); + }); }); + await page.setContent(''); + await issuePromise; + await listConsoleMessages.handler({params: {}}, response, context); + await getConsoleMessage.handler( + {params: {msgid: 1}}, + response, + context, + ); + const formattedResponse = await response.handle('test', context); + const textContent = formattedResponse[0] as {text: string}; + assert.ok( + textContent.text.includes( + "Message: issue> An element doesn't have an autocomplete attribute", + ), + ); }); - await page.setContent(''); - await issuePromise; - await listConsoleMessages.handler({params: {}}, response, context); - await getConsoleMessage.handler( - {params: {msgid: 1}}, - response, - context, - ); - const formattedResponse = await response.handle('test', context); - const textContent = formattedResponse[0] as {text: string}; - assert.ok( - textContent.text.includes( - "Message: issue> An element doesn't have an autocomplete attribute", - ), - ); }); }); }); From c2abf01c68d66cfd7e91cbf272e4cda03123991b Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Mon, 24 Nov 2025 10:27:17 +0000 Subject: [PATCH 3/7] Refactor test, add issues test description and learn more links --- tests/tools/console.test.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/tools/console.test.ts b/tests/tools/console.test.ts index 1c8cea85..0724a503 100644 --- a/tests/tools/console.test.ts +++ b/tests/tools/console.test.ts @@ -9,6 +9,7 @@ import {afterEach, before, beforeEach, describe, it} from 'node:test'; import {setIssuesEnabled} from '../../src/features.js'; import {loadIssueDescriptions} from '../../src/issue-descriptions.js'; +import {McpResponse} from '../../src/McpResponse.js'; import { getConsoleMessage, listConsoleMessages, @@ -129,12 +130,6 @@ describe('console', () => { }); describe('get_console_message', () => { - beforeEach(() => { - setIssuesEnabled(true); - }); - afterEach(() => { - setIssuesEnabled(false); - }); it('gets a specific console message', async () => { await withBrowser(async (response, context) => { const page = await context.newPage(); @@ -165,7 +160,7 @@ describe('console', () => { setIssuesEnabled(false); }); - it('lists issues', async () => { + it('gets issue details', async () => { await withBrowser(async (response, context) => { const page = await context.newPage(); const issuePromise = new Promise(resolve => { @@ -176,18 +171,23 @@ describe('console', () => { await page.setContent(''); await issuePromise; await listConsoleMessages.handler({params: {}}, response, context); + const response2 = new McpResponse(); await getConsoleMessage.handler( {params: {msgid: 1}}, - response, + response2, context, ); - const formattedResponse = await response.handle('test', context); + const formattedResponse = await response2.handle('test', context); const textContent = formattedResponse[0] as {text: string}; - assert.ok( - textContent.text.includes( - "Message: issue> An element doesn't have an autocomplete attribute", - ), - ); + const learnMoreLinks = + '[HTML attribute: autocomplete](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete#values)'; + const detailsDescription = + "A form field has an `id` or `name` attribute that the browser's autofill recognizes. However, it doesn't have an `autocomplete` attribute assigned. This might prevent the browser from correctly autofilling the form.\n\nTo fix this issue, provide an `autocomplete` attribute."; + const title = + "Message: issue> An element doesn't have an autocomplete attribute"; + assert.ok(textContent.text.includes(title)); + assert.ok(textContent.text.includes(detailsDescription)); + assert.ok(textContent.text.includes(learnMoreLinks)); }); }); }); From 3f165da4a07b715ff3952d38a1d4212ff416817d Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Mon, 24 Nov 2025 16:31:36 +0000 Subject: [PATCH 4/7] Update item type from unknown to AggregatedIssue --- src/formatters/consoleFormatter.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/formatters/consoleFormatter.ts b/src/formatters/consoleFormatter.ts index 2360171c..3dc36c11 100644 --- a/src/formatters/consoleFormatter.ts +++ b/src/formatters/consoleFormatter.ts @@ -9,7 +9,7 @@ import {AggregatedIssue} from '../../node_modules/chrome-devtools-frontend/mcp/m export interface ConsoleMessageData { consoleMessageStableId: number; type?: string; - item?: unknown; + item?: AggregatedIssue; message?: string; count?: number; description?: string; @@ -37,18 +37,11 @@ function getArgs(msg: ConsoleMessageData) { // The verbose format for a console message, including all details. export function formatConsoleEventVerbose(msg: ConsoleMessageData): string { - if (msg.item instanceof AggregatedIssue) { - const result = [ - `ID: ${msg.consoleMessageStableId}`, - `Message: ${msg.type}> ${formatIssue(msg.item, msg.description)}`, - ]; - return result.join('\n'); - } - + const aggregatedIssue = msg.item; const result = [ `ID: ${msg.consoleMessageStableId}`, - `Message: ${msg.type}> ${msg.message}`, - formatArgs(msg), + `Message: ${msg.type}> ${aggregatedIssue ? formatIssue(aggregatedIssue, msg.description) : msg.message}`, + aggregatedIssue ? undefined: formatArgs(msg), ].filter(line => !!line); return result.join('\n'); } From f83f9b6c9ea6922caae20e06ce2360650c4bff77 Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Mon, 24 Nov 2025 18:30:08 +0000 Subject: [PATCH 5/7] added formatting --- src/formatters/consoleFormatter.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/formatters/consoleFormatter.ts b/src/formatters/consoleFormatter.ts index 3dc36c11..0e4672aa 100644 --- a/src/formatters/consoleFormatter.ts +++ b/src/formatters/consoleFormatter.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {AggregatedIssue} from '../../node_modules/chrome-devtools-frontend/mcp/mcp.js'; +import type {AggregatedIssue} from '../../node_modules/chrome-devtools-frontend/mcp/mcp.js'; export interface ConsoleMessageData { consoleMessageStableId: number; @@ -41,7 +41,7 @@ export function formatConsoleEventVerbose(msg: ConsoleMessageData): string { const result = [ `ID: ${msg.consoleMessageStableId}`, `Message: ${msg.type}> ${aggregatedIssue ? formatIssue(aggregatedIssue, msg.description) : msg.message}`, - aggregatedIssue ? undefined: formatArgs(msg), + aggregatedIssue ? undefined : formatArgs(msg), ].filter(line => !!line); return result.join('\n'); } From e37a40aebab0e2d3a7c9a1f384691ae0ffd0e73e Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Tue, 25 Nov 2025 07:59:02 +0000 Subject: [PATCH 6/7] Handle null issues messages --- src/McpResponse.ts | 10 +++++---- tests/McpResponse.test.ts | 44 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/McpResponse.ts b/src/McpResponse.ts index fcda81e1..406d723e 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -252,10 +252,11 @@ export class McpResponse implements Response { ), }; } else if (message instanceof AggregatedIssue) { - const result = mapIssueToMessageObject(message); + const mappedIssueMessage = mapIssueToMessageObject(message); + if (!mappedIssueMessage) throw new Error('Can\'t prpovide detals for the msgid ' + consoleMessageStableId); consoleData = { consoleMessageStableId, - ...result, + ...mappedIssueMessage, }; } else { consoleData = { @@ -310,10 +311,11 @@ export class McpResponse implements Response { }; } if (item instanceof AggregatedIssue) { - const message = mapIssueToMessageObject(item); + const mappedIssueMessage = mapIssueToMessageObject(item); + if (!mappedIssueMessage) return null; return { consoleMessageStableId, - ...message, + ...mappedIssueMessage, }; } return { diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index 2c9516bc..1ff6897b 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -10,6 +10,10 @@ import {tmpdir} from 'node:os'; import {join} from 'node:path'; import {describe, it} from 'node:test'; +import sinon from 'sinon'; + +import { AggregatedIssue } from '../node_modules/chrome-devtools-frontend/mcp/mcp.js'; + import { getMockRequest, getMockResponse, @@ -295,6 +299,46 @@ describe('McpResponse', () => { t.assert.snapshot?.(result[0].text); }); }); + + it('doesn\'t list the issue message if mapping returns null', async () => { + await withBrowser(async (response, context) => { + const mockAggregatedIssue = sinon.createStubInstance(AggregatedIssue); + const mockDescription = { + file: 'not-existing-description-file.md', + links: [], + }; + mockAggregatedIssue.getDescription.returns(mockDescription); + response.setIncludeConsoleData(true); + context.getConsoleData = () => { + return [mockAggregatedIssue]; + }; + + const result = await response.handle('test', context); + const text = (result[0].text as string).toString(); + assert.ok(text.includes('')); + }); + }); + + it('throws error if mapping returns null on get issue details', async () => { + await withBrowser(async (response, context) => { + const mockAggregatedIssue = sinon.createStubInstance(AggregatedIssue); + const mockDescription = { + file: 'not-existing-description-file.md', + links: [], + }; + mockAggregatedIssue.getDescription.returns(mockDescription); + response.attachConsoleMessage(1); + context.getConsoleMessageById = () => { + return mockAggregatedIssue; + }; + + try { + await response.handle('test', context); + } catch (e) { + assert.ok(e.message.includes('Can\'t prpovide detals for the msgid 1')); + } + }); + }); }); describe('McpResponse network request filtering', () => { From 5484180712ee72dd9e4aab80faf902c88fcdadbf Mon Sep 17 00:00:00 2001 From: Natallia Harshunova Date: Tue, 25 Nov 2025 10:08:01 +0000 Subject: [PATCH 7/7] Formatting --- src/McpResponse.ts | 5 ++++- tests/McpResponse.test.ts | 24 ++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 406d723e..b7de0440 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -253,7 +253,10 @@ export class McpResponse implements Response { }; } else if (message instanceof AggregatedIssue) { const mappedIssueMessage = mapIssueToMessageObject(message); - if (!mappedIssueMessage) throw new Error('Can\'t prpovide detals for the msgid ' + consoleMessageStableId); + if (!mappedIssueMessage) + throw new Error( + "Can't prpovide detals for the msgid " + consoleMessageStableId, + ); consoleData = { consoleMessageStableId, ...mappedIssueMessage, diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index 1ff6897b..b045454d 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -12,7 +12,7 @@ import {describe, it} from 'node:test'; import sinon from 'sinon'; -import { AggregatedIssue } from '../node_modules/chrome-devtools-frontend/mcp/mcp.js'; +import {AggregatedIssue} from '../node_modules/chrome-devtools-frontend/mcp/mcp.js'; import { getMockRequest, @@ -300,13 +300,13 @@ describe('McpResponse', () => { }); }); - it('doesn\'t list the issue message if mapping returns null', async () => { + it("doesn't list the issue message if mapping returns null", async () => { await withBrowser(async (response, context) => { const mockAggregatedIssue = sinon.createStubInstance(AggregatedIssue); - const mockDescription = { - file: 'not-existing-description-file.md', - links: [], - }; + const mockDescription = { + file: 'not-existing-description-file.md', + links: [], + }; mockAggregatedIssue.getDescription.returns(mockDescription); response.setIncludeConsoleData(true); context.getConsoleData = () => { @@ -322,10 +322,10 @@ describe('McpResponse', () => { it('throws error if mapping returns null on get issue details', async () => { await withBrowser(async (response, context) => { const mockAggregatedIssue = sinon.createStubInstance(AggregatedIssue); - const mockDescription = { - file: 'not-existing-description-file.md', - links: [], - }; + const mockDescription = { + file: 'not-existing-description-file.md', + links: [], + }; mockAggregatedIssue.getDescription.returns(mockDescription); response.attachConsoleMessage(1); context.getConsoleMessageById = () => { @@ -333,9 +333,9 @@ describe('McpResponse', () => { }; try { - await response.handle('test', context); + await response.handle('test', context); } catch (e) { - assert.ok(e.message.includes('Can\'t prpovide detals for the msgid 1')); + assert.ok(e.message.includes("Can't prpovide detals for the msgid 1")); } }); });