diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 7ed2e7577..43a26b68a 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -307,7 +307,6 @@ export class McpResponse implements Response { if (this.#listExtensions) { extensions = context.listExtensions(); } - let consoleMessages: Array | undefined; if (this.#consoleDataOptions?.include) { let messages = context.getConsoleData( @@ -378,13 +377,8 @@ export class McpResponse implements Response { } if (requests.length) { - const data = this.#dataWithPagination( - requests, - this.#networkRequestsOptions.pagination, - ); - networkRequests = await Promise.all( - data.items.map(request => + requests.map(request => NetworkFormatter.from(request, { requestId: context.getNetworkRequestStableId(request), selectedInDevToolsUI: @@ -424,9 +418,36 @@ export class McpResponse implements Response { extensions?: InstalledExtension[]; }, ): {content: Array; structuredContent: object} { + const structuredContent: { + snapshot?: object; + snapshotFilePath?: string; + tabId?: string; + networkRequest?: object; + networkRequests?: object[]; + consoleMessage?: object; + consoleMessages?: object[]; + traceSummary?: string; + traceInsights?: Array<{insightName: string; insightKey: string}>; + extensions?: object[]; + message?: string; + networkConditions?: string; + navigationTimeout?: number; + viewport?: object; + userAgent?: string; + cpuThrottlingRate?: number; + dialog?: { + type: string; + message: string; + defaultValue?: string; + }; + pages?: object[]; + pagination?: object; + } = {}; + const response = [`# ${toolName} response`]; - for (const line of this.#textResponseLines) { - response.push(line); + if (this.#textResponseLines.length) { + structuredContent.message = this.#textResponseLines.join('\n'); + response.push(...this.#textResponseLines); } const networkConditions = context.getNetworkConditions(); @@ -436,24 +457,29 @@ export class McpResponse implements Response { response.push( `Default navigation timeout set to ${context.getNavigationTimeout()} ms`, ); + structuredContent.networkConditions = networkConditions; + structuredContent.navigationTimeout = context.getNavigationTimeout(); } const viewport = context.getViewport(); if (viewport) { response.push(`## Viewport emulation`); response.push(`Emulating viewport: ${JSON.stringify(viewport)}`); + structuredContent.viewport = viewport; } const userAgent = context.getUserAgent(); if (userAgent) { response.push(`## UserAgent emulation`); response.push(`Emulating userAgent: ${userAgent}`); + structuredContent.userAgent = userAgent; } const cpuThrottlingRate = context.getCpuThrottlingRate(); if (cpuThrottlingRate > 1) { response.push(`## CPU emulation`); response.push(`Emulating: ${cpuThrottlingRate}x slowdown`); + structuredContent.cpuThrottlingRate = cpuThrottlingRate; } const dialog = context.getDialog(); @@ -465,6 +491,11 @@ export class McpResponse implements Response { response.push(`# Open dialog ${dialog.type()}: ${dialog.message()}${defaultValueIfNeeded}. Call ${handleDialog.name} to handle it before continuing.`); + structuredContent.dialog = { + type: dialog.type(), + message: dialog.message(), + defaultValue: dialog.defaultValue(), + }; } if (this.#includePages) { @@ -475,21 +506,15 @@ Call ${handleDialog.name} to handle it before continuing.`); ); } response.push(...parts); + structuredContent.pages = context.getPages().map(page => { + return { + id: context.getPageId(page), + url: page.url(), + selected: context.isPageSelected(page), + }; + }); } - const structuredContent: { - snapshot?: object; - snapshotFilePath?: string; - tabId?: string; - networkRequest?: object; - networkRequests?: object[]; - consoleMessage?: object; - consoleMessages?: object[]; - traceSummary?: string; - traceInsights?: Array<{insightName: string; insightKey: string}>; - extensions?: object[]; - } = {}; - if (this.#tabId) { structuredContent.tabId = this.#tabId; } @@ -582,6 +607,7 @@ Call ${handleDialog.name} to handle it before continuing.`); requests, this.#networkRequestsOptions.pagination, ); + structuredContent.pagination = paginationData.pagination; response.push(...paginationData.info); if (data.networkRequests) { structuredContent.networkRequests = []; @@ -600,13 +626,16 @@ Call ${handleDialog.name} to handle it before continuing.`); response.push('## Console messages'); if (messages.length) { - const data = this.#dataWithPagination( + const paginationData = this.#dataWithPagination( messages, this.#consoleDataOptions.pagination, ); - response.push(...data.info); - response.push(...data.items.map(message => message.toString())); - structuredContent.consoleMessages = data.items.map(message => + structuredContent.pagination = paginationData.pagination; + response.push(...paginationData.info); + response.push( + ...paginationData.items.map(message => message.toString()), + ); + structuredContent.consoleMessages = paginationData.items.map(message => message.toJSON(), ); } else { @@ -654,6 +683,15 @@ Call ${handleDialog.name} to handle it before continuing.`); return { info: response, items: paginationResult.items, + pagination: { + currentPage: paginationResult.currentPage, + totalPages: paginationResult.totalPages, + hasNextPage: paginationResult.hasNextPage, + hasPreviousPage: paginationResult.hasPreviousPage, + startIndex: paginationResult.startIndex, + endIndex: paginationResult.endIndex, + invalidPage: paginationResult.invalidPage, + }, }; } diff --git a/tests/McpContext.test.js.snapshot b/tests/McpContext.test.js.snapshot index 7c8928def..b688da713 100644 --- a/tests/McpContext.test.js.snapshot +++ b/tests/McpContext.test.js.snapshot @@ -23,6 +23,15 @@ exports[`McpContext > should include file paths in structured content when savin exports[`McpContext > should include network requests in structured content 1`] = ` { + "pagination": { + "currentPage": 0, + "totalPages": 1, + "hasNextPage": false, + "hasPreviousPage": false, + "startIndex": 0, + "endIndex": 1, + "invalidPage": false + }, "networkRequests": [ { "requestId": 123, diff --git a/tests/McpResponse.test.js.snapshot b/tests/McpResponse.test.js.snapshot index 6e12fff21..1181d9744 100644 --- a/tests/McpResponse.test.js.snapshot +++ b/tests/McpResponse.test.js.snapshot @@ -9,6 +9,38 @@ Showing 1-1 of 1 (Page 1 of 1). reqid=1 GET http://example.com [pending] `; +exports[`McpResponse > add network request when attached 2`] = ` +{ + "networkRequest": { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "requestHeaders": { + "content-size": "10" + } + }, + "pagination": { + "currentPage": 0, + "totalPages": 1, + "hasNextPage": false, + "hasPreviousPage": false, + "startIndex": 0, + "endIndex": 1, + "invalidPage": false + }, + "networkRequests": [ + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + } + ] +} +`; + exports[`McpResponse > add network request when attached with POST data 1`] = ` # test response ## Request http://example.com @@ -26,6 +58,43 @@ Showing 1-1 of 1 (Page 1 of 1). reqid=1 POST http://example.com [success - 200] `; +exports[`McpResponse > add network request when attached with POST data 2`] = ` +{ + "networkRequest": { + "requestId": 1, + "method": "POST", + "url": "http://example.com", + "status": "[success - 200]", + "requestHeaders": { + "content-size": "10" + }, + "requestBody": "{\\"request\\":\\"body\\"}", + "responseHeaders": { + "Content-Type": "application/json" + }, + "responseBody": "{\\"response\\":\\"body\\"}" + }, + "pagination": { + "currentPage": 0, + "totalPages": 1, + "hasNextPage": false, + "hasPreviousPage": false, + "startIndex": 0, + "endIndex": 1, + "invalidPage": false + }, + "networkRequests": [ + { + "requestId": 1, + "method": "POST", + "url": "http://example.com", + "status": "[success - 200]", + "selectedInDevToolsUI": false + } + ] +} +`; + exports[`McpResponse > add network requests when setting is true 1`] = ` # test response ## Network requests @@ -34,12 +103,46 @@ reqid=1 GET http://example.com [pending] reqid=2 GET http://example.com [pending] `; +exports[`McpResponse > add network requests when setting is true 2`] = ` +{ + "pagination": { + "currentPage": 0, + "totalPages": 1, + "hasNextPage": false, + "hasPreviousPage": false, + "startIndex": 0, + "endIndex": 2, + "invalidPage": false + }, + "networkRequests": [ + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 2, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + } + ] +} +`; + exports[`McpResponse > adds a message when no console messages exist 1`] = ` # test response ## Console messages `; +exports[`McpResponse > adds a message when no console messages exist 2`] = ` +{} +`; + exports[`McpResponse > adds a prompt dialog 1`] = ` # test response # Open dialog @@ -47,6 +150,16 @@ prompt: message (default value: "default"). Call handle_dialog to handle it before continuing. `; +exports[`McpResponse > adds a prompt dialog 2`] = ` +{ + "dialog": { + "type": "prompt", + "message": "message", + "defaultValue": "default" + } +} +`; + exports[`McpResponse > adds an alert dialog 1`] = ` # test response # Open dialog @@ -54,6 +167,16 @@ alert: message. Call handle_dialog to handle it before continuing. `; +exports[`McpResponse > adds an alert dialog 2`] = ` +{ + "dialog": { + "type": "alert", + "message": "message", + "defaultValue": "" + } +} +`; + exports[`McpResponse > adds console messages when the setting is true 1`] = ` # test response ## Console messages @@ -61,12 +184,44 @@ Showing 1-1 of 1 (Page 1 of 1). msgid=1 [log] Hello from the test (1 args) `; +exports[`McpResponse > adds console messages when the setting is true 2`] = ` +{ + "pagination": { + "currentPage": 0, + "totalPages": 1, + "hasNextPage": false, + "hasPreviousPage": false, + "startIndex": 0, + "endIndex": 1, + "invalidPage": false + }, + "consoleMessages": [ + { + "type": "log", + "text": "Hello from the test", + "argsCount": 1, + "id": 1 + } + ] +} +`; + exports[`McpResponse > adds cpu throttling setting when it is over 1 1`] = ` # test response ## CPU emulation Emulating: 4x slowdown `; +exports[`McpResponse > adds cpu throttling setting when it is over 1 2`] = ` +{ + "cpuThrottlingRate": 4 +} +`; + +exports[`McpResponse > adds image when image is attached 1`] = ` +{} +`; + exports[`McpResponse > adds throttling setting when it is not null 1`] = ` # test response ## Network emulation @@ -74,30 +229,91 @@ Emulating: Slow 3G Default navigation timeout set to 100000 ms `; +exports[`McpResponse > adds throttling setting when it is not null 2`] = ` +{ + "networkConditions": "Slow 3G", + "navigationTimeout": 100000 +} +`; + exports[`McpResponse > adds userAgent emulation setting when it is set 1`] = ` # test response ## UserAgent emulation Emulating userAgent: MyUA `; +exports[`McpResponse > adds userAgent emulation setting when it is set 2`] = ` +{ + "userAgent": "MyUA" +} +`; + exports[`McpResponse > adds viewport emulation setting when it is set 1`] = ` # test response ## Viewport emulation Emulating viewport: {"width":400,"height":400,"deviceScaleFactor":1} `; +exports[`McpResponse > adds viewport emulation setting when it is set 2`] = ` +{ + "viewport": { + "width": 400, + "height": 400, + "deviceScaleFactor": 1 + } +} +`; + exports[`McpResponse > allows response text lines to be added 1`] = ` # test response Testing 1 Testing 2 `; +exports[`McpResponse > allows response text lines to be added 2`] = ` +{ + "message": "Testing 1\\nTesting 2" +} +`; + +exports[`McpResponse > does not include anything in response if snapshot is null 1`] = ` +{} +`; + +exports[`McpResponse > does not include cpu throttling setting when it is 1 1`] = ` +{} +`; + +exports[`McpResponse > does not include network requests when setting is false 1`] = ` +{} +`; + +exports[`McpResponse > does not include throttling setting when it is null 1`] = ` +{} +`; + +exports[`McpResponse > doesn't list the issue message if mapping returns null 1`] = ` +{} +`; + exports[`McpResponse > list pages 1`] = ` # test response ## Pages 1: about:blank [selected] `; +exports[`McpResponse > list pages 2`] = ` +{ + "pages": [ + { + "id": 1, + "url": "about:blank", + "selected": true + } + ] +} +`; + exports[`McpResponse > returns correctly formatted snapshot for a simple tree 1`] = ` # test response ## Latest page snapshot @@ -107,6 +323,31 @@ uid=1_0 RootWebArea "My test page" url="about:blank" `; +exports[`McpResponse > returns correctly formatted snapshot for a simple tree 2`] = ` +{ + "snapshot": { + "id": "1_0", + "role": "RootWebArea", + "name": "My test page", + "url": "about:blank", + "children": [ + { + "id": "1_1", + "role": "button", + "name": "Click me", + "focusable": true, + "focused": true + }, + { + "id": "1_2", + "role": "textbox", + "value": "Input" + } + ] + } +} +`; + exports[`McpResponse > returns values for textboxes 1`] = ` # test response ## Latest page snapshot @@ -116,6 +357,32 @@ uid=1_0 RootWebArea "My test page" url="about:blank" `; +exports[`McpResponse > returns values for textboxes 2`] = ` +{ + "snapshot": { + "id": "1_0", + "role": "RootWebArea", + "name": "My test page", + "url": "about:blank", + "children": [ + { + "id": "1_1", + "role": "StaticText", + "name": "username" + }, + { + "id": "1_2", + "role": "textbox", + "name": "username", + "focusable": true, + "focused": true, + "value": "mcp" + } + ] + } +} +`; + exports[`McpResponse > returns verbose snapshot and structured content 1`] = ` # test response ## Latest page snapshot @@ -192,11 +459,534 @@ uid=1_0 RootWebArea "My test page" url="about:blank" `; +exports[`McpResponse network pagination > handles invalid page number by showing first page 1`] = ` +{ + "pagination": { + "currentPage": 0, + "totalPages": 3, + "hasNextPage": true, + "hasPreviousPage": false, + "startIndex": 0, + "endIndex": 2, + "invalidPage": true + }, + "networkRequests": [ + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + } + ] +} +`; + +exports[`McpResponse network pagination > returns all requests when pagination is not provided 1`] = ` +{ + "pagination": { + "currentPage": 0, + "totalPages": 1, + "hasNextPage": false, + "hasPreviousPage": false, + "startIndex": 0, + "endIndex": 5, + "invalidPage": false + }, + "networkRequests": [ + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + } + ] +} +`; + +exports[`McpResponse network pagination > returns first page by default 1`] = ` +{ + "pagination": { + "currentPage": 0, + "totalPages": 3, + "hasNextPage": true, + "hasPreviousPage": false, + "startIndex": 0, + "endIndex": 10, + "invalidPage": false + }, + "networkRequests": [ + { + "requestId": 1, + "method": "GET-0", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-1", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-2", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-3", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-4", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-5", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-6", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-7", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-8", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-9", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-10", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-11", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-12", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-13", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-14", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-15", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-16", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-17", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-18", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-19", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-20", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-21", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-22", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-23", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-24", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-25", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-26", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-27", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-28", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-29", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + } + ] +} +`; + +exports[`McpResponse network pagination > returns subsequent page when pageIdx provided 1`] = ` +{ + "pagination": { + "currentPage": 1, + "totalPages": 3, + "hasNextPage": true, + "hasPreviousPage": true, + "startIndex": 10, + "endIndex": 20, + "invalidPage": false + }, + "networkRequests": [ + { + "requestId": 1, + "method": "GET-0", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-1", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-2", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-3", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-4", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-5", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-6", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-7", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-8", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-9", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-10", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-11", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-12", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-13", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-14", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-15", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-16", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-17", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-18", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-19", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-20", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-21", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-22", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-23", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET-24", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + } + ] +} +`; + exports[`McpResponse network pagination > trace insights > includes error if insight not found 1`] = ` # test response No Performance Insights for the given insight set id. Only use ids given in the "Available insight sets" list. `; +exports[`McpResponse network pagination > trace insights > includes error if insight not found 2`] = ` +{} +`; + exports[`McpResponse network pagination > trace insights > includes the trace insight output 1`] = ` # test response ## Insight Title: LCP breakdown @@ -243,6 +1033,10 @@ We can break this time down into the 4 phases that combine to make the LCP time: - https://web.dev/articles/optimize-lcp `; +exports[`McpResponse network pagination > trace insights > includes the trace insight output 2`] = ` +{} +`; + exports[`McpResponse network pagination > trace summaries > includes the trace summary text and structured data 1`] = ` # test response ## Summary of Performance trace findings: @@ -446,6 +1240,36 @@ reqid=1 GET http://example.com [pending] reqid=1 GET http://example.com [pending] `; +exports[`McpResponse network request filtering > filters network requests by resource type 2`] = ` +{ + "pagination": { + "currentPage": 0, + "totalPages": 1, + "hasNextPage": false, + "hasPreviousPage": false, + "startIndex": 0, + "endIndex": 2, + "invalidPage": false + }, + "networkRequests": [ + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + } + ] +} +`; + exports[`McpResponse network request filtering > filters network requests by single resource type 1`] = ` # test response ## Network requests @@ -453,6 +1277,29 @@ Showing 1-1 of 1 (Page 1 of 1). reqid=1 GET http://example.com [pending] `; +exports[`McpResponse network request filtering > filters network requests by single resource type 2`] = ` +{ + "pagination": { + "currentPage": 0, + "totalPages": 1, + "hasNextPage": false, + "hasPreviousPage": false, + "startIndex": 0, + "endIndex": 1, + "invalidPage": false + }, + "networkRequests": [ + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + } + ] +} +`; + exports[`McpResponse network request filtering > shows all requests when empty resourceTypes array is provided 1`] = ` # test response ## Network requests @@ -464,6 +1311,57 @@ reqid=1 GET http://example.com [pending] reqid=1 GET http://example.com [pending] `; +exports[`McpResponse network request filtering > shows all requests when empty resourceTypes array is provided 2`] = ` +{ + "pagination": { + "currentPage": 0, + "totalPages": 1, + "hasNextPage": false, + "hasPreviousPage": false, + "startIndex": 0, + "endIndex": 5, + "invalidPage": false + }, + "networkRequests": [ + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + } + ] +} +`; + exports[`McpResponse network request filtering > shows all requests when no filters are provided 1`] = ` # test response ## Network requests @@ -475,12 +1373,67 @@ reqid=1 GET http://example.com [pending] reqid=1 GET http://example.com [pending] `; +exports[`McpResponse network request filtering > shows all requests when no filters are provided 2`] = ` +{ + "pagination": { + "currentPage": 0, + "totalPages": 1, + "hasNextPage": false, + "hasPreviousPage": false, + "startIndex": 0, + "endIndex": 5, + "invalidPage": false + }, + "networkRequests": [ + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + }, + { + "requestId": 1, + "method": "GET", + "url": "http://example.com", + "status": "[pending]", + "selectedInDevToolsUI": false + } + ] +} +`; + exports[`McpResponse network request filtering > shows no requests when filter matches nothing 1`] = ` # test response ## Network requests No requests found. `; +exports[`McpResponse network request filtering > shows no requests when filter matches nothing 2`] = ` +{} +`; + exports[`extensions > lists extensions 1`] = ` # test response ## Extensions diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index 68eac2f0b..b03e85d18 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -34,9 +34,15 @@ describe('McpResponse', () => { it('list pages', async t => { await withMcpContext(async (response, context) => { response.setIncludePages(true); - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); assert.equal(content[0].type, 'text'); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); @@ -44,19 +50,31 @@ describe('McpResponse', () => { await withMcpContext(async (response, context) => { response.appendResponseLine('Testing 1'); response.appendResponseLine('Testing 2'); - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); assert.equal(content[0].type, 'text'); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); - it('does not include anything in response if snapshot is null', async () => { + it('does not include anything in response if snapshot is null', async t => { await withMcpContext(async (response, context) => { const page = context.getSelectedPage(); page.accessibility.snapshot = async () => null; - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); assert.equal(content[0].type, 'text'); assert.deepStrictEqual(getTextContent(content[0]), `# test response`); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); @@ -72,9 +90,15 @@ describe('McpResponse', () => { ); await page.focus('button'); response.includeSnapshot(); - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); assert.equal(content[0].type, 'text'); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); @@ -90,9 +114,15 @@ describe('McpResponse', () => { ); await page.focus('input'); response.includeSnapshot(); - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); assert.equal(content[0].type, 'text'); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); @@ -254,60 +284,102 @@ describe('McpResponse', () => { it('adds throttling setting when it is not null', async t => { await withMcpContext(async (response, context) => { context.setNetworkConditions('Slow 3G'); - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); assert.equal(content[0].type, 'text'); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); - it('does not include throttling setting when it is null', async () => { + it('does not include throttling setting when it is null', async t => { await withMcpContext(async (response, context) => { - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); context.setNetworkConditions(null); assert.equal(content[0].type, 'text'); assert.strictEqual(getTextContent(content[0]), `# test response`); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); - it('adds image when image is attached', async () => { + it('adds image when image is attached', async t => { await withMcpContext(async (response, context) => { response.attachImage({data: 'imageBase64', mimeType: 'image/png'}); - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); assert.strictEqual(getTextContent(content[0]), `# test response`); assert.equal(content[1].type, 'image'); assert.strictEqual(getImageContent(content[1]).data, 'imageBase64'); assert.strictEqual(getImageContent(content[1]).mimeType, 'image/png'); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); it('adds cpu throttling setting when it is over 1', async t => { await withMcpContext(async (response, context) => { context.setCpuThrottlingRate(4); - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); - it('does not include cpu throttling setting when it is 1', async () => { + it('does not include cpu throttling setting when it is 1', async t => { await withMcpContext(async (response, context) => { context.setCpuThrottlingRate(1); - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); assert.strictEqual(getTextContent(content[0]), `# test response`); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); it('adds viewport emulation setting when it is set', async t => { await withMcpContext(async (response, context) => { context.setViewport({width: 400, height: 400, deviceScaleFactor: 1}); - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); it('adds userAgent emulation setting when it is set', async t => { await withMcpContext(async (response, context) => { context.setUserAgent('MyUA'); - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); @@ -323,9 +395,15 @@ describe('McpResponse', () => { prompt('message', 'default'); }); await dialogPromise; - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); await context.getDialog()?.dismiss(); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); @@ -341,9 +419,15 @@ describe('McpResponse', () => { alert('message'); }); await dialogPromise; - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); await context.getDialog()?.dismiss(); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); @@ -353,19 +437,31 @@ describe('McpResponse', () => { context.getNetworkRequests = () => { return [getMockRequest({stableId: 1}), getMockRequest({stableId: 2})]; }; - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); - it('does not include network requests when setting is false', async () => { + it('does not include network requests when setting is false', async t => { await withMcpContext(async (response, context) => { response.setIncludeNetworkRequests(false); context.getNetworkRequests = () => { return [getMockRequest()]; }; - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); assert.strictEqual(getTextContent(content[0]), `# test response`); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); @@ -395,9 +491,15 @@ describe('McpResponse', () => { }; response.attachNetworkRequest(1); - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); @@ -412,8 +514,14 @@ describe('McpResponse', () => { return request; }; response.attachNetworkRequest(1); - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); @@ -430,22 +538,34 @@ describe('McpResponse', () => { console.log('Hello from the test'); }); await consoleMessagePromise; - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); assert.ok(getTextContent(content[0])); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); it('adds a message when no console messages exist', async t => { await withMcpContext(async (response, context) => { response.setIncludeConsoleData(true); - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); assert.ok(getTextContent(content[0])); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); - it("doesn't list the issue message if mapping returns null", async () => { + it("doesn't list the issue message if mapping returns null", async t => { await withMcpContext(async (response, context) => { const mockAggregatedIssue = getMockAggregatedIssue(); const mockDescription = { @@ -458,9 +578,15 @@ describe('McpResponse', () => { return [mockAggregatedIssue]; }; - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); const text = getTextContent(content[0]); assert.ok(text.includes('')); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); @@ -500,8 +626,14 @@ describe('McpResponse network request filtering', () => { getMockRequest({resourceType: 'document'}), ]; }; - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); @@ -517,8 +649,14 @@ describe('McpResponse network request filtering', () => { getMockRequest({resourceType: 'stylesheet'}), ]; }; - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); @@ -534,8 +672,14 @@ describe('McpResponse network request filtering', () => { getMockRequest({resourceType: 'stylesheet'}), ]; }; - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); @@ -551,9 +695,15 @@ describe('McpResponse network request filtering', () => { getMockRequest({resourceType: 'font'}), ]; }; - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); @@ -571,27 +721,39 @@ describe('McpResponse network request filtering', () => { getMockRequest({resourceType: 'font'}), ]; }; - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); }); describe('McpResponse network pagination', () => { - it('returns all requests when pagination is not provided', async () => { + it('returns all requests when pagination is not provided', async t => { await withMcpContext(async (response, context) => { const requests = Array.from({length: 5}, () => getMockRequest()); context.getNetworkRequests = () => requests; response.setIncludeNetworkRequests(true); - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); const text = getTextContent(content[0]); assert.ok(text.includes('Showing 1-5 of 5 (Page 1 of 1).')); assert.ok(!text.includes('Next page:')); assert.ok(!text.includes('Previous page:')); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); - it('returns first page by default', async () => { + it('returns first page by default', async t => { await withMcpContext(async (response, context) => { const requests = Array.from({length: 30}, (_, idx) => getMockRequest({method: `GET-${idx}`}), @@ -600,15 +762,21 @@ describe('McpResponse network pagination', () => { return requests; }; response.setIncludeNetworkRequests(true, {pageSize: 10}); - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); const text = getTextContent(content[0]); assert.ok(text.includes('Showing 1-10 of 30 (Page 1 of 3).')); assert.ok(text.includes('Next page: 1')); assert.ok(!text.includes('Previous page:')); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); - it('returns subsequent page when pageIdx provided', async () => { + it('returns subsequent page when pageIdx provided', async t => { await withMcpContext(async (response, context) => { const requests = Array.from({length: 25}, (_, idx) => getMockRequest({method: `GET-${idx}`}), @@ -618,15 +786,21 @@ describe('McpResponse network pagination', () => { pageSize: 10, pageIdx: 1, }); - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); const text = getTextContent(content[0]); assert.ok(text.includes('Showing 11-20 of 25 (Page 2 of 3).')); assert.ok(text.includes('Next page: 2')); assert.ok(text.includes('Previous page: 0')); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); - it('handles invalid page number by showing first page', async () => { + it('handles invalid page number by showing first page', async t => { await withMcpContext(async (response, context) => { const requests = Array.from({length: 5}, () => getMockRequest()); context.getNetworkRequests = () => requests; @@ -634,12 +808,18 @@ describe('McpResponse network pagination', () => { pageSize: 2, pageIdx: 10, // Invalid page number }); - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); const text = getTextContent(content[0]); assert.ok( text.includes('Invalid page number provided. Showing first page.'), ); assert.ok(text.includes('Showing 1-2 of 5 (Page 1 of 3).')); + t.assert.snapshot?.( + JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2), + ); }); }); @@ -687,9 +867,19 @@ describe('McpResponse network pagination', () => { 'NAVIGATION_0', 'LCPBreakdown' as InsightName, ); - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify( + stabilizeStructuredContent(structuredContent), + null, + 2, + ), + ); }); }); @@ -706,9 +896,19 @@ describe('McpResponse network pagination', () => { 'BAD_ID', 'LCPBreakdown' as InsightName, ); - const {content} = await response.handle('test', context); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); t.assert.snapshot?.(getTextContent(content[0])); + t.assert.snapshot?.( + JSON.stringify( + stabilizeStructuredContent(structuredContent), + null, + 2, + ), + ); }); }); });