Compare commits

..

1 Commits

Author SHA1 Message Date
Yury Semikhatsky
fd1ef543cb chore: roll 1.59.0-alpha-1774622285000 2026-03-27 18:02:14 -07:00
10 changed files with 60 additions and 215 deletions

View File

@@ -53,6 +53,7 @@ FROM base
ARG PLAYWRIGHT_BROWSERS_PATH ARG PLAYWRIGHT_BROWSERS_PATH
ARG USERNAME=node ARG USERNAME=node
ENV NODE_ENV=production ENV NODE_ENV=production
ENV PLAYWRIGHT_MCP_OUTPUT_DIR=/tmp/playwright-output
# Set the correct ownership for the runtime user on production `node_modules` # Set the correct ownership for the runtime user on production `node_modules`
RUN chown -R ${USERNAME}:${USERNAME} node_modules RUN chown -R ${USERNAME}:${USERNAME} node_modules
@@ -62,8 +63,5 @@ USER ${USERNAME}
COPY --from=browser --chown=${USERNAME}:${USERNAME} ${PLAYWRIGHT_BROWSERS_PATH} ${PLAYWRIGHT_BROWSERS_PATH} COPY --from=browser --chown=${USERNAME}:${USERNAME} ${PLAYWRIGHT_BROWSERS_PATH} ${PLAYWRIGHT_BROWSERS_PATH}
COPY --chown=${USERNAME}:${USERNAME} packages/playwright-mcp/cli.js packages/playwright-mcp/package.json ./ COPY --chown=${USERNAME}:${USERNAME} packages/playwright-mcp/cli.js packages/playwright-mcp/package.json ./
# Current working directory must be writable as MCP may need to create default output dir in it.
WORKDIR /home/${USERNAME}
# Run in headless and only with chromium (other browsers need more dependencies not included in this image) # Run in headless and only with chromium (other browsers need more dependencies not included in this image)
ENTRYPOINT ["node", "/app/cli.js", "--headless", "--browser", "chromium", "--no-sandbox"] ENTRYPOINT ["node", "cli.js", "--headless", "--browser", "chromium", "--no-sandbox"]

View File

@@ -372,7 +372,6 @@ Playwright MCP server supports following arguments. They can be provided in the
| --device <device> | device to emulate, for example: "iPhone 15"<br>*env* `PLAYWRIGHT_MCP_DEVICE` | | --device <device> | device to emulate, for example: "iPhone 15"<br>*env* `PLAYWRIGHT_MCP_DEVICE` |
| --executable-path <path> | path to the browser executable.<br>*env* `PLAYWRIGHT_MCP_EXECUTABLE_PATH` | | --executable-path <path> | path to the browser executable.<br>*env* `PLAYWRIGHT_MCP_EXECUTABLE_PATH` |
| --extension | Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.<br>*env* `PLAYWRIGHT_MCP_EXTENSION` | | --extension | Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.<br>*env* `PLAYWRIGHT_MCP_EXTENSION` |
| --endpoint <endpoint> | Bound browser endpoint to connect to.<br>*env* `PLAYWRIGHT_MCP_ENDPOINT` |
| --grant-permissions <permissions...> | List of permissions to grant to the browser context, for example "geolocation", "clipboard-read", "clipboard-write".<br>*env* `PLAYWRIGHT_MCP_GRANT_PERMISSIONS` | | --grant-permissions <permissions...> | List of permissions to grant to the browser context, for example "geolocation", "clipboard-read", "clipboard-write".<br>*env* `PLAYWRIGHT_MCP_GRANT_PERMISSIONS` |
| --headless | run browser in headless mode, headed by default<br>*env* `PLAYWRIGHT_MCP_HEADLESS` | | --headless | run browser in headless mode, headed by default<br>*env* `PLAYWRIGHT_MCP_HEADLESS` |
| --host <host> | host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.<br>*env* `PLAYWRIGHT_MCP_HOST` | | --host <host> | host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.<br>*env* `PLAYWRIGHT_MCP_HOST` |
@@ -413,17 +412,15 @@ Persistent profile is located at the following locations and you can override it
```bash ```bash
# Windows # Windows
%USERPROFILE%\AppData\Local\ms-playwright\mcp-{channel}-{workspace-hash} %USERPROFILE%\AppData\Local\ms-playwright\mcp-{channel}-profile
# macOS # macOS
- ~/Library/Caches/ms-playwright/mcp-{channel}-{workspace-hash} - ~/Library/Caches/ms-playwright/mcp-{channel}-profile
# Linux # Linux
- ~/.cache/ms-playwright/mcp-{channel}-{workspace-hash} - ~/.cache/ms-playwright/mcp-{channel}-profile
``` ```
`{workspace-hash}` is derived from the MCP client's workspace root, so different projects get separate profiles automatically.
**Isolated** **Isolated**
In the isolated mode, each session is started in the isolated profile. Every time you ask MCP to close the browser, In the isolated mode, each session is started in the isolated profile. Every time you ask MCP to close the browser,
@@ -1021,7 +1018,6 @@ http.createServer(async (req, res) => {
- Parameters: - Parameters:
- `action` (string): Operation to perform - `action` (string): Operation to perform
- `index` (number, optional): Tab index, used for close/select. If omitted for close, current tab is closed. - `index` (number, optional): Tab index, used for close/select. If omitted for close, current tab is closed.
- `url` (string, optional): URL to navigate to in the new tab, used for new.
- Read-only: **false** - Read-only: **false**
</details> </details>
@@ -1279,7 +1275,6 @@ http.createServer(async (req, res) => {
- Title: Start video - Title: Start video
- Description: Start video recording - Description: Start video recording
- Parameters: - Parameters:
- `filename` (string, optional): Filename to save the video.
- `size` (object, optional): Video size - `size` (object, optional): Video size
- Read-only: **true** - Read-only: **true**
@@ -1296,7 +1291,8 @@ http.createServer(async (req, res) => {
- **browser_stop_video** - **browser_stop_video**
- Title: Stop video - Title: Stop video
- Description: Stop video recording - Description: Stop video recording
- Parameters: None - Parameters:
- `filename` (string, optional): Filename to save the video
- Read-only: **true** - Read-only: **true**
<!-- NOTE: This has been generated via update-readme.js --> <!-- NOTE: This has been generated via update-readme.js -->

39
package-lock.json generated
View File

@@ -1,19 +1,19 @@
{ {
"name": "playwright-mcp-internal", "name": "playwright-mcp-internal",
"version": "0.0.70", "version": "0.0.68",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "playwright-mcp-internal", "name": "playwright-mcp-internal",
"version": "0.0.70", "version": "0.0.68",
"license": "Apache-2.0", "license": "Apache-2.0",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
], ],
"devDependencies": { "devDependencies": {
"@modelcontextprotocol/sdk": "^1.25.2", "@modelcontextprotocol/sdk": "^1.25.2",
"@playwright/test": "1.60.0-alpha-1775258971000", "@playwright/test": "1.59.0-alpha-1774656214000",
"@types/node": "^24.3.0" "@types/node": "^24.3.0"
} }
}, },
@@ -854,13 +854,12 @@
"link": true "link": true
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.60.0-alpha-1775258971000", "version": "1.59.0-alpha-1774656214000",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0-alpha-1775258971000.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.0-alpha-1774656214000.tgz",
"integrity": "sha512-6ZvWKDxCRxSNWtqcXdjjUfdiFkPXWb+q1EgfiG5JL5Hmpz1klCSfFgqssSpjOsZdjVlFTkPBruxClEaKCL3lgw==", "integrity": "sha512-1BmlLuGD6XAOLv98iCtgbvjRWdVjOdEh2fnDjWqHiD9ygnNXupsJZzkDVOfXSlaoZQoO26kcOfV03qbaUWua/A==",
"dev": true, "dev": true,
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.60.0-alpha-1775258971000" "playwright": "1.59.0-alpha-1774656214000"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -2624,12 +2623,11 @@
} }
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.60.0-alpha-1775258971000", "version": "1.59.0-alpha-1774656214000",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0-alpha-1775258971000.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.0-alpha-1774656214000.tgz",
"integrity": "sha512-xaS8b8clhxs1uLiBQFdrKYPAxZPI1eZDshsK9LMvMyuNkwiFlK0FxWNWj01kA/dUf5/hm26xarLiM/Ah1DugyA==", "integrity": "sha512-aIR09o0T9Y/LpCcBaEE6tldqNI8wrbxuZYH8uruD8kJx/5GtwyA6LxPujy2n8pOQUOmHpySDGBQYFWfhCbD/uA==",
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.60.0-alpha-1775258971000" "playwright-core": "1.59.0-alpha-1774656214000"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -2646,10 +2644,9 @@
"link": true "link": true
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.60.0-alpha-1775258971000", "version": "1.59.0-alpha-1774656214000",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0-alpha-1775258971000.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.0-alpha-1774656214000.tgz",
"integrity": "sha512-SJ5hXQUf50wA/wI1Z9xkFoJq7IwLhfX4o7hXZJExxtGo3U8UQsS3AtD1TVWkO0KTeMdFnaQ8XE4BMa5wziegnw==", "integrity": "sha512-pFMQqZDbTSvKntHMh4ZF+iB7jAm5c0nyM5t7rX6tSa7wQdZiswJrFBN+hKmMq338+y6iSJhT61vxuYxORakivA==",
"license": "Apache-2.0",
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
}, },
@@ -3416,7 +3413,7 @@
}, },
"packages/extension": { "packages/extension": {
"name": "@playwright/mcp-extension", "name": "@playwright/mcp-extension",
"version": "0.0.70", "version": "0.0.68",
"license": "Apache-2.0", "license": "Apache-2.0",
"devDependencies": { "devDependencies": {
"@types/chrome": "^0.0.315", "@types/chrome": "^0.0.315",
@@ -3441,11 +3438,11 @@
}, },
"packages/playwright-mcp": { "packages/playwright-mcp": {
"name": "@playwright/mcp", "name": "@playwright/mcp",
"version": "0.0.70", "version": "0.0.68",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.60.0-alpha-1775258971000", "playwright": "1.59.0-alpha-1774656214000",
"playwright-core": "1.60.0-alpha-1775258971000" "playwright-core": "1.59.0-alpha-1774656214000"
}, },
"bin": { "bin": {
"playwright-mcp": "cli.js" "playwright-mcp": "cli.js"

View File

@@ -1,6 +1,6 @@
{ {
"name": "playwright-mcp-internal", "name": "playwright-mcp-internal",
"version": "0.0.70", "version": "0.0.68",
"private": true, "private": true,
"repository": { "repository": {
"type": "git", "type": "git",
@@ -26,7 +26,7 @@
], ],
"devDependencies": { "devDependencies": {
"@modelcontextprotocol/sdk": "^1.25.2", "@modelcontextprotocol/sdk": "^1.25.2",
"@playwright/test": "1.60.0-alpha-1775258971000", "@playwright/test": "1.59.0-alpha-1774656214000",
"@types/node": "^24.3.0" "@types/node": "^24.3.0"
} }
} }

View File

@@ -1,13 +1,12 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Playwright MCP Bridge", "name": "Playwright MCP Bridge",
"version": "0.0.70", "version": "0.0.68",
"description": "Share browser tabs with Playwright MCP server", "description": "Share browser tabs with Playwright MCP server",
"permissions": [ "permissions": [
"debugger", "debugger",
"activeTab", "activeTab",
"tabs", "tabs"
"tabGroups"
], ],
"host_permissions": [ "host_permissions": [
"<all_urls>" "<all_urls>"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@playwright/mcp-extension", "name": "@playwright/mcp-extension",
"version": "0.0.70", "version": "0.0.68",
"description": "Playwright MCP Browser Extension", "description": "Playwright MCP Browser Extension",
"private": true, "private": true,
"repository": { "repository": {

View File

@@ -35,7 +35,6 @@ type PageMessage = {
class TabShareExtension { class TabShareExtension {
private _activeConnection: RelayConnection | undefined; private _activeConnection: RelayConnection | undefined;
private _connectedTabId: number | null = null; private _connectedTabId: number | null = null;
private _groupId: number | null = null;
private _pendingTabSelection = new Map<number, { connection: RelayConnection, timerId?: number }>(); private _pendingTabSelection = new Map<number, { connection: RelayConnection, timerId?: number }>();
constructor() { constructor() {
@@ -97,7 +96,6 @@ class TabShareExtension {
// TODO: show error in the selector tab? // TODO: show error in the selector tab?
}; };
this._pendingTabSelection.set(selectorTabId, { connection }); this._pendingTabSelection.set(selectorTabId, { connection });
await this._addTabToGroup(selectorTabId);
debugLog(`Connected to MCP relay`); debugLog(`Connected to MCP relay`);
} catch (error: any) { } catch (error: any) {
const message = `Failed to connect to MCP relay: ${error.message}`; const message = `Failed to connect to MCP relay: ${error.message}`;
@@ -126,12 +124,10 @@ class TabShareExtension {
debugLog('MCP connection closed'); debugLog('MCP connection closed');
this._activeConnection = undefined; this._activeConnection = undefined;
void this._setConnectedTabId(null); void this._setConnectedTabId(null);
chrome.tabs.ungroup([tabId]).catch(() => {});
}; };
await Promise.all([ await Promise.all([
this._setConnectedTabId(tabId), this._setConnectedTabId(tabId),
this._addTabToGroup(tabId),
chrome.tabs.update(tabId, { active: true }), chrome.tabs.update(tabId, { active: true }),
chrome.windows.update(windowId, { focused: true }), chrome.windows.update(windowId, { focused: true }),
]); ]);
@@ -208,31 +204,11 @@ class TabShareExtension {
return tabs.filter(tab => tab.url && !['chrome:', 'edge:', 'devtools:'].some(scheme => tab.url!.startsWith(scheme))); return tabs.filter(tab => tab.url && !['chrome:', 'edge:', 'devtools:'].some(scheme => tab.url!.startsWith(scheme)));
} }
private async _addTabToGroup(tabId: number): Promise<void> {
try {
if (this._groupId !== null) {
try {
await chrome.tabs.group({ groupId: this._groupId, tabIds: [tabId] });
await chrome.tabGroups.update(this._groupId, { color: 'green', title: 'Playwright' });
return;
} catch {
this._groupId = null;
}
}
this._groupId = await chrome.tabs.group({ tabIds: [tabId] });
await chrome.tabGroups.update(this._groupId, { color: 'green', title: 'Playwright' });
} catch (error: any) {
debugLog('Error adding tab to group:', error);
}
}
private async _onActionClicked(): Promise<void> { private async _onActionClicked(): Promise<void> {
const tab = await chrome.tabs.create({ await chrome.tabs.create({
url: chrome.runtime.getURL('status.html'), url: chrome.runtime.getURL('status.html'),
active: true active: true
}); });
if (tab.id)
await this._addTabToGroup(tab.id);
} }
private async _disconnect(): Promise<void> { private async _disconnect(): Promise<void> {

View File

@@ -123,14 +123,6 @@ const test = base.extend<TestFixtures>({
}, },
}); });
function cliEnv() {
return {
PLAYWRIGHT_SERVER_REGISTRY: test.info().outputPath('registry'),
PLAYWRIGHT_DAEMON_SESSION_DIR: test.info().outputPath('daemon'),
PLAYWRIGHT_SOCKETS_DIR: path.join(test.info().project.outputDir, 'ds', String(test.info().parallelIndex)),
};
}
async function runCli( async function runCli(
args: string[], args: string[],
options: { mcpBrowser?: string, testInfo: any }, options: { mcpBrowser?: string, testInfo: any },
@@ -141,7 +133,7 @@ async function runCli(
const testInfo = options.testInfo; const testInfo = options.testInfo;
// Path to the terminal CLI // Path to the terminal CLI
const cliPath = path.join(__dirname, '../../../node_modules/playwright-core/lib/tools/cli-client/cli.js'); const cliPath = path.join(__dirname, '../../../node_modules/playwright/lib/cli/client/program.js');
return new Promise<CliResult>((resolve, reject) => { return new Promise<CliResult>((resolve, reject) => {
let stdout = ''; let stdout = '';
@@ -151,7 +143,9 @@ async function runCli(
cwd: testInfo.outputPath(), cwd: testInfo.outputPath(),
env: { env: {
...process.env, ...process.env,
...cliEnv(), PLAYWRIGHT_DAEMON_INSTALL_DIR: testInfo.outputPath(),
PLAYWRIGHT_DAEMON_SESSION_DIR: testInfo.outputPath('daemon'),
PLAYWRIGHT_DAEMON_SOCKETS_DIR: path.join(testInfo.project.outputDir, 'daemon-sockets'),
PLAYWRIGHT_MCP_BROWSER: options.mcpBrowser, PLAYWRIGHT_MCP_BROWSER: options.mcpBrowser,
PLAYWRIGHT_MCP_HEADLESS: 'false', PLAYWRIGHT_MCP_HEADLESS: 'false',
}, },
@@ -219,7 +213,8 @@ test(`navigate with extension`, async ({ browserWithExtension, startClient, serv
}); });
const selectorPage = await confirmationPagePromise; const selectorPage = await confirmationPagePromise;
await selectorPage.locator('.tab-item', { hasText: 'Playwright MCP extension' }).getByRole('button', { name: 'Connect' }).click(); // For browser_navigate command, the UI shows Allow/Reject buttons instead of tab selector
await selectorPage.getByRole('button', { name: 'Allow' }).click();
expect(await navigateResponse).toHaveResponse({ expect(await navigateResponse).toHaveResponse({
snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
@@ -300,7 +295,8 @@ testWithOldExtensionVersion(`works with old extension version`, async ({ browser
}); });
const selectorPage = await confirmationPagePromise; const selectorPage = await confirmationPagePromise;
await selectorPage.locator('.tab-item', { hasText: 'Playwright MCP extension' }).getByRole('button', { name: 'Connect' }).click(); // For browser_navigate command, the UI shows Allow/Reject buttons instead of tab selector
await selectorPage.getByRole('button', { name: 'Allow' }).click();
expect(await navigateResponse).toHaveResponse({ expect(await navigateResponse).toHaveResponse({
snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
@@ -390,101 +386,8 @@ test(`bypass connection dialog with token`, async ({ browserWithExtension, start
}); });
}); });
test.describe('tab grouping', () => {
test('connect page is added to green Playwright group on relay connect', async ({ browserWithExtension, startClient, server }) => {
const browserContext = await browserWithExtension.launch();
const client = await startWithExtensionFlag(browserWithExtension, startClient);
const connectPagePromise = browserContext.waitForEvent('page', page =>
page.url().startsWith(`chrome-extension://${extensionId}/connect.html`)
);
const navigatePromise = client.callTool({ name: 'browser_navigate', arguments: { url: server.HELLO_WORLD } });
const connectPage = await connectPagePromise;
// Wait for the tab list to appear — this means connectToMCPRelay was processed
// by the background and _addTabToGroup has been called.
await expect(connectPage.locator('.tab-item').first()).toBeVisible();
const group = await connectPage.evaluate(async () => {
const chrome = (window as any).chrome;
const tab = await chrome.tabs.getCurrent();
if (!tab || tab.groupId === -1)
return null;
const g = await chrome.tabGroups.get(tab.groupId);
return { color: g.color, title: g.title };
});
expect(group).toEqual({ color: 'green', title: 'Playwright' });
await connectPage.locator('.tab-item', { hasText: 'Playwright MCP extension' }).getByRole('button', { name: 'Connect' }).click();
await navigatePromise;
});
test('connected tab is added to same Playwright group', async ({ browserWithExtension, startClient, server }) => {
const browserContext = await browserWithExtension.launch();
const page = await browserContext.newPage();
await page.goto(server.HELLO_WORLD);
const client = await startWithExtensionFlag(browserWithExtension, startClient);
const connectPagePromise = browserContext.waitForEvent('page', page =>
page.url().startsWith(`chrome-extension://${extensionId}/connect.html`)
);
const navigatePromise = client.callTool({ name: 'browser_navigate', arguments: { url: server.HELLO_WORLD } });
const connectPage = await connectPagePromise;
await connectPage.locator('.tab-item', { hasText: 'Title' }).getByRole('button', { name: 'Connect' }).click();
await navigatePromise;
const { connectGroupId, connectedGroupId } = await connectPage.evaluate(async () => {
const chrome = (window as any).chrome;
const connectTab = await chrome.tabs.getCurrent();
const [connectedTab] = await chrome.tabs.query({ title: 'Title' });
return {
connectGroupId: connectTab?.groupId,
connectedGroupId: connectedTab?.groupId,
};
});
expect(connectGroupId).not.toBe(-1);
expect(connectedGroupId).toBe(connectGroupId);
});
test('connected tab is removed from group on disconnect', async ({ browserWithExtension, startClient, server }) => {
const browserContext = await browserWithExtension.launch();
const page = await browserContext.newPage();
await page.goto(server.HELLO_WORLD);
const client = await startWithExtensionFlag(browserWithExtension, startClient);
const connectPagePromise = browserContext.waitForEvent('page', page =>
page.url().startsWith(`chrome-extension://${extensionId}/connect.html`)
);
const navigatePromise = client.callTool({ name: 'browser_navigate', arguments: { url: server.HELLO_WORLD } });
const connectPage = await connectPagePromise;
await connectPage.locator('.tab-item', { hasText: 'Title' }).getByRole('button', { name: 'Connect' }).click();
await navigatePromise;
await client.close();
await expect.poll(async () => {
return connectPage.evaluate(async () => {
const chrome = (window as any).chrome;
const [tab] = await chrome.tabs.query({ title: 'Title' });
return tab?.groupId ?? -1;
});
}).toBe(-1);
});
});
test.describe('CLI with extension', () => { test.describe('CLI with extension', () => {
test('attach <url> --extension', async ({ browserWithExtension, cli, server }, testInfo) => { test('open <url> --extension', async ({ browserWithExtension, cli, server }, testInfo) => {
const browserContext = await browserWithExtension.launch(); const browserContext = await browserWithExtension.launch();
// Write config file with userDataDir // Write config file with userDataDir
@@ -500,7 +403,7 @@ test.describe('CLI with extension', () => {
}); });
// Start the CLI command in the background // Start the CLI command in the background
const cliPromise = cli('attach', '--extension', `--config=cli-config.json`); const cliPromise = cli('open', server.HELLO_WORLD, '--extension', `--config=cli-config.json`);
// Wait for the confirmation page to appear // Wait for the confirmation page to appear
const confirmationPage = await confirmationPagePromise; const confirmationPage = await confirmationPagePromise;
@@ -508,21 +411,12 @@ test.describe('CLI with extension', () => {
// Click the Connect button // Click the Connect button
await confirmationPage.locator('.tab-item', { hasText: 'Playwright MCP extension' }).getByRole('button', { name: 'Connect' }).click(); await confirmationPage.locator('.tab-item', { hasText: 'Playwright MCP extension' }).getByRole('button', { name: 'Connect' }).click();
{ // Wait for the CLI command to complete
// Wait for the CLI command to complete const { output } = await cliPromise;
const { output } = await cliPromise;
// Verify the output
expect(output).toContain(`### Page`);
expect(output).toContain(`- Page URL: chrome-extension://${extensionId}/connect.html?`);
expect(output).toContain(`- Page Title: Playwright MCP extension`);
}
{ // Verify the output
const { output } = await cli('goto', server.HELLO_WORLD); expect(output).toContain(`### Page`);
// Verify the output expect(output).toContain(`- Page URL: ${server.HELLO_WORLD}`);
expect(output).toContain(`### Page`); expect(output).toContain(`- Page Title: Title`);
expect(output).toContain(`- Page URL: ${server.HELLO_WORLD}`);
expect(output).toContain(`- Page Title: Title`);
}
}); });
}); });

View File

@@ -1,6 +1,6 @@
{ {
"name": "@playwright/mcp", "name": "@playwright/mcp",
"version": "0.0.70", "version": "0.0.68",
"description": "Playwright Tools for MCP", "description": "Playwright Tools for MCP",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -33,8 +33,8 @@
} }
}, },
"dependencies": { "dependencies": {
"playwright": "1.60.0-alpha-1775258971000", "playwright": "1.59.0-alpha-1774656214000",
"playwright-core": "1.60.0-alpha-1775258971000" "playwright-core": "1.59.0-alpha-1774656214000"
}, },
"bin": { "bin": {
"playwright-mcp": "cli.js" "playwright-mcp": "cli.js"

View File

@@ -74,10 +74,10 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
}, },
startClient: async ({ mcpHeadless, mcpBrowser, mcpMode, mcpArgs }, use, testInfo) => { startClient: async ({ mcpHeadless, mcpBrowser, mcpMode, mcpArgs }, use, testInfo) => {
const configDir = path.dirname(test.info().config.configFile!);
const clients: Client[] = []; const clients: Client[] = [];
await use(async options => { await use(async options => {
const cwd = testInfo.outputPath();
const args: string[] = mcpArgs ?? []; const args: string[] = mcpArgs ?? [];
if (process.env.CI && process.platform === 'linux') if (process.env.CI && process.platform === 'linux')
args.push('--no-sandbox'); args.push('--no-sandbox');
@@ -90,7 +90,7 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
if (options?.config) { if (options?.config) {
const configFile = testInfo.outputPath('config.json'); const configFile = testInfo.outputPath('config.json');
await fs.promises.writeFile(configFile, JSON.stringify(options.config, null, 2)); await fs.promises.writeFile(configFile, JSON.stringify(options.config, null, 2));
args.push(`--config=${path.relative(cwd, configFile)}`); args.push(`--config=${path.relative(configDir, configFile)}`);
} }
const client = new Client({ name: options?.clientName ?? 'test', version: '1.0.0' }, options?.roots ? { capabilities: { roots: {} } } : undefined); const client = new Client({ name: options?.clientName ?? 'test', version: '1.0.0' }, options?.roots ? { capabilities: { roots: {} } } : undefined);
@@ -103,7 +103,7 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
}; };
}); });
} }
const { transport, stderr } = await createTransport(args, cwd, mcpMode, testInfo.outputPath('ms-playwright'), options?.extensionToken); const { transport, stderr } = await createTransport(args, mcpMode, testInfo.outputPath('ms-playwright'), options?.extensionToken);
let stderrBuffer = ''; let stderrBuffer = '';
stderr?.on('data', data => { stderr?.on('data', data => {
if (process.env.PWMCP_DEBUG) if (process.env.PWMCP_DEBUG)
@@ -182,14 +182,12 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
}, },
}); });
async function createTransport(args: string[], cwd: string, mcpMode: TestOptions['mcpMode'], profilesDir: string, extensionToken?: string): Promise<{ async function createTransport(args: string[], mcpMode: TestOptions['mcpMode'], profilesDir: string, extensionToken?: string): Promise<{
transport: Transport, transport: Transport,
stderr: Stream | null, stderr: Stream | null,
}> { }> {
if (mcpMode === 'docker') { if (mcpMode === 'docker') {
const relCwd = path.relative(test.info().project.outputDir, cwd); const dockerArgs = ['run', '--rm', '-i', '--network=host', '-v', `${test.info().project.outputDir}:/app/test-results`];
const dockerCwd = path.posix.join('/app/test-results', relCwd.split(path.sep).join('/'));
const dockerArgs = ['run', '--rm', '-i', '--network=host', '-v', `${test.info().project.outputDir}:/app/test-results`, '-w', dockerCwd];
const transport = new StdioClientTransport({ const transport = new StdioClientTransport({
command: 'docker', command: 'docker',
args: [...dockerArgs, 'playwright-mcp-dev:latest', ...args], args: [...dockerArgs, 'playwright-mcp-dev:latest', ...args],
@@ -203,7 +201,7 @@ async function createTransport(args: string[], cwd: string, mcpMode: TestOptions
const transport = new StdioClientTransport({ const transport = new StdioClientTransport({
command: 'node', command: 'node',
args: [path.join(__dirname, '../cli.js'), ...args], args: [path.join(__dirname, '../cli.js'), ...args],
cwd, cwd: path.dirname(test.info().config.configFile!),
stderr: 'pipe', stderr: 'pipe',
env: { env: {
...process.env, ...process.env,
@@ -224,7 +222,7 @@ type Response = Awaited<ReturnType<Client['callTool']>>;
export const expect = baseExpect.extend({ export const expect = baseExpect.extend({
toHaveResponse(response: Response, object: any) { toHaveResponse(response: Response, object: any) {
const parsed = parseResponse(response, test.info().outputPath()); const parsed = parseResponse(response);
const isNot = this.isNot; const isNot = this.isNot;
try { try {
if (isNot) if (isNot)
@@ -248,7 +246,7 @@ export function formatOutput(output: string): string[] {
return output.split('\n').map(line => line.replace(/^pw:mcp:test /, '').replace(/user data dir.*/, 'user data dir').trim()).filter(Boolean); return output.split('\n').map(line => line.replace(/^pw:mcp:test /, '').replace(/user data dir.*/, 'user data dir').trim()).filter(Boolean);
} }
function parseResponse(response: any, cwd: string) { function parseResponse(response: any) {
const text = response.content[0].text; const text = response.content[0].text;
const sections = parseSections(text); const sections = parseSections(text);
@@ -257,7 +255,7 @@ function parseResponse(response: any, cwd: string) {
const code = sections.get('Ran Playwright code'); const code = sections.get('Ran Playwright code');
const tabs = sections.get('Open tabs'); const tabs = sections.get('Open tabs');
const pageState = sections.get('Page state'); const pageState = sections.get('Page state');
const snapshotSection = sections.get('Snapshot'); const snapshot = sections.get('Snapshot');
const consoleMessages = sections.get('New console messages'); const consoleMessages = sections.get('New console messages');
const modalState = sections.get('Modal state'); const modalState = sections.get('Modal state');
const downloads = sections.get('Downloads'); const downloads = sections.get('Downloads');
@@ -265,19 +263,6 @@ function parseResponse(response: any, cwd: string) {
const isError = response.isError; const isError = response.isError;
const attachments = response.content.slice(1); const attachments = response.content.slice(1);
let snapshot: string | undefined;
if (snapshotSection) {
const match = snapshotSection.match(/\[Snapshot\]\(([^)]+)\)/);
if (match) {
try {
snapshot = fs.readFileSync(path.resolve(cwd, match[1]), 'utf-8');
} catch {
}
} else {
snapshot = snapshotSection.replace(/^```yaml\n?/, '').replace(/\n?```$/, '');
}
}
return { return {
error, error,
result, result,