chore(extension): connection timeout when extension not installed (#896)

This commit is contained in:
Yury Semikhatsky
2025-08-15 09:09:35 -07:00
committed by GitHub
parent 2fc4e88048
commit ba726fb44a
3 changed files with 102 additions and 61 deletions

View File

@@ -19,10 +19,12 @@ import { chromium } from 'playwright';
import { test as base, expect } from '../../tests/fixtures.js'; import { test as base, expect } from '../../tests/fixtures.js';
import type { BrowserContext } from 'playwright'; import type { BrowserContext } from 'playwright';
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
import type { StartClient } from '../../tests/fixtures.js';
type BrowserWithExtension = { type BrowserWithExtension = {
userDataDir: string; userDataDir: string;
launch: () => Promise<BrowserContext>; launch: (mode?: 'disable-extension') => Promise<BrowserContext>;
}; };
const test = base.extend<{ browserWithExtension: BrowserWithExtension }>({ const test = base.extend<{ browserWithExtension: BrowserWithExtension }>({
@@ -37,14 +39,14 @@ const test = base.extend<{ browserWithExtension: BrowserWithExtension }>({
const userDataDir = testInfo.outputPath('extension-user-data-dir'); const userDataDir = testInfo.outputPath('extension-user-data-dir');
await use({ await use({
userDataDir, userDataDir,
launch: async () => { launch: async (mode?: 'disable-extension') => {
browserContext = await chromium.launchPersistentContext(userDataDir, { browserContext = await chromium.launchPersistentContext(userDataDir, {
channel: mcpBrowser, channel: mcpBrowser,
// Opening the browser singleton only works in headed. // Opening the browser singleton only works in headed.
headless: false, headless: false,
// Automation disables singleton browser process behavior, which is necessary for the extension. // Automation disables singleton browser process behavior, which is necessary for the extension.
ignoreDefaultArgs: ['--enable-automation'], ignoreDefaultArgs: ['--enable-automation'],
args: [ args: mode === 'disable-extension' ? [] : [
`--disable-extensions-except=${pathToExtension}`, `--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`, `--load-extension=${pathToExtension}`,
], ],
@@ -63,9 +65,7 @@ const test = base.extend<{ browserWithExtension: BrowserWithExtension }>({
}, },
}); });
test('navigate with extension', async ({ browserWithExtension, startClient, server }) => { async function startAndCallConnectTool(browserWithExtension: BrowserWithExtension, startClient: StartClient): Promise<Client> {
const browserContext = await browserWithExtension.launch();
const { client } = await startClient({ const { client } = await startClient({
args: [`--connect-tool`], args: [`--connect-tool`],
config: { config: {
@@ -84,6 +84,31 @@ test('navigate with extension', async ({ browserWithExtension, startClient, serv
result: 'Successfully changed connection method.', result: 'Successfully changed connection method.',
}); });
return client;
}
async function startWithExtensionFlag(browserWithExtension: BrowserWithExtension, startClient: StartClient): Promise<Client> {
const { client } = await startClient({
args: [`--extension`],
config: {
browser: {
userDataDir: browserWithExtension.userDataDir,
}
},
});
return client;
}
for (const [mode, startClientMethod] of [
['connect-tool', startAndCallConnectTool],
['extension-flag', startWithExtensionFlag],
] as const) {
test(`navigate with extension (${mode})`, async ({ browserWithExtension, startClient, server }) => {
const browserContext = await browserWithExtension.launch();
const client = await startClientMethod(browserWithExtension, startClient);
const confirmationPagePromise = browserContext.waitForEvent('page', page => { const confirmationPagePromise = browserContext.waitForEvent('page', page => {
return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html');
}); });
@@ -101,7 +126,7 @@ test('navigate with extension', async ({ browserWithExtension, startClient, serv
}); });
}); });
test('snapshot of an existing page', async ({ browserWithExtension, startClient, server }) => { test(`snapshot of an existing page (${mode})`, async ({ browserWithExtension, startClient, server }) => {
const browserContext = await browserWithExtension.launch(); const browserContext = await browserWithExtension.launch();
const page = await browserContext.newPage(); const page = await browserContext.newPage();
@@ -111,23 +136,7 @@ test('snapshot of an existing page', async ({ browserWithExtension, startClient,
await browserContext.newPage(); await browserContext.newPage();
expect(browserContext.pages()).toHaveLength(3); expect(browserContext.pages()).toHaveLength(3);
const { client } = await startClient({ const client = await startClientMethod(browserWithExtension, startClient);
args: [`--connect-tool`],
config: {
browser: {
userDataDir: browserWithExtension.userDataDir,
}
},
});
expect(await client.callTool({
name: 'browser_connect',
arguments: {
name: 'extension'
}
})).toHaveResponse({
result: 'Successfully changed connection method.',
});
expect(browserContext.pages()).toHaveLength(3); expect(browserContext.pages()).toHaveLength(3);
const confirmationPagePromise = browserContext.waitForEvent('page', page => { const confirmationPagePromise = browserContext.waitForEvent('page', page => {
@@ -150,3 +159,29 @@ test('snapshot of an existing page', async ({ browserWithExtension, startClient,
expect(browserContext.pages()).toHaveLength(4); expect(browserContext.pages()).toHaveLength(4);
}); });
test(`extension not installed timeout (${mode})`, async ({ browserWithExtension, startClient, server }) => {
process.env.PWMCP_TEST_CONNECTION_TIMEOUT = '100';
const browserContext = await browserWithExtension.launch();
const client = await startClientMethod(browserWithExtension, startClient);
const confirmationPagePromise = browserContext.waitForEvent('page', page => {
return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html');
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toHaveResponse({
result: expect.stringContaining('Extension connection timeout. Make sure the "Playwright MCP Bridge" extension is installed.'),
isError: true,
});
await confirmationPagePromise;
process.env.PWMCP_TEST_CONNECTION_TIMEOUT = undefined;
});
}

View File

@@ -100,6 +100,9 @@ export class CDPRelayServer {
debugLogger('Waiting for incoming extension connection'); debugLogger('Waiting for incoming extension connection');
await Promise.race([ await Promise.race([
this._extensionConnectionPromise, this._extensionConnectionPromise,
new Promise((_, reject) => setTimeout(() => {
reject(new Error(`Extension connection timeout. Make sure the "Playwright MCP Bridge" extension is installed. See https://github.com/microsoft/playwright-mcp/blob/main/extension/README.md for installation instructions.`));
}, process.env.PWMCP_TEST_CONNECTION_TIMEOUT ? parseInt(process.env.PWMCP_TEST_CONNECTION_TIMEOUT, 10) : 5_000)),
new Promise((_, reject) => abortSignal.addEventListener('abort', reject)) new Promise((_, reject) => abortSignal.addEventListener('abort', reject))
]); ]);
debugLogger('Extension connection established'); debugLogger('Extension connection established');

View File

@@ -40,15 +40,18 @@ type CDPServer = {
start: () => Promise<BrowserContext>; start: () => Promise<BrowserContext>;
}; };
type TestFixtures = { export type StartClient = (options?: {
client: Client;
startClient: (options?: {
clientName?: string, clientName?: string,
args?: string[], args?: string[],
config?: Config, config?: Config,
roots?: { name: string, uri: string }[], roots?: { name: string, uri: string }[],
rootsResponseDelay?: number, rootsResponseDelay?: number,
}) => Promise<{ client: Client, stderr: () => string }>; }) => Promise<{ client: Client, stderr: () => string }>;
type TestFixtures = {
client: Client;
startClient: StartClient;
wsEndpoint: string; wsEndpoint: string;
cdpServer: CDPServer; cdpServer: CDPServer;
server: TestServer; server: TestServer;