diff --git a/packages/playwright-core/src/tools/cli-daemon/commands.ts b/packages/playwright-core/src/tools/cli-daemon/commands.ts index 5acd3c3b3dcdf..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.array(z.string()).optional().describe('Modifier keys to press'), + 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.array(z.string()).optional().describe('Modifier keys to press'), + 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 17c2a3fdd5250..575b182cade57 100644 --- a/tests/mcp/cli-core.spec.ts +++ b/tests/mcp/cli-core.spec.ts @@ -74,6 +74,17 @@ test('dblclick', async ({ cli, server }) => { expect(snapshot).toContain('dblclick 0'); }); +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 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 }) => { server.setContent('/', ``, 'text/html'); const { snapshot } = await cli('open', server.PREFIX);