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
11 changes: 7 additions & 4 deletions src/McpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ export class McpResponse implements Response {
const mappedIssueMessage = mapIssueToMessageObject(message);
if (!mappedIssueMessage)
throw new Error(
"Can't prpovide detals for the msgid " + consoleMessageStableId,
"Can't provide detals for the msgid " + consoleMessageStableId,
);
consoleData = {
consoleMessageStableId,
Expand Down Expand Up @@ -402,7 +402,7 @@ Call ${handleDialog.name} to handle it before continuing.`);
}

response.push(...this.#formatNetworkRequestData(context, data.bodies));
response.push(...this.#formatConsoleData(data.consoleData));
response.push(...this.#formatConsoleData(context, data.consoleData));

if (this.#networkRequestsOptions?.include) {
let requests = context.getNetworkRequests(
Expand Down Expand Up @@ -500,13 +500,16 @@ Call ${handleDialog.name} to handle it before continuing.`);
};
}

#formatConsoleData(data: ConsoleMessageData | undefined): string[] {
#formatConsoleData(
context: McpContext,
data: ConsoleMessageData | undefined,
): string[] {
const response: string[] = [];
if (!data) {
return response;
}

response.push(formatConsoleEventVerbose(data));
response.push(formatConsoleEventVerbose(data, context));
return response;
}

Expand Down
99 changes: 96 additions & 3 deletions src/formatters/consoleFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import type {AggregatedIssue} from '../../node_modules/chrome-devtools-frontend/mcp/mcp.js';
import type {McpContext} from '../McpContext.js';

export interface ConsoleMessageData {
consoleMessageStableId: number;
Expand Down Expand Up @@ -36,11 +37,14 @@ function getArgs(msg: ConsoleMessageData) {
}

// The verbose format for a console message, including all details.
export function formatConsoleEventVerbose(msg: ConsoleMessageData): string {
export function formatConsoleEventVerbose(
msg: ConsoleMessageData,
context?: McpContext,
): string {
const aggregatedIssue = msg.item;
const result = [
`ID: ${msg.consoleMessageStableId}`,
`Message: ${msg.type}> ${aggregatedIssue ? formatIssue(aggregatedIssue, msg.description) : msg.message}`,
`Message: ${msg.type}> ${aggregatedIssue ? formatIssue(aggregatedIssue, msg.description, context) : msg.message}`,
aggregatedIssue ? undefined : formatArgs(msg),
].filter(line => !!line);
return result.join('\n');
Expand All @@ -65,10 +69,19 @@ function formatArgs(consoleData: ConsoleMessageData): string {

return result.join('\n');
}

interface IssueDetailsWithResources {
violatingNodeId?: number;
nodeId?: number;
documentNodeId?: number;
request?: {
requestId?: string;
url: string;
};
}
export function formatIssue(
issue: AggregatedIssue,
description?: string,
context?: McpContext,
): string {
const result: string[] = [];

Expand All @@ -87,6 +100,86 @@ export function formatIssue(
}
}

const issues: Array<{
details?: () => IssueDetailsWithResources;
getDetails?: () => IssueDetailsWithResources;
}> = [
...issue.getCorsIssues(),
...issue.getMixedContentIssues(),
...issue.getGenericIssues(),
...issue.getLowContrastIssues(),
...issue.getElementAccessibilityIssues(),
...issue.getQuirksModeIssues(),
];
const affectedResources: Array<{
uid?: string;
data?: object;
request?: string | number;
}> = [];
for (const singleIssue of issues) {
if (!singleIssue.details && !singleIssue.getDetails) continue;

let details =
singleIssue.details?.() as unknown as IssueDetailsWithResources;
if (!details)
details =
singleIssue.getDetails?.() as unknown as IssueDetailsWithResources;
if (!details) continue;

let uid;
let request: number | string | undefined;
if (details.violatingNodeId && context) {
uid = context.resolveCdpElementId(details.violatingNodeId);
}
if (details.nodeId && context) {
uid = context.resolveCdpElementId(details.nodeId);
}
if (details.documentNodeId && context) {
uid = context.resolveCdpElementId(details.documentNodeId);
}

if (details.request) {
request = details.request.url;
if (details.request.requestId && context) {
const resolvedId = context.resolveCdpRequestId(
details.request.requestId,
);
if (resolvedId) {
request = resolvedId;
}
}
}

// eslint-disable-next-line
const data = structuredClone(details) as any;
delete data.violatingNodeId;
delete data.nodeId;
delete data.documentNodeId;
delete data.errorType;
delete data.frameId;
delete data.request;
affectedResources.push({
uid,
data: data,
request,
});
}
if (affectedResources.length) {
result.push('### Affected resources');
}
result.push(
...affectedResources.map(item => {
const details = [];
if (item.uid) details.push(`uid=${item.uid}`);
if (item.request) {
details.push(
(typeof item.request === 'number' ? `reqid=` : 'url=') + item.request,
);
}
if (item.data) details.push(`data=${JSON.stringify(item.data)}`);
return details.join(' ');
}),
);
if (result.length === 0)
return 'No details provided for the issue ' + issue.code();
return result.join('\n');
Expand Down
11 changes: 4 additions & 7 deletions tests/McpResponse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@ 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 {
getMockAggregatedIssue,
getMockRequest,
getMockResponse,
html,
Expand Down Expand Up @@ -302,7 +299,7 @@ describe('McpResponse', () => {

it("doesn't list the issue message if mapping returns null", async () => {
await withBrowser(async (response, context) => {
const mockAggregatedIssue = sinon.createStubInstance(AggregatedIssue);
const mockAggregatedIssue = getMockAggregatedIssue();
const mockDescription = {
file: 'not-existing-description-file.md',
links: [],
Expand All @@ -321,7 +318,7 @@ 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 mockAggregatedIssue = getMockAggregatedIssue();
const mockDescription = {
file: 'not-existing-description-file.md',
links: [],
Expand All @@ -335,7 +332,7 @@ describe('McpResponse', () => {
try {
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 provide detals for the msgid 1"));
}
});
});
Expand Down
2 changes: 2 additions & 0 deletions tests/formatters/consoleFormatter.test.js.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,6 @@ This is a mock issue description
Learn more:
[Learn more](http://example.com/learnmore)
[Learn more 2](http://example.com/another-learnmore)
### Affected resources
data={"violatingNodeAttribute":"test"}
`;
16 changes: 12 additions & 4 deletions tests/formatters/consoleFormatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@

import {describe, it} from 'node:test';

import sinon from 'sinon';

import {AggregatedIssue} from '../../node_modules/chrome-devtools-frontend/mcp/mcp.js';
import type {ConsoleMessageData} from '../../src/formatters/consoleFormatter.js';
import {
formatConsoleEventShort,
formatConsoleEventVerbose,
} from '../../src/formatters/consoleFormatter.js';
import {getMockAggregatedIssue} from '../utils.js';

describe('consoleFormatter', () => {
describe('formatConsoleEventShort', () => {
Expand Down Expand Up @@ -97,7 +95,15 @@ describe('consoleFormatter', () => {
});

it('formats a console.log message with issue type', t => {
const mockAggregatedIssue = sinon.createStubInstance(AggregatedIssue);
const testGenericIssue = {
details: () => {
return {
violatingNodeId: 2,
violatingNodeAttribute: 'test',
};
},
};
const mockAggregatedIssue = getMockAggregatedIssue();
const mockDescription = {
file: 'mock.md',
links: [
Expand All @@ -109,6 +115,8 @@ describe('consoleFormatter', () => {
],
};
mockAggregatedIssue.getDescription.returns(mockDescription);
// @ts-expect-error generic issue stub bypass
mockAggregatedIssue.getGenericIssues.returns(new Set([testGenericIssue]));
const mockDescriptionFileContent =
'# Mock Issue Title\n\nThis is a mock issue description';

Expand Down
29 changes: 29 additions & 0 deletions tests/tools/console.test.js.snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
exports[`console > get_console_message > issues type > gets issue details with node id parsing 1`] = `
# test response
ID: 1
Message: issue> An element doesn't have an autocomplete attribute

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.

To fix this issue, provide an \`autocomplete\` attribute.
Learn more:
[HTML attribute: autocomplete](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete#values)
### Affected resources
uid=1_1 data={"violatingNodeAttribute":"name"}
`;

exports[`console > get_console_message > issues type > gets issue details with request id parsing 1`] = `
# test response
ID: <ID>
Message: issue> Ensure CORS response header values are valid

A cross-origin resource sharing (CORS) request was blocked because of invalid or missing response headers of the request or the associated [preflight request](issueCorsPreflightRequest).

To fix this issue, ensure the response to the CORS request and/or the associated [preflight request](issueCorsPreflightRequest) are not missing headers and use valid header values.

Note that if an opaque response is sufficient, the request's mode can be set to \`no-cors\` to fetch the resource with CORS disabled; that way CORS headers are not required but the response content is inaccessible (opaque).
Learn more:
[Cross-Origin Resource Sharing (\`CORS\`)](https://web.dev/cross-origin-resource-sharing)
### Affected resources
reqid=1 data={"corsErrorStatus":{"corsError":"PreflightMissingAllowOriginHeader","failedParameter":""},"isWarning":false,"initiatorOrigin":"","clientSecurityState":{"initiatorIsSecureContext":false,"initiatorIPAddressSpace":"Loopback","privateNetworkRequestPolicy":"BlockFromInsecureToMorePrivate"}}
`;
Loading