Compare commits

..

5 Commits

Author SHA1 Message Date
Simon Knott
621590897f bump to 1s everywhere 2025-06-11 16:06:52 +02:00
Simon Knott
43d4d6f0bb increase download waiting 2025-06-11 15:32:04 +02:00
Simon Knott
6c98594b7a Revert "fix test"
This reverts commit ab696c2b10.
2025-06-11 15:21:56 +02:00
Simon Knott
ab696c2b10 fix test 2025-06-11 15:10:17 +02:00
Dmitry Gozman
77946c0f5d chore: roll playwright to Jun 10th, v1.53 2025-06-11 12:07:50 +01:00
23 changed files with 142 additions and 388 deletions

View File

@@ -44,7 +44,6 @@ jobs:
- name: Login to ACR
run: az acr login --name playwright
- name: Build and push Docker image
id: build-push
uses: docker/build-push-action@v6
with:
context: .
@@ -54,17 +53,3 @@ jobs:
tags: |
playwright.azurecr.io/public/playwright/mcp:${{ github.event.release.tag_name }}
playwright.azurecr.io/public/playwright/mcp:latest
- uses: oras-project/setup-oras@v1
- name: Set oras tags
run: |
attach_eol_manifest() {
local image="$1"
local today=$(date -u +'%Y-%m-%d')
# oras is re-using Docker credentials, so we don't need to login.
# Following the advice in https://portal.microsofticm.com/imp/v3/incidents/incident/476783820/summary
oras attach --artifact-type application/vnd.microsoft.artifact.lifecycle --annotation "vnd.microsoft.artifact.lifecycle.end-of-life.date=$today" $image
}
# for each tag, attach the eol manifest
for tag in $(echo ${{ steps.build-push.outputs.metadata['image.name'] }} | tr ',' '\n'); do
attach_eol_manifest $tag
done

View File

@@ -10,7 +10,7 @@ A Model Context Protocol (MCP) server that provides browser automation capabilit
### Requirements
- Node.js 18 or newer
- VS Code, Cursor, Windsurf, Claude Desktop, Goose or any other MCP client
- VS Code, Cursor, Windsurf, Claude Desktop or any other MCP client
<!--
// Generate using:
@@ -77,7 +77,7 @@ Go to `Cursor Settings` -> `MCP` -> `Add new MCP Server`. Name to your liking, u
<details>
<summary><b>Install in Windsurf</b></summary>
Follow Windsurf MCP [documentation](https://docs.windsurf.com/windsurf/cascade/mcp). Use following configuration:
Follow Windsuff MCP [documentation](https://docs.windsurf.com/windsurf/cascade/mcp). Use following configuration:
```js
{
@@ -112,28 +112,6 @@ Follow the MCP install [guide](https://modelcontextprotocol.io/quickstart/user),
```
</details>
<details>
<summary><b>Install in Claude Code</b></summary>
Use the Claude Code CLI to add the Playwright MCP server:
```bash
claude mcp add playwright npx @playwright/mcp@latest
```
</details>
<details>
<summary><b>Install in Goose</b></summary>
#### Click the button to install:
[![Install in Goose](https://block.github.io/goose/img/extension-install-dark.svg)](https://block.github.io/goose/extension?cmd=npx&arg=%40playwright%2Fmcp%40latest&id=playwright&name=Playwright&description=Interact%20with%20web%20pages%20through%20structured%20accessibility%20snapshots%20using%20Playwright)
#### Or install manually:
Go to `Advanced settings` -> `Extensions` -> `Add custom extension`. Name to your liking, use type `STDIO`, and set the `command` to `npx @playwright/mcp`. Click "Add Extension".
</details>
<details>
<summary><b>Install in Qodo Gen</b></summary>
@@ -155,25 +133,6 @@ Open [Qodo Gen](https://docs.qodo.ai/qodo-documentation/qodo-gen) chat panel in
Click <code>Save</code>.
</details>
<details>
<summary><b>Install in Gemini CLI</b></summary>
Follow the MCP install [guide](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md#configure-the-mcp-server-in-settingsjson), use following configuration:
```js
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp@latest"
]
}
}
}
```
</details>
### Configuration
Playwright MCP server supports following arguments. They can be provided in the JSON configuration above, as a part of the `"args"` list:
@@ -357,10 +316,9 @@ npx @playwright/mcp@latest --config path/to/config.json
};
/**
* Whether to send image responses to the client. Can be "allow", "omit", or "auto".
* Defaults to "auto", images are omitted for Cursor clients and sent for all other clients.
* Do not send image responses to the client.
*/
imageResponses?: 'allow' | 'omit' | 'auto';
noImageResponses?: boolean;
}
```
</details>
@@ -478,7 +436,6 @@ X Y coordinate space, based on the provided screenshot.
- Parameters:
- `element` (string): Human-readable element description used to obtain permission to interact with the element
- `ref` (string): Exact target element reference from the page snapshot
- `doubleClick` (boolean, optional): Whether to perform a double click instead of a single click
- Read-only: **false**
<!-- NOTE: This has been generated via update-readme.js -->

103
package-lock.json generated
View File

@@ -1,20 +1,19 @@
{
"name": "@playwright/mcp",
"version": "0.0.30",
"version": "0.0.29",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@playwright/mcp",
"version": "0.0.30",
"version": "0.0.29",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.11.0",
"commander": "^13.1.0",
"debug": "^4.4.1",
"mime": "^4.0.7",
"playwright": "1.54.1",
"ws": "^8.18.1",
"playwright": "1.53.0",
"zod-to-json-schema": "^3.24.4"
},
"bin": {
@@ -23,12 +22,10 @@
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.19.0",
"@playwright/test": "1.54.1",
"@playwright/test": "1.53.0",
"@stylistic/eslint-plugin": "^3.0.1",
"@types/chrome": "^0.0.315",
"@types/debug": "^4.1.12",
"@types/node": "^22.13.10",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"@typescript-eslint/utils": "^8.26.1",
@@ -292,13 +289,12 @@
}
},
"node_modules/@playwright/test": {
"version": "1.54.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz",
"integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==",
"version": "1.53.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.0.tgz",
"integrity": "sha512-15hjKreZDcp7t6TL/7jkAo6Df5STZN09jGiv5dbP9A6vMVncXRqE7/B2SncsyOwrkZRBH2i6/TPOL8BVmm3c7w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.54.1"
"playwright": "1.53.0"
},
"bin": {
"playwright": "cli.js"
@@ -360,17 +356,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@types/chrome": {
"version": "0.0.315",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.315.tgz",
"integrity": "sha512-Oy1dYWkr6BCmgwBtOngLByCHstQ3whltZg7/7lubgIZEYvKobDneqplgc6LKERNRBwckFviV4UU5AZZNUFrJ4A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/filesystem": "*",
"@types/har-format": "*"
}
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -388,30 +373,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/filesystem": {
"version": "0.0.36",
"resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz",
"integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/filewriter": "*"
}
},
"node_modules/@types/filewriter": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz",
"integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/har-format": {
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz",
"integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -443,16 +404,6 @@
"undici-types": "~6.20.0"
}
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.27.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.27.0.tgz",
@@ -2033,7 +1984,6 @@
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -3300,12 +3250,11 @@
}
},
"node_modules/playwright": {
"version": "1.54.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz",
"integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==",
"license": "Apache-2.0",
"version": "1.53.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.0.tgz",
"integrity": "sha512-ghGNnIEYZC4E+YtclRn4/p6oYbdPiASELBIYkBXfaTVKreQUYbMUYQDwS12a8F0/HtIjr/CkGjtwABeFPGcS4Q==",
"dependencies": {
"playwright-core": "1.54.1"
"playwright-core": "1.53.0"
},
"bin": {
"playwright": "cli.js"
@@ -3318,10 +3267,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.54.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz",
"integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==",
"license": "Apache-2.0",
"version": "1.53.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.0.tgz",
"integrity": "sha512-mGLg8m0pm4+mmtB7M89Xw/GSqoNC+twivl8ITteqvAndachozYe2ZA7srU6uleV1vEdAHYqjq+SV8SNxRRFYBw==",
"bin": {
"playwright-core": "cli.js"
},
@@ -4295,27 +4243,6 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@playwright/mcp",
"version": "0.0.30",
"version": "0.0.29",
"description": "Playwright Tools for MCP",
"type": "module",
"repository": {
@@ -40,19 +40,16 @@
"commander": "^13.1.0",
"debug": "^4.4.1",
"mime": "^4.0.7",
"playwright": "1.54.1",
"ws": "^8.18.1",
"playwright": "1.53.0",
"zod-to-json-schema": "^3.24.4"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.19.0",
"@playwright/test": "1.54.1",
"@playwright/test": "1.53.0",
"@stylistic/eslint-plugin": "^3.0.1",
"@types/chrome": "^0.0.315",
"@types/debug": "^4.1.12",
"@types/node": "^22.13.10",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"@typescript-eslint/utils": "^8.26.1",

View File

@@ -142,9 +142,6 @@ export async function configFromCLIOptions(cliOptions: CLIOptions): Promise<Conf
launchOptions.proxy.bypass = cliOptions.proxyBypass;
}
if (cliOptions.device && cliOptions.cdpEndpoint)
throw new Error('Device emulation is not supported with cdpEndpoint.');
// Context options
const contextOptions: BrowserContextOptions = cliOptions.device ? devices[cliOptions.device] : {};
if (cliOptions.storageState)

View File

@@ -29,6 +29,7 @@ import type { BrowserContextFactory } from './browserContextFactory.js';
export function createConnection(config: FullConfig, browserContextFactory: BrowserContextFactory): Connection {
const allTools = config.vision ? visionTools : snapshotTools;
const tools = allTools.filter(tool => !config.capabilities || tool.capability === 'core' || config.capabilities.includes(tool.capability));
const context = new Context(tools, config, browserContextFactory);
const server = new McpServer({ name: 'Playwright', version: packageJSON.version }, {
capabilities: {

View File

@@ -18,7 +18,7 @@ import { program } from 'commander';
// @ts-ignore
import { startTraceViewerServer } from 'playwright-core/lib/server';
import { startHttpServer, startHttpTransport, startStdioTransport } from './transport.js';
import { startHttpTransport, startStdioTransport } from './transport.js';
import { resolveCLIConfig } from './config.js';
import { Server } from './server.js';
import { packageJSON } from './package.js';
@@ -54,13 +54,11 @@ program
.option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
.action(async options => {
const config = await resolveCLIConfig(options);
const httpServer = config.server.port !== undefined ? await startHttpServer(config.server) : undefined;
const server = new Server(config);
server.setupExitWatchdog();
if (httpServer)
startHttpTransport(httpServer, server);
if (config.server.port !== undefined)
startHttpTransport(server);
else
await startStdioTransport(server);

36
src/resources/resource.ts Normal file
View File

@@ -0,0 +1,36 @@
/**
* 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.
*/
import type { Context } from '../context.js';
export type ResourceSchema = {
uri: string;
name: string;
description?: string;
mimeType?: string;
};
export type ResourceResult = {
uri: string;
mimeType?: string;
text?: string;
blob?: string;
};
export type Resource = {
schema: ResourceSchema;
read: (context: Context, uri: string) => Promise<ResourceResult[]>;
};

View File

@@ -46,17 +46,13 @@ const elementSchema = z.object({
ref: z.string().describe('Exact target element reference from the page snapshot'),
});
const clickSchema = elementSchema.extend({
doubleClick: z.boolean().optional().describe('Whether to perform a double click instead of a single click'),
});
const click = defineTool({
capability: 'core',
schema: {
name: 'browser_click',
title: 'Click',
description: 'Perform click on a web page',
inputSchema: clickSchema,
inputSchema: elementSchema,
type: 'destructive',
},
@@ -64,18 +60,14 @@ const click = defineTool({
const tab = context.currentTabOrDie();
const locator = tab.snapshotOrDie().refLocator(params);
const code: string[] = [];
if (params.doubleClick) {
code.push(`// Double click ${params.element}`);
code.push(`await page.${await generateLocator(locator)}.dblclick();`);
} else {
code.push(`// Click ${params.element}`);
code.push(`await page.${await generateLocator(locator)}.click();`);
}
const code = [
`// Click ${params.element}`,
`await page.${await generateLocator(locator)}.click();`
];
return {
code,
action: () => params.doubleClick ? locator.dblclick() : locator.click(),
action: () => locator.click(),
captureSnapshot: true,
waitForNetwork: true,
};

View File

@@ -78,13 +78,7 @@ export function sanitizeForFilePath(s: string) {
}
export async function generateLocator(locator: playwright.Locator): Promise<string> {
try {
return await (locator as any)._generateLocatorString();
} catch (e) {
if (e instanceof Error && /locator._generateLocatorString: No element matching locator/.test(e.message))
throw new Error('Ref not found, likely because element was removed. Use browser_snapshot to see what elements are currently on the page.');
throw e;
}
return (locator as any)._generateLocatorString();
}
export async function callOnPageNoTrace<T>(page: playwright.Page, callback: (page: playwright.Page) => Promise<T>): Promise<T> {

View File

@@ -23,7 +23,6 @@ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import type { AddressInfo } from 'node:net';
import type { Server } from './server.js';
export async function startStdioTransport(server: Server) {
@@ -97,53 +96,43 @@ async function handleStreamable(server: Server, req: http.IncomingMessage, res:
res.end('Invalid request');
}
export async function startHttpServer(config: { host?: string, port?: number }): Promise<http.Server> {
const { host, port } = config;
const httpServer = http.createServer();
await new Promise<void>((resolve, reject) => {
httpServer.on('error', reject);
httpServer.listen(port, host, () => {
resolve();
httpServer.removeListener('error', reject);
});
});
return httpServer;
}
export function startHttpTransport(httpServer: http.Server, mcpServer: Server) {
export function startHttpTransport(server: Server) {
const sseSessions = new Map<string, SSEServerTransport>();
const streamableSessions = new Map<string, StreamableHTTPServerTransport>();
httpServer.on('request', async (req, res) => {
const httpServer = http.createServer(async (req, res) => {
const url = new URL(`http://localhost${req.url}`);
if (url.pathname.startsWith('/mcp'))
await handleStreamable(mcpServer, req, res, streamableSessions);
await handleStreamable(server, req, res, streamableSessions);
else
await handleSSE(mcpServer, req, res, url, sseSessions);
await handleSSE(server, req, res, url, sseSessions);
});
const url = httpAddressToString(httpServer.address());
const message = [
`Listening on ${url}`,
'Put this in your client config:',
JSON.stringify({
'mcpServers': {
'playwright': {
'url': `${url}/sse`
const { host, port } = server.config.server;
httpServer.listen(port, host, () => {
const address = httpServer.address();
assert(address, 'Could not bind server socket');
let url: string;
if (typeof address === 'string') {
url = address;
} else {
const resolvedPort = address.port;
let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]')
resolvedHost = 'localhost';
url = `http://${resolvedHost}:${resolvedPort}`;
}
const message = [
`Listening on ${url}`,
'Put this in your client config:',
JSON.stringify({
'mcpServers': {
'playwright': {
'url': `${url}/sse`
}
}
}
}, undefined, 2),
'If your client supports streamable HTTP, you can use the /mcp endpoint instead.',
].join('\n');
}, undefined, 2),
'If your client supports streamable HTTP, you can use the /mcp endpoint instead.',
].join('\n');
// eslint-disable-next-line no-console
console.error(message);
}
export function httpAddressToString(address: string | AddressInfo | null): string {
assert(address, 'Could not bind server socket');
if (typeof address === 'string')
return address;
const resolvedPort = address.port;
let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]')
resolvedHost = 'localhost';
return `http://${resolvedHost}:${resolvedPort}`;
console.error(message);
});
}

View File

@@ -14,9 +14,6 @@
* limitations under the License.
*/
import url from 'node:url';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { test, expect } from './fixtures.js';
test('cdp server', async ({ cdpServer, startClient, server }) => {
@@ -25,7 +22,7 @@ test('cdp server', async ({ cdpServer, startClient, server }) => {
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`- generic [active] [ref=e1]: Hello, world!`);
})).toContainTextContent(`- generic [ref=e1]: Hello, world!`);
});
test('cdp server reuse tab', async ({ cdpServer, startClient, server }) => {
@@ -55,7 +52,7 @@ test('cdp server reuse tab', async ({ cdpServer, startClient, server }) => {
- Page Title: Title
- Page Snapshot
\`\`\`yaml
- generic [active] [ref=e1]: Hello, world!
- generic [ref=e1]: Hello, world!
\`\`\`
`);
});
@@ -76,17 +73,5 @@ test('should throw connection error and allow re-connecting', async ({ cdpServer
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toContainTextContent(`- generic [active] [ref=e1]: Hello, world!`);
});
// NOTE: Can be removed when we drop Node.js 18 support and changed to import.meta.filename.
const __filename = url.fileURLToPath(import.meta.url);
test('does not support --device', async () => {
const result = spawnSync('node', [
path.join(__filename, '../../cli.js'), '--device=Pixel 5', '--cdp-endpoint=http://localhost:1234',
]);
expect(result.error).toBeUndefined();
expect(result.status).toBe(1);
expect(result.stderr.toString()).toContain('Device emulation is not supported with cdpEndpoint.');
})).toContainTextContent(`- generic [ref=e1]: Hello, world!`);
});

View File

@@ -19,7 +19,7 @@ import fs from 'node:fs';
import { Config } from '../config.js';
import { test, expect } from './fixtures.js';
test('config user data dir', async ({ startClient, server, mcpMode }, testInfo) => {
test('config user data dir', async ({ startClient, server }, testInfo) => {
server.setContent('/', `
<title>Title</title>
<body>Hello, world!</body>
@@ -45,7 +45,7 @@ test('config user data dir', async ({ startClient, server, mcpMode }, testInfo)
test.describe(() => {
test.use({ mcpBrowser: '' });
test('browserName', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/458' } }, async ({ startClient, mcpMode }, testInfo) => {
test('browserName', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/458' } }, async ({ startClient }, testInfo) => {
const config: Config = {
browser: {
browserName: 'firefox',

View File

@@ -31,13 +31,13 @@ await page.goto('${server.HELLO_WORLD}');
- Page Title: Title
- Page Snapshot
\`\`\`yaml
- generic [active] [ref=e1]: Hello, world!
- generic [ref=e1]: Hello, world!
\`\`\`
`
);
});
test('browser_click', async ({ client, server, mcpBrowser }) => {
test('browser_click', async ({ client, server }) => {
server.setContent('/', `
<title>Title</title>
<button>Submit</button>
@@ -65,46 +65,7 @@ await page.getByRole('button', { name: 'Submit' }).click();
- Page Title: Title
- Page Snapshot
\`\`\`yaml
- button "Submit" ${mcpBrowser !== 'webkit' || process.platform === 'linux' ? '[active] ' : ''}[ref=e2]
\`\`\`
`);
});
test('browser_click (double)', async ({ client, server }) => {
server.setContent('/', `
<title>Title</title>
<script>
function handle() {
document.querySelector('h1').textContent = 'Double clicked';
}
</script>
<h1 ondblclick="handle()">Click me</h1>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_click',
arguments: {
element: 'Click me',
ref: 'e2',
doubleClick: true,
},
})).toHaveTextContent(`
- Ran Playwright code:
\`\`\`js
// Double click Click me
await page.getByRole('heading', { name: 'Click me' }).dblclick();
\`\`\`
- Page URL: ${server.PREFIX}
- Page Title: Title
- Page Snapshot
\`\`\`yaml
- heading "Double clicked" [level=1] [ref=e3]
- button "Submit" [ref=e2]
\`\`\`
`);
});
@@ -276,60 +237,3 @@ await page.setViewportSize({ width: 390, height: 780 });
\`\`\``);
await expect.poll(() => client.callTool({ name: 'browser_snapshot' })).toContainTextContent('Window size: 390x780');
});
test('old locator error message', async ({ client, server }) => {
server.setContent('/', `
<button>Button 1</button>
<button>Button 2</button>
<script>
document.querySelector('button').addEventListener('click', () => {
document.querySelectorAll('button')[1].remove();
});
</script>
`, 'text/html');
expect(await client.callTool({
name: 'browser_navigate',
arguments: {
url: server.PREFIX,
},
})).toContainTextContent(`
- button "Button 1" [ref=e2]
- button "Button 2" [ref=e3]
`.trim());
await client.callTool({
name: 'browser_click',
arguments: {
element: 'Button 1',
ref: 'e2',
},
});
expect(await client.callTool({
name: 'browser_click',
arguments: {
element: 'Button 2',
ref: 'e3',
},
})).toContainTextContent('Ref not found');
});
test('visibility: hidden > visible should be shown', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/535' } }, async ({ client, server }) => {
server.setContent('/', `
<div style="visibility: hidden;">
<div style="visibility: visible;">
<button>Button</button>
</div>
</div>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_snapshot'
})).toContainTextContent('- button "Button"');
});

View File

@@ -16,7 +16,7 @@
import { test, expect } from './fixtures.js';
test('--device should work', async ({ startClient, server, mcpMode }) => {
test('--device should work', async ({ startClient, server }) => {
const { client } = await startClient({
args: ['--device', 'iPhone 15'],
});

View File

@@ -58,7 +58,7 @@ await page.getByRole('button', { name: 'Button' }).click();
- Page Title:
- Page Snapshot
\`\`\`yaml
- button "Button" [active] [ref=e2]
- button "Button" [ref=e2]
\`\`\`
`);
});
@@ -136,7 +136,7 @@ test('confirm dialog (true)', async ({ client, server }) => {
expect(result).toContainTextContent('// <internal code to handle "confirm" dialog>');
expect(result).toContainTextContent(`- Page Snapshot
\`\`\`yaml
- generic [active] [ref=e1]: "true"
- generic [ref=e1]: "true"
\`\`\``);
});
@@ -171,7 +171,7 @@ test('confirm dialog (false)', async ({ client, server }) => {
expect(result).toContainTextContent(`- Page Snapshot
\`\`\`yaml
- generic [active] [ref=e1]: "false"
- generic [ref=e1]: "false"
\`\`\``);
});
@@ -207,6 +207,6 @@ test('prompt dialog', async ({ client, server }) => {
expect(result).toContainTextContent(`- Page Snapshot
\`\`\`yaml
- generic [active] [ref=e1]: Answer
- generic [ref=e1]: Answer
\`\`\``);
});

View File

@@ -28,7 +28,7 @@ test('browser_file_upload', async ({ client, server }, testInfo) => {
arguments: { url: server.PREFIX },
})).toContainTextContent(`
\`\`\`yaml
- generic [active] [ref=e1]:
- generic [ref=e1]:
- button "Choose File" [ref=e2]
- button "Button" [ref=e3]
\`\`\``);
@@ -65,6 +65,12 @@ The tool "browser_file_upload" can only be used when there is related modal stat
});
expect(response).not.toContainTextContent('### Modal state');
expect(response).toContainTextContent(`
\`\`\`yaml
- generic [ref=e1]:
- button "Choose File" [ref=e2]
- button "Button" [ref=e3]
\`\`\``);
}
{
@@ -94,7 +100,7 @@ The tool "browser_file_upload" can only be used when there is related modal stat
}
});
test('clicking on download link emits download', async ({ startClient, server, mcpMode }, testInfo) => {
test('clicking on download link emits download', async ({ startClient, server }, testInfo) => {
const { client } = await startClient({
config: { outputDir: testInfo.outputPath('output') },
});
@@ -118,7 +124,7 @@ test('clicking on download link emits download', async ({ startClient, server, m
- Downloaded file test.txt to ${testInfo.outputPath('output', 'test.txt')}`);
});
test('navigating to download link emits download', async ({ startClient, server, mcpBrowser, mcpMode }, testInfo) => {
test('navigating to download link emits download', async ({ startClient, server, mcpBrowser }, testInfo) => {
const { client } = await startClient({
config: { outputDir: testInfo.outputPath('output') },
});

View File

@@ -26,8 +26,6 @@ import { TestServer } from './testserver/index.ts';
import type { Config } from '../config';
import type { BrowserContext } from 'playwright';
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import type { Stream } from 'stream';
export type TestOptions = {
mcpBrowser: string | undefined;
@@ -90,16 +88,16 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
}
client = new Client({ name: options?.clientName ?? 'test', version: '1.0.0' });
const { transport, stderr } = await createTransport(args, mcpMode);
let stderrBuffer = '';
stderr?.on('data', data => {
const transport = createTransport(args, mcpMode);
let stderr = '';
transport.stderr?.on('data', data => {
if (process.env.PWMCP_DEBUG)
process.stderr.write(data);
stderrBuffer += data.toString();
stderr += data.toString();
});
await client.connect(transport);
await client.ping();
return { client, stderr: () => stderrBuffer };
return { client, stderr: () => stderr };
});
await client?.close();
@@ -140,7 +138,7 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
mcpMode: [undefined, { option: true }],
_workerServers: [async ({ }, use, workerInfo) => {
_workerServers: [async ({}, use, workerInfo) => {
const port = 8907 + workerInfo.workerIndex * 4;
const server = await TestServer.create(port);
@@ -166,25 +164,17 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
},
});
async function createTransport(args: string[], mcpMode: TestOptions['mcpMode']): Promise<{
transport: Transport,
stderr: Stream | null,
}> {
function createTransport(args: string[], mcpMode: TestOptions['mcpMode']) {
// NOTE: Can be removed when we drop Node.js 18 support and changed to import.meta.filename.
const __filename = url.fileURLToPath(import.meta.url);
if (mcpMode === 'docker') {
const dockerArgs = ['run', '--rm', '-i', '--network=host', '-v', `${test.info().project.outputDir}:/app/test-results`];
const transport = new StdioClientTransport({
return new StdioClientTransport({
command: 'docker',
args: [...dockerArgs, 'playwright-mcp-dev:latest', ...args],
});
return {
transport,
stderr: transport.stderr,
};
}
const transport = new StdioClientTransport({
return new StdioClientTransport({
command: 'node',
args: [path.join(path.dirname(__filename), '../cli.js'), ...args],
cwd: path.join(path.dirname(__filename), '..'),
@@ -196,10 +186,6 @@ async function createTransport(args: string[], mcpMode: TestOptions['mcpMode']):
DEBUG_HIDE_DATE: '1',
},
});
return {
transport,
stderr: transport.stderr!,
};
}
type Response = Awaited<ReturnType<Client['callTool']>>;

View File

@@ -24,10 +24,10 @@ test('stitched aria frames', async ({ client }) => {
},
})).toContainTextContent(`
\`\`\`yaml
- generic [active] [ref=e1]:
- generic [ref=e1]:
- heading "Hello" [level=1] [ref=e2]
- iframe [ref=e3]:
- generic [active] [ref=f1e1]:
- generic [ref=f1e1]:
- button "World" [ref=f1e2]
- main [ref=f1e3]:
- iframe [ref=f1e4]:

View File

@@ -18,7 +18,7 @@ import fs from 'fs';
import { test, expect, formatOutput } from './fixtures.js';
test('test reopen browser', async ({ startClient, server, mcpMode }) => {
test('test reopen browser', async ({ startClient, server }) => {
const { client, stderr } = await startClient();
await client.callTool({
name: 'browser_navigate',
@@ -32,7 +32,7 @@ test('test reopen browser', async ({ startClient, server, mcpMode }) => {
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`- generic [active] [ref=e1]: Hello, world!`);
})).toContainTextContent(`- generic [ref=e1]: Hello, world!`);
await client.close();

View File

@@ -40,7 +40,7 @@ test('save as pdf', async ({ startClient, mcpBrowser, server }, testInfo) => {
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`- generic [active] [ref=e1]: Hello, world!`);
})).toContainTextContent(`- generic [ref=e1]: Hello, world!`);
const response = await client.callTool({
name: 'browser_pdf_save',
@@ -58,7 +58,7 @@ test('save as pdf (filename: output.pdf)', async ({ startClient, mcpBrowser, ser
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`- generic [active] [ref=e1]: Hello, world!`);
})).toContainTextContent(`- generic [ref=e1]: Hello, world!`);
expect(await client.callTool({
name: 'browser_pdf_save',

View File

@@ -59,7 +59,7 @@ test('create new tab', async ({ client }) => {
- Page Title: Tab one
- Page Snapshot
\`\`\`yaml
- generic [active] [ref=e1]: Body one
- generic [ref=e1]: Body one
\`\`\``);
expect(await createTab(client, 'Tab two', 'Body two')).toHaveTextContent(`
@@ -78,7 +78,7 @@ test('create new tab', async ({ client }) => {
- Page Title: Tab two
- Page Snapshot
\`\`\`yaml
- generic [active] [ref=e1]: Body two
- generic [ref=e1]: Body two
\`\`\``);
});
@@ -106,7 +106,7 @@ test('select tab', async ({ client }) => {
- Page Title: Tab one
- Page Snapshot
\`\`\`yaml
- generic [active] [ref=e1]: Body one
- generic [ref=e1]: Body one
\`\`\``);
});
@@ -133,7 +133,7 @@ test('close tab', async ({ client }) => {
- Page Title: Tab one
- Page Snapshot
\`\`\`yaml
- generic [active] [ref=e1]: Body one
- generic [ref=e1]: Body one
\`\`\``);
});

View File

@@ -19,7 +19,7 @@ import path from 'path';
import { test, expect } from './fixtures.js';
test('check that trace is saved', async ({ startClient, server, mcpMode }, testInfo) => {
test('check that trace is saved', async ({ startClient, server }, testInfo) => {
const outputDir = testInfo.outputPath('output');
const { client } = await startClient({