mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2026-04-04 04:33:09 +00:00
Compare commits
1 Commits
main
...
roll-1.59.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd1ef543cb |
@@ -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"]
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -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
39
package-lock.json
generated
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>"
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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`);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user