This commit is contained in:
Chris Tate
2026-01-10 13:06:36 -06:00
parent 55d40e1dba
commit ff05842fd8
5 changed files with 333 additions and 4 deletions
+9
View File
@@ -31,13 +31,19 @@ veb click <sel> # Click element
veb type <sel> <text> # Type into element
veb fill <sel> <text> # Clear and fill
veb press <key> # Press key (Enter, Tab, Control+a)
veb keydown <key> # Hold key down
veb keyup <key> # Release key
veb insert <text> # Insert text (no key events)
veb hover <sel> # Hover element
veb select <sel> <val> # Select dropdown option
veb multiselect <sel> <v1> <v2> # Multi-select
veb check <sel> # Check checkbox
veb uncheck <sel> # Uncheck checkbox
veb scroll <dir> [px] # Scroll (up/down/left/right)
veb scrollinto <sel> # Scroll element into view
veb drag <src> <tgt> # Drag and drop
veb upload <sel> <files> # Upload files
veb download [path] # Wait for download
veb screenshot [path] # Take screenshot (--full for full page)
veb pdf <path> # Save as PDF
veb snapshot # Accessibility tree (best for AI)
@@ -100,6 +106,7 @@ veb wait <ms> # 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 <url> --body <json> # Mock response
veb network unroute [url] # Remove routes
veb network requests # View tracked requests
veb network requests --filter api # Filter requests
veb response <url> # Get response body (waits for matching request)
```
### Tabs & Windows
@@ -186,6 +194,7 @@ veb errors # View page errors
veb highlight <sel> # Highlight element
veb state save <path> # Save auth state
veb state load <path> # Load auth state
veb initscript <js> # Run JS on every page load
```
### Navigation
+138
View File
@@ -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<Response> {
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<Response> {
const page = browser.getPage();
await page.locator(command.selector).scrollIntoViewIfNeeded();
return successResponse(command.id, { scrolled: true });
}
async function handleAddInitScript(
command: AddInitScriptCommand,
browser: BrowserManager
): Promise<Response> {
const context = browser.getPage().context();
await context.addInitScript(command.script);
return successResponse(command.id, { added: true });
}
async function handleKeyDown(
command: KeyDownCommand,
browser: BrowserManager
): Promise<Response> {
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<Response> {
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<Response> {
const page = browser.getPage();
await page.keyboard.insertText(command.text);
return successResponse(command.id, { inserted: true });
}
async function handleMultiSelect(
command: MultiSelectCommand,
browser: BrowserManager
): Promise<Response> {
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<Response> {
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<Response> {
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,
});
}
+61 -3
View File
@@ -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<void> {
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<void> {
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<void> {
} else if (target) {
cmd = { id, action: 'wait', selector: target };
} else {
err('Usage: veb wait <selector|ms|--text text|--url pattern|--load state>');
err('Usage: veb wait <selector|ms|--text|--url|--load|--fn>');
}
break;
}
@@ -799,6 +826,37 @@ async function main(): Promise<void> {
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 <selector> <value1> [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();
+58
View File
@@ -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
+67 -1
View File
@@ -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<T = unknown> {