chore(extension): connection timeout when extension not installed (#896)
This commit is contained in:
@@ -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;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user