From ff05842fd8066e56890c752968fe8a075aacd479 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 10 Jan 2026 13:06:36 -0600 Subject: [PATCH] more --- README.md | 9 ++++ src/actions.ts | 138 ++++++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 64 ++++++++++++++++++++-- src/protocol.ts | 58 ++++++++++++++++++++ src/types.ts | 68 +++++++++++++++++++++++- 5 files changed, 333 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5a540d8..edf0519 100644 --- a/README.md +++ b/README.md @@ -31,13 +31,19 @@ veb click # Click element veb type # Type into element veb fill # Clear and fill veb press # Press key (Enter, Tab, Control+a) +veb keydown # Hold key down +veb keyup # Release key +veb insert # Insert text (no key events) veb hover # Hover element veb select # Select dropdown option +veb multiselect # Multi-select veb check # Check checkbox veb uncheck # Uncheck checkbox veb scroll [px] # Scroll (up/down/left/right) +veb scrollinto # Scroll element into view veb drag # Drag and drop veb upload # Upload files +veb download [path] # Wait for download veb screenshot [path] # Take screenshot (--full for full page) veb pdf # Save as PDF veb snapshot # Accessibility tree (best for AI) @@ -100,6 +106,7 @@ veb wait # Wait for time veb wait --text "Welcome" # Wait for text veb wait --url "**/dash" # Wait for URL pattern veb wait --load networkidle # Wait for load state +veb wait --fn "window.ready === true" # Wait for JS condition ``` **Load states:** `load`, `domcontentloaded`, `networkidle` @@ -149,6 +156,7 @@ veb network route --body # Mock response veb network unroute [url] # Remove routes veb network requests # View tracked requests veb network requests --filter api # Filter requests +veb response # Get response body (waits for matching request) ``` ### Tabs & Windows @@ -186,6 +194,7 @@ veb errors # View page errors veb highlight # Highlight element veb state save # Save auth state veb state load # Load auth state +veb initscript # Run JS on every page load ``` ### Navigation diff --git a/src/actions.ts b/src/actions.ts index 8725276..883d99c 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -85,6 +85,15 @@ import type { MouseMoveCommand, MouseDownCommand, MouseUpCommand, + WaitForFunctionCommand, + ScrollIntoViewCommand, + AddInitScriptCommand, + KeyDownCommand, + KeyUpCommand, + InsertTextCommand, + MultiSelectCommand, + WaitForDownloadCommand, + ResponseBodyCommand, NavigateData, ScreenshotData, EvaluateData, @@ -320,6 +329,24 @@ export async function executeCommand( return await handleMouseUp(command, browser); case 'bringtofront': return await handleBringToFront(command, browser); + case 'waitforfunction': + return await handleWaitForFunction(command, browser); + case 'scrollintoview': + return await handleScrollIntoView(command, browser); + case 'addinitscript': + return await handleAddInitScript(command, browser); + case 'keydown': + return await handleKeyDown(command, browser); + case 'keyup': + return await handleKeyUp(command, browser); + case 'inserttext': + return await handleInsertText(command, browser); + case 'multiselect': + return await handleMultiSelect(command, browser); + case 'waitfordownload': + return await handleWaitForDownload(command, browser); + case 'responsebody': + return await handleResponseBody(command, browser); default: { // TypeScript narrows to never here, but we handle it for safety const unknownCommand = command as { id: string; action: string }; @@ -1620,3 +1647,114 @@ async function handleBringToFront( await page.bringToFront(); return successResponse(command.id, { focused: true }); } + +async function handleWaitForFunction( + command: WaitForFunctionCommand, + browser: BrowserManager +): Promise { + const page = browser.getPage(); + await page.waitForFunction(command.expression, { timeout: command.timeout }); + return successResponse(command.id, { waited: true }); +} + +async function handleScrollIntoView( + command: ScrollIntoViewCommand, + browser: BrowserManager +): Promise { + const page = browser.getPage(); + await page.locator(command.selector).scrollIntoViewIfNeeded(); + return successResponse(command.id, { scrolled: true }); +} + +async function handleAddInitScript( + command: AddInitScriptCommand, + browser: BrowserManager +): Promise { + const context = browser.getPage().context(); + await context.addInitScript(command.script); + return successResponse(command.id, { added: true }); +} + +async function handleKeyDown( + command: KeyDownCommand, + browser: BrowserManager +): Promise { + const page = browser.getPage(); + await page.keyboard.down(command.key); + return successResponse(command.id, { down: true, key: command.key }); +} + +async function handleKeyUp( + command: KeyUpCommand, + browser: BrowserManager +): Promise { + const page = browser.getPage(); + await page.keyboard.up(command.key); + return successResponse(command.id, { up: true, key: command.key }); +} + +async function handleInsertText( + command: InsertTextCommand, + browser: BrowserManager +): Promise { + const page = browser.getPage(); + await page.keyboard.insertText(command.text); + return successResponse(command.id, { inserted: true }); +} + +async function handleMultiSelect( + command: MultiSelectCommand, + browser: BrowserManager +): Promise { + const page = browser.getPage(); + const selected = await page.locator(command.selector).selectOption(command.values); + return successResponse(command.id, { selected }); +} + +async function handleWaitForDownload( + command: WaitForDownloadCommand, + browser: BrowserManager +): Promise { + const page = browser.getPage(); + const download = await page.waitForEvent('download', { timeout: command.timeout }); + + let filePath: string; + if (command.path) { + filePath = command.path; + await download.saveAs(filePath); + } else { + filePath = await download.path() || download.suggestedFilename(); + } + + return successResponse(command.id, { + path: filePath, + filename: download.suggestedFilename(), + url: download.url(), + }); +} + +async function handleResponseBody( + command: ResponseBodyCommand, + browser: BrowserManager +): Promise { + const page = browser.getPage(); + const response = await page.waitForResponse( + resp => resp.url().includes(command.url), + { timeout: command.timeout } + ); + + const body = await response.text(); + let parsed: unknown = body; + + try { + parsed = JSON.parse(body); + } catch { + // Keep as string if not JSON + } + + return successResponse(command.id, { + url: response.url(), + status: response.status(), + body: parsed, + }); +} diff --git a/src/index.ts b/src/index.ts index b721ed8..1d6d5a5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -205,6 +205,18 @@ function printResponse(response: Response, jsonMode: boolean): void { else reqs.forEach(r => console.log(`${c('cyan', r.method)} ${r.url}`)); } else if (data.moved) { console.log(c('green', '✓'), `Moved to (${data.x}, ${data.y})`); + } else if (data.body !== undefined && data.status !== undefined) { + // Response body + console.log(c('green', '✓'), `${data.status} ${data.url}`); + console.log(typeof data.body === 'object' ? JSON.stringify(data.body, null, 2) : data.body); + } else if (data.filename) { + // Download + console.log(c('green', '✓'), `Downloaded: ${data.filename}`); + console.log(c('dim', ` Path: ${data.path}`)); + } else if (data.inserted) { + console.log(c('green', '✓'), 'Text inserted'); + } else if (data.key) { + console.log(c('green', '✓'), `Key ${data.down ? 'down' : 'up'}: ${data.key}`); } else if (data.note) { console.log(c('yellow', '⚠'), data.note); } else if (data.closed === true) { @@ -507,6 +519,7 @@ interface Flags { exact: boolean; url?: string; load?: string; + fn?: string; } function parseFlags(args: string[]): { flags: Flags; cleanArgs: string[] } { @@ -545,6 +558,8 @@ function parseFlags(args: string[]): { flags: Flags; cleanArgs: string[] } { flags.url = args[++i]; } else if (arg === '--load' && args[i + 1]) { flags.load = args[++i]; + } else if ((arg === '--fn' || arg === '--function') && args[i + 1]) { + flags.fn = args[++i]; } else if (!arg.startsWith('-')) { cleanArgs.push(arg); } @@ -613,6 +628,16 @@ async function main(): Promise { cmd = { id, action: 'press', key: args[0] }; break; + case 'keydown': + if (!args[0]) err('Key required'); + cmd = { id, action: 'keydown', key: args[0] }; + break; + + case 'keyup': + if (!args[0]) err('Key required'); + cmd = { id, action: 'keyup', key: args[0] }; + break; + case 'hover': if (!args[0]) err('Selector required'); cmd = { id, action: 'hover', selector: args[0] }; @@ -657,8 +682,10 @@ async function main(): Promise { case 'wait': { const target = args[0]; - // Check for --url and --load flags - if (flags.url) { + // Check for flags + if (flags.fn) { + cmd = { id, action: 'waitforfunction', expression: flags.fn }; + } else if (flags.url) { cmd = { id, action: 'waitforurl', url: flags.url }; } else if (flags.load) { cmd = { id, action: 'waitforloadstate', state: flags.load }; @@ -670,7 +697,7 @@ async function main(): Promise { } else if (target) { cmd = { id, action: 'wait', selector: target }; } else { - err('Usage: veb wait '); + err('Usage: veb wait '); } break; } @@ -799,6 +826,37 @@ async function main(): Promise { cmd = { id, action: 'highlight', selector: args[0] }; break; + case 'scrollintoview': + case 'scrollinto': + if (!args[0]) err('Selector required'); + cmd = { id, action: 'scrollintoview', selector: args[0] }; + break; + + case 'initscript': + if (!args[0]) err('Script required'); + cmd = { id, action: 'addinitscript', script: args.join(' ') }; + break; + + case 'inserttext': + case 'insert': + if (!args[0]) err('Text required'); + cmd = { id, action: 'inserttext', text: args.join(' ') }; + break; + + case 'multiselect': + if (!args[0] || args.length < 2) err('Usage: veb multiselect [value2...]'); + cmd = { id, action: 'multiselect', selector: args[0], values: args.slice(1) }; + break; + + case 'download': + cmd = { id, action: 'waitfordownload', path: args[0] }; + break; + + case 'response': + if (!args[0]) err('URL pattern required'); + cmd = { id, action: 'responsebody', url: args[0] }; + break; + case 'session': if (args[0] === 'list' || args[0] === 'ls') { const sessions = listSessions(); diff --git a/src/protocol.ts b/src/protocol.ts index 6bb27e0..43c3531 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -527,6 +527,55 @@ const bringToFrontSchema = baseCommandSchema.extend({ action: z.literal('bringtofront'), }); +const waitForFunctionSchema = baseCommandSchema.extend({ + action: z.literal('waitforfunction'), + expression: z.string().min(1), + timeout: z.number().positive().optional(), +}); + +const scrollIntoViewSchema = baseCommandSchema.extend({ + action: z.literal('scrollintoview'), + selector: z.string().min(1), +}); + +const addInitScriptSchema = baseCommandSchema.extend({ + action: z.literal('addinitscript'), + script: z.string().min(1), +}); + +const keyDownSchema = baseCommandSchema.extend({ + action: z.literal('keydown'), + key: z.string().min(1), +}); + +const keyUpSchema = baseCommandSchema.extend({ + action: z.literal('keyup'), + key: z.string().min(1), +}); + +const insertTextSchema = baseCommandSchema.extend({ + action: z.literal('inserttext'), + text: z.string(), +}); + +const multiSelectSchema = baseCommandSchema.extend({ + action: z.literal('multiselect'), + selector: z.string().min(1), + values: z.array(z.string()), +}); + +const waitForDownloadSchema = baseCommandSchema.extend({ + action: z.literal('waitfordownload'), + path: z.string().optional(), + timeout: z.number().positive().optional(), +}); + +const responseBodySchema = baseCommandSchema.extend({ + action: z.literal('responsebody'), + url: z.string().min(1), + timeout: z.number().positive().optional(), +}); + const pressSchema = baseCommandSchema.extend({ action: z.literal('press'), key: z.string().min(1), @@ -722,6 +771,15 @@ const commandSchema = z.discriminatedUnion('action', [ mouseDownSchema, mouseUpSchema, bringToFrontSchema, + waitForFunctionSchema, + scrollIntoViewSchema, + addInitScriptSchema, + keyDownSchema, + keyUpSchema, + insertTextSchema, + multiSelectSchema, + waitForDownloadSchema, + responseBodySchema, ]); // Parse result type diff --git a/src/types.ts b/src/types.ts index fe50ad2..a58f259 100644 --- a/src/types.ts +++ b/src/types.ts @@ -386,6 +386,63 @@ export interface BringToFrontCommand extends BaseCommand { action: 'bringtofront'; } +// Wait for JS function to return truthy +export interface WaitForFunctionCommand extends BaseCommand { + action: 'waitforfunction'; + expression: string; + timeout?: number; +} + +// Scroll element into view +export interface ScrollIntoViewCommand extends BaseCommand { + action: 'scrollintoview'; + selector: string; +} + +// Add init script (runs on every navigation) +export interface AddInitScriptCommand extends BaseCommand { + action: 'addinitscript'; + script: string; +} + +// Keyboard down/up (hold keys) +export interface KeyDownCommand extends BaseCommand { + action: 'keydown'; + key: string; +} + +export interface KeyUpCommand extends BaseCommand { + action: 'keyup'; + key: string; +} + +// Insert text (without key events) +export interface InsertTextCommand extends BaseCommand { + action: 'inserttext'; + text: string; +} + +// Multi-select dropdown +export interface MultiSelectCommand extends BaseCommand { + action: 'multiselect'; + selector: string; + values: string[]; +} + +// Wait for download +export interface WaitForDownloadCommand extends BaseCommand { + action: 'waitfordownload'; + path?: string; + timeout?: number; +} + +// Get response body from intercepted request +export interface ResponseBodyCommand extends BaseCommand { + action: 'responsebody'; + url: string; + timeout?: number; +} + // Video recording export interface VideoStartCommand extends BaseCommand { action: 'video_start'; @@ -760,7 +817,16 @@ export type Command = | MouseMoveCommand | MouseDownCommand | MouseUpCommand - | BringToFrontCommand; + | BringToFrontCommand + | WaitForFunctionCommand + | ScrollIntoViewCommand + | AddInitScriptCommand + | KeyDownCommand + | KeyUpCommand + | InsertTextCommand + | MultiSelectCommand + | WaitForDownloadCommand + | ResponseBodyCommand; // Response types export interface SuccessResponse {