mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2025-07-26 16:42:27 +08:00
chore: allow reusing tab over cdp (#170)
Fixes https://github.com/microsoft/playwright-mcp/issues/164
This commit is contained in:
parent
e729494bd9
commit
606b898a71
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@ -22,6 +22,9 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Playwright install
|
||||||
|
run: npx playwright install --with-deps
|
||||||
|
|
||||||
- name: Run linting
|
- name: Run linting
|
||||||
run: npm run lint
|
run: npm run lint
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ export class Context {
|
|||||||
|
|
||||||
currentTab(): Tab {
|
currentTab(): Tab {
|
||||||
if (!this._currentTab)
|
if (!this._currentTab)
|
||||||
throw new Error('Navigate to a location to create a tab');
|
throw new Error('No current snapshot available. Capture a snapshot of navigate to a new location first.');
|
||||||
return this._currentTab;
|
return this._currentTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,8 +236,8 @@ class Tab {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async runAndWaitWithSnapshot(callback: (tab: Tab) => Promise<void>, options?: RunOptions): Promise<ToolResult> {
|
async runAndWaitWithSnapshot(callback: (snapshot: PageSnapshot) => Promise<void>, options?: RunOptions): Promise<ToolResult> {
|
||||||
return await this.run(callback, {
|
return await this.run(tab => callback(tab.lastSnapshot()), {
|
||||||
captureSnapshot: true,
|
captureSnapshot: true,
|
||||||
waitForCompletion: true,
|
waitForCompletion: true,
|
||||||
...options,
|
...options,
|
||||||
|
@ -28,7 +28,7 @@ const screenshot: Tool = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
handle: async context => {
|
handle: async context => {
|
||||||
const tab = context.currentTab();
|
const tab = await context.ensureTab();
|
||||||
const screenshot = await tab.page.screenshot({ type: 'jpeg', quality: 50, scale: 'css' });
|
const screenshot = await tab.page.screenshot({ type: 'jpeg', quality: 50, scale: 'css' });
|
||||||
return {
|
return {
|
||||||
content: [{ type: 'image', data: screenshot.toString('base64'), mimeType: 'image/jpeg' }],
|
content: [{ type: 'image', data: screenshot.toString('base64'), mimeType: 'image/jpeg' }],
|
||||||
|
@ -29,7 +29,8 @@ const snapshot: Tool = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
handle: async context => {
|
handle: async context => {
|
||||||
return await context.currentTab().run(async () => {}, { captureSnapshot: true });
|
const tab = await context.ensureTab();
|
||||||
|
return await tab.run(async () => {}, { captureSnapshot: true });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -48,8 +49,8 @@ const click: Tool = {
|
|||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
const validatedParams = elementSchema.parse(params);
|
const validatedParams = elementSchema.parse(params);
|
||||||
return await context.currentTab().runAndWaitWithSnapshot(async tab => {
|
return await context.currentTab().runAndWaitWithSnapshot(async snapshot => {
|
||||||
const locator = tab.lastSnapshot().refLocator(validatedParams.ref);
|
const locator = snapshot.refLocator(validatedParams.ref);
|
||||||
await locator.click();
|
await locator.click();
|
||||||
}, {
|
}, {
|
||||||
status: `Clicked "${validatedParams.element}"`,
|
status: `Clicked "${validatedParams.element}"`,
|
||||||
@ -74,9 +75,9 @@ const drag: Tool = {
|
|||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
const validatedParams = dragSchema.parse(params);
|
const validatedParams = dragSchema.parse(params);
|
||||||
return await context.currentTab().runAndWaitWithSnapshot(async tab => {
|
return await context.currentTab().runAndWaitWithSnapshot(async snapshot => {
|
||||||
const startLocator = tab.lastSnapshot().refLocator(validatedParams.startRef);
|
const startLocator = snapshot.refLocator(validatedParams.startRef);
|
||||||
const endLocator = tab.lastSnapshot().refLocator(validatedParams.endRef);
|
const endLocator = snapshot.refLocator(validatedParams.endRef);
|
||||||
await startLocator.dragTo(endLocator);
|
await startLocator.dragTo(endLocator);
|
||||||
}, {
|
}, {
|
||||||
status: `Dragged "${validatedParams.startElement}" to "${validatedParams.endElement}"`,
|
status: `Dragged "${validatedParams.startElement}" to "${validatedParams.endElement}"`,
|
||||||
@ -94,8 +95,8 @@ const hover: Tool = {
|
|||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
const validatedParams = elementSchema.parse(params);
|
const validatedParams = elementSchema.parse(params);
|
||||||
return await context.currentTab().runAndWaitWithSnapshot(async tab => {
|
return await context.currentTab().runAndWaitWithSnapshot(async snapshot => {
|
||||||
const locator = tab.lastSnapshot().refLocator(validatedParams.ref);
|
const locator = snapshot.refLocator(validatedParams.ref);
|
||||||
await locator.hover();
|
await locator.hover();
|
||||||
}, {
|
}, {
|
||||||
status: `Hovered over "${validatedParams.element}"`,
|
status: `Hovered over "${validatedParams.element}"`,
|
||||||
@ -119,8 +120,8 @@ const type: Tool = {
|
|||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
const validatedParams = typeSchema.parse(params);
|
const validatedParams = typeSchema.parse(params);
|
||||||
return await context.currentTab().runAndWaitWithSnapshot(async tab => {
|
return await context.currentTab().runAndWaitWithSnapshot(async snapshot => {
|
||||||
const locator = tab.lastSnapshot().refLocator(validatedParams.ref);
|
const locator = snapshot.refLocator(validatedParams.ref);
|
||||||
if (validatedParams.slowly)
|
if (validatedParams.slowly)
|
||||||
await locator.pressSequentially(validatedParams.text);
|
await locator.pressSequentially(validatedParams.text);
|
||||||
else
|
else
|
||||||
@ -147,8 +148,8 @@ const selectOption: Tool = {
|
|||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
const validatedParams = selectOptionSchema.parse(params);
|
const validatedParams = selectOptionSchema.parse(params);
|
||||||
return await context.currentTab().runAndWaitWithSnapshot(async tab => {
|
return await context.currentTab().runAndWaitWithSnapshot(async snapshot => {
|
||||||
const locator = tab.lastSnapshot().refLocator(validatedParams.ref);
|
const locator = snapshot.refLocator(validatedParams.ref);
|
||||||
await locator.selectOption(validatedParams.values);
|
await locator.selectOption(validatedParams.values);
|
||||||
}, {
|
}, {
|
||||||
status: `Selected option in "${validatedParams.element}"`,
|
status: `Selected option in "${validatedParams.element}"`,
|
||||||
|
@ -89,7 +89,7 @@ const closeTab: ToolFactory = captureSnapshot => ({
|
|||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
const validatedParams = closeTabSchema.parse(params);
|
const validatedParams = closeTabSchema.parse(params);
|
||||||
await context.closeTab(validatedParams.index);
|
await context.closeTab(validatedParams.index);
|
||||||
const currentTab = await context.currentTab();
|
const currentTab = context.currentTab();
|
||||||
if (currentTab)
|
if (currentTab)
|
||||||
return await currentTab.run(async () => {}, { captureSnapshot });
|
return await currentTab.run(async () => {}, { captureSnapshot });
|
||||||
return {
|
return {
|
||||||
|
@ -35,3 +35,27 @@ Navigated to data:text/html,<html><title>Title</title><body>Hello, world!</body>
|
|||||||
`
|
`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('cdp server reuse tab', async ({ cdpEndpoint, startClient }) => {
|
||||||
|
const client = await startClient({ args: [`--cdp-endpoint=${cdpEndpoint}`] });
|
||||||
|
|
||||||
|
expect(await client.callTool({
|
||||||
|
name: 'browser_click',
|
||||||
|
arguments: {
|
||||||
|
element: 'Hello, world!',
|
||||||
|
ref: 'f0',
|
||||||
|
},
|
||||||
|
})).toHaveTextContent(`Error: No current snapshot available. Capture a snapshot of navigate to a new location first.`);
|
||||||
|
|
||||||
|
expect(await client.callTool({
|
||||||
|
name: 'browser_snapshot',
|
||||||
|
arguments: {},
|
||||||
|
})).toHaveTextContent(`
|
||||||
|
- Page URL: data:text/html,hello world
|
||||||
|
- Page Title:
|
||||||
|
- Page Snapshot
|
||||||
|
\`\`\`yaml
|
||||||
|
- text: hello world
|
||||||
|
\`\`\`
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
@ -20,6 +20,7 @@ import { chromium } from 'playwright';
|
|||||||
import { test as baseTest, expect as baseExpect } from '@playwright/test';
|
import { test as baseTest, expect as baseExpect } from '@playwright/test';
|
||||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
|
||||||
type Fixtures = {
|
type Fixtures = {
|
||||||
client: Client;
|
client: Client;
|
||||||
@ -68,12 +69,25 @@ export const test = baseTest.extend<Fixtures>({
|
|||||||
|
|
||||||
cdpEndpoint: async ({ }, use, testInfo) => {
|
cdpEndpoint: async ({ }, use, testInfo) => {
|
||||||
const port = 3200 + (+process.env.TEST_PARALLEL_INDEX!);
|
const port = 3200 + (+process.env.TEST_PARALLEL_INDEX!);
|
||||||
const browser = await chromium.launchPersistentContext(testInfo.outputPath('user-data-dir'), {
|
const executablePath = chromium.executablePath();
|
||||||
channel: 'chrome',
|
const browserProcess = spawn(executablePath, [
|
||||||
args: [`--remote-debugging-port=${port}`],
|
`--user-data-dir=${testInfo.outputPath('user-data-dir')}`,
|
||||||
|
`--remote-debugging-port=${port}`,
|
||||||
|
`--no-first-run`,
|
||||||
|
`--no-sandbox`,
|
||||||
|
`--headless`,
|
||||||
|
`data:text/html,hello world`,
|
||||||
|
], {
|
||||||
|
stdio: 'pipe',
|
||||||
|
});
|
||||||
|
await new Promise<void>(resolve => {
|
||||||
|
browserProcess.stderr.on('data', data => {
|
||||||
|
if (data.toString().includes('DevTools listening on '))
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
await use(`http://localhost:${port}`);
|
await use(`http://localhost:${port}`);
|
||||||
await browser.close();
|
browserProcess.kill();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user