mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-14 19:45:30 +08:00
fix: fix formatter bugs (#2552)
This PR is created on top of PR #2519 and addresses two formatter bugs discovered during review: ConsoleFormatter bug: When a console log has an empty message text but contains arguments, the formatter correctly shifts the first argument out of the arguments list to serve as the message text. However, it never sets the text field to this shifted argument, causing the first argument to be completely lost in the output. IssueFormatter bug: When a concise issue has no description, the formatting helper prints the literal string "undefined" instead of falling back to "Unknown Issue". Closes #2519 --------- Co-authored-by: ZayanKhan-12 <khanzayan200@gmail.com>
This commit is contained in:
committed by
GitHub
parent
12f989a75b
commit
b24731572f
@@ -196,6 +196,16 @@ export class ConsoleFormatter {
|
||||
return convertConsoleMessageConciseDetailedToString(this.toJSONDetailed());
|
||||
}
|
||||
|
||||
#getText(): string {
|
||||
if (this.#text) {
|
||||
return this.#text;
|
||||
}
|
||||
if (this.#resolvedArgs.length > 0) {
|
||||
return formatArg(this.#resolvedArgs[0], this);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
#getArgs(): unknown[] {
|
||||
if (this.#resolvedArgs.length > 0) {
|
||||
const args = [...this.#resolvedArgs];
|
||||
@@ -211,7 +221,7 @@ export class ConsoleFormatter {
|
||||
toJSON(): ConsoleMessageConcise {
|
||||
const json: ConsoleMessageConcise = {
|
||||
type: this.#type,
|
||||
text: this.#text,
|
||||
text: this.#getText(),
|
||||
argsCount: this.#argCount,
|
||||
id: this.#id,
|
||||
};
|
||||
@@ -241,7 +251,7 @@ export class ConsoleFormatter {
|
||||
prev.message instanceof ConsoleFormatter &&
|
||||
msg instanceof ConsoleFormatter &&
|
||||
prev.message.#type === msg.#type &&
|
||||
prev.message.#text === msg.#text &&
|
||||
prev.message.#getText() === msg.#getText() &&
|
||||
prev.message.#argCount === msg.#argCount
|
||||
) {
|
||||
prev.count++;
|
||||
@@ -272,7 +282,7 @@ export class ConsoleFormatter {
|
||||
return {
|
||||
id: this.#id,
|
||||
type: this.#type,
|
||||
text: this.#text,
|
||||
text: this.#getText(),
|
||||
argsCount: this.#argCount,
|
||||
args: this.#getArgs().map(arg => formatArg(arg, this)),
|
||||
stackTrace: this.#stack
|
||||
|
||||
@@ -214,7 +214,7 @@ export class IssueFormatter {
|
||||
}
|
||||
|
||||
function convertIssueConciseToString(issue: IssueConcise): string {
|
||||
return `msgid=${issue.id} [issue] ${issue.title} (count: ${issue.count})`;
|
||||
return `msgid=${issue.id} [issue] ${issue.title ?? 'Unknown Issue'} (count: ${issue.count})`;
|
||||
}
|
||||
|
||||
function convertIssueDetailedToString(issue: IssueDetailed): string {
|
||||
|
||||
@@ -321,6 +321,53 @@ at schedule (util.ts:5:2)
|
||||
Note: line and column numbers use 1-based indexing
|
||||
`;
|
||||
|
||||
exports[`ConsoleFormatter > toStringDetailed/toJSONDetailed > formats frames without a name or url and unnamed async fragments toJSONDetailed 1`] = `
|
||||
{
|
||||
"id": 16,
|
||||
"type": "log",
|
||||
"text": "Hello stack trace!",
|
||||
"argsCount": 0,
|
||||
"args": [],
|
||||
"stackTrace": "at <anonymous>\\nat bar (foo.ts:20:2)\\n--- async ------------------------------\\nat schedule (util.ts:5:2)\\nNote: line and column numbers use 1-based indexing"
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`ConsoleFormatter > toStringDetailed/toJSONDetailed > formats frames without a name or url and unnamed async fragments toStringDetailed 1`] = `
|
||||
ID: 16
|
||||
Message: log> Hello stack trace!
|
||||
### Stack trace
|
||||
at <anonymous>
|
||||
at bar (foo.ts:20:2)
|
||||
--- async ------------------------------
|
||||
at schedule (util.ts:5:2)
|
||||
Note: line and column numbers use 1-based indexing
|
||||
`;
|
||||
|
||||
exports[`ConsoleFormatter > toStringDetailed/toJSONDetailed > formats object and primitive arguments toJSONDetailed 1`] = `
|
||||
{
|
||||
"id": 15,
|
||||
"type": "log",
|
||||
"text": "Mixed args:",
|
||||
"argsCount": 4,
|
||||
"args": [
|
||||
"{\\"user\\":\\"alice\\",\\"roles\\":[\\"admin\\"]}",
|
||||
"42",
|
||||
"null",
|
||||
"true"
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`ConsoleFormatter > toStringDetailed/toJSONDetailed > formats object and primitive arguments toStringDetailed 1`] = `
|
||||
ID: 15
|
||||
Message: log> Mixed args:
|
||||
### Arguments
|
||||
Arg #0: {"user":"alice","roles":["admin"]}
|
||||
Arg #1: 42
|
||||
Arg #2: null
|
||||
Arg #3: true
|
||||
`;
|
||||
|
||||
exports[`ConsoleFormatter > toStringDetailed/toJSONDetailed > handles \"Execution context is not available\" error in args toJSONDetailed 1`] = `
|
||||
{
|
||||
"id": 6,
|
||||
@@ -408,3 +455,22 @@ at fn49 (main.js:49:49)
|
||||
... and 50 more frames
|
||||
Note: line and column numbers use 1-based indexing
|
||||
`;
|
||||
|
||||
exports[`ConsoleFormatter > toStringDetailed/toJSONDetailed > uses the first argument as text when the message text is empty toJSONDetailed 1`] = `
|
||||
{
|
||||
"id": 14,
|
||||
"type": "log",
|
||||
"text": "Actual message",
|
||||
"argsCount": 2,
|
||||
"args": [
|
||||
"extra-arg"
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`ConsoleFormatter > toStringDetailed/toJSONDetailed > uses the first argument as text when the message text is empty toStringDetailed 1`] = `
|
||||
ID: 14
|
||||
Message: log> Actual message
|
||||
### Arguments
|
||||
Arg #0: extra-arg
|
||||
`;
|
||||
|
||||
@@ -727,5 +727,81 @@ describe('ConsoleFormatter', () => {
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
formatterTestDetailed(
|
||||
'uses the first argument as text when the message text is empty',
|
||||
async () => {
|
||||
const message = createMockMessage({
|
||||
type: () => 'log',
|
||||
text: () => '',
|
||||
});
|
||||
return await ConsoleFormatter.from(message, {
|
||||
id: 14,
|
||||
resolvedArgsForTesting: ['Actual message', 'extra-arg'],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
formatterTestDetailed(
|
||||
'formats object and primitive arguments',
|
||||
async () => {
|
||||
const message = createMockMessage({
|
||||
type: () => 'log',
|
||||
text: () => 'Mixed args:',
|
||||
});
|
||||
return await ConsoleFormatter.from(message, {
|
||||
id: 15,
|
||||
resolvedArgsForTesting: [
|
||||
{user: 'alice', roles: ['admin']},
|
||||
42,
|
||||
null,
|
||||
true,
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
formatterTestDetailed(
|
||||
'formats frames without a name or url and unnamed async fragments',
|
||||
async () => {
|
||||
const message = createMockMessage({
|
||||
type: () => 'log',
|
||||
text: () => 'Hello stack trace!',
|
||||
});
|
||||
const stackTrace = {
|
||||
syncFragment: {
|
||||
frames: [
|
||||
{
|
||||
line: 10,
|
||||
column: 2,
|
||||
},
|
||||
{
|
||||
line: 20,
|
||||
column: 2,
|
||||
url: 'foo.ts',
|
||||
name: 'bar',
|
||||
},
|
||||
],
|
||||
},
|
||||
asyncFragments: [
|
||||
{
|
||||
frames: [
|
||||
{
|
||||
line: 5,
|
||||
column: 2,
|
||||
url: 'util.ts',
|
||||
name: 'schedule',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as unknown as DevTools.StackTrace.StackTrace.StackTrace;
|
||||
|
||||
return await ConsoleFormatter.from(message, {
|
||||
id: 16,
|
||||
resolvedStackTraceForTesting: stackTrace,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +32,65 @@ Learn more:
|
||||
uid=1_1 data={"violatingNodeAttribute":"test"}
|
||||
`;
|
||||
|
||||
exports[`IssueFormatter > formats a detailed issue with a resolved request id toJSONDetailed 1`] = `
|
||||
{
|
||||
"id": 6,
|
||||
"type": "issue",
|
||||
"title": "Mock Issue Title",
|
||||
"description": "# Mock Issue Title\\n\\nThis is a mock issue description",
|
||||
"links": [],
|
||||
"affectedResources": [
|
||||
{
|
||||
"data": {
|
||||
"request": {
|
||||
"url": "http://example.com/data.json"
|
||||
}
|
||||
},
|
||||
"request": 42
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`IssueFormatter > formats a detailed issue with a resolved request id toStringDetailed 1`] = `
|
||||
ID: 6
|
||||
Message: issue> Mock Issue Title
|
||||
|
||||
This is a mock issue description
|
||||
### Affected resources
|
||||
reqid=42 data={"request":{"url":"http://example.com/data.json"}}
|
||||
`;
|
||||
|
||||
exports[`IssueFormatter > formats a detailed issue with an unresolved request id toJSONDetailed 1`] = `
|
||||
{
|
||||
"id": 7,
|
||||
"type": "issue",
|
||||
"title": "Mock Issue Title",
|
||||
"description": "# Mock Issue Title\\n\\nThis is a mock issue description",
|
||||
"links": [],
|
||||
"affectedResources": [
|
||||
{
|
||||
"data": {
|
||||
"request": {
|
||||
"url": "http://example.com/data.json",
|
||||
"requestId": "REQUEST-1"
|
||||
}
|
||||
},
|
||||
"request": "http://example.com/data.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`IssueFormatter > formats a detailed issue with an unresolved request id toStringDetailed 1`] = `
|
||||
ID: 7
|
||||
Message: issue> Mock Issue Title
|
||||
|
||||
This is a mock issue description
|
||||
### Affected resources
|
||||
url=http://example.com/data.json data={"request":{"url":"http://example.com/data.json","requestId":"REQUEST-1"}}
|
||||
`;
|
||||
|
||||
exports[`IssueFormatter > formats a simplified issue toJSON 1`] = `
|
||||
{
|
||||
"type": "issue",
|
||||
|
||||
@@ -52,6 +52,20 @@ describe('IssueFormatter', () => {
|
||||
});
|
||||
}
|
||||
|
||||
function getMockIssueWithDetails(details: object | null) {
|
||||
const mockAggregatedIssue = getMockAggregatedIssue();
|
||||
mockAggregatedIssue.getDescription.returns({
|
||||
file: 'mock.md',
|
||||
links: [],
|
||||
});
|
||||
getIssueDescriptionStub
|
||||
.withArgs('mock.md')
|
||||
.returns('# Mock Issue Title\n\nThis is a mock issue description');
|
||||
// @ts-expect-error stubbed issue does not match the complete type.
|
||||
mockAggregatedIssue.getAllIssues.returns([{details: () => details}]);
|
||||
return mockAggregatedIssue;
|
||||
}
|
||||
|
||||
formatterTestConcise('formats an issue message', async () => {
|
||||
const testGenericIssue = {
|
||||
details: () => {
|
||||
@@ -134,6 +148,105 @@ describe('IssueFormatter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
formatterTestDetailed(
|
||||
'formats a detailed issue with a resolved request id',
|
||||
async () => {
|
||||
const mockAggregatedIssue = getMockIssueWithDetails({
|
||||
request: {
|
||||
url: 'http://example.com/data.json',
|
||||
requestId: 'REQUEST-1',
|
||||
},
|
||||
errorType: 'MockError',
|
||||
frameId: 'FRAME-1',
|
||||
});
|
||||
|
||||
return new IssueFormatter(mockAggregatedIssue, {
|
||||
id: 6,
|
||||
requestIdResolver: requestId =>
|
||||
requestId === 'REQUEST-1' ? 42 : undefined,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
formatterTestDetailed(
|
||||
'formats a detailed issue with an unresolved request id',
|
||||
async () => {
|
||||
const mockAggregatedIssue = getMockIssueWithDetails({
|
||||
request: {
|
||||
url: 'http://example.com/data.json',
|
||||
requestId: 'REQUEST-1',
|
||||
},
|
||||
});
|
||||
|
||||
return new IssueFormatter(mockAggregatedIssue, {
|
||||
id: 7,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('falls back to "Unknown Issue" when there is no description metadata', () => {
|
||||
const mockAggregatedIssue = getMockAggregatedIssue();
|
||||
mockAggregatedIssue.getDescription.returns(null);
|
||||
mockAggregatedIssue.getAggregatedIssuesCount.returns(1);
|
||||
|
||||
const formatter = new IssueFormatter(mockAggregatedIssue, {id: 3});
|
||||
assert.strictEqual(
|
||||
formatter.toString(),
|
||||
'msgid=3 [issue] Unknown Issue (count: 1)',
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatter.toStringDetailed(),
|
||||
'ID: 3\nMessage: issue> Unknown Issue',
|
||||
);
|
||||
});
|
||||
|
||||
describe('affected resources', () => {
|
||||
it('resolves nodeId with the element id resolver', () => {
|
||||
const formatter = new IssueFormatter(
|
||||
getMockIssueWithDetails({nodeId: 42, extra: 'info'}),
|
||||
{
|
||||
id: 1,
|
||||
elementIdResolver: backendNodeId =>
|
||||
backendNodeId === 42 ? '2_7' : undefined,
|
||||
},
|
||||
);
|
||||
assert.deepStrictEqual(formatter.toJSONDetailed().affectedResources, [
|
||||
{uid: '2_7', data: {extra: 'info'}, request: undefined},
|
||||
]);
|
||||
});
|
||||
|
||||
it('resolves documentNodeId with the element id resolver', () => {
|
||||
const formatter = new IssueFormatter(
|
||||
getMockIssueWithDetails({documentNodeId: 7}),
|
||||
{
|
||||
id: 1,
|
||||
elementIdResolver: backendNodeId =>
|
||||
backendNodeId === 7 ? '3_1' : undefined,
|
||||
},
|
||||
);
|
||||
assert.deepStrictEqual(formatter.toJSONDetailed().affectedResources, [
|
||||
{uid: '3_1', data: {}, request: undefined},
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps node ids if there is no element id resolver', () => {
|
||||
const formatter = new IssueFormatter(
|
||||
getMockIssueWithDetails({nodeId: 42}),
|
||||
{id: 1},
|
||||
);
|
||||
assert.deepStrictEqual(formatter.toJSONDetailed().affectedResources, [
|
||||
{uid: undefined, data: {nodeId: 42}, request: undefined},
|
||||
]);
|
||||
});
|
||||
|
||||
it('skips issues without details', () => {
|
||||
const formatter = new IssueFormatter(getMockIssueWithDetails(null), {
|
||||
id: 1,
|
||||
});
|
||||
assert.deepStrictEqual(formatter.toJSONDetailed().affectedResources, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValid', () => {
|
||||
it('returns false for the issue with no description', () => {
|
||||
const mockAggregatedIssue = getMockAggregatedIssue();
|
||||
|
||||
@@ -143,6 +143,17 @@ describe('snapshotFormatter', () => {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '1_4',
|
||||
role: 'slider',
|
||||
name: 'volume',
|
||||
valuemin: 0,
|
||||
valuemax: 100,
|
||||
children: [],
|
||||
elementHandle: async (): Promise<ElementHandle<Element> | null> => {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
],
|
||||
elementHandle: async (): Promise<ElementHandle<Element> | null> => {
|
||||
return null;
|
||||
@@ -156,6 +167,7 @@ describe('snapshotFormatter', () => {
|
||||
`uid=1_1 root "root"
|
||||
uid=1_2 button "button" disableable disabled focusable focused
|
||||
uid=1_3 textbox "textbox" value="value"
|
||||
uid=1_4 slider "volume" valuemax="100" valuemin="0"
|
||||
`,
|
||||
);
|
||||
});
|
||||
@@ -272,6 +284,37 @@ describe('snapshotFormatter', () => {
|
||||
t.assert.snapshot(formatted);
|
||||
});
|
||||
|
||||
it('formats a node with role "none" as ignored', () => {
|
||||
const node: TextSnapshotNode = {
|
||||
id: '1_1',
|
||||
role: 'none',
|
||||
name: '',
|
||||
children: [
|
||||
{
|
||||
id: '1_2',
|
||||
role: 'statictext',
|
||||
name: 'text',
|
||||
children: [],
|
||||
elementHandle: async (): Promise<ElementHandle<Element> | null> => {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
],
|
||||
elementHandle: async (): Promise<ElementHandle<Element> | null> => {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
const formatter = new SnapshotFormatter({root: node} as TextSnapshot);
|
||||
const formatted = formatter.toString();
|
||||
assert.strictEqual(
|
||||
formatted,
|
||||
`uid=1_1 ignored
|
||||
uid=1_2 statictext "text"
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
it('toJSON returns expected structure', () => {
|
||||
const node: TextSnapshotNode = {
|
||||
id: '1_1',
|
||||
|
||||
Reference in New Issue
Block a user