G5: browse type --selector <sel> <text> (#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 <thomasreuser@gmail.com>
This commit is contained in:
garrytanandThomas Reuser committed 2026-10-03 20:55:38 +00:00
1 parent 616773158b
commit 5015a939a9
4 files changed
+56 -5

No files matched your search

+1 -1
View File
@@ -1647,7 +1647,7 @@ Usage: browse <command> [args...]
Navigation: goto <url> | back | forward | reload | url
Content: text | html [sel] | links | forms | accessibility
Interaction: click <sel> | fill <sel> <val> | select <sel> <val>
hover <sel> | type <text> | press <key>
hover <sel> | type [--selector <sel>] <text> | press <key>
scroll [sel] | wait <sel|--networkidle|--load> | viewport <WxH>
upload <sel> <file1> [file2...]
cookie-import <json-file>
+1 -1
View File
@@ -122,7 +122,7 @@ export const COMMAND_DESCRIPTIONS: Record<string, { category: string; descriptio
'fill': { category: 'Interaction', description: 'Fill input', usage: 'fill <sel> <val>' },
'select': { category: 'Interaction', description: 'Select dropdown option by value, label, or visible text', usage: 'select <sel> <val>' },
'hover': { category: 'Interaction', description: 'Hover element', usage: 'hover <sel>' },
'type': { category: 'Interaction', description: 'Type into focused element', usage: 'type <text>' },
'type': { category: 'Interaction', description: 'Send keystrokes to the focused element, or to <sel> with --selector (focuses it first). Text that starts with -- goes after a -- separator.', usage: 'type [--selector <sel>] [--] <text>' },
'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 <key>' },
'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 <sel|--networkidle|--load>' },
+25 -3
View File
@@ -420,10 +420,32 @@ export async function handleWriteCommand(
}
case 'type': {
const text = args.join(' ');
if (!text) throw new Error('Usage: browse type <text>');
// #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 <sel>] [--] <text>';
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}' <text>`
: `Typed ${text.length} characters`;
}
case 'press': {
+29
View File
@@ -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' <text>");
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 <sel>] [--] <text>');
await expect(handleWriteCommand('type', ['--selector', '#name'], bm)).rejects.toThrow('Usage');
});
});
// ─── SPA / Console / Network ───────────────────────────────────