From c86de452f14e457e0d5365fbd97ae356bab9aa2c Mon Sep 17 00:00:00 2001 From: Nicholas Roscino Date: Thu, 26 Feb 2026 11:09:16 +0100 Subject: [PATCH 1/8] feat: implement trigger extension action --- src/McpContext.ts | 18 +++++++++++++++++ src/tools/ToolDefinition.ts | 1 + src/tools/extensions.ts | 18 +++++++++++++++++ tests/tools/extensions.test.ts | 36 ++++++++++++++++++++++++++++++++++ tests/utils.ts | 4 +++- 5 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/McpContext.ts b/src/McpContext.ts index 0d8b6d76f..d583e9aec 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -939,6 +939,24 @@ export class McpContext implements Context { this.#extensionRegistry.remove(id); } + async triggerExtensionAction(id: string): Promise { + const page = this.getSelectedPage(); + // @ts-expect-error internal puppeteer api is needed since we don't have a way to get + // a tab id at the moment + const theTarget = page._tabId; + const session = await this.browser.target().createCDPSession(); + + try { + // @ts-expect-error triggerAction is not yet available + await session.send('Extensions.triggerAction', { + id, + targetId: theTarget, + }); + } finally { + await session.detach(); + } + } + listExtensions(): InstalledExtension[] { return this.#extensionRegistry.list(); } diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 543b202db..4e6436963 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -190,6 +190,7 @@ export type Context = Readonly<{ ): void; installExtension(path: string): Promise; uninstallExtension(id: string): Promise; + triggerExtensionAction(id: string): Promise; listExtensions(): InstalledExtension[]; getExtension(id: string): InstalledExtension | undefined; getSelectedMcpPage(): McpPage; diff --git a/src/tools/extensions.ts b/src/tools/extensions.ts index 0ab2d43ff..76cec7fb3 100644 --- a/src/tools/extensions.ts +++ b/src/tools/extensions.ts @@ -85,3 +85,21 @@ export const reloadExtension = defineTool({ response.appendResponseLine('Extension reloaded.'); }, }); + +export const triggerExtensionAction = defineTool({ + name: 'trigger_extension_action', + description: 'Triggers an action in a Chrome extension.', + annotations: { + category: ToolCategory.EXTENSIONS, + readOnlyHint: false, + conditions: [EXTENSIONS_CONDITION], + }, + schema: { + id: zod.string().describe('ID of the extension.'), + }, + handler: async (request, response, context) => { + const {id} = request.params; + await context.triggerExtensionAction(id); + response.appendResponseLine(`Extension action triggered. Id: ${id}`); + }, +}); diff --git a/tests/tools/extensions.test.ts b/tests/tools/extensions.test.ts index 58fa0a40b..4d3fa6d0a 100644 --- a/tests/tools/extensions.test.ts +++ b/tests/tools/extensions.test.ts @@ -16,6 +16,7 @@ import { uninstallExtension, listExtensions, reloadExtension, + triggerExtensionAction, } from '../../src/tools/extensions.js'; import {withMcpContext} from '../utils.js'; @@ -128,4 +129,39 @@ describe('extension', () => { assert.ok(reinstalled, 'Extension should be present after reload'); }); }); + + it('triggers an extension action', async () => { + await withMcpContext( + async (response, context) => { + const triggerSpy = sinon.spy(context, 'triggerExtensionAction'); + + await installExtension.handler( + {params: {path: EXTENSION_PATH}}, + response, + context, + ); + + const extensionId = extractId(response); + response.resetResponseLineForTesting(); + + await triggerExtensionAction.handler( + {params: {id: extensionId}}, + response, + context, + ); + + assert.ok( + triggerSpy.calledOnceWithExactly(extensionId), + 'triggerExtensionAction should be called with correct params', + ); + assert.ok( + response.responseLines[0].includes( + `Extension action triggered. Id: ${extensionId}`, + ), + 'Response should indicate action triggered', + ); + }, + {channel: 'chrome-canary'}, + ); + }); }); diff --git a/tests/utils.ts b/tests/utils.ts index d26dc309b..0819dfecc 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -47,10 +47,11 @@ let context: McpContext | undefined; export async function withBrowser( cb: (browser: Browser, page: Page) => Promise, - options: {debug?: boolean; autoOpenDevTools?: boolean} = {}, + options: {debug?: boolean; autoOpenDevTools?: boolean; channel?: string} = {}, ) { const launchOptions: LaunchOptions = { executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, + channel: options.channel as any, headless: !options.debug, defaultViewport: null, devtools: options.autoOpenDevTools ?? false, @@ -85,6 +86,7 @@ export async function withMcpContext( debug?: boolean; autoOpenDevTools?: boolean; performanceCrux?: boolean; + channel?: string; } = {}, args: ParsedArguments = {} as ParsedArguments, ) { From cfaec9c45e66d67f71822a5a9a44a57cb3906e2d Mon Sep 17 00:00:00 2001 From: Nicholas Roscino Date: Thu, 26 Feb 2026 11:16:05 +0100 Subject: [PATCH 2/8] chore: fixed type cast error --- tests/utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/utils.ts b/tests/utils.ts index 0819dfecc..4588358ca 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -9,6 +9,7 @@ import logger from 'debug'; import type {Browser} from 'puppeteer'; import puppeteer, {Locator} from 'puppeteer'; import type { + ChromeReleaseChannel, Frame, HTTPRequest, HTTPResponse, @@ -51,7 +52,7 @@ export async function withBrowser( ) { const launchOptions: LaunchOptions = { executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, - channel: options.channel as any, + channel: options.channel as ChromeReleaseChannel, headless: !options.debug, defaultViewport: null, devtools: options.autoOpenDevTools ?? false, From 632f65afeee0e0c7c2e71f072329a2a88ce8c003 Mon Sep 17 00:00:00 2001 From: Nicholas Roscino Date: Fri, 27 Feb 2026 18:29:52 +0100 Subject: [PATCH 3/8] chore: added download of canary to ci --- .github/workflows/run-tests.yml | 6 +++ src/McpContext.ts | 2 +- tests/tools/extensions.test.ts | 65 ++++++++++++++++++++----- tests/tools/fixtures/extension-sw/sw.js | 4 +- tests/utils.ts | 15 ++++-- 5 files changed, 75 insertions(+), 17 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 8aa274f0d..cab31d9ea 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -41,6 +41,12 @@ jobs: shell: bash run: npm ci + - name: Install Chrome Canary + shell: bash + run: | + CANARY_PATH=$(npx @puppeteer/browsers install chrome@canary --format "{{path}}") + echo "CANARY_EXECUTABLE_PATH=$CANARY_PATH" >> $GITHUB_ENV + - name: Build run: npm run bundle env: diff --git a/src/McpContext.ts b/src/McpContext.ts index d583e9aec..e5b793a31 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -940,7 +940,7 @@ export class McpContext implements Context { } async triggerExtensionAction(id: string): Promise { - const page = this.getSelectedPage(); + const page = this.getSelectedPptrPage(); // @ts-expect-error internal puppeteer api is needed since we don't have a way to get // a tab id at the moment const theTarget = page._tabId; diff --git a/tests/tools/extensions.test.ts b/tests/tools/extensions.test.ts index 4d3fa6d0a..ea292d604 100644 --- a/tests/tools/extensions.test.ts +++ b/tests/tools/extensions.test.ts @@ -10,6 +10,7 @@ import {afterEach, describe, it} from 'node:test'; import sinon from 'sinon'; +import type { ParsedArguments } from '../../src/cli.js'; import type {McpResponse} from '../../src/McpResponse.js'; import { installExtension, @@ -18,8 +19,13 @@ import { reloadExtension, triggerExtensionAction, } from '../../src/tools/extensions.js'; +import { listPages } from '../../src/tools/pages.js'; import {withMcpContext} from '../utils.js'; +const EXTENSION_WITH_SW_PATH = path.join( + import.meta.dirname, + '../../../tests/tools/fixtures/extension-sw', +); const EXTENSION_PATH = path.join( import.meta.dirname, '../../../tests/tools/fixtures/extension', @@ -129,39 +135,74 @@ describe('extension', () => { assert.ok(reinstalled, 'Extension should be present after reload'); }); }); - it('triggers an extension action', async () => { await withMcpContext( async (response, context) => { - const triggerSpy = sinon.spy(context, 'triggerExtensionAction'); - await installExtension.handler( - {params: {path: EXTENSION_PATH}}, + {params: {path: EXTENSION_WITH_SW_PATH}}, response, context, ); const extensionId = extractId(response); + response.resetResponseLineForTesting(); + const listPageDef = listPages({ + categoryExtensions: true, + } as ParsedArguments); + await listPageDef.handler( + {params: {}, page: context.getSelectedMcpPage()}, + response, + context, + ); + let result = await response.handle(listPageDef.name, context); + let textContent = result.content.find(c => c.type === 'text') as { + type: 'text'; + text: string; + }; + assert.ok( + !textContent.text.includes(extensionId), + 'Response should not contain extension service worker id', + ); await triggerExtensionAction.handler( {params: {id: extensionId}}, response, context, ); + + const swTarget = await context.browser.waitForTarget( + t => + t.type() === 'service_worker' && + t.url().includes(extensionId), + ); + const swUrl = swTarget.url(); - assert.ok( - triggerSpy.calledOnceWithExactly(extensionId), - 'triggerExtensionAction should be called with correct params', + response.resetResponseLineForTesting(); + await listPageDef.handler( + {params: {}, page: context.getSelectedMcpPage()}, + response, + context, ); + result = await response.handle(listPageDef.name, context); + textContent = result.content.find(c => c.type === 'text') as { + type: 'text'; + text: string; + }; assert.ok( - response.responseLines[0].includes( - `Extension action triggered. Id: ${extensionId}`, - ), - 'Response should indicate action triggered', + textContent.text.includes(swUrl), + 'Response should contain extension service worker url', ); }, - {channel: 'chrome-canary'}, + { + channel: process.env.CANARY_EXECUTABLE_PATH + ? undefined + : 'chrome-canary', + executablePath: process.env.CANARY_EXECUTABLE_PATH, + }, + { + categoryExtensions: true, + } as ParsedArguments ); }); }); diff --git a/tests/tools/fixtures/extension-sw/sw.js b/tests/tools/fixtures/extension-sw/sw.js index f44ddb739..efa7a5a27 100644 --- a/tests/tools/fixtures/extension-sw/sw.js +++ b/tests/tools/fixtures/extension-sw/sw.js @@ -1 +1,3 @@ -console.log('Service worker loaded'); +chrome.action.onClicked.addListener((tab) => { + console.log('Action clicked'); +}); \ No newline at end of file diff --git a/tests/utils.ts b/tests/utils.ts index 4588358ca..4ec24f0ed 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -48,11 +48,19 @@ let context: McpContext | undefined; export async function withBrowser( cb: (browser: Browser, page: Page) => Promise, - options: {debug?: boolean; autoOpenDevTools?: boolean; channel?: string} = {}, + options: { + debug?: boolean; + autoOpenDevTools?: boolean; + channel?: string; + executablePath?: string; + } = {}, ) { const launchOptions: LaunchOptions = { - executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, - channel: options.channel as ChromeReleaseChannel, + executablePath: + options.executablePath ?? process.env.PUPPETEER_EXECUTABLE_PATH, + channel: (options.executablePath + ? undefined + : options.channel) as ChromeReleaseChannel, headless: !options.debug, defaultViewport: null, devtools: options.autoOpenDevTools ?? false, @@ -88,6 +96,7 @@ export async function withMcpContext( autoOpenDevTools?: boolean; performanceCrux?: boolean; channel?: string; + executablePath?: string; } = {}, args: ParsedArguments = {} as ParsedArguments, ) { From 802b5f74cf79884bb62c0180514e6a496e7105d6 Mon Sep 17 00:00:00 2001 From: Nicholas Roscino Date: Fri, 27 Feb 2026 18:38:40 +0100 Subject: [PATCH 4/8] chore: fix lint --- tests/tools/extensions.test.ts | 12 +++++------- tests/tools/fixtures/extension-sw/sw.js | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/tools/extensions.test.ts b/tests/tools/extensions.test.ts index ea292d604..05b21c1bd 100644 --- a/tests/tools/extensions.test.ts +++ b/tests/tools/extensions.test.ts @@ -10,7 +10,7 @@ import {afterEach, describe, it} from 'node:test'; import sinon from 'sinon'; -import type { ParsedArguments } from '../../src/cli.js'; +import type {ParsedArguments} from '../../src/cli.js'; import type {McpResponse} from '../../src/McpResponse.js'; import { installExtension, @@ -19,7 +19,7 @@ import { reloadExtension, triggerExtensionAction, } from '../../src/tools/extensions.js'; -import { listPages } from '../../src/tools/pages.js'; +import {listPages} from '../../src/tools/pages.js'; import {withMcpContext} from '../utils.js'; const EXTENSION_WITH_SW_PATH = path.join( @@ -170,11 +170,9 @@ describe('extension', () => { response, context, ); - + const swTarget = await context.browser.waitForTarget( - t => - t.type() === 'service_worker' && - t.url().includes(extensionId), + t => t.type() === 'service_worker' && t.url().includes(extensionId), ); const swUrl = swTarget.url(); @@ -202,7 +200,7 @@ describe('extension', () => { }, { categoryExtensions: true, - } as ParsedArguments + } as ParsedArguments, ); }); }); diff --git a/tests/tools/fixtures/extension-sw/sw.js b/tests/tools/fixtures/extension-sw/sw.js index efa7a5a27..b49093e40 100644 --- a/tests/tools/fixtures/extension-sw/sw.js +++ b/tests/tools/fixtures/extension-sw/sw.js @@ -1,3 +1,3 @@ -chrome.action.onClicked.addListener((tab) => { +chrome.action.onClicked.addListener(tab => { console.log('Action clicked'); -}); \ No newline at end of file +}); From fb1215b55656353a2e7c1fc0e32102cf8795da4d Mon Sep 17 00:00:00 2001 From: Nicholas Roscino Date: Mon, 2 Mar 2026 09:37:58 +0100 Subject: [PATCH 5/8] chore: removing unnecessary channel specification --- tests/tools/extensions.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/tools/extensions.test.ts b/tests/tools/extensions.test.ts index 05b21c1bd..8017fd2ef 100644 --- a/tests/tools/extensions.test.ts +++ b/tests/tools/extensions.test.ts @@ -193,9 +193,6 @@ describe('extension', () => { ); }, { - channel: process.env.CANARY_EXECUTABLE_PATH - ? undefined - : 'chrome-canary', executablePath: process.env.CANARY_EXECUTABLE_PATH, }, { From 417cdf5b16d0bec643eb0a21796e9d4747e70f70 Mon Sep 17 00:00:00 2001 From: Nicholas Roscino Date: Mon, 2 Mar 2026 14:24:26 +0100 Subject: [PATCH 6/8] chore: trigger action test updated to use snapshot tests --- src/McpResponse.ts | 2 +- tests/tools/extensions.test.js.snapshot | 13 +++++++++ tests/tools/extensions.test.ts | 35 ++++++++----------------- tests/tools/script.test.ts | 4 +-- tests/utils.ts | 16 +++++++---- 5 files changed, 37 insertions(+), 33 deletions(-) create mode 100644 tests/tools/extensions.test.js.snapshot diff --git a/src/McpResponse.ts b/src/McpResponse.ts index a4514767f..e87ebac04 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -595,7 +595,7 @@ Call ${handleDialog.name} to handle it before continuing.`); } if (this.#includeExtensionServiceWorkers) { - if (!context.getExtensionServiceWorkers().length) { + if (context.getExtensionServiceWorkers().length) { response.push(`## Extension Service Workers`); } diff --git a/tests/tools/extensions.test.js.snapshot b/tests/tools/extensions.test.js.snapshot new file mode 100644 index 000000000..2f35055d0 --- /dev/null +++ b/tests/tools/extensions.test.js.snapshot @@ -0,0 +1,13 @@ +exports[`extension > triggers an extension action 1`] = ` +# list_pages response +## Pages +1: about:blank [selected] +`; + +exports[`extension > triggers an extension action 2`] = ` +# list_pages response +## Pages +1: about:blank [selected] +## Extension Service Workers +sw-1: chrome-extension:///sw.js +`; diff --git a/tests/tools/extensions.test.ts b/tests/tools/extensions.test.ts index 8017fd2ef..4b03e413d 100644 --- a/tests/tools/extensions.test.ts +++ b/tests/tools/extensions.test.ts @@ -11,7 +11,6 @@ import {afterEach, describe, it} from 'node:test'; import sinon from 'sinon'; import type {ParsedArguments} from '../../src/cli.js'; -import type {McpResponse} from '../../src/McpResponse.js'; import { installExtension, uninstallExtension, @@ -20,7 +19,7 @@ import { triggerExtensionAction, } from '../../src/tools/extensions.js'; import {listPages} from '../../src/tools/pages.js'; -import {withMcpContext} from '../utils.js'; +import {extractId, withMcpContext} from '../utils.js'; const EXTENSION_WITH_SW_PATH = path.join( import.meta.dirname, @@ -31,15 +30,6 @@ const EXTENSION_PATH = path.join( '../../../tests/tools/fixtures/extension', ); -export function extractId(response: McpResponse) { - const responseLine = response.responseLines[0]; - assert.ok(responseLine, 'Response should not be empty'); - const match = responseLine.match(/Extension installed\. Id: (.+)/); - const extensionId = match ? match[1] : null; - assert.ok(extensionId, 'Response should contain a valid key'); - return extensionId; -} - describe('extension', () => { afterEach(() => { sinon.restore(); @@ -135,17 +125,13 @@ describe('extension', () => { assert.ok(reinstalled, 'Extension should be present after reload'); }); }); - it('triggers an extension action', async () => { + it('triggers an extension action', async t => { await withMcpContext( async (response, context) => { - await installExtension.handler( - {params: {path: EXTENSION_WITH_SW_PATH}}, - response, - context, + const extensionId = await context.installExtension( + EXTENSION_WITH_SW_PATH, ); - const extensionId = extractId(response); - response.resetResponseLineForTesting(); const listPageDef = listPages({ categoryExtensions: true, @@ -160,9 +146,8 @@ describe('extension', () => { type: 'text'; text: string; }; - assert.ok( - !textContent.text.includes(extensionId), - 'Response should not contain extension service worker id', + t.assert.snapshot?.( + textContent.text.replaceAll(extensionId, ''), ); await triggerExtensionAction.handler( @@ -187,9 +172,11 @@ describe('extension', () => { type: 'text'; text: string; }; - assert.ok( - textContent.text.includes(swUrl), - 'Response should contain extension service worker url', + t.assert.snapshot?.( + textContent.text + .replaceAll(extensionId, '') + .replaceAll(swUrl, '') + .replaceAll(/localhost:\d+/g, 'localhost:'), ); }, { diff --git a/tests/tools/script.test.ts b/tests/tools/script.test.ts index 787367269..5e900b3ee 100644 --- a/tests/tools/script.test.ts +++ b/tests/tools/script.test.ts @@ -12,9 +12,7 @@ import type {ParsedArguments} from '../../src/cli.js'; import {installExtension} from '../../src/tools/extensions.js'; import {evaluateScript} from '../../src/tools/script.js'; import {serverHooks} from '../server.js'; -import {html, withMcpContext} from '../utils.js'; - -import {extractId} from './extensions.test.js'; +import {extractId, html, withMcpContext} from '../utils.js'; const EXTENSION_PATH = path.join( import.meta.dirname, diff --git a/tests/utils.ts b/tests/utils.ts index 4ec24f0ed..cc4349c52 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -4,12 +4,13 @@ * SPDX-License-Identifier: Apache-2.0 */ +import assert from 'node:assert'; + import type {CallToolResult} from '@modelcontextprotocol/sdk/types.js'; import logger from 'debug'; import type {Browser} from 'puppeteer'; import puppeteer, {Locator} from 'puppeteer'; import type { - ChromeReleaseChannel, Frame, HTTPRequest, HTTPResponse, @@ -43,6 +44,15 @@ export function getImageContent(content: CallToolResult['content'][number]): { throw new Error(`Expected image content but got ${content.type}`); } +export function extractId(response: McpResponse) { + const responseLine = response.responseLines[0]; + assert.ok(responseLine, 'Response should not be empty'); + const match = responseLine.match(/Extension installed\. Id: (.+)/); + const extensionId = match ? match[1] : null; + assert.ok(extensionId, 'Response should contain a valid key'); + return extensionId; +} + const browsers = new Map(); let context: McpContext | undefined; @@ -51,16 +61,12 @@ export async function withBrowser( options: { debug?: boolean; autoOpenDevTools?: boolean; - channel?: string; executablePath?: string; } = {}, ) { const launchOptions: LaunchOptions = { executablePath: options.executablePath ?? process.env.PUPPETEER_EXECUTABLE_PATH, - channel: (options.executablePath - ? undefined - : options.channel) as ChromeReleaseChannel, headless: !options.debug, defaultViewport: null, devtools: options.autoOpenDevTools ?? false, From 4d5a62ab29a02757bb31d84a63716bc527992ffc Mon Sep 17 00:00:00 2001 From: Nicholas Roscino Date: Mon, 2 Mar 2026 14:52:13 +0100 Subject: [PATCH 7/8] refactor: address pr comments --- tests/tools/extensions.test.ts | 7 +++---- tests/tools/script.test.ts | 4 ++-- tests/utils.ts | 3 +-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/tools/extensions.test.ts b/tests/tools/extensions.test.ts index 4b03e413d..b1f64d991 100644 --- a/tests/tools/extensions.test.ts +++ b/tests/tools/extensions.test.ts @@ -19,7 +19,7 @@ import { triggerExtensionAction, } from '../../src/tools/extensions.js'; import {listPages} from '../../src/tools/pages.js'; -import {extractId, withMcpContext} from '../utils.js'; +import {extractExtensionId, withMcpContext} from '../utils.js'; const EXTENSION_WITH_SW_PATH = path.join( import.meta.dirname, @@ -44,7 +44,7 @@ describe('extension', () => { context, ); - const extensionId = extractId(response); + const extensionId = extractExtensionId(response); const page = context.getSelectedPptrPage(); await page.goto('chrome://extensions'); @@ -99,7 +99,7 @@ describe('extension', () => { context, ); - const extensionId = extractId(response); + const extensionId = extractExtensionId(response); const installSpy = sinon.spy(context, 'installExtension'); response.resetResponseLineForTesting(); @@ -132,7 +132,6 @@ describe('extension', () => { EXTENSION_WITH_SW_PATH, ); - response.resetResponseLineForTesting(); const listPageDef = listPages({ categoryExtensions: true, } as ParsedArguments); diff --git a/tests/tools/script.test.ts b/tests/tools/script.test.ts index 5e900b3ee..c3ad9f1af 100644 --- a/tests/tools/script.test.ts +++ b/tests/tools/script.test.ts @@ -12,7 +12,7 @@ import type {ParsedArguments} from '../../src/cli.js'; import {installExtension} from '../../src/tools/extensions.js'; import {evaluateScript} from '../../src/tools/script.js'; import {serverHooks} from '../server.js'; -import {extractId, html, withMcpContext} from '../utils.js'; +import {extractExtensionId, html, withMcpContext} from '../utils.js'; const EXTENSION_PATH = path.join( import.meta.dirname, @@ -207,7 +207,7 @@ describe('script', () => { context, ); - const extensionId = extractId(response); + const extensionId = extractExtensionId(response); const swTarget = await context.browser.waitForTarget( t => t.type() === 'service_worker' && t.url().includes(extensionId), ); diff --git a/tests/utils.ts b/tests/utils.ts index cc4349c52..ac88f9398 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -44,7 +44,7 @@ export function getImageContent(content: CallToolResult['content'][number]): { throw new Error(`Expected image content but got ${content.type}`); } -export function extractId(response: McpResponse) { +export function extractExtensionId(response: McpResponse) { const responseLine = response.responseLines[0]; assert.ok(responseLine, 'Response should not be empty'); const match = responseLine.match(/Extension installed\. Id: (.+)/); @@ -101,7 +101,6 @@ export async function withMcpContext( debug?: boolean; autoOpenDevTools?: boolean; performanceCrux?: boolean; - channel?: string; executablePath?: string; } = {}, args: ParsedArguments = {} as ParsedArguments, From 012b30ec0f35e152d76ea7c73979937f07869ca9 Mon Sep 17 00:00:00 2001 From: Nicholas Roscino Date: Mon, 2 Mar 2026 16:37:45 +0100 Subject: [PATCH 8/8] chore: improving test reliability --- tests/tools/extensions.test.js.snapshot | 13 ------- tests/tools/extensions.test.ts | 46 +++++-------------------- 2 files changed, 8 insertions(+), 51 deletions(-) delete mode 100644 tests/tools/extensions.test.js.snapshot diff --git a/tests/tools/extensions.test.js.snapshot b/tests/tools/extensions.test.js.snapshot deleted file mode 100644 index 2f35055d0..000000000 --- a/tests/tools/extensions.test.js.snapshot +++ /dev/null @@ -1,13 +0,0 @@ -exports[`extension > triggers an extension action 1`] = ` -# list_pages response -## Pages -1: about:blank [selected] -`; - -exports[`extension > triggers an extension action 2`] = ` -# list_pages response -## Pages -1: about:blank [selected] -## Extension Service Workers -sw-1: chrome-extension:///sw.js -`; diff --git a/tests/tools/extensions.test.ts b/tests/tools/extensions.test.ts index b1f64d991..61f85b903 100644 --- a/tests/tools/extensions.test.ts +++ b/tests/tools/extensions.test.ts @@ -18,7 +18,6 @@ import { reloadExtension, triggerExtensionAction, } from '../../src/tools/extensions.js'; -import {listPages} from '../../src/tools/pages.js'; import {extractExtensionId, withMcpContext} from '../utils.js'; const EXTENSION_WITH_SW_PATH = path.join( @@ -125,29 +124,18 @@ describe('extension', () => { assert.ok(reinstalled, 'Extension should be present after reload'); }); }); - it('triggers an extension action', async t => { + it('triggers an extension action', async () => { await withMcpContext( async (response, context) => { const extensionId = await context.installExtension( EXTENSION_WITH_SW_PATH, ); - const listPageDef = listPages({ - categoryExtensions: true, - } as ParsedArguments); - await listPageDef.handler( - {params: {}, page: context.getSelectedMcpPage()}, - response, - context, - ); - let result = await response.handle(listPageDef.name, context); - let textContent = result.content.find(c => c.type === 'text') as { - type: 'text'; - text: string; - }; - t.assert.snapshot?.( - textContent.text.replaceAll(extensionId, ''), + const targetsBefore = context.browser.targets(); + const pageTargetBefore = targetsBefore.find( + t => t.type() === 'page' && t.url().includes(extensionId), ); + assert.ok(!pageTargetBefore, 'Page should not exist before action'); await triggerExtensionAction.handler( {params: {id: extensionId}}, @@ -155,28 +143,10 @@ describe('extension', () => { context, ); - const swTarget = await context.browser.waitForTarget( - t => t.type() === 'service_worker' && t.url().includes(extensionId), - ); - const swUrl = swTarget.url(); - - response.resetResponseLineForTesting(); - await listPageDef.handler( - {params: {}, page: context.getSelectedMcpPage()}, - response, - context, - ); - result = await response.handle(listPageDef.name, context); - textContent = result.content.find(c => c.type === 'text') as { - type: 'text'; - text: string; - }; - t.assert.snapshot?.( - textContent.text - .replaceAll(extensionId, '') - .replaceAll(swUrl, '') - .replaceAll(/localhost:\d+/g, 'localhost:'), + const pageTargetAfter = await context.browser.waitForTarget( + t => t.type() === 'page' && t.url().includes(extensionId), ); + assert.ok(pageTargetAfter, 'Page should exist after action'); }, { executablePath: process.env.CANARY_EXECUTABLE_PATH,