From 5015a939a9c16decfc8005a5a856cb227584e89f Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 3 Oct 2026 20:55:38 +0000 Subject: [PATCH] G5: browse type --selector (#2936) --selector focuses the element (sel or @ref) and types into it. The bare form still types its text literally into the focused element; text that starts with -- goes after a -- separator. When the first word looks like a CSS selector, the result adds a hint to use --selector without echoing the typed text, and never refuses based on page contents. Help and command usage updated. Co-authored-by: Thomas Reuser --- browse/src/cli.ts | 2 +- browse/src/commands.ts | 2 +- browse/src/write-commands.ts | 28 +++++++++++++++++++++++++--- browse/test/commands.test.ts | 29 +++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/browse/src/cli.ts b/browse/src/cli.ts index 9f523d6ed..02369ab84 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -1647,7 +1647,7 @@ Usage: browse [args...] Navigation: goto | back | forward | reload | url Content: text | html [sel] | links | forms | accessibility Interaction: click | fill | select - hover | type | press + hover | type [--selector ] | press scroll [sel] | wait | viewport upload [file2...] cookie-import diff --git a/browse/src/commands.ts b/browse/src/commands.ts index 3be130715..9a0bfdf3d 100644 --- a/browse/src/commands.ts +++ b/browse/src/commands.ts @@ -122,7 +122,7 @@ export const COMMAND_DESCRIPTIONS: Record ' }, 'select': { category: 'Interaction', description: 'Select dropdown option by value, label, or visible text', usage: 'select ' }, 'hover': { category: 'Interaction', description: 'Hover element', usage: 'hover ' }, - 'type': { category: 'Interaction', description: 'Type into focused element', usage: 'type ' }, + 'type': { category: 'Interaction', description: 'Send keystrokes to the focused element, or to with --selector (focuses it first). Text that starts with -- goes after a -- separator.', usage: 'type [--selector ] [--] ' }, 'press': { category: 'Interaction', description: 'Press a Playwright keyboard key against the focused element. Names are case-sensitive: Enter, Tab, Escape, ArrowUp/Down/Left/Right, Backspace, Delete, Home, End, PageUp, PageDown. Modifiers combine with +: Shift+Enter, Control+A, Meta+K. Single printable chars (a, A, 1) work too. Full key list: https://playwright.dev/docs/api/class-keyboard#keyboard-press', usage: 'press ' }, 'scroll': { category: 'Interaction', description: 'With a selector, smooth-scrolls the element into view. Without a selector, jumps to page bottom. No --by/--to amount option; for pixel-precise scrolling use `js window.scrollTo(0, N)`.', usage: 'scroll [sel|@ref]' }, 'wait': { category: 'Interaction', description: 'Wait for element, network idle, or page load (timeout: 15s)', usage: 'wait ' }, diff --git a/browse/src/write-commands.ts b/browse/src/write-commands.ts index 2e8109d07..571957a69 100644 --- a/browse/src/write-commands.ts +++ b/browse/src/write-commands.ts @@ -420,10 +420,32 @@ export async function handleWriteCommand( } case 'type': { - const text = args.join(' '); - if (!text) throw new Error('Usage: browse type '); + // #2936: bare `type` sends keystrokes to whatever has focus, so a selector + // given as the first word was typed as text. --selector targets an + // element; `--` ends flags so literal text may start with "--". + const usage = 'Usage: browse type [--selector ] [--] '; + let rest = args; + let selector: string | undefined; + if (rest[0] === '--selector') { + selector = rest[1]; + if (!selector) throw new Error(usage); + rest = rest.slice(2); + } + if (rest[0] === '--') rest = rest.slice(1); + const text = rest.join(' '); + if (!text) throw new Error(usage); + if (selector) { + const resolved = await session.resolveRef(selector); + const locator = 'locator' in resolved ? resolved.locator : target.locator(resolved.selector); + await locator.pressSequentially(text, { timeout: 5000 }); + return `Typed ${text.length} characters into ${selector}`; + } await page.keyboard.type(text); - return `Typed ${text.length} characters`; + const first = rest[0]; + const selectorLike = /^([#.][A-Za-z_-]|\[[^\]]+\]$|[A-Za-z][\w-]*[#.[][A-Za-z_-])/.test(first) || rest.includes('>'); + return selectorLike + ? `Typed ${text.length} characters into the focused element\nhint: "${first}" looks like a CSS selector. To type into that element: browse type --selector '${first}' ` + : `Typed ${text.length} characters`; } case 'press': { diff --git a/browse/test/commands.test.ts b/browse/test/commands.test.ts index 4e649b208..c0f72128f 100644 --- a/browse/test/commands.test.ts +++ b/browse/test/commands.test.ts @@ -559,6 +559,35 @@ describe('Interaction', () => { const val = await handleReadCommand('js', ['document.querySelector("#name").value'], bm); expect(val).toBe('John Doe'); }); + + test('type --selector types into that element, not the focused one (#2936)', async () => { + await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm); + await handleWriteCommand('click', ['#email'], bm); + const result = await handleWriteCommand('type', ['--selector', '#name', 'Jane', 'Roe'], bm); + expect(result).toBe('Typed 8 characters into #name'); + expect(await handleReadCommand('js', ['document.querySelector("#name").value'], bm)).toBe('Jane Roe'); + expect(await handleReadCommand('js', ['document.querySelector("#email").value'], bm)).toBe(''); + }); + + test('bare type keeps typing selector-looking text literally, with a hint and no refusal', async () => { + await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm); + await handleWriteCommand('click', ['#bio'], bm); + const result = await handleWriteCommand('type', ['#name', 'secret-value'], bm); + expect(result).toContain('Typed 18 characters into the focused element'); + expect(result).toContain("hint: \"#name\" looks like a CSS selector. To type into that element: browse type --selector '#name' "); + expect(result).not.toContain('secret-value'); + expect(await handleReadCommand('js', ['document.querySelector("#bio").value'], bm)).toBe('#name secret-value'); + expect(await handleReadCommand('js', ['document.querySelector("#name").value'], bm)).toBe(''); + }); + + test('type -- types text that starts with -- literally; plain text gets no hint', async () => { + await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm); + await handleWriteCommand('click', ['#bio'], bm); + expect(await handleWriteCommand('type', ['--', '--selector', 'x'], bm)).toBe('Typed 12 characters'); + expect(await handleReadCommand('js', ['document.querySelector("#bio").value'], bm)).toBe('--selector x'); + await expect(handleWriteCommand('type', ['--selector'], bm)).rejects.toThrow('Usage: browse type [--selector ] [--] '); + await expect(handleWriteCommand('type', ['--selector', '#name'], bm)).rejects.toThrow('Usage'); + }); }); // ─── SPA / Console / Network ───────────────────────────────────