mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	chore: add mcp server fixture (#35262)
This commit is contained in:
		
							parent
							
								
									23c4c256b0
								
							
						
					
					
						commit
						0350ca32b4
					
				@ -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}`,
 | 
			
		||||
        }],
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -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}"` }],
 | 
			
		||||
    };
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -71,20 +71,25 @@ async function waitForCompletion<R>(page: playwright.Page, callback: () => Promi
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function runAndWait(context: Context, callback: (page: playwright.Page) => Promise<any>, snapshot: boolean = false): Promise<ToolResult> {
 | 
			
		||||
export async function runAndWait(context: Context, status: string, callback: (page: playwright.Page) => Promise<any>, snapshot: boolean = false): Promise<ToolResult> {
 | 
			
		||||
  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<ToolResult> {
 | 
			
		||||
export async function captureAriaSnapshot(page: playwright.Page, status: string = ''): Promise<ToolResult> {
 | 
			
		||||
  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}
 | 
			
		||||
\`\`\`
 | 
			
		||||
`
 | 
			
		||||
    }],
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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<MCPServer> {
 | 
			
		||||
  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,<html><title>Title</title><body>Hello, world!</body></html>',
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
@ -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,<html><title>Title</title><body>Hello, world!</body></html>
 | 
			
		||||
- 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,<html><title>Title</title><button>Submit</button></html>',
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  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,<html><title>Title</title><button>Submit</button></html>
 | 
			
		||||
- Page Title: Title
 | 
			
		||||
- Page Snapshot
 | 
			
		||||
\`\`\`yaml
 | 
			
		||||
- document [ref=s2e2]:
 | 
			
		||||
  - button \"Submit\" [ref=s2e4]
 | 
			
		||||
\`\`\`
 | 
			
		||||
`,
 | 
			
		||||
      }],
 | 
			
		||||
    },
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user