From 145d7981fb4bed8a47338ab08557d2b2e47b0eff Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 10 Feb 2026 08:58:35 -0600 Subject: [PATCH] fix: forward --exact flag to Playwright for role, label, and placeholder locators (#402) Summary - The `--exact` flag on `find role`, `find label`, and `find placeholder` was accepted by the CLI but silently dropped by the server. The Zod validation schema, TypeScript types, and action handlers all lacked the `exact` field, so it was stripped before reaching Playwright's `getByRole`, `getByLabel`, and `getByPlaceholder` calls. - Added `exact` to the schema, types, and handler for all three locators so the flag is forwarded to Playwright as intended. - Added tests confirming `exact: true` survives protocol parsing for `getbyrole`, `getbylabel`, and `getbyplaceholder`. Fixes #402 --- src/actions.ts | 6 +++--- src/protocol.test.ts | 51 ++++++++++++++++++++++++++++++++++++++++++++ src/protocol.ts | 3 +++ src/types.ts | 3 +++ 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/actions.ts b/src/actions.ts index c7a626c9..c420630a 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -887,7 +887,7 @@ async function handleGetByRole( browser: BrowserManager ): Promise { const page = browser.getPage(); - const locator = page.getByRole(command.role as any, { name: command.name }); + const locator = page.getByRole(command.role as any, { name: command.name, exact: command.exact }); switch (command.subaction) { case 'click': @@ -927,7 +927,7 @@ async function handleGetByLabel( browser: BrowserManager ): Promise { const page = browser.getPage(); - const locator = page.getByLabel(command.label); + const locator = page.getByLabel(command.label, { exact: command.exact }); switch (command.subaction) { case 'click': @@ -947,7 +947,7 @@ async function handleGetByPlaceholder( browser: BrowserManager ): Promise { const page = browser.getPage(); - const locator = page.getByPlaceholder(command.placeholder); + const locator = page.getByPlaceholder(command.placeholder, { exact: command.exact }); switch (command.subaction) { case 'click': diff --git a/src/protocol.test.ts b/src/protocol.test.ts index 55a4c205..68d6e197 100644 --- a/src/protocol.test.ts +++ b/src/protocol.test.ts @@ -364,6 +364,23 @@ describe('parseCommand', () => { expect(result.success).toBe(true); }); + it('should parse getbyrole with exact', () => { + const result = parseCommand( + cmd({ + id: '1', + action: 'getbyrole', + role: 'button', + name: 'Continue', + subaction: 'click', + exact: true, + }) + ); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.exact).toBe(true); + } + }); + it('should parse getbytext', () => { const result = parseCommand( cmd({ @@ -388,6 +405,40 @@ describe('parseCommand', () => { ); expect(result.success).toBe(true); }); + + it('should parse getbylabel with exact', () => { + const result = parseCommand( + cmd({ + id: '1', + action: 'getbylabel', + label: 'Email', + subaction: 'fill', + value: 'test@test.com', + exact: true, + }) + ); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.exact).toBe(true); + } + }); + + it('should parse getbyplaceholder with exact', () => { + const result = parseCommand( + cmd({ + id: '1', + action: 'getbyplaceholder', + placeholder: 'Search', + subaction: 'fill', + value: 'test', + exact: true, + }) + ); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.exact).toBe(true); + } + }); }); describe('tabs', () => { diff --git a/src/protocol.ts b/src/protocol.ts index 52fe07d3..d787dc52 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -126,6 +126,7 @@ const getByRoleSchema = baseCommandSchema.extend({ action: z.literal('getbyrole'), role: z.string().min(1), name: z.string().optional(), + exact: z.boolean().optional(), subaction: z.enum(['click', 'fill', 'check', 'hover']), value: z.string().optional(), }); @@ -140,6 +141,7 @@ const getByTextSchema = baseCommandSchema.extend({ const getByLabelSchema = baseCommandSchema.extend({ action: z.literal('getbylabel'), label: z.string().min(1), + exact: z.boolean().optional(), subaction: z.enum(['click', 'fill', 'check']), value: z.string().optional(), }); @@ -147,6 +149,7 @@ const getByLabelSchema = baseCommandSchema.extend({ const getByPlaceholderSchema = baseCommandSchema.extend({ action: z.literal('getbyplaceholder'), placeholder: z.string().min(1), + exact: z.boolean().optional(), subaction: z.enum(['click', 'fill']), value: z.string().optional(), }); diff --git a/src/types.ts b/src/types.ts index d1a9f1a4..5ebea3fd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -108,6 +108,7 @@ export interface GetByRoleCommand extends BaseCommand { action: 'getbyrole'; role: string; name?: string; + exact?: boolean; subaction: 'click' | 'fill' | 'check' | 'hover'; value?: string; } @@ -122,6 +123,7 @@ export interface GetByTextCommand extends BaseCommand { export interface GetByLabelCommand extends BaseCommand { action: 'getbylabel'; label: string; + exact?: boolean; subaction: 'click' | 'fill' | 'check'; value?: string; } @@ -129,6 +131,7 @@ export interface GetByLabelCommand extends BaseCommand { export interface GetByPlaceholderCommand extends BaseCommand { action: 'getbyplaceholder'; placeholder: string; + exact?: boolean; subaction: 'click' | 'fill'; value?: string; }