diff --git a/packages/playwright-mcp/src/tools/common.ts b/packages/playwright-mcp/src/tools/common.ts index 4fc7bd5168..868be9554f 100644 --- a/packages/playwright-mcp/src/tools/common.ts +++ b/packages/playwright-mcp/src/tools/common.ts @@ -60,15 +60,9 @@ export const goBack: ToolFactory = snapshot => ({ inputSchema: zodToJsonSchema(goBackSchema), }, handle: async context => { - return await runAndWait(context, async () => { + return await runAndWait(context, 'Navigated back', async () => { const page = await context.ensurePage(); await page.goBack(); - return { - content: [{ - type: 'text', - text: `Navigated back`, - }], - }; }, snapshot); }, }); @@ -82,15 +76,9 @@ export const goForward: ToolFactory = snapshot => ({ inputSchema: zodToJsonSchema(goForwardSchema), }, handle: async context => { - return await runAndWait(context, async () => { + return await runAndWait(context, 'Navigated forward', async () => { const page = await context.ensurePage(); await page.goForward(); - return { - content: [{ - type: 'text', - text: `Navigated forward`, - }], - }; }, snapshot); }, }); @@ -130,14 +118,8 @@ export const pressKey: Tool = { }, handle: async (context, params) => { const validatedParams = pressKeySchema.parse(params); - return await runAndWait(context, async page => { + return await runAndWait(context, `Pressed key ${validatedParams.key}`, async page => { await page.keyboard.press(validatedParams.key); - return { - content: [{ - type: 'text', - text: `Pressed key ${validatedParams.key}`, - }], - }; }); }, }; diff --git a/packages/playwright-mcp/src/tools/screenshot.ts b/packages/playwright-mcp/src/tools/screenshot.ts index 4fdc6c7e5d..7a5cf2a1fa 100644 --- a/packages/playwright-mcp/src/tools/screenshot.ts +++ b/packages/playwright-mcp/src/tools/screenshot.ts @@ -76,15 +76,12 @@ export const click: Tool = { }, handle: async (context, params) => { - await runAndWait(context, async page => { + return await runAndWait(context, 'Clicked mouse', async page => { const validatedParams = clickSchema.parse(params); await page.mouse.move(validatedParams.x, validatedParams.y); await page.mouse.down(); await page.mouse.up(); }); - return { - content: [{ type: 'text', text: 'Clicked mouse' }], - }; }, }; @@ -104,15 +101,12 @@ export const drag: Tool = { handle: async (context, params) => { const validatedParams = dragSchema.parse(params); - await runAndWait(context, async page => { + return await runAndWait(context, `Dragged mouse from (${validatedParams.startX}, ${validatedParams.startY}) to (${validatedParams.endX}, ${validatedParams.endY})`, async page => { await page.mouse.move(validatedParams.startX, validatedParams.startY); await page.mouse.down(); await page.mouse.move(validatedParams.endX, validatedParams.endY); await page.mouse.up(); }); - return { - content: [{ type: 'text', text: `Dragged mouse from (${validatedParams.startX}, ${validatedParams.startY}) to (${validatedParams.endX}, ${validatedParams.endY})` }], - }; }, }; @@ -130,13 +124,10 @@ export const type: Tool = { handle: async (context, params) => { const validatedParams = typeSchema.parse(params); - await runAndWait(context, async page => { + return await runAndWait(context, `Typed text "${validatedParams.text}"`, async page => { await page.keyboard.type(validatedParams.text); if (validatedParams.submit) await page.keyboard.press('Enter'); - }, true); - return { - content: [{ type: 'text', text: `Typed text "${validatedParams.text}"` }], - }; + }); }, }; diff --git a/packages/playwright-mcp/src/tools/snapshot.ts b/packages/playwright-mcp/src/tools/snapshot.ts index 866af5c1b0..054e539a37 100644 --- a/packages/playwright-mcp/src/tools/snapshot.ts +++ b/packages/playwright-mcp/src/tools/snapshot.ts @@ -48,7 +48,7 @@ export const click: Tool = { handle: async (context, params) => { const validatedParams = elementSchema.parse(params); - return runAndWait(context, page => refLocator(page, validatedParams.ref).click(), true); + return runAndWait(context, `"${validatedParams.element}" clicked`, page => refLocator(page, validatedParams.ref).click(), true); }, }; @@ -68,7 +68,7 @@ export const drag: Tool = { handle: async (context, params) => { const validatedParams = dragSchema.parse(params); - return runAndWait(context, async page => { + return runAndWait(context, `Dragged "${validatedParams.startElement}" to "${validatedParams.endElement}"`, async page => { const startLocator = refLocator(page, validatedParams.startRef); const endLocator = refLocator(page, validatedParams.endRef); await startLocator.dragTo(endLocator); @@ -85,7 +85,7 @@ export const hover: Tool = { handle: async (context, params) => { const validatedParams = elementSchema.parse(params); - return runAndWait(context, page => refLocator(page, validatedParams.ref).hover(), true); + return runAndWait(context, `Hovered over "${validatedParams.element}"`, page => refLocator(page, validatedParams.ref).hover(), true); }, }; @@ -103,7 +103,7 @@ export const type: Tool = { handle: async (context, params) => { const validatedParams = typeSchema.parse(params); - return await runAndWait(context, async page => { + return await runAndWait(context, `Typed "${validatedParams.text}" into "${validatedParams.element}"`, async page => { const locator = refLocator(page, validatedParams.ref); await locator.fill(validatedParams.text); if (validatedParams.submit) diff --git a/packages/playwright-mcp/src/tools/utils.ts b/packages/playwright-mcp/src/tools/utils.ts index d58f57d4ed..4db7016709 100644 --- a/packages/playwright-mcp/src/tools/utils.ts +++ b/packages/playwright-mcp/src/tools/utils.ts @@ -71,20 +71,25 @@ async function waitForCompletion(page: playwright.Page, callback: () => Promi } } -export async function runAndWait(context: Context, callback: (page: playwright.Page) => Promise, snapshot: boolean = false): Promise { +export async function runAndWait(context: Context, status: string, callback: (page: playwright.Page) => Promise, snapshot: boolean = false): Promise { const page = await context.ensurePage(); - const result = await waitForCompletion(page, () => callback(page)); - return snapshot ? captureAriaSnapshot(page) : result; + await waitForCompletion(page, () => callback(page)); + return snapshot ? captureAriaSnapshot(page, status) : { + content: [{ type: 'text', text: status }], + }; } -export async function captureAriaSnapshot(page: playwright.Page): Promise { +export async function captureAriaSnapshot(page: playwright.Page, status: string = ''): Promise { const snapshot = await page.locator('html').ariaSnapshot({ ref: true }); return { - content: [{ type: 'text', text: ` -# Page URL: ${page.url()} -# Page Title: ${page.title()} -# Page Snapshot -${snapshot}` + content: [{ type: 'text', text: `${status ? `${status}\n` : ''} +- Page URL: ${page.url()} +- Page Title: ${await page.title()} +- Page Snapshot +\`\`\`yaml +${snapshot} +\`\`\` +` }], }; } diff --git a/packages/playwright-mcp/tests/basic.spec.ts b/packages/playwright-mcp/tests/basic.spec.ts index 2a194285c5..4878a0ec5b 100644 --- a/packages/playwright-mcp/tests/basic.spec.ts +++ b/packages/playwright-mcp/tests/basic.spec.ts @@ -14,54 +14,9 @@ * limitations under the License. */ -import path from 'path'; - -import { test, expect } from '@playwright/test'; - -import { MCPServer } from './fixtures'; - -async function startServer(): Promise { - const server = new MCPServer('node', [path.join(__dirname, '../cli.js'), '--headless']); - const initialize = await server.send({ - jsonrpc: '2.0', - id: 0, - method: 'initialize', - params: { - protocolVersion: '2024-11-05', - capabilities: {}, - clientInfo: { - name: 'Playwright Test', - version: '0.0.0', - }, - }, - }); - - expect(initialize).toEqual(expect.objectContaining({ - id: 0, - result: expect.objectContaining({ - protocolVersion: '2024-11-05', - capabilities: { - tools: {}, - resources: {}, - }, - serverInfo: expect.objectContaining({ - name: 'Playwright', - version: expect.any(String), - }), - }), - })); - - await server.sendNoReply({ - jsonrpc: '2.0', - method: 'notifications/initialized', - }); - - return server; -} - -test('test tool list', async ({}) => { - const server = await startServer(); +import { test, expect } from './fixtures'; +test('test tool list', async ({ server }) => { const list = await server.send({ jsonrpc: '2.0', id: 1, @@ -110,9 +65,7 @@ test('test tool list', async ({}) => { })); }); -test('test resources list', async ({}) => { - const server = await startServer(); - +test('test resources list', async ({ server }) => { const list = await server.send({ jsonrpc: '2.0', id: 2, @@ -132,9 +85,7 @@ test('test resources list', async ({}) => { })); }); -test('test browser_navigate', async ({}) => { - const server = await startServer(); - +test('test browser_navigate', async ({ server }) => { const response = await server.send({ jsonrpc: '2.0', id: 2, @@ -142,7 +93,7 @@ test('test browser_navigate', async ({}) => { params: { name: 'browser_navigate', arguments: { - url: 'https://example.com', + url: 'data:text/html,TitleHello, world!', }, }, }); @@ -152,11 +103,60 @@ test('test browser_navigate', async ({}) => { result: { content: [{ type: 'text', - text: expect.stringContaining(` -# Page URL: https://example.com/ -# Page Title: [object Promise] -# Page Snapshot -- document`), + text: ` +- Page URL: data:text/html,TitleHello, world! +- Page Title: Title +- Page Snapshot +\`\`\`yaml +- document [ref=s1e2]: Hello, world! +\`\`\` +`, + }], + }, + })); +}); + +test('test browser_click', async ({ server }) => { + await server.send({ + jsonrpc: '2.0', + id: 2, + method: 'tools/call', + params: { + name: 'browser_navigate', + arguments: { + url: 'data:text/html,Title', + }, + }, + }); + + const response = await server.send({ + jsonrpc: '2.0', + id: 3, + method: 'tools/call', + params: { + name: 'browser_click', + arguments: { + element: 'Submit button', + ref: 's1e4', + }, + }, + }); + + expect(response).toEqual(expect.objectContaining({ + id: 3, + result: { + content: [{ + type: 'text', + text: `\"Submit button\" clicked + +- Page URL: data:text/html,Title +- Page Title: Title +- Page Snapshot +\`\`\`yaml +- document [ref=s2e2]: + - button \"Submit\" [ref=s2e4] +\`\`\` +`, }], }, })); diff --git a/packages/playwright-mcp/tests/fixtures.ts b/packages/playwright-mcp/tests/fixtures.ts index b1b71d4b56..b597322792 100644 --- a/packages/playwright-mcp/tests/fixtures.ts +++ b/packages/playwright-mcp/tests/fixtures.ts @@ -14,12 +14,17 @@ * limitations under the License. */ +import path from 'path'; import { spawn } from 'child_process'; import EventEmitter from 'events'; +import { test as baseTest, expect } from '@playwright/test'; + import type { ChildProcess } from 'child_process'; -export class MCPServer extends EventEmitter { +export { expect } from '@playwright/test'; + +class MCPServer extends EventEmitter { private _child: ChildProcess; private _messageQueue: any[] = []; private _messageResolvers: ((value: any) => void)[] = []; @@ -102,3 +107,45 @@ export class MCPServer extends EventEmitter { }); } } + +export const test = baseTest.extend<{ server: MCPServer }>({ + server: async ({}, use) => { + const server = new MCPServer('node', [path.join(__dirname, '../cli.js'), '--headless']); + const initialize = await server.send({ + jsonrpc: '2.0', + id: 0, + method: 'initialize', + params: { + protocolVersion: '2024-11-05', + capabilities: {}, + clientInfo: { + name: 'Playwright Test', + version: '0.0.0', + }, + }, + }); + + expect(initialize).toEqual(expect.objectContaining({ + id: 0, + result: expect.objectContaining({ + protocolVersion: '2024-11-05', + capabilities: { + tools: {}, + resources: {}, + }, + serverInfo: expect.objectContaining({ + name: 'Playwright', + version: expect.any(String), + }), + }), + })); + + await server.sendNoReply({ + jsonrpc: '2.0', + method: 'notifications/initialized', + }); + + await use(server); + await server.close(); + }, +});