8 Commits

Author SHA1 Message Date
Pavel Feldman
bc48600a49 chore: mark v0.0.13 (#190) 2025-04-15 15:27:29 -07:00
Yury Semikhatsky
0d6bb2f547 devops: add bots for other browsers/platforms (#174) 2025-04-15 13:16:56 -07:00
Pavel Feldman
795a9d578a chore: generalize status & action as code (#188) 2025-04-15 12:54:45 -07:00
Simon Knott
4a19e18999 feat: respond with action and generated locator (#181)
Closes https://github.com/microsoft/playwright-mcp/issues/163
2025-04-15 10:55:20 -07:00
Simon Knott
4d59e06184 test: fix flaky test (#180)
Closes https://github.com/microsoft/playwright-mcp/issues/177

`ResizeObserver` isn't instant!
2025-04-15 16:10:49 +02:00
Pavel Feldman
6891a525b3 chore: add npx install step to the publish workflow (#178) 2025-04-14 20:09:38 -07:00
Yury Semikhatsky
0f7fd1362f chore: mark 0.0.12 (#176) 2025-04-14 19:39:10 -07:00
Yury Semikhatsky
de08c24b96 fix: consider DISPLAY only on linux (#175) 2025-04-14 19:07:39 -07:00
23 changed files with 317 additions and 111 deletions

View File

@@ -8,7 +8,11 @@ on:
jobs:
build-and-test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
@@ -25,6 +29,10 @@ jobs:
- name: Playwright install
run: npx playwright install --with-deps
- name: Install MS Edge
if: ${{ matrix.os == 'macos-latest' }} # MS Edge is not preinstalled on macOS runners.
run: npx playwright install msedge
- name: Run linting
run: npm run lint

View File

@@ -15,9 +15,10 @@ jobs:
node-version: 18
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run build
- run: npm run lint
- run: npm run test
- run: npm run ctest
- run: npm publish --provenance
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
lib/
node_modules/
test-results/
.vscode/mcp.json

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@playwright/mcp",
"version": "0.0.11",
"version": "0.0.13",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@playwright/mcp",
"version": "0.0.11",
"version": "0.0.13",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.6.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@playwright/mcp",
"version": "0.0.11",
"version": "0.0.13",
"description": "Playwright Tools for MCP",
"repository": {
"type": "git",
@@ -19,6 +19,7 @@
"lint": "eslint .",
"watch": "tsc --watch",
"test": "playwright test",
"ctest": "playwright test --project=chrome",
"clean": "rm -rf lib",
"npm-publish": "npm run clean && npm run build && npm run test && npm publish"
},

View File

@@ -16,6 +16,8 @@
import { defineConfig } from '@playwright/test';
import type { Project } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
@@ -23,5 +25,11 @@ export default defineConfig({
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'list',
projects: [{ name: 'default' }],
projects: [
{ name: 'chrome' },
{ name: 'msedge', use: { mcpBrowser: 'msedge' } },
{ name: 'chromium', use: { mcpBrowser: 'chromium' } },
{ name: 'firefox', use: { mcpBrowser: 'firefox' } },
{ name: 'webkit', use: { mcpBrowser: 'webkit' } },
].filter(Boolean) as Project[],
});

View File

@@ -33,7 +33,6 @@ type PageOrFrameLocator = playwright.Page | playwright.FrameLocator;
type RunOptions = {
captureSnapshot?: boolean;
waitForCompletion?: boolean;
status?: string;
noClearFileChooser?: boolean;
};
@@ -79,8 +78,8 @@ export class Context {
async listTabs(): Promise<string> {
if (!this._tabs.length)
return 'No tabs open';
const lines: string[] = ['Open tabs:'];
return '### No tabs open';
const lines: string[] = ['### Open tabs'];
for (let i = 0; i < this._tabs.length; i++) {
const tab = this._tabs[i];
const title = await tab.page.title();
@@ -172,6 +171,10 @@ export class Context {
}
}
type RunResult = {
code: string[];
};
class Tab {
readonly context: Context;
readonly page: playwright.Page;
@@ -207,36 +210,52 @@ class Tab {
await this.page.waitForLoadState('load', { timeout: 5000 }).catch(() => {});
}
async run(callback: (tab: Tab) => Promise<void>, options?: RunOptions): Promise<ToolResult> {
async run(callback: (tab: Tab) => Promise<RunResult>, options?: RunOptions): Promise<ToolResult> {
let runResult: RunResult | undefined;
try {
if (!options?.noClearFileChooser)
this._fileChooser = undefined;
if (options?.waitForCompletion)
await waitForCompletion(this.page, () => callback(this));
runResult = await waitForCompletion(this.page, () => callback(this)) ?? undefined;
else
await callback(this);
runResult = await callback(this) ?? undefined;
} finally {
if (options?.captureSnapshot)
this._snapshot = await PageSnapshot.create(this.page);
}
const tabList = this.context.tabs().length > 1 ? await this.context.listTabs() + '\n\nCurrent tab:' + '\n' : '';
const snapshot = this._snapshot?.text({ status: options?.status, hasFileChooser: !!this._fileChooser }) ?? options?.status ?? '';
const result: string[] = [];
result.push(`- Ran code:
\`\`\`js
${runResult.code.join('\n')}
\`\`\`
`);
if (this.context.tabs().length > 1)
result.push(await this.context.listTabs(), '');
if (this._snapshot) {
if (this.context.tabs().length > 1)
result.push('### Current tab');
result.push(this._snapshot.text({ hasFileChooser: !!this._fileChooser }));
}
return {
content: [{
type: 'text',
text: tabList + snapshot,
text: result.join('\n'),
}],
};
}
async runAndWait(callback: (tab: Tab) => Promise<void>, options?: RunOptions): Promise<ToolResult> {
async runAndWait(callback: (tab: Tab) => Promise<RunResult>, options?: RunOptions): Promise<ToolResult> {
return await this.run(callback, {
waitForCompletion: true,
...options,
});
}
async runAndWaitWithSnapshot(callback: (snapshot: PageSnapshot) => Promise<void>, options?: RunOptions): Promise<ToolResult> {
async runAndWaitWithSnapshot(callback: (snapshot: PageSnapshot) => Promise<RunResult>, options?: RunOptions): Promise<ToolResult> {
return await this.run(tab => callback(tab.lastSnapshot()), {
captureSnapshot: true,
waitForCompletion: true,
@@ -275,13 +294,9 @@ class PageSnapshot {
return snapshot;
}
text(options?: { status?: string, hasFileChooser?: boolean }): string {
text(options: { hasFileChooser: boolean }): string {
const results: string[] = [];
if (options?.status) {
results.push(options.status);
results.push('');
}
if (options?.hasFileChooser) {
if (options.hasFileChooser) {
results.push('- There is a file chooser visible that requires browser_file_upload to be called');
results.push('');
}
@@ -359,3 +374,7 @@ class PageSnapshot {
return frame.locator(`aria-ref=${ref}`);
}
}
export async function generateLocator(locator: playwright.Locator): Promise<string> {
return (locator as any)._generateLocatorString();
}

53
src/javascript.ts Normal file
View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// adapted from:
// - https://github.com/microsoft/playwright/blob/76ee48dc9d4034536e3ec5b2c7ce8be3b79418a8/packages/playwright-core/src/utils/isomorphic/stringUtils.ts
// - https://github.com/microsoft/playwright/blob/76ee48dc9d4034536e3ec5b2c7ce8be3b79418a8/packages/playwright-core/src/server/codegen/javascript.ts
// NOTE: this function should not be used to escape any selectors.
export function escapeWithQuotes(text: string, char: string = '\'') {
const stringified = JSON.stringify(text);
const escapedText = stringified.substring(1, stringified.length - 1).replace(/\\"/g, '"');
if (char === '\'')
return char + escapedText.replace(/[']/g, '\\\'') + char;
if (char === '"')
return char + escapedText.replace(/["]/g, '\\"') + char;
if (char === '`')
return char + escapedText.replace(/[`]/g, '`') + char;
throw new Error('Invalid escape char');
}
export function quote(text: string) {
return escapeWithQuotes(text, '\'');
}
export function formatObject(value: any, indent = ' '): string {
if (typeof value === 'string')
return quote(value);
if (Array.isArray(value))
return `[${value.map(o => formatObject(o)).join(', ')}]`;
if (typeof value === 'object') {
const keys = Object.keys(value).filter(key => value[key] !== undefined).sort();
if (!keys.length)
return '{}';
const tokens: string[] = [];
for (const key of keys)
tokens.push(`${key}: ${formatObject(value[key])}`);
return `{\n${indent}${tokens.join(`,\n${indent}`)}\n}`;
}
return String(value);
}

View File

@@ -74,7 +74,7 @@ program
}
const launchOptions: LaunchOptions = {
headless: options.headless ?? !process.env.DISPLAY,
headless: !!(options.headless ?? (os.platform() === 'linux' && !process.env.DISPLAY)),
channel,
executablePath: options.executablePath,
};

View File

@@ -78,13 +78,16 @@ const resize: ToolFactory = captureSnapshot => ({
const validatedParams = resizeSchema.parse(params);
const tab = context.currentTab();
return await tab.run(
tab => tab.page.setViewportSize({ width: validatedParams.width, height: validatedParams.height }),
{
status: `Resized browser window`,
captureSnapshot,
}
);
return await tab.run(async tab => {
await tab.page.setViewportSize({ width: validatedParams.width, height: validatedParams.height });
const code = [
`// Resize browser window to ${validatedParams.width}x${validatedParams.height}`,
`await page.setViewportSize({ width: ${validatedParams.width}, height: ${validatedParams.height} });`
];
return { code };
}, {
captureSnapshot,
});
},
});

View File

@@ -35,8 +35,11 @@ const uploadFile: ToolFactory = captureSnapshot => ({
const tab = context.currentTab();
return await tab.runAndWait(async () => {
await tab.submitFileChooser(validatedParams.paths);
const code = [
`// <internal code to chose files ${validatedParams.paths.join(', ')}`,
];
return { code };
}, {
status: `Chose files ${validatedParams.paths.join(', ')}`,
captureSnapshot,
noClearFileChooser: true,
});

View File

@@ -34,8 +34,12 @@ const pressKey: ToolFactory = captureSnapshot => ({
const validatedParams = pressKeySchema.parse(params);
return await context.currentTab().runAndWait(async tab => {
await tab.page.keyboard.press(validatedParams.key);
const code = [
`// Press ${validatedParams.key}`,
`await page.keyboard.press('${validatedParams.key}');`,
];
return { code };
}, {
status: `Pressed key ${validatedParams.key}`,
captureSnapshot,
});
},

View File

@@ -35,8 +35,12 @@ const navigate: ToolFactory = captureSnapshot => ({
const currentTab = await context.ensureTab();
return await currentTab.run(async tab => {
await tab.navigate(validatedParams.url);
const code = [
`// Navigate to ${validatedParams.url}`,
`await page.goto('${validatedParams.url}');`,
];
return { code };
}, {
status: `Navigated to ${validatedParams.url}`,
captureSnapshot,
});
},
@@ -54,8 +58,12 @@ const goBack: ToolFactory = snapshot => ({
handle: async context => {
return await context.currentTab().runAndWait(async tab => {
await tab.page.goBack();
const code = [
`// Navigate back`,
`await page.goBack();`,
];
return { code };
}, {
status: 'Navigated back',
captureSnapshot: snapshot,
});
},
@@ -73,8 +81,12 @@ const goForward: ToolFactory = snapshot => ({
handle: async context => {
return await context.currentTab().runAndWait(async tab => {
await tab.page.goForward();
const code = [
`// Navigate forward`,
`await page.goForward();`,
];
return { code };
}, {
status: 'Navigated forward',
captureSnapshot: snapshot,
});
},

View File

@@ -79,11 +79,16 @@ const click: Tool = {
handle: async (context, params) => {
return await context.currentTab().runAndWait(async tab => {
const validatedParams = clickSchema.parse(params);
const code = [
`// Click mouse at coordinates (${validatedParams.x}, ${validatedParams.y})`,
`await page.mouse.move(${validatedParams.x}, ${validatedParams.y});`,
`await page.mouse.down();`,
`await page.mouse.up();`,
];
await tab.page.mouse.move(validatedParams.x, validatedParams.y);
await tab.page.mouse.down();
await tab.page.mouse.up();
}, {
status: 'Clicked mouse',
return { code };
});
},
};
@@ -110,8 +115,14 @@ const drag: Tool = {
await tab.page.mouse.down();
await tab.page.mouse.move(validatedParams.endX, validatedParams.endY);
await tab.page.mouse.up();
}, {
status: `Dragged mouse from (${validatedParams.startX}, ${validatedParams.startY}) to (${validatedParams.endX}, ${validatedParams.endY})`,
const code = [
`// Drag mouse from (${validatedParams.startX}, ${validatedParams.startY}) to (${validatedParams.endX}, ${validatedParams.endY})`,
`await page.mouse.move(${validatedParams.startX}, ${validatedParams.startY});`,
`await page.mouse.down();`,
`await page.mouse.move(${validatedParams.endX}, ${validatedParams.endY});`,
`await page.mouse.up();`,
];
return { code };
});
},
};
@@ -132,11 +143,17 @@ const type: Tool = {
handle: async (context, params) => {
const validatedParams = typeSchema.parse(params);
return await context.currentTab().runAndWait(async tab => {
const code = [
`// Type ${validatedParams.text}`,
`await page.keyboard.type('${validatedParams.text}');`,
];
await tab.page.keyboard.type(validatedParams.text);
if (validatedParams.submit)
if (validatedParams.submit) {
code.push(`// Submit text`);
code.push(`await page.keyboard.press('Enter');`);
await tab.page.keyboard.press('Enter');
}, {
status: `Typed text "${validatedParams.text}"`,
}
return { code };
});
},
};

View File

@@ -19,6 +19,8 @@ import zodToJsonSchema from 'zod-to-json-schema';
import type * as playwright from 'playwright';
import type { Tool } from './tool';
import { generateLocator } from '../context';
import * as javascript from '../javascript';
const snapshot: Tool = {
capability: 'core',
@@ -30,7 +32,10 @@ const snapshot: Tool = {
handle: async context => {
const tab = await context.ensureTab();
return await tab.run(async () => {}, { captureSnapshot: true });
return await tab.run(async () => {
const code = [`// <internal code to capture accessibility snapshot>`];
return { code };
}, { captureSnapshot: true });
},
};
@@ -51,9 +56,12 @@ const click: Tool = {
const validatedParams = elementSchema.parse(params);
return await context.currentTab().runAndWaitWithSnapshot(async snapshot => {
const locator = snapshot.refLocator(validatedParams.ref);
const code = [
`// Click ${validatedParams.element}`,
`await page.${await generateLocator(locator)}.click();`
];
await locator.click();
}, {
status: `Clicked "${validatedParams.element}"`,
return { code };
});
},
};
@@ -78,9 +86,12 @@ const drag: Tool = {
return await context.currentTab().runAndWaitWithSnapshot(async snapshot => {
const startLocator = snapshot.refLocator(validatedParams.startRef);
const endLocator = snapshot.refLocator(validatedParams.endRef);
const code = [
`// Drag ${validatedParams.startElement} to ${validatedParams.endElement}`,
`await page.${await generateLocator(startLocator)}.dragTo(page.${await generateLocator(endLocator)});`
];
await startLocator.dragTo(endLocator);
}, {
status: `Dragged "${validatedParams.startElement}" to "${validatedParams.endElement}"`,
return { code };
});
},
};
@@ -97,9 +108,12 @@ const hover: Tool = {
const validatedParams = elementSchema.parse(params);
return await context.currentTab().runAndWaitWithSnapshot(async snapshot => {
const locator = snapshot.refLocator(validatedParams.ref);
const code = [
`// Hover over ${validatedParams.element}`,
`await page.${await generateLocator(locator)}.hover();`
];
await locator.hover();
}, {
status: `Hovered over "${validatedParams.element}"`,
return { code };
});
},
};
@@ -122,14 +136,23 @@ const type: Tool = {
const validatedParams = typeSchema.parse(params);
return await context.currentTab().runAndWaitWithSnapshot(async snapshot => {
const locator = snapshot.refLocator(validatedParams.ref);
if (validatedParams.slowly)
const code: string[] = [];
if (validatedParams.slowly) {
code.push(`// Press "${validatedParams.text}" sequentially into "${validatedParams.element}"`);
code.push(`await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(validatedParams.text)});`);
await locator.pressSequentially(validatedParams.text);
else
} else {
code.push(`// Fill "${validatedParams.text}" into "${validatedParams.element}"`);
code.push(`await page.${await generateLocator(locator)}.fill(${javascript.quote(validatedParams.text)});`);
await locator.fill(validatedParams.text);
if (validatedParams.submit)
}
if (validatedParams.submit) {
code.push(`// Submit text`);
code.push(`await page.${await generateLocator(locator)}.press('Enter');`);
await locator.press('Enter');
}, {
status: `Typed "${validatedParams.text}" into "${validatedParams.element}"`,
}
return { code };
});
},
};
@@ -150,9 +173,12 @@ const selectOption: Tool = {
const validatedParams = selectOptionSchema.parse(params);
return await context.currentTab().runAndWaitWithSnapshot(async snapshot => {
const locator = snapshot.refLocator(validatedParams.ref);
const code = [
`// Select options [${validatedParams.values.join(', ')}] in ${validatedParams.element}`,
`await page.${await generateLocator(locator)}.selectOption(${javascript.formatObject(validatedParams.values)});`
];
await locator.selectOption(validatedParams.values);
}, {
status: `Selected option in "${validatedParams.element}"`,
return { code };
});
},
};

View File

@@ -51,7 +51,12 @@ const selectTab: ToolFactory = captureSnapshot => ({
const validatedParams = selectTabSchema.parse(params);
await context.selectTab(validatedParams.index);
const currentTab = await context.ensureTab();
return await currentTab.run(async () => {}, { captureSnapshot });
return await currentTab.run(async () => {
const code = [
`// <internal code to select tab ${validatedParams.index}>`,
];
return { code };
}, { captureSnapshot });
},
});
@@ -71,7 +76,12 @@ const newTab: Tool = {
await context.newTab();
if (validatedParams.url)
await context.currentTab().navigate(validatedParams.url);
return await context.currentTab().run(async () => {}, { captureSnapshot: true });
return await context.currentTab().run(async () => {
const code = [
`// <internal code to open a new tab>`,
];
return { code };
}, { captureSnapshot: true });
},
};
@@ -90,8 +100,14 @@ const closeTab: ToolFactory = captureSnapshot => ({
const validatedParams = closeTabSchema.parse(params);
await context.closeTab(validatedParams.index);
const currentTab = context.currentTab();
if (currentTab)
return await currentTab.run(async () => {}, { captureSnapshot });
if (currentTab) {
return await currentTab.run(async () => {
const code = [
`// <internal code to close tab ${validatedParams.index}>`,
];
return { code };
}, { captureSnapshot });
}
return {
content: [{
type: 'text',

View File

@@ -24,7 +24,11 @@ test('browser_navigate', async ({ client }) => {
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
},
})).toHaveTextContent(`
Navigated to data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
- Ran code:
\`\`\`js
// Navigate to data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
await page.goto('data:text/html,<html><title>Title</title><body>Hello, world!</body></html>');
\`\`\`
- Page URL: data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
- Page Title: Title
@@ -50,7 +54,12 @@ test('browser_click', async ({ client }) => {
element: 'Submit button',
ref: 's1e3',
},
})).toHaveTextContent(`Clicked "Submit button"
})).toHaveTextContent(`
- Ran code:
\`\`\`js
// Click Submit button
await page.getByRole('button', { name: 'Submit' }).click();
\`\`\`
- Page URL: data:text/html,<html><title>Title</title><button>Submit</button></html>
- Page Title: Title
@@ -77,7 +86,12 @@ test('browser_select_option', async ({ client }) => {
ref: 's1e3',
values: ['bar'],
},
})).toHaveTextContent(`Selected option in "Select"
})).toHaveTextContent(`
- Ran code:
\`\`\`js
// Select options [bar] in Select
await page.getByRole('combobox').selectOption(['bar']);
\`\`\`
- Page URL: data:text/html,<html><title>Title</title><select><option value="foo">Foo</option><option value="bar">Bar</option></select></html>
- Page Title: Title
@@ -105,7 +119,12 @@ test('browser_select_option (multiple)', async ({ client }) => {
ref: 's1e3',
values: ['bar', 'baz'],
},
})).toHaveTextContent(`Selected option in "Select"
})).toHaveTextContent(`
- Ran code:
\`\`\`js
// Select options [bar, baz] in Select
await page.getByRole('listbox').selectOption(['bar', 'baz']);
\`\`\`
- Page URL: data:text/html,<html><title>Title</title><select multiple><option value="foo">Foo</option><option value="bar">Bar</option><option value="baz">Baz</option></select></html>
- Page Title: Title
@@ -249,6 +268,10 @@ test('browser_resize', async ({ client }) => {
height: 780,
},
});
expect(response).toContainTextContent('Resized browser window');
expect(response).toContainTextContent('Window size: 390x780');
expect(response).toContainTextContent(`- Ran code:
\`\`\`js
// Resize browser window to 390x780
await page.setViewportSize({ width: 390, height: 780 });
\`\`\``);
await expect.poll(() => client.callTool({ name: 'browser_snapshot' })).toContainTextContent('Window size: 390x780');
});

View File

@@ -23,17 +23,7 @@ test('cdp server', async ({ cdpEndpoint, startClient }) => {
arguments: {
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
},
})).toHaveTextContent(`
Navigated to data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
- Page URL: data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
- Page Title: Title
- Page Snapshot
\`\`\`yaml
- text: Hello, world!
\`\`\`
`
);
})).toContainTextContent(`- text: Hello, world!`);
});
test('cdp server reuse tab', async ({ cdpEndpoint, startClient }) => {
@@ -51,6 +41,11 @@ test('cdp server reuse tab', async ({ cdpEndpoint, startClient }) => {
name: 'browser_snapshot',
arguments: {},
})).toHaveTextContent(`
- Ran code:
\`\`\`js
// <internal code to capture accessibility snapshot>
\`\`\`
- Page URL: data:text/html,hello world
- Page Title:
- Page Snapshot

View File

@@ -28,6 +28,10 @@ type Fixtures = {
startClient: (options?: { args?: string[] }) => Promise<Client>;
wsEndpoint: string;
cdpEndpoint: string;
// Cli options.
mcpHeadless: boolean;
mcpBrowser: string | undefined;
};
export const test = baseTest.extend<Fixtures>({
@@ -40,12 +44,16 @@ export const test = baseTest.extend<Fixtures>({
await use(await startClient({ args: ['--vision'] }));
},
startClient: async ({ }, use, testInfo) => {
startClient: async ({ mcpHeadless, mcpBrowser }, use, testInfo) => {
const userDataDir = testInfo.outputPath('user-data-dir');
let client: StdioClientTransport | undefined;
use(async options => {
const args = ['--headless', '--user-data-dir', userDataDir];
const args = ['--user-data-dir', userDataDir];
if (mcpHeadless)
args.push('--headless');
if (mcpBrowser)
args.push(`--browser=${mcpBrowser}`);
if (options?.args)
args.push(...options.args);
const transport = new StdioClientTransport({
@@ -89,6 +97,12 @@ export const test = baseTest.extend<Fixtures>({
await use(`http://localhost:${port}`);
browserProcess.kill();
},
mcpHeadless: async ({ headless }, use) => {
await use(headless);
},
mcpBrowser: ['chromium', { option: true }],
});
type Response = Awaited<ReturnType<Client['callTool']>>;

View File

@@ -39,5 +39,5 @@ test('stitched aria frames', async ({ client }) => {
element: 'World',
ref: 'f1s1e3',
},
})).toContainTextContent('Clicked "World"');
})).toContainTextContent(`// Click World`);
});

View File

@@ -33,16 +33,7 @@ test('test reopen browser', async ({ client }) => {
arguments: {
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
},
})).toHaveTextContent(`
Navigated to data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
- Page URL: data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
- Page Title: Title
- Page Snapshot
\`\`\`yaml
- text: Hello, world!
\`\`\`
`);
})).toContainTextContent(`- text: Hello, world!`);
});
test('executable path', async ({ startClient }) => {

View File

@@ -30,23 +30,14 @@ test('save as pdf unavailable', async ({ startClient }) => {
})).toHaveTextContent(/Tool \"browser_pdf_save\" not found/);
});
test('save as pdf', async ({ client }) => {
test('save as pdf', async ({ client, mcpBrowser }) => {
test.skip(!!mcpBrowser && !['chromium', 'chrome', 'msedge'].includes(mcpBrowser), 'Save as PDF is only supported in Chromium.');
expect(await client.callTool({
name: 'browser_navigate',
arguments: {
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
},
})).toHaveTextContent(`
Navigated to data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
- Page URL: data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
- Page Title: Title
- Page Snapshot
\`\`\`yaml
- text: Hello, world!
\`\`\`
`
);
})).toContainTextContent(`- text: Hello, world!`);
const response = await client.callTool({
name: 'browser_pdf_save',

View File

@@ -31,11 +31,16 @@ async function createTab(client: Client, title: string, body: string) {
test('create new tab', async ({ client }) => {
expect(await createTab(client, 'Tab one', 'Body one')).toHaveTextContent(`
Open tabs:
- Ran code:
\`\`\`js
// <internal code to open a new tab>
\`\`\`
### Open tabs
- 1: [] (about:blank)
- 2: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)
Current tab:
### Current tab
- Page URL: data:text/html,<title>Tab one</title><body>Body one</body>
- Page Title: Tab one
- Page Snapshot
@@ -44,12 +49,17 @@ Current tab:
\`\`\``);
expect(await createTab(client, 'Tab two', 'Body two')).toHaveTextContent(`
Open tabs:
- Ran code:
\`\`\`js
// <internal code to open a new tab>
\`\`\`
### Open tabs
- 1: [] (about:blank)
- 2: [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)
- 3: (current) [Tab two] (data:text/html,<title>Tab two</title><body>Body two</body>)
Current tab:
### Current tab
- Page URL: data:text/html,<title>Tab two</title><body>Body two</body>
- Page Title: Tab two
- Page Snapshot
@@ -67,12 +77,17 @@ test('select tab', async ({ client }) => {
index: 2,
},
})).toHaveTextContent(`
Open tabs:
- Ran code:
\`\`\`js
// <internal code to select tab 2>
\`\`\`
### Open tabs
- 1: [] (about:blank)
- 2: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)
- 3: [Tab two] (data:text/html,<title>Tab two</title><body>Body two</body>)
Current tab:
### Current tab
- Page URL: data:text/html,<title>Tab one</title><body>Body one</body>
- Page Title: Tab one
- Page Snapshot
@@ -90,11 +105,16 @@ test('close tab', async ({ client }) => {
index: 3,
},
})).toHaveTextContent(`
Open tabs:
- Ran code:
\`\`\`js
// <internal code to close tab 3>
\`\`\`
### Open tabs
- 1: [] (about:blank)
- 2: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)
Current tab:
### Current tab
- Page URL: data:text/html,<title>Tab one</title><body>Body one</body>
- Page Title: Tab one
- Page Snapshot