Skip to content

Commit 95a728a

Browse files
authored
chore(cli): implement test harness (#38914)
1 parent 7b68bf6 commit 95a728a

18 files changed

Lines changed: 575 additions & 175 deletions

packages/playwright/src/mcp/browser/response.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,12 @@ export class Response {
7272
// What can go into a file goes into a file in outputMode === file.
7373
if (this._context.config.outputMode === 'file') {
7474
if (!result.suggestedFilename)
75-
result.suggestedFilename = dateAsFileName(result.ext ?? (result.text ? 'txt' : 'bin'));
75+
result.suggestedFilename = dateAsFileName(result.ext ?? (result.text !== undefined ? 'txt' : 'bin'));
7676
}
7777

7878
const entry: Result = { text: result.text, data: result.data, title: result.title };
7979
if (result.suggestedFilename)
80-
entry.filename = await this._context.outputFile(result.suggestedFilename, { origin: 'llm', title: result.title ?? 'Saved result' });
80+
entry.filename = await this._context.outputFile(result.suggestedFilename, { origin: 'llm', title: result.title || 'Saved result' });
8181

8282
this._results.push(entry);
8383
return { fileName: entry.filename };
@@ -124,10 +124,11 @@ export class Response {
124124
const text = addSection('Result');
125125
for (const result of this._results) {
126126
if (result.filename) {
127-
text.push(`- [${result.title}](${rootPath ? path.relative(rootPath, result.filename) : result.filename})`);
127+
if (result.text !== undefined || result.data)
128+
text.push(`- [${result.title}](${rootPath ? path.relative(rootPath, result.filename) : result.filename})`);
128129
if (result.data)
129130
await fs.promises.writeFile(result.filename, result.data);
130-
else if (result.text)
131+
else if (result.text !== undefined)
131132
await fs.promises.writeFile(result.filename, this._redactText(result.text));
132133
} else if (result.text) {
133134
text.push(result.text);
@@ -247,7 +248,7 @@ export function renderTabMarkdown(tab: TabHeader): string[] {
247248

248249
export function renderTabsMarkdown(tabs: TabHeader[]): string[] {
249250
if (!tabs.length)
250-
return ['No open tabs. Use the "browser_navigate" tool to navigate to a page first.'];
251+
return ['No open tabs. Navigate to a URL to create one.'];
251252

252253
const lines: string[] = [];
253254
for (let i = 0; i < tabs.length; i++) {

packages/playwright/src/mcp/browser/tools/common.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,7 @@ const resize = defineTabTool({
5252

5353
handle: async (tab, params, response) => {
5454
response.addCode(`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`);
55-
56-
await tab.waitForCompletion(async () => {
57-
await tab.page.setViewportSize({ width: params.width, height: params.height });
58-
});
55+
await tab.page.setViewportSize({ width: params.width, height: params.height });
5956
},
6057
});
6158

packages/playwright/src/mcp/browser/tools/console.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const console = defineTabTool({
3232
handle: async (tab, params, response) => {
3333
const messages = await tab.consoleMessages(params.level);
3434
const text = messages.map(message => message.toString()).join('\n');
35-
await response.addResult({ text, suggestedFilename: params.filename });
35+
await response.addResult({ text, suggestedFilename: params.filename, title: 'Console' });
3636
},
3737
});
3838

packages/playwright/src/mcp/browser/tools/dialogs.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ export const handleDialog = defineTabTool({
3232
},
3333

3434
handle: async (tab, params, response) => {
35-
response.setIncludeSnapshot();
36-
3735
const dialogState = tab.modalStates().find(state => state.type === 'dialog');
3836
if (!dialogState)
3937
throw new Error('No dialog visible');

packages/playwright/src/mcp/browser/tools/evaluate.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ const evaluateSchema = z.object({
2525
function: z.string().describe('() => { /* code */ } or (element) => { /* code */ } when element is provided'),
2626
element: z.string().optional().describe('Human-readable element description used to obtain permission to interact with the element'),
2727
ref: z.string().optional().describe('Exact target element reference from the page snapshot'),
28-
filename: z.string().optional().describe('Filename to save the result to. If not provided, result is returned as JSON string.'),
2928
});
3029

3130
const evaluate = defineTabTool({
@@ -39,11 +38,9 @@ const evaluate = defineTabTool({
3938
},
4039

4140
handle: async (tab, params, response) => {
42-
response.setIncludeSnapshot();
43-
4441
let locator: Awaited<ReturnType<Tab['refLocator']>> | undefined;
45-
if (params.ref && params.element) {
46-
locator = await tab.refLocator({ ref: params.ref, element: params.element });
42+
if (params.ref) {
43+
locator = await tab.refLocator({ ref: params.ref, element: params.element || 'element' });
4744
response.addCode(`await page.${locator.resolved}.evaluate(${escapeWithQuotes(params.function)});`);
4845
} else {
4946
response.addCode(`await page.evaluate(${escapeWithQuotes(params.function)});`);
@@ -53,7 +50,7 @@ const evaluate = defineTabTool({
5350
const receiver = locator?.locator ?? tab.page;
5451
const result = await receiver._evaluateFunction(params.function);
5552
const text = JSON.stringify(result, null, 2) || 'undefined';
56-
await response.addResult({ text, suggestedFilename: params.filename });
53+
response.addTextResult(text);
5754
});
5855
},
5956
});

packages/playwright/src/mcp/browser/tools/network.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const requests = defineTabTool({
4242
if (rendered)
4343
text.push(rendered);
4444
}
45-
await response.addResult({ text: text.join('\n'), suggestedFilename: params.filename });
45+
await response.addResult({ text: text.join('\n'), suggestedFilename: params.filename, title: 'Network' });
4646
},
4747
});
4848

packages/playwright/src/mcp/browser/tools/runCode.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import { defineTabTool } from './tool';
2323

2424
const codeSchema = z.object({
2525
code: z.string().describe(`A JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction. For example: \`async (page) => { await page.getByRole('button', { name: 'Submit' }).click(); return await page.title(); }\``),
26-
filename: z.string().optional().describe('Filename to save the result to. If not provided, result is returned as JSON string.'),
2726
});
2827

2928
const runCode = defineTabTool({
@@ -37,7 +36,6 @@ const runCode = defineTabTool({
3736
},
3837

3938
handle: async (tab, params, response) => {
40-
response.setIncludeSnapshot();
4139
response.addCode(`await (${params.code})(page);`);
4240
const __end__ = new ManualPromise<void>();
4341
const context = {
@@ -57,7 +55,7 @@ const runCode = defineTabTool({
5755
await vm.runInContext(snippet, context);
5856
const result = await __end__;
5957
if (typeof result === 'string')
60-
await response.addResult({ text: result, suggestedFilename: params.filename });
58+
response.addTextResult(result);
6159
});
6260
},
6361
});

packages/playwright/src/mcp/browser/tools/screenshot.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@ const screenshot = defineTabTool({
4343
},
4444

4545
handle: async (tab, params, response) => {
46-
if (!!params.element !== !!params.ref)
47-
throw new Error('Both element and ref must be provided or neither.');
4846
if (params.fullPage && params.ref)
4947
throw new Error('fullPage cannot be used with element screenshots.');
5048

@@ -55,9 +53,8 @@ const screenshot = defineTabTool({
5553
scale: 'css',
5654
...(params.fullPage !== undefined && { fullPage: params.fullPage })
5755
};
58-
const isElementScreenshot = params.element && params.ref;
5956

60-
const screenshotTarget = isElementScreenshot ? params.element : (params.fullPage ? 'full page' : 'viewport');
57+
const screenshotTarget = params.ref ? params.element || 'element' : (params.fullPage ? 'full page' : 'viewport');
6158
const ref = params.ref ? await tab.refLocator({ element: params.element || '', ref: params.ref }) : null;
6259

6360
const data = ref ? await ref.locator.screenshot(options) : await tab.page.screenshot(options);

packages/playwright/src/mcp/browser/tools/snapshot.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,9 @@ const check = defineTabTool({
191191
},
192192

193193
handle: async (tab, params, response) => {
194-
const { resolved } = await tab.refLocator(params);
194+
const { locator, resolved } = await tab.refLocator(params);
195195
response.addCode(`await page.${resolved}.check();`);
196+
await locator.check();
196197
},
197198
});
198199

@@ -208,8 +209,9 @@ const uncheck = defineTabTool({
208209
},
209210

210211
handle: async (tab, params, response) => {
211-
const { resolved } = await tab.refLocator(params);
212+
const { locator, resolved } = await tab.refLocator(params);
212213
response.addCode(`await page.${resolved}.uncheck();`);
214+
await locator.uncheck();
213215
},
214216
});
215217

packages/playwright/src/mcp/terminal/cli.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
/* eslint-disable no-restricted-properties */
1919

2020
import { spawn } from 'child_process';
21+
2122
import crypto from 'crypto';
2223
import fs from 'fs';
2324
import net from 'net';
@@ -26,6 +27,8 @@ import path from 'path';
2627
import { debug } from 'playwright-core/lib/utilsBundle';
2728
import { SocketConnection } from './socketConnection';
2829

30+
import type { SpawnOptions } from 'child_process';
31+
2932
const debugCli = debug('pw:cli');
3033
const packageJSON = require('../../../package.json');
3134

@@ -158,7 +161,7 @@ class SessionManager {
158161
}
159162

160163
private async _connect(sessionName: string): Promise<Session> {
161-
const socketPath = this._daemonSocketPath(sessionName);
164+
const socketPath = process.env.PLAYWRIGHT_DAEMON_SOCKET_PATH || this._daemonSocketPath(sessionName);
162165
debugCli(`Connecting to daemon at ${socketPath}`);
163166

164167
const socketExists = await fs.promises.stat(socketPath)
@@ -176,11 +179,11 @@ class SessionManager {
176179
}
177180
}
178181

179-
const cliPath = path.join(__dirname, '../../../cli.js');
180-
debugCli(`Will launch daemon process: ${cliPath}`);
182+
if (process.env.PLAYWRIGHT_DAEMON_SOCKET_PATH)
183+
throw new Error(`Socket path ${socketPath} does not exist`);
181184

182185
const userDataDir = path.resolve(daemonSocketDir, `${sessionName}-user-data`);
183-
const child = spawn(process.execPath, [cliPath, 'run-mcp-server', `--daemon=${socketPath}`, `--user-data-dir=${userDataDir}`], {
186+
const child = spawnDaemon(socketPath, userDataDir, {
184187
detached: true,
185188
stdio: 'ignore',
186189
cwd: process.cwd(), // Will be used as root.
@@ -298,6 +301,12 @@ const daemonSocketDir = (() => {
298301
return path.join(localCacheDir, 'ms-playwright', 'daemon', 'daemon', socketDirHash);
299302
})();
300303

304+
function spawnDaemon(socketPath: string, userDataDir: string, options: SpawnOptions) {
305+
const cliPath = path.join(__dirname, '../../../cli.js');
306+
debugCli(`Will launch daemon process: ${cliPath}`);
307+
return spawn(process.execPath, [cliPath, 'run-mcp-server', `--daemon=${socketPath}`, `--user-data-dir=${userDataDir}`], options);
308+
}
309+
301310
async function main() {
302311
const argv = process.argv.slice(2);
303312
const args = require('minimist')(argv);

0 commit comments

Comments
 (0)