Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/playwright/src/common/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,18 @@ export type AttachmentPayload = {
export type TestInfoErrorImpl = TestInfoError;

export type TestPausedPayload = {
testId: string;
errors: TestInfoErrorImpl[];
extraData: any;
};

export type CustomMessageRequestPayload = {
testId: string;
request: any;
};

export type CustomMessageResponsePayload = {
response: any;
error?: TestInfoErrorImpl;
};

export type TestEndPayload = {
Expand Down
6 changes: 3 additions & 3 deletions packages/playwright/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { setBoxedStackPrefixes, createGuid, currentZone, debugMode, jsonStringif

import { currentTestInfo } from './common/globals';
import { rootTestType } from './common/testType';
import { runBrowserBackendOnTestPause } from './mcp/test/browserBackend';
import { createCustomMessageHandler } from './mcp/test/browserBackend';

import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test';
import type { ContextReuseMode } from './common/config';
Expand Down Expand Up @@ -417,14 +417,14 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
attachConnectedHeaderIfNeeded(testInfo, browserImpl);
if (!_reuseContext) {
const { context, close } = await _contextFactory();
(testInfo as TestInfoImpl)._onDidPauseTestCallback = () => runBrowserBackendOnTestPause(testInfo, context);
(testInfo as TestInfoImpl)._onCustomMessageCallback = createCustomMessageHandler(testInfo, context);
await use(context);
await close();
return;
}

const context = await browserImpl._wrapApiCall(() => browserImpl._newContextForReuse(), { internal: true });
(testInfo as TestInfoImpl)._onDidPauseTestCallback = () => runBrowserBackendOnTestPause(testInfo, context);
(testInfo as TestInfoImpl)._onCustomMessageCallback = createCustomMessageHandler(testInfo, context);
await use(context);
const closeReason = testInfo.status === 'timedOut' ? 'Test timeout of ' + testInfo.timeout + 'ms exceeded.' : 'Test ended.';
await browserImpl._wrapApiCall(() => browserImpl._disconnectFromReusedContext(closeReason), { internal: true });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class BrowserServerBackend implements ServerBackend {
this._tools = filteredTools(config);
}

async initialize(server: mcpServer.Server, clientInfo: mcpServer.ClientInfo): Promise<void> {
async initialize(clientInfo: mcpServer.ClientInfo): Promise<void> {
this._sessionLog = this._config.saveSession ? await SessionLog.create(this._config, clientInfo) : undefined;
this._context = new Context({
config: this._config,
Expand Down
1 change: 0 additions & 1 deletion packages/playwright/src/mcp/sdk/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,3 @@ export * from './proxyBackend';
export * from './server';
export * from './tool';
export * from './http';
export * from './mdb';
143 changes: 0 additions & 143 deletions packages/playwright/src/mcp/sdk/mdb.ts

This file was deleted.

4 changes: 2 additions & 2 deletions packages/playwright/src/mcp/sdk/proxyBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { debug } from 'playwright-core/lib/utilsBundle';

import * as mcpBundle from './bundle';

import type { ServerBackend, ClientInfo, Server } from './server';
import type { ServerBackend, ClientInfo } from './server';
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import type { Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
Expand All @@ -43,7 +43,7 @@ export class ProxyBackend implements ServerBackend {
this._contextSwitchTool = this._defineContextSwitchTool();
}

async initialize(server: Server, clientInfo: ClientInfo): Promise<void> {
async initialize(clientInfo: ClientInfo): Promise<void> {
this._clientInfo = clientInfo;
}

Expand Down
5 changes: 2 additions & 3 deletions packages/playwright/src/mcp/sdk/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,8 @@ export type ProgressParams = { message?: string, progress?: number, total?: numb
export type ProgressCallback = (params: ProgressParams) => void;

export interface ServerBackend {
initialize?(server: Server, clientInfo: ClientInfo): Promise<void>;
initialize?(clientInfo: ClientInfo): Promise<void>;
listTools(): Promise<Tool[]>;
afterCallTool?(name: string, args: CallToolRequest['params']['arguments'], result: CallToolResult): Promise<void>;
callTool(name: string, args: CallToolRequest['params']['arguments'], progress: ProgressCallback): Promise<CallToolResult>;
serverClosed?(server: Server): void;
}
Expand Down Expand Up @@ -135,7 +134,7 @@ const initializeServer = async (server: Server, backend: ServerBackend, runHeart
timestamp: Date.now(),
};

await backend.initialize?.(server, clientInfo);
await backend.initialize?.(clientInfo);
if (runHeartbeat)
startHeartbeat(server);
};
Expand Down
83 changes: 61 additions & 22 deletions packages/playwright/src/mcp/test/browserBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,75 @@
*/

import * as mcp from '../sdk/exports';
import { defaultConfig, FullConfig } from '../browser/config';
import { defaultConfig } from '../browser/config';
import { BrowserServerBackend } from '../browser/browserServerBackend';
import { Tab } from '../browser/tab';
import { stripAnsiEscapes } from '../../util';

import type * as playwright from '../../../index';
import type { Page } from '../../../../playwright-core/src/client/page';
import type { BrowserContextFactory } from '../browser/browserContextFactory';
import type { TestInfo } from '../../../test';

export type TestPausedExtraData = {
mcpUrl: string;
contextState: string;
export type BrowserMCPRequest = {
initialize?: { clientInfo: mcp.ClientInfo },
listTools?: {},
callTool?: { name: string, arguments: mcp.CallToolRequest['params']['arguments'] },
close?: {},
};

export async function runBrowserBackendOnTestPause(testInfo: TestInfo, context: playwright.BrowserContext) {
export type BrowserMCPResponse = {
initialize?: { pausedMessage: string },
listTools?: mcp.Tool[],
callTool?: mcp.CallToolResult,
close?: {},
};

export function createCustomMessageHandler(testInfo: TestInfo, context: playwright.BrowserContext) {
let backend: BrowserServerBackend | undefined;
return async (data: BrowserMCPRequest): Promise<BrowserMCPResponse> => {
if (data.initialize) {
if (backend)
throw new Error('MCP backend is already initialized');
backend = new BrowserServerBackend({ ...defaultConfig, capabilities: ['testing'] }, identityFactory(context));
await backend.initialize(data.initialize.clientInfo);
const pausedMessage = await generatePausedMessage(testInfo, context);
return { initialize: { pausedMessage } };
}

if (data.listTools) {
if (!backend)
throw new Error('MCP backend is not initialized');
return { listTools: await backend.listTools() };
}

if (data.callTool) {
if (!backend)
throw new Error('MCP backend is not initialized');
return { callTool: await backend.callTool(data.callTool.name, data.callTool.arguments) };
}

if (data.close) {
backend?.serverClosed();
backend = undefined;
return { close: {} };
}

throw new Error('Unknown MCP request');
};
}

async function generatePausedMessage(testInfo: TestInfo, context: playwright.BrowserContext) {
const lines: string[] = [];

if (testInfo.errors.length) {
lines.push(`### Paused on error:`);
for (const error of testInfo.errors)
lines.push(stripAnsiEscapes(error.message || ''));
} else {
lines.push(`### Paused at end of test. ready for interaction`);
}

for (let i = 0; i < context.pages().length; i++) {
const page = context.pages()[i];
const stateSuffix = context.pages().length > 1 ? (i + 1) + ' of ' + (context.pages().length) : 'state';
Expand All @@ -57,24 +109,11 @@ export async function runBrowserBackendOnTestPause(testInfo: TestInfo, context:
);
}

const config: FullConfig = {
...defaultConfig,
capabilities: ['testing'],
};
lines.push('');
if (testInfo.errors.length)
lines.push(`### Task`, `Try recovering from the error prior to continuing`);

const factory: mcp.ServerBackendFactory = {
name: 'Playwright',
nameInConfig: 'playwright',
version: '0.0.0',
create: () => new BrowserServerBackend(config, identityFactory(context))
};
const httpServer = await mcp.startHttpServer({ port: 0 });
const mcpUrl = await mcp.installHttpTransport(httpServer, factory, true);
const dispose = async () => {
await new Promise(cb => httpServer.close(cb));
};
const extraData = { mcpUrl, contextState: lines.join('\n') } as TestPausedExtraData;
return { extraData, dispose };
return lines.join('\n');
}

function identityFactory(browserContext: playwright.BrowserContext): BrowserContextFactory {
Expand Down
Loading
Loading