From e4805e74a5d17b2f405f4014606a814e84be7b82 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 11 May 2026 11:21:00 -0700 Subject: [PATCH 1/2] fix(cli): accept single --modifiers value for click and dblclick Minimist returns a string for a single occurrence of an option and only returns an array when the option is repeated, so `playwright-cli click e55 --modifiers Control` failed schema validation. Accept both forms and normalize to an array, matching the pattern used for `--header`, `--path`, and `--data`. Fixes: https://github.com/microsoft/playwright-cli/issues/406 --- .../src/tools/cli-daemon/commands.ts | 4 ++-- tests/mcp/cli-core.spec.ts | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/tools/cli-daemon/commands.ts b/packages/playwright-core/src/tools/cli-daemon/commands.ts index 5acd3c3b3dcdf..ac4366c3e9637 100644 --- a/packages/playwright-core/src/tools/cli-daemon/commands.ts +++ b/packages/playwright-core/src/tools/cli-daemon/commands.ts @@ -235,7 +235,7 @@ const click = declareCommand({ button: z.string().optional().describe('Button to click, defaults to left'), }), options: z.object({ - modifiers: z.array(z.string()).optional().describe('Modifier keys to press'), + modifiers: z.union([z.string(), z.array(z.string())]).optional().transform(v => v ? (Array.isArray(v) ? v : [v]) : undefined).describe('Modifier key to press (repeatable)'), }), toolName: 'browser_click', toolParams: ({ target, button, modifiers }) => ({ target, button, modifiers }), @@ -250,7 +250,7 @@ const doubleClick = declareCommand({ button: z.string().optional().describe('Button to click, defaults to left'), }), options: z.object({ - modifiers: z.array(z.string()).optional().describe('Modifier keys to press'), + modifiers: z.union([z.string(), z.array(z.string())]).optional().transform(v => v ? (Array.isArray(v) ? v : [v]) : undefined).describe('Modifier key to press (repeatable)'), }), toolName: 'browser_click', toolParams: ({ target, button, modifiers }) => ({ target, button, modifiers, doubleClick: true }), diff --git a/tests/mcp/cli-core.spec.ts b/tests/mcp/cli-core.spec.ts index 17c2a3fdd5250..2fcee2af81f1b 100644 --- a/tests/mcp/cli-core.spec.ts +++ b/tests/mcp/cli-core.spec.ts @@ -74,6 +74,20 @@ test('dblclick', async ({ cli, server }) => { expect(snapshot).toContain('dblclick 0'); }); +test('click with single --modifiers', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-cli/issues/406' } }, async ({ cli, server }) => { + server.setContent('/', ``, 'text/html'); + await cli('open', server.PREFIX); + const { output } = await cli('click', 'e2', '--modifiers', 'Control'); + expect(output).toContain(`modifiers: ['Control']`); +}); + +test('click with repeated --modifiers', async ({ cli, server }) => { + server.setContent('/', ``, 'text/html'); + await cli('open', server.PREFIX); + const { output } = await cli('click', 'e2', '--modifiers', 'Control', '--modifiers', 'Shift'); + expect(output).toContain(`modifiers: ['Control', 'Shift']`); +}); + test('type', async ({ cli, server }) => { server.setContent('/', ``, 'text/html'); const { snapshot } = await cli('open', server.PREFIX); From 06d6986d8e0975ed303b46198e139cec765c2b1b Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 11 May 2026 11:27:50 -0700 Subject: [PATCH 2/2] chore(cli): extract stringArrayArg helper for repeatable options Five identical inline copies of `z.union([z.string(), z.array(z.string())]).optional().transform(...)` collapsed into one `stringArrayArg` helper, mirroring the existing `numberArg` helper in the same file. Merge the two `--modifiers` test cases into one and tighten assertions to match the full generated `click({ modifiers: [...] })` snippet. --- .../src/tools/cli-daemon/commands.ts | 13 ++++++++----- tests/mcp/cli-core.spec.ts | 15 ++++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/playwright-core/src/tools/cli-daemon/commands.ts b/packages/playwright-core/src/tools/cli-daemon/commands.ts index ac4366c3e9637..a69b00ec0dc16 100644 --- a/packages/playwright-core/src/tools/cli-daemon/commands.ts +++ b/packages/playwright-core/src/tools/cli-daemon/commands.ts @@ -33,6 +33,9 @@ const numberArg = z.preprocess((val, ctx) => { return number; }, z.number()); +// Minimist returns a string for a single occurrence and an array for repeated. +const stringArrayArg = z.union([z.string(), z.array(z.string())]).transform(v => Array.isArray(v) ? v : [v]); + // Navigation commands const open = declareCommand({ @@ -235,7 +238,7 @@ const click = declareCommand({ button: z.string().optional().describe('Button to click, defaults to left'), }), options: z.object({ - modifiers: z.union([z.string(), z.array(z.string())]).optional().transform(v => v ? (Array.isArray(v) ? v : [v]) : undefined).describe('Modifier key to press (repeatable)'), + modifiers: stringArrayArg.optional().describe('Modifier key to press (repeatable)'), }), toolName: 'browser_click', toolParams: ({ target, button, modifiers }) => ({ target, button, modifiers }), @@ -250,7 +253,7 @@ const doubleClick = declareCommand({ button: z.string().optional().describe('Button to click, defaults to left'), }), options: z.object({ - modifiers: z.union([z.string(), z.array(z.string())]).optional().transform(v => v ? (Array.isArray(v) ? v : [v]) : undefined).describe('Modifier key to press (repeatable)'), + modifiers: stringArrayArg.optional().describe('Modifier key to press (repeatable)'), }), toolName: 'browser_click', toolParams: ({ target, button, modifiers }) => ({ target, button, modifiers, doubleClick: true }), @@ -276,8 +279,8 @@ const drop = declareCommand({ target: z.string().describe(elementTargetDescription), }), options: z.object({ - path: z.union([z.string(), z.array(z.string())]).optional().transform(v => v ? (Array.isArray(v) ? v : [v]) : undefined).describe('Absolute path to a file to drop onto the element (repeatable)'), - data: z.union([z.string(), z.array(z.string())]).optional().transform(v => v ? (Array.isArray(v) ? v : [v]) : undefined).describe('Data to drop in "mime/type=value" format, e.g. --data "text/plain=hello" (repeatable)'), + path: stringArrayArg.optional().describe('Absolute path to a file to drop onto the element (repeatable)'), + data: stringArrayArg.optional().describe('Data to drop in "mime/type=value" format, e.g. --data "text/plain=hello" (repeatable)'), }), toolName: 'browser_drop', toolParams: ({ target, path, data }) => { @@ -730,7 +733,7 @@ const routeMock = declareCommand({ status: numberArg.optional().describe('HTTP status code (default: 200)'), body: z.string().optional().describe('Response body (text or JSON string)'), ['content-type']: z.string().optional().describe('Content-Type header'), - header: z.union([z.string(), z.array(z.string())]).optional().transform(v => v ? (Array.isArray(v) ? v : [v]) : undefined).describe('Header to add in "Name: Value" format (repeatable)'), + header: stringArrayArg.optional().describe('Header to add in "Name: Value" format (repeatable)'), ['remove-header']: z.string().optional().describe('Comma-separated header names to remove'), }), toolName: 'browser_route', diff --git a/tests/mcp/cli-core.spec.ts b/tests/mcp/cli-core.spec.ts index 2fcee2af81f1b..575b182cade57 100644 --- a/tests/mcp/cli-core.spec.ts +++ b/tests/mcp/cli-core.spec.ts @@ -74,18 +74,15 @@ test('dblclick', async ({ cli, server }) => { expect(snapshot).toContain('dblclick 0'); }); -test('click with single --modifiers', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-cli/issues/406' } }, async ({ cli, server }) => { +test('click with --modifiers', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-cli/issues/406' } }, async ({ cli, server }) => { server.setContent('/', ``, 'text/html'); await cli('open', server.PREFIX); - const { output } = await cli('click', 'e2', '--modifiers', 'Control'); - expect(output).toContain(`modifiers: ['Control']`); -}); -test('click with repeated --modifiers', async ({ cli, server }) => { - server.setContent('/', ``, 'text/html'); - await cli('open', server.PREFIX); - const { output } = await cli('click', 'e2', '--modifiers', 'Control', '--modifiers', 'Shift'); - expect(output).toContain(`modifiers: ['Control', 'Shift']`); + const single = await cli('click', 'e2', '--modifiers', 'Control'); + expect(single.output).toContain(`await page.getByRole('button', { name: 'Submit' }).click({\n modifiers: ['Control']\n});`); + + const repeated = await cli('click', 'e2', '--modifiers', 'Control', '--modifiers', 'Shift'); + expect(repeated.output).toContain(`await page.getByRole('button', { name: 'Submit' }).click({\n modifiers: ['Control', 'Shift']\n});`); }); test('type', async ({ cli, server }) => {