diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index e7d7906f..198f1e8a 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -9,6 +9,7 @@ on: jobs: pre-release: + name: 'Verify MCP server schema unchanged' runs-on: ubuntu-latest steps: - name: Check out repository diff --git a/docs/tool-reference.md b/docs/tool-reference.md index be12f101..216a37fa 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -258,7 +258,7 @@ **Parameters:** -- **url** (string) **(required)**: The URL of the request. +- **reqid** (number) **(required)**: The reqid of a request on the page from the listed network requests --- diff --git a/src/McpContext.ts b/src/McpContext.ts index 4cf6b661..99b82696 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -155,19 +155,8 @@ export class McpContext implements Context { await page.close({runBeforeUnload: false}); } - getNetworkRequestByUrl(url: string): HTTPRequest { - const requests = this.getNetworkRequests(); - if (!requests.length) { - throw new Error('No requests found for selected page'); - } - - for (const request of requests) { - if (request.url() === url) { - return request; - } - } - - throw new Error('Request not found for selected page'); + getNetworkRequestById(reqid: number): HTTPRequest { + return this.#networkCollector.getById(this.getSelectedPage(), reqid); } setNetworkConditions(conditions: string | null): void { @@ -426,4 +415,8 @@ export class McpContext implements Context { ); return waitForHelper.waitForEventsAfterAction(action); } + + getNetworkRequestStableId(request: HTTPRequest): number { + return this.#networkCollector.getIdForResource(request); + } } diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 4ea9f796..366cca1b 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -24,7 +24,7 @@ import type {ImageContentData, Response} from './tools/ToolDefinition.js'; import {paginate, type PaginationOptions} from './utils/pagination.js'; interface NetworkRequestData { - networkRequestUrl: string; + networkRequestStableId: number; requestBody?: string; responseBody?: string; } @@ -33,15 +33,18 @@ export class McpResponse implements Response { #includePages = false; #includeSnapshot = false; #attachedNetworkRequestData?: NetworkRequestData; - #includeConsoleData = false; #textResponseLines: string[] = []; - #formattedConsoleData?: string[]; #images: ImageContentData[] = []; #networkRequestsOptions?: { include: boolean; pagination?: PaginationOptions; resourceTypes?: ResourceType[]; }; + #consoleDataOptions?: { + include: boolean; + pagination?: PaginationOptions; + types?: string[]; + }; setIncludePages(value: boolean): void { this.#includePages = value; @@ -77,13 +80,35 @@ export class McpResponse implements Response { }; } - setIncludeConsoleData(value: boolean): void { - this.#includeConsoleData = value; + setIncludeConsoleData( + value: boolean, + options?: { + pageSize?: number; + pageIdx?: number; + types?: string[]; + }, + ): void { + if (!value) { + this.#consoleDataOptions = undefined; + return; + } + + this.#consoleDataOptions = { + include: value, + pagination: + options?.pageSize || options?.pageIdx + ? { + pageSize: options.pageSize, + pageIdx: options.pageIdx, + } + : undefined, + types: options?.types, + }; } - attachNetworkRequest(url: string): void { + attachNetworkRequest(reqid: number): void { this.#attachedNetworkRequestData = { - networkRequestUrl: url, + networkRequestStableId: reqid, }; } @@ -96,14 +121,20 @@ export class McpResponse implements Response { } get includeConsoleData(): boolean { - return this.#includeConsoleData; + return this.#consoleDataOptions?.include ?? false; } - get attachedNetworkRequestUrl(): string | undefined { - return this.#attachedNetworkRequestData?.networkRequestUrl; + get attachedNetworkRequestId(): number | undefined { + return this.#attachedNetworkRequestData?.networkRequestStableId; } get networkRequestsPageIdx(): number | undefined { return this.#networkRequestsOptions?.pagination?.pageIdx; } + get consoleMessagesPageIdx(): number | undefined { + return this.#consoleDataOptions?.pagination?.pageIdx; + } + get consoleMessagesTypes(): string[] | undefined { + return this.#consoleDataOptions?.types; + } appendResponseLine(value: string): void { this.#textResponseLines.push(value); @@ -136,11 +167,9 @@ export class McpResponse implements Response { await context.createTextSnapshot(); } - let formattedConsoleMessages: string[]; - - if (this.#attachedNetworkRequestData?.networkRequestUrl) { - const request = context.getNetworkRequestByUrl( - this.#attachedNetworkRequestData.networkRequestUrl, + if (this.#attachedNetworkRequestData?.networkRequestStableId) { + const request = context.getNetworkRequestById( + this.#attachedNetworkRequestData.networkRequestStableId, ); this.#attachedNetworkRequestData.requestBody = @@ -153,23 +182,13 @@ export class McpResponse implements Response { } } - if (this.#includeConsoleData) { - const consoleMessages = context.getConsoleData(); - if (consoleMessages) { - formattedConsoleMessages = await Promise.all( - consoleMessages.map(message => formatConsoleEvent(message)), - ); - this.#formattedConsoleData = formattedConsoleMessages; - } - } - - return this.format(toolName, context); + return await this.format(toolName, context); } - format( + async format( toolName: string, context: McpContext, - ): Array { + ): Promise> { const response = [`# ${toolName} response`]; for (const line of this.#textResponseLines) { response.push(line); @@ -246,17 +265,44 @@ Call ${handleDialog.name} to handle it before continuing.`); ); response.push(...data.info); for (const request of data.items) { - response.push(getShortDescriptionForRequest(request)); + response.push( + getShortDescriptionForRequest( + request, + context.getNetworkRequestStableId(request), + ), + ); } } else { response.push('No requests found.'); } } - if (this.#includeConsoleData && this.#formattedConsoleData) { + if (this.#consoleDataOptions?.include) { + let messages = context.getConsoleData(); + + if (this.#consoleDataOptions.types?.length) { + const normalizedTypes = new Set(this.#consoleDataOptions.types); + messages = messages.filter(message => { + if (!('type' in message)) { + return normalizedTypes.has('error'); + } + const type = message.type(); + return normalizedTypes.has(type); + }); + } + response.push('## Console messages'); - if (this.#formattedConsoleData.length) { - response.push(...this.#formattedConsoleData); + if (messages.length) { + const data = this.#dataWithPagination( + messages, + this.#consoleDataOptions.pagination, + ); + response.push(...data.info); + response.push( + ...(await Promise.all( + data.items.map(message => formatConsoleEvent(message)), + )), + ); } else { response.push(''); } @@ -304,12 +350,12 @@ Call ${handleDialog.name} to handle it before continuing.`); #getIncludeNetworkRequestsData(context: McpContext): string[] { const response: string[] = []; - const url = this.#attachedNetworkRequestData?.networkRequestUrl; + const url = this.#attachedNetworkRequestData?.networkRequestStableId; if (!url) { return response; } - const httpRequest = context.getNetworkRequestByUrl(url); + const httpRequest = context.getNetworkRequestById(url); response.push(`## Request ${httpRequest.url()}`); response.push(`Status: ${getStatusFromRequest(httpRequest)}`); response.push(`### Request Headers`); @@ -347,7 +393,7 @@ Call ${handleDialog.name} to handle it before continuing.`); let indent = 0; for (const request of redirectChain.reverse()) { response.push( - `${' '.repeat(indent)}${getShortDescriptionForRequest(request)}`, + `${' '.repeat(indent)}${getShortDescriptionForRequest(request, context.getNetworkRequestStableId(request))}`, ); indent++; } diff --git a/src/PageCollector.ts b/src/PageCollector.ts index acbf3239..69615c50 100644 --- a/src/PageCollector.ts +++ b/src/PageCollector.ts @@ -17,6 +17,21 @@ export type ListenerMap = { [K in keyof EventMap]?: (event: EventMap[K]) => void; }; +function createIdGenerator() { + let i = 1; + return () => { + if (i === Number.MAX_SAFE_INTEGER) { + i = 0; + } + return i++; + }; +} + +export const stableIdSymbol = Symbol('stableIdSymbol'); +type WithSymbolId = T & { + [stableIdSymbol]?: number; +}; + export class PageCollector { #browser: Browser; #listenersInitializer: ( @@ -28,7 +43,7 @@ export class PageCollector { * As we use the reference to it. * Use methods that manipulate the array in place. */ - protected storage = new WeakMap(); + protected storage = new WeakMap>>(); constructor( browser: Browser, @@ -56,7 +71,6 @@ export class PageCollector { if (!page) { return; } - console.log('destro'); this.#cleanupPageDestroyed(page); }); } @@ -70,10 +84,14 @@ export class PageCollector { return; } - const stored: T[] = []; + const idGenerator = createIdGenerator(); + const stored: Array> = []; this.storage.set(page, stored); + const listeners = this.#listenersInitializer(value => { - stored.push(value); + const withId = value as WithSymbolId; + withId[stableIdSymbol] = idGenerator(); + stored.push(withId); }); listeners['framenavigated'] = (frame: Frame) => { // Only reset the storage on main frame navigation @@ -111,6 +129,25 @@ export class PageCollector { getData(page: Page): T[] { return this.storage.get(page) ?? []; } + + getIdForResource(resource: WithSymbolId): number { + return resource[stableIdSymbol] ?? -1; + } + + getById(page: Page, stableId: number): T { + const data = this.storage.get(page); + if (!data || !data.length) { + throw new Error('No requests found for selected page'); + } + + for (const collected of data) { + if (collected[stableIdSymbol] === stableId) { + return collected; + } + } + + throw new Error('Request not found for selected page'); + } } export class NetworkCollector extends PageCollector { diff --git a/src/formatters/consoleFormatter.ts b/src/formatters/consoleFormatter.ts index b6627496..6b74b0b5 100644 --- a/src/formatters/consoleFormatter.ts +++ b/src/formatters/consoleFormatter.ts @@ -4,11 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { - ConsoleMessage, - JSHandle, - ConsoleMessageLocation, -} from 'puppeteer-core'; +import type {ConsoleMessage, JSHandle} from 'puppeteer-core'; const logLevels: Record = { log: 'Log', @@ -31,66 +27,37 @@ export async function formatConsoleEvent( async function formatConsoleMessage(msg: ConsoleMessage): Promise { const logLevel = logLevels[msg.type()]; + const text = msg.text(); const args = msg.args(); - if (logLevel === 'Error') { - let message = `${logLevel}> `; - if (msg.text() === 'JSHandle@error') { - const errorHandle = args[0] as JSHandle; - message += await errorHandle - .evaluate(error => { - return error.toString(); - }) - .catch(() => { - return 'Error occurred'; - }); - void errorHandle.dispose().catch(); + const formattedArgs = await formatArgs(args, text); + return `${logLevel}> ${text} ${formattedArgs}`.trim(); +} - const formattedArgs = await formatArgs(args.slice(1)); - if (formattedArgs) { - message += ` ${formattedArgs}`; - } - } else { - message += msg.text(); - const formattedArgs = await formatArgs(args); - if (formattedArgs) { - message += ` ${formattedArgs}`; - } - for (const frame of msg.stackTrace()) { - message += '\n' + formatStackFrame(frame); - } - } - return message; +// Only includes the first arg and indicates that there are more args +async function formatArgs( + args: readonly JSHandle[], + messageText: string, +): Promise { + if (args.length === 0) { + return ''; } - const formattedArgs = await formatArgs(args); - const text = msg.text(); - - return `${logLevel}> ${formatStackFrame( - msg.location(), - )}: ${text} ${formattedArgs}`.trim(); -} - -async function formatArgs(args: readonly JSHandle[]): Promise { - const argValues = await Promise.all( - args.map(arg => - arg.jsonValue().catch(() => { - // Ignore errors - }), - ), - ); + let formattedArgs = ''; + const firstArg = await args[0].jsonValue().catch(() => { + // Ignore errors + }); - return argValues - .map(value => { - return typeof value === 'object' ? JSON.stringify(value) : String(value); - }) - .join(' '); -} + if (firstArg !== messageText) { + formattedArgs += + typeof firstArg === 'object' + ? JSON.stringify(firstArg) + : String(firstArg); + } -function formatStackFrame(stackFrame: ConsoleMessageLocation): string { - if (!stackFrame?.url) { - return ''; + if (args.length > 1) { + return `${formattedArgs} ...`; } - const filename = stackFrame.url.replace(/^.*\//, ''); - return `${filename}:${stackFrame.lineNumber}:${stackFrame.columnNumber}`; + + return formattedArgs; } diff --git a/src/formatters/networkFormatter.ts b/src/formatters/networkFormatter.ts index 7796f01a..57279733 100644 --- a/src/formatters/networkFormatter.ts +++ b/src/formatters/networkFormatter.ts @@ -10,8 +10,12 @@ import type {HTTPRequest, HTTPResponse} from 'puppeteer-core'; const BODY_CONTEXT_SIZE_LIMIT = 10000; -export function getShortDescriptionForRequest(request: HTTPRequest): string { - return `${request.url()} ${request.method()} ${getStatusFromRequest(request)}`; +export function getShortDescriptionForRequest( + request: HTTPRequest, + id: number, +): string { + // TODO truncate the URL + return `reqid=${id} ${request.method()} ${request.url()} ${getStatusFromRequest(request)}`; } export function getStatusFromRequest(request: HTTPRequest): string { diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 56fdb53a..7e902255 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -47,10 +47,13 @@ export interface Response { value: boolean, options?: {pageSize?: number; pageIdx?: number; resourceTypes?: string[]}, ): void; - setIncludeConsoleData(value: boolean): void; + setIncludeConsoleData( + value: boolean, + options?: {pageSize?: number; pageIdx?: number; types?: string[]}, + ): void; setIncludeSnapshot(value: boolean): void; attachImage(value: ImageContentData): void; - attachNetworkRequest(url: string): void; + attachNetworkRequest(reqid: number): void; } /** diff --git a/src/tools/console.ts b/src/tools/console.ts index 4fb752f2..47a1ed17 100644 --- a/src/tools/console.ts +++ b/src/tools/console.ts @@ -4,19 +4,74 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type {ConsoleMessageType} from 'puppeteer-core'; +import z from 'zod'; + import {ToolCategories} from './categories.js'; import {defineTool} from './ToolDefinition.js'; +const FILTERABLE_MESSAGE_TYPES: readonly [ + ConsoleMessageType, + ...ConsoleMessageType[], +] = [ + 'log', + 'debug', + 'info', + 'error', + 'warn', + 'dir', + 'dirxml', + 'table', + 'trace', + 'clear', + 'startGroup', + 'startGroupCollapsed', + 'endGroup', + 'assert', + 'profile', + 'profileEnd', + 'count', + 'timeEnd', + 'verbose', +]; + export const consoleTool = defineTool({ name: 'list_console_messages', description: 'List all console messages for the currently selected page since the last navigation.', annotations: { - category: ToolCategories.DEBUGGING, + category: ToolCategories.NETWORK, readOnlyHint: true, }, - schema: {}, - handler: async (_request, response) => { - response.setIncludeConsoleData(true); + schema: { + pageSize: z + .number() + .int() + .positive() + .optional() + .describe( + 'Maximum number of messages to return. When omitted, returns all requests.', + ), + pageIdx: z + .number() + .int() + .min(0) + .optional() + .describe( + 'Page number to return (0-based). When omitted, returns the first page.', + ), + types: z + .array(z.enum(FILTERABLE_MESSAGE_TYPES)) + .optional() + .describe( + 'Filter messages to only return messages of the specified resource types. When omitted or empty, returns all messages.', + ), + }, + handler: async (request, response) => { + response.setIncludeConsoleData(true, { + pageSize: request.params.pageSize, + pageIdx: request.params.pageIdx, + types: request.params.types, + }); }, }); diff --git a/src/tools/network.ts b/src/tools/network.ts index 1fc82a5a..23b2c1f6 100644 --- a/src/tools/network.ts +++ b/src/tools/network.ts @@ -80,9 +80,13 @@ export const getNetworkRequest = defineTool({ readOnlyHint: true, }, schema: { - url: z.string().describe('The URL of the request.'), + reqid: z + .number() + .describe( + 'The reqid of a request on the page from the listed network requests', + ), }, handler: async (request, response, _context) => { - response.attachNetworkRequest(request.params.url); + response.attachNetworkRequest(request.params.reqid); }, }); diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index 11416447..17cd4ae4 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -202,15 +202,16 @@ Call handle_dialog to handle it before continuing.`, await withBrowser(async (response, context) => { response.setIncludeNetworkRequests(true); context.getNetworkRequests = () => { - return [getMockRequest()]; + return [getMockRequest({stableId: 1}), getMockRequest({stableId: 2})]; }; const result = await response.handle('test', context); assert.strictEqual( result[0].text, `# test response ## Network requests -Showing 1-1 of 1 (Page 1 of 1). -http://example.com GET [pending]`, +Showing 1-2 of 2 (Page 1 of 1). +reqid=1 GET http://example.com [pending] +reqid=2 GET http://example.com [pending]`, ); }); }); @@ -247,7 +248,10 @@ http://example.com GET [pending]`, context.getNetworkRequests = () => { return [request]; }; - response.attachNetworkRequest(request.url()); + context.getNetworkRequestById = () => { + return request; + }; + response.attachNetworkRequest(1); const result = await response.handle('test', context); @@ -266,7 +270,7 @@ ${JSON.stringify({request: 'body'})} ${JSON.stringify({response: 'body'})} ## Network requests Showing 1-1 of 1 (Page 1 of 1). -http://example.com POST [success - 200]`, +reqid=1 POST http://example.com [success - 200]`, ); }); }); @@ -278,7 +282,10 @@ http://example.com POST [success - 200]`, context.getNetworkRequests = () => { return [request]; }; - response.attachNetworkRequest(request.url()); + context.getNetworkRequestById = () => { + return request; + }; + response.attachNetworkRequest(1); const result = await response.handle('test', context); assert.strictEqual( result[0].text, @@ -289,7 +296,7 @@ Status: [pending] - content-size:10 ## Network requests Showing 1-1 of 1 (Page 1 of 1). -http://example.com GET [pending]`, +reqid=1 GET http://example.com [pending]`, ); }); }); @@ -312,8 +319,7 @@ http://example.com GET [pending]`, // Cannot check the full text because it contains local file path assert.ok( result[0].text.toString().startsWith(`# test response -## Console messages -Log>`), +## Console messages`), ); assert.ok(result[0].text.toString().includes('Hello from the test')); }); @@ -354,8 +360,8 @@ describe('McpResponse network request filtering', () => { `# test response ## Network requests Showing 1-2 of 2 (Page 1 of 1). -http://example.com GET [pending] -http://example.com GET [pending]`, +reqid=1 GET http://example.com [pending] +reqid=1 GET http://example.com [pending]`, ); }); }); @@ -378,7 +384,7 @@ http://example.com GET [pending]`, `# test response ## Network requests Showing 1-1 of 1 (Page 1 of 1). -http://example.com GET [pending]`, +reqid=1 GET http://example.com [pending]`, ); }); }); @@ -423,11 +429,11 @@ No requests found.`, `# test response ## Network requests Showing 1-5 of 5 (Page 1 of 1). -http://example.com GET [pending] -http://example.com GET [pending] -http://example.com GET [pending] -http://example.com GET [pending] -http://example.com GET [pending]`, +reqid=1 GET http://example.com [pending] +reqid=1 GET http://example.com [pending] +reqid=1 GET http://example.com [pending] +reqid=1 GET http://example.com [pending] +reqid=1 GET http://example.com [pending]`, ); }); }); @@ -452,11 +458,11 @@ http://example.com GET [pending]`, `# test response ## Network requests Showing 1-5 of 5 (Page 1 of 1). -http://example.com GET [pending] -http://example.com GET [pending] -http://example.com GET [pending] -http://example.com GET [pending] -http://example.com GET [pending]`, +reqid=1 GET http://example.com [pending] +reqid=1 GET http://example.com [pending] +reqid=1 GET http://example.com [pending] +reqid=1 GET http://example.com [pending] +reqid=1 GET http://example.com [pending]`, ); }); }); diff --git a/tests/PageCollector.test.ts b/tests/PageCollector.test.ts index 43c82380..431687aa 100644 --- a/tests/PageCollector.test.ts +++ b/tests/PageCollector.test.ts @@ -6,7 +6,7 @@ import assert from 'node:assert'; import {describe, it} from 'node:test'; -import type {Browser, Frame, Page, Target} from 'puppeteer-core'; +import type {Browser, Frame, HTTPRequest, Page, Target} from 'puppeteer-core'; import type {ListenerMap} from '../src/PageCollector.js'; import {PageCollector} from '../src/PageCollector.js'; @@ -196,4 +196,27 @@ describe('PageCollector', () => { assert.equal(collector.getData(page).length, 0); }); + + it('should assign ids to requests', async () => { + const browser = getMockBrowser(); + const page = (await browser.pages())[0]; + const request1 = getMockRequest(); + const request2 = getMockRequest(); + const collector = new PageCollector(browser, collect => { + return { + request: req => { + collect(req); + }, + } as ListenerMap; + }); + await collector.init(); + + page.emit('request', request1); + page.emit('request', request2); + + assert.equal(collector.getData(page).length, 2); + + assert.equal(collector.getIdForResource(request1), 1); + assert.equal(collector.getIdForResource(request2), 2); + }); }); diff --git a/tests/formatters/consoleFormatter.test.ts b/tests/formatters/consoleFormatter.test.ts index 4fd6213c..7f34d1c3 100644 --- a/tests/formatters/consoleFormatter.test.ts +++ b/tests/formatters/consoleFormatter.test.ts @@ -65,17 +65,22 @@ describe('consoleFormatter', () => { const message = getMockConsoleMessage({ type: 'log', text: 'Hello, world!', - location: { - url: 'http://example.com/script.js', - lineNumber: 10, - columnNumber: 5, - }, }); const result = await formatConsoleEvent(message); - assert.equal(result, 'Log> script.js:10:5: Hello, world!'); + assert.equal(result, 'Log> Hello, world!'); }); - it('formats a console.log message with arguments', async () => { + it('formats a console.log message with one argument', async () => { + const message = getMockConsoleMessage({ + type: 'log', + text: 'Processing file:', + args: ['file.txt'], + }); + const result = await formatConsoleEvent(message); + assert.equal(result, 'Log> Processing file: file.txt'); + }); + + it('formats a console.log message with multiple arguments', async () => { const message = getMockConsoleMessage({ type: 'log', text: 'Processing file:', @@ -87,10 +92,7 @@ describe('consoleFormatter', () => { }, }); const result = await formatConsoleEvent(message); - assert.equal( - result, - 'Log> script.js:10:5: Processing file: file.txt {"id":1,"status":"done"}', - ); + assert.equal(result, 'Log> Processing file: file.txt ...'); }); it('formats a console.error message', async () => { @@ -102,76 +104,42 @@ describe('consoleFormatter', () => { assert.equal(result, 'Error> Something went wrong'); }); - it('formats a console.error message with arguments', async () => { + it('formats a console.error message with one argument', async () => { const message = getMockConsoleMessage({ type: 'error', text: 'Something went wrong:', args: ['details', {code: 500}], }); const result = await formatConsoleEvent(message); - assert.equal(result, 'Error> Something went wrong: details {"code":500}'); + assert.equal(result, 'Error> Something went wrong: details'); }); - it('formats a console.error message with a stack trace', async () => { + it('formats a console.error message with multiple arguments', async () => { const message = getMockConsoleMessage({ type: 'error', - text: 'Something went wrong', - stackTrace: [ - { - url: 'http://example.com/script.js', - lineNumber: 10, - columnNumber: 5, - }, - { - url: 'http://example.com/script2.js', - lineNumber: 20, - columnNumber: 10, - }, - ], - }); - const result = await formatConsoleEvent(message); - assert.equal( - result, - 'Error> Something went wrong\nscript.js:10:5\nscript2.js:20:10', - ); - }); - - it('formats a console.error message with a JSHandle@error', async () => { - const message = getMockConsoleMessage({ - type: 'error', - text: 'JSHandle@error', - args: [new Error('mock stack')], + text: 'Something went wrong:', + args: ['details', {code: 500}], }); const result = await formatConsoleEvent(message); - assert.ok(result.startsWith('Error> Error: mock stack')); + assert.equal(result, 'Error> Something went wrong: details ...'); }); it('formats a console.warn message', async () => { const message = getMockConsoleMessage({ type: 'warning', text: 'This is a warning', - location: { - url: 'http://example.com/script.js', - lineNumber: 10, - columnNumber: 5, - }, }); const result = await formatConsoleEvent(message); - assert.equal(result, 'Warning> script.js:10:5: This is a warning'); + assert.equal(result, 'Warning> This is a warning'); }); it('formats a console.info message', async () => { const message = getMockConsoleMessage({ type: 'info', text: 'This is an info message', - location: { - url: 'http://example.com/script.js', - lineNumber: 10, - columnNumber: 5, - }, }); const result = await formatConsoleEvent(message); - assert.equal(result, 'Info> script.js:10:5: This is an info message'); + assert.equal(result, 'Info> This is an info message'); }); it('formats a page error', async () => { @@ -187,28 +155,5 @@ describe('consoleFormatter', () => { const result = await formatConsoleEvent(error); assert.equal(result, 'Error: Page crashed'); }); - - it('formats a console.log message from a removed iframe - no location', async () => { - const message = getMockConsoleMessage({ - type: 'log', - text: 'Hello from iframe', - location: {}, - }); - const result = await formatConsoleEvent(message); - assert.equal(result, 'Log> : Hello from iframe'); - }); - - it('formats a console.log message from a removed iframe with partial location', async () => { - const message = getMockConsoleMessage({ - type: 'log', - text: 'Hello from iframe', - location: { - lineNumber: 10, - columnNumber: 5, - }, - }); - const result = await formatConsoleEvent(message); - assert.equal(result, 'Log> : Hello from iframe'); - }); }); }); diff --git a/tests/formatters/networkFormatter.test.ts b/tests/formatters/networkFormatter.test.ts index 23c8a323..e9608f50 100644 --- a/tests/formatters/networkFormatter.test.ts +++ b/tests/formatters/networkFormatter.test.ts @@ -21,40 +21,40 @@ describe('networkFormatter', () => { describe('getShortDescriptionForRequest', () => { it('works', async () => { const request = getMockRequest(); - const result = getShortDescriptionForRequest(request); + const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'http://example.com GET [pending]'); + assert.equal(result, 'reqid=1 GET http://example.com [pending]'); }); it('shows correct method', async () => { const request = getMockRequest({method: 'POST'}); - const result = getShortDescriptionForRequest(request); + const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'http://example.com POST [pending]'); + assert.equal(result, 'reqid=1 POST http://example.com [pending]'); }); it('shows correct status for request with response code in 200', async () => { const response = getMockResponse(); const request = getMockRequest({response}); - const result = getShortDescriptionForRequest(request); + const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'http://example.com GET [success - 200]'); + assert.equal(result, 'reqid=1 GET http://example.com [success - 200]'); }); it('shows correct status for request with response code in 100', async () => { const response = getMockResponse({ status: 199, }); const request = getMockRequest({response}); - const result = getShortDescriptionForRequest(request); + const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'http://example.com GET [failed - 199]'); + assert.equal(result, 'reqid=1 GET http://example.com [failed - 199]'); }); it('shows correct status for request with response code above 200', async () => { const response = getMockResponse({ status: 300, }); const request = getMockRequest({response}); - const result = getShortDescriptionForRequest(request); + const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'http://example.com GET [failed - 300]'); + assert.equal(result, 'reqid=1 GET http://example.com [failed - 300]'); }); it('shows correct status for request that failed', async () => { const request = getMockRequest({ @@ -64,11 +64,11 @@ describe('networkFormatter', () => { }; }, }); - const result = getShortDescriptionForRequest(request); + const result = getShortDescriptionForRequest(request, 1); assert.equal( result, - 'http://example.com GET [failed - Error in Network]', + 'reqid=1 GET http://example.com [failed - Error in Network]', ); }); }); diff --git a/tests/tools/console.test.ts b/tests/tools/console.test.ts index b25ef15b..170b7959 100644 --- a/tests/tools/console.test.ts +++ b/tests/tools/console.test.ts @@ -17,5 +17,18 @@ describe('console', () => { assert.ok(response.includeConsoleData); }); }); + + it('lists error messages', async () => { + await withBrowser(async (response, context) => { + const page = await context.newPage(); + await page.setContent( + '', + ); + await consoleTool.handler({params: {}}, response, context); + const formattedResponse = await response.format('test', context); + const textContent = formattedResponse[0] as {text: string}; + assert.ok(textContent.text.includes('This is an error')); + }); + }); }); }); diff --git a/tests/tools/network.test.ts b/tests/tools/network.test.ts index c53cbc1d..fa6439dd 100644 --- a/tests/tools/network.test.ts +++ b/tests/tools/network.test.ts @@ -28,14 +28,11 @@ describe('network', () => { const page = await context.getSelectedPage(); await page.goto('data:text/html,
Hello MCP
'); await getNetworkRequest.handler( - {params: {url: 'data:text/html,
Hello MCP
'}}, + {params: {reqid: 1}}, response, context, ); - assert.equal( - response.attachedNetworkRequestUrl, - 'data:text/html,
Hello MCP
', - ); + assert.equal(response.attachedNetworkRequestId, 1); }); }); it('should not add the request list', async () => { @@ -43,7 +40,7 @@ describe('network', () => { const page = await context.getSelectedPage(); await page.goto('data:text/html,
Hello MCP
'); await getNetworkRequest.handler( - {params: {url: 'data:text/html,
Hello MCP
'}}, + {params: {reqid: 1}}, response, context, ); diff --git a/tests/utils.ts b/tests/utils.ts index 0197e181..48cd6b77 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -10,6 +10,7 @@ import type {HTTPRequest, HTTPResponse} from 'puppeteer-core'; import {McpContext} from '../src/McpContext.js'; import {McpResponse} from '../src/McpResponse.js'; +import {stableIdSymbol} from '../src/PageCollector.js'; let browser: Browser | undefined; @@ -49,6 +50,7 @@ export function getMockRequest( hasPostData?: boolean; postData?: string; fetchPostData?: Promise; + stableId?: number; } = {}, ): HTTPRequest { return { @@ -84,7 +86,8 @@ export function getMockRequest( redirectChain(): HTTPRequest[] { return []; }, - } as HTTPRequest; + [stableIdSymbol]: options.stableId ?? 1, + } as unknown as HTTPRequest; } export function getMockResponse(