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
28 changes: 20 additions & 8 deletions packages/playwright/src/mcp/terminal/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ import { declareCommand } from './command';

import type { AnyCommandSchema } from './command';

const numberArg = z.preprocess((val, ctx) => {
const number = Number(val);
if (Number.isNaN(number)) {
ctx.issues.push({
code: 'custom',
message: `expected number, received '${val}'`,
input: val,
});
}
return number;
}, z.number());

// Navigation commands

const open = declareCommand({
Expand Down Expand Up @@ -143,8 +155,8 @@ const mouseMove = declareCommand({
description: 'Move mouse to a given position',
category: 'mouse',
args: z.object({
x: z.number().describe('X coordinate'),
y: z.number().describe('Y coordinate'),
x: numberArg.describe('X coordinate'),
y: numberArg.describe('Y coordinate'),
}),
toolName: 'browser_mouse_move_xy',
toolParams: ({ x, y }) => ({ x, y }),
Expand Down Expand Up @@ -177,8 +189,8 @@ const mouseWheel = declareCommand({
description: 'Scroll mouse wheel',
category: 'mouse',
args: z.object({
dx: z.number().describe('Y delta'),
dy: z.number().describe('X delta'),
dx: numberArg.describe('Y delta'),
dy: numberArg.describe('X delta'),
}),
toolName: 'browser_mouse_wheel',
toolParams: ({ dx: deltaY, dy: deltaX }) => ({ deltaY, deltaX }),
Expand Down Expand Up @@ -348,8 +360,8 @@ const resize = declareCommand({
description: 'Resize the browser window',
category: 'core',
args: z.object({
w: z.number().describe('Width of the browser window'),
h: z.number().describe('Height of the browser window'),
w: numberArg.describe('Width of the browser window'),
h: numberArg.describe('Height of the browser window'),
}),
toolName: 'browser_resize',
toolParams: ({ w: width, h: height }) => ({ width, height }),
Expand Down Expand Up @@ -393,7 +405,7 @@ const tabClose = declareCommand({
description: 'Close a browser tab',
category: 'tabs',
args: z.object({
index: z.number().optional().describe('Tab index. If omitted, current tab is closed.'),
index: numberArg.optional().describe('Tab index. If omitted, current tab is closed.'),
}),
toolName: 'browser_tabs',
toolParams: ({ index }) => ({ action: 'close', index }),
Expand All @@ -404,7 +416,7 @@ const tabSelect = declareCommand({
description: 'Select a browser tab',
category: 'tabs',
args: z.object({
index: z.number().describe('Tab index'),
index: numberArg.describe('Tab index'),
}),
toolName: 'browser_tabs',
toolParams: ({ index }) => ({ action: 'select', index }),
Expand Down
25 changes: 25 additions & 0 deletions packages/playwright/src/mcp/terminal/helpGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* limitations under the License.
*/

import { z } from 'playwright-core/lib/mcpBundle';

import { commands } from './commands';

import type zodType from 'zod';
Expand Down Expand Up @@ -146,12 +148,35 @@ function generateReadmeEntry(command: AnyCommandSchema): string {
return formatWithGap(prefix, suffix, 40);
}

function unwrapZodType(schema: zodType.ZodTypeAny): zodType.ZodTypeAny {
if ('unwrap' in schema && typeof schema.unwrap === 'function')
return unwrapZodType(schema.unwrap());
return schema;
}

export function generateHelpJSON() {
const stringOptions = new Set<string>();
const booleanOptions = new Set<string>();
for (const command of Object.values(commands)) {
if (!command.options)
continue;
const optionsShape = (command.options as zodType.ZodObject<any>).shape;
for (const [name, schema] of Object.entries(optionsShape)) {
const innerSchema = unwrapZodType(schema as zodType.ZodTypeAny);
if (innerSchema instanceof z.ZodString)
stringOptions.add(name);
if (innerSchema instanceof z.ZodBoolean)
booleanOptions.add(name);
}
}

const help = {
global: generateHelp(),
commands: Object.fromEntries(
Object.entries(commands).map(([name, command]) => [name, generateCommandHelp(command)])
),
stringOptions: [...stringOptions],
booleanOptions: [...booleanOptions],

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this outputs a lot more booleanOptions than we currently have:

"stringOptions": [
    "browser",
    "config",
    "profile",
    "modifiers",
    "filename",
    "domain",
    "path",
    "body",
    "content-type",
    "remove-header"
  ],
  "booleanOptions": [
    "extension",
    "headed",
    "persistent",
    "submit",
    "clear",
    "full-page",
    "httpOnly",
    "secure",
    "skills",
    "static"
  ]

};
return help;
}
Expand Down
11 changes: 8 additions & 3 deletions packages/playwright/src/mcp/terminal/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,20 +493,25 @@ const booleanOptions: (keyof (GlobalOptions & OpenOptions & { all?: boolean }))[

export async function program(packageLocation: string) {
const clientInfo = createClientInfo(packageLocation);
const help = require('./help.json');

const argv = process.argv.slice(2);
const args: MinimistArgs = require('minimist')(argv, { boolean: booleanOptions });
for (const option of booleanOptions) {
const boolean = [...help.booleanOptions, ...booleanOptions];
const args: MinimistArgs = require('minimist')(argv, { boolean, string: [...help.stringOptions, '_'] });
for (const option of boolean) {
if (!argv.includes(`--${option}`) && !argv.includes(`--no-${option}`))
delete args[option];
if (argv.some(arg => arg.startsWith(`--${option}=`) || arg.startsWith(`--no-${option}=`))) {
console.error(`boolean option '--${option}' should not be passed with '=value', use '--${option}' or '--no-${option}' instead`);
process.exit(1);
}
}
// Normalize -s alias to --session
if (args.s) {
args.session = args.s;
delete args.s;
}

const help = require('./help.json');
const commandName = args._?.[0];

if (args.version || args.v) {
Expand Down
9 changes: 9 additions & 0 deletions tests/mcp/cli-core.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ test('fill', async ({ cli, server }) => {
expect(fillSnapshot).toBe(`- textbox [active] [ref=e2]: Hello, world!`);
});

test('fill numeric', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-cli/issues/235' } }, async ({ cli, server }) => {
server.setContent('/', `<input type=text>`, 'text/html');
const { snapshot } = await cli('open', server.PREFIX);
expect(snapshot).toContain(`- textbox [ref=e2]`);

const { snapshot: fillSnapshot } = await cli('fill', 'e2', '42', '--submit');
expect(fillSnapshot).toContain(`[ref=e2]: "42"`);
});

test('hover', async ({ cli, server }) => {
server.setContent('/', eventsPage, 'text/html');
await cli('open', server.PREFIX);
Expand Down
17 changes: 13 additions & 4 deletions tests/mcp/cli-parsing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,18 @@ test('too many arguments', async ({ cli, server }) => {

test('wrong option type', async ({ cli, server }) => {
await cli('open', server.HELLO_WORLD);
const { error, exitCode } = await cli('type', 'foo', '--submit=bar');
expect(exitCode).toBe(1);
expect(error).toContain(`error: '--submit' option: expected boolean, received string`);
const boolean = await cli('type', 'foo', '--submit=bar');
expect(boolean.exitCode).toBe(1);
expect(boolean.error).toContain(`boolean option '--submit' should not be passed with '=value', use '--submit' or '--no-submit' instead`);
const status = await cli('route', '.', '--status=OK');
expect(status.exitCode).toBe(1);
expect(status.error).toContain(`error: '--status' option: expected number, received string`);
});

test('arg after boolean option', async ({ cli, server }) => {
await cli('open', server.HELLO_WORLD);
const boolean = await cli('type', '--submit', 'foo');
expect(boolean.exitCode).toBe(0);
});

test('missing argument', async ({ cli, server }) => {
Expand All @@ -46,5 +55,5 @@ test('wrong argument type', async ({ cli, server }) => {
await cli('open', server.HELLO_WORLD);
const { error, exitCode } = await cli('mousemove', '12', 'foo');
expect(exitCode).toBe(1);
expect(error).toContain(`error: 'y' argument: expected number, received string`);
expect(error).toContain(`error: 'y' argument: expected number, received 'foo'`);
});
10 changes: 10 additions & 0 deletions tests/mcp/cli-route.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ test('route with header', async ({ cli, server }) => {
expect(output).toContain('Authorization');
});

test('route with numerical body', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-cli/issues/235' } }, async ({ cli, server }) => {
await cli('open', server.EMPTY_PAGE);

await cli('route', '**/api/data', '--body', '42', '--content-type', 'text/plain');

const { output } = await cli('route-list');
expect(output).toContain('**/api/data');
expect(output).toContain('contentType=text/plain');
});

test('unroute removes specific route', async ({ cli, server }) => {
await cli('open', server.EMPTY_PAGE);

Expand Down
Loading