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 type <sel> <text> # Type into element
veb fill <sel> <text> # Clear and fill veb fill <sel> <text> # Clear and fill
veb press <key> # Press key (Enter, Tab, Control+a) 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 hover <sel> # Hover element
veb select <sel> <val> # Select dropdown option veb select <sel> <val> # Select dropdown option
veb multiselect <sel> <v1> <v2> # Multi-select
veb check <sel> # Check checkbox veb check <sel> # Check checkbox
veb uncheck <sel> # Uncheck checkbox veb uncheck <sel> # Uncheck checkbox
veb scroll <dir> [px] # Scroll (up/down/left/right) veb scroll <dir> [px] # Scroll (up/down/left/right)
veb scrollinto <sel> # Scroll element into view
veb drag <src> <tgt> # Drag and drop veb drag <src> <tgt> # Drag and drop
veb upload <sel> <files> # Upload files veb upload <sel> <files> # Upload files
veb download [path] # Wait for download
veb screenshot [path] # Take screenshot (--full for full page) veb screenshot [path] # Take screenshot (--full for full page)
veb pdf <path> # Save as PDF veb pdf <path> # Save as PDF
veb snapshot # Accessibility tree (best for AI) 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 --text "Welcome" # Wait for text
veb wait --url "**/dash" # Wait for URL pattern veb wait --url "**/dash" # Wait for URL pattern
veb wait --load networkidle # Wait for load state veb wait --load networkidle # Wait for load state
veb wait --fn "window.ready === true" # Wait for JS condition
``` ```
**Load states:** `load`, `domcontentloaded`, `networkidle` **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 unroute [url] # Remove routes
veb network requests # View tracked requests veb network requests # View tracked requests
veb network requests --filter api # Filter requests veb network requests --filter api # Filter requests
veb response <url> # Get response body (waits for matching request)
``` ```
### Tabs & Windows ### Tabs & Windows
@@ -186,6 +194,7 @@ veb errors # View page errors
veb highlight <sel> # Highlight element veb highlight <sel> # Highlight element
veb state save <path> # Save auth state veb state save <path> # Save auth state
veb state load <path> # Load auth state veb state load <path> # Load auth state
veb initscript <js> # Run JS on every page load
``` ```
### Navigation ### Navigation
+138
View File
@@ -85,6 +85,15 @@ import type {
MouseMoveCommand, MouseMoveCommand,
MouseDownCommand, MouseDownCommand,
MouseUpCommand, MouseUpCommand,
WaitForFunctionCommand,
ScrollIntoViewCommand,
AddInitScriptCommand,
KeyDownCommand,
KeyUpCommand,
InsertTextCommand,
MultiSelectCommand,
WaitForDownloadCommand,
ResponseBodyCommand,
NavigateData, NavigateData,
ScreenshotData, ScreenshotData,
EvaluateData, EvaluateData,
@@ -320,6 +329,24 @@ export async function executeCommand(
return await handleMouseUp(command, browser); return await handleMouseUp(command, browser);
case 'bringtofront': case 'bringtofront':
return await handleBringToFront(command, browser); 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: { default: {
// TypeScript narrows to never here, but we handle it for safety // TypeScript narrows to never here, but we handle it for safety
const unknownCommand = command as { id: string; action: string }; const unknownCommand = command as { id: string; action: string };
@@ -1620,3 +1647,114 @@ async function handleBringToFront(
await page.bringToFront(); await page.bringToFront();
return successResponse(command.id, { focused: true }); 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 reqs.forEach(r => console.log(`${c('cyan', r.method)} ${r.url}`));
} else if (data.moved) { } else if (data.moved) {
console.log(c('green', '✓'), `Moved to (${data.x}, ${data.y})`); 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) { } else if (data.note) {
console.log(c('yellow', '⚠'), data.note); console.log(c('yellow', '⚠'), data.note);
} else if (data.closed === true) { } else if (data.closed === true) {
@@ -507,6 +519,7 @@ interface Flags {
exact: boolean; exact: boolean;
url?: string; url?: string;
load?: string; load?: string;
fn?: string;
} }
function parseFlags(args: string[]): { flags: Flags; cleanArgs: 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]; flags.url = args[++i];
} else if (arg === '--load' && args[i + 1]) { } else if (arg === '--load' && args[i + 1]) {
flags.load = args[++i]; flags.load = args[++i];
} else if ((arg === '--fn' || arg === '--function') && args[i + 1]) {
flags.fn = args[++i];
} else if (!arg.startsWith('-')) { } else if (!arg.startsWith('-')) {
cleanArgs.push(arg); cleanArgs.push(arg);
} }
@@ -613,6 +628,16 @@ async function main(): Promise<void> {
cmd = { id, action: 'press', key: args[0] }; cmd = { id, action: 'press', key: args[0] };
break; 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': case 'hover':
if (!args[0]) err('Selector required'); if (!args[0]) err('Selector required');
cmd = { id, action: 'hover', selector: args[0] }; cmd = { id, action: 'hover', selector: args[0] };
@@ -657,8 +682,10 @@ async function main(): Promise<void> {
case 'wait': { case 'wait': {
const target = args[0]; const target = args[0];
// Check for --url and --load flags // Check for flags
if (flags.url) { if (flags.fn) {
cmd = { id, action: 'waitforfunction', expression: flags.fn };
} else if (flags.url) {
cmd = { id, action: 'waitforurl', url: flags.url }; cmd = { id, action: 'waitforurl', url: flags.url };
} else if (flags.load) { } else if (flags.load) {
cmd = { id, action: 'waitforloadstate', state: flags.load }; cmd = { id, action: 'waitforloadstate', state: flags.load };
@@ -670,7 +697,7 @@ async function main(): Promise<void> {
} else if (target) { } else if (target) {
cmd = { id, action: 'wait', selector: target }; cmd = { id, action: 'wait', selector: target };
} else { } else {
err('Usage: veb wait <selector|ms|--text text|--url pattern|--load state>'); err('Usage: veb wait <selector|ms|--text|--url|--load|--fn>');
} }
break; break;
} }
@@ -799,6 +826,37 @@ async function main(): Promise<void> {
cmd = { id, action: 'highlight', selector: args[0] }; cmd = { id, action: 'highlight', selector: args[0] };
break; 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': case 'session':
if (args[0] === 'list' || args[0] === 'ls') { if (args[0] === 'list' || args[0] === 'ls') {
const sessions = listSessions(); const sessions = listSessions();
+58
View File
@@ -527,6 +527,55 @@ const bringToFrontSchema = baseCommandSchema.extend({
action: z.literal('bringtofront'), 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({ const pressSchema = baseCommandSchema.extend({
action: z.literal('press'), action: z.literal('press'),
key: z.string().min(1), key: z.string().min(1),
@@ -722,6 +771,15 @@ const commandSchema = z.discriminatedUnion('action', [
mouseDownSchema, mouseDownSchema,
mouseUpSchema, mouseUpSchema,
bringToFrontSchema, bringToFrontSchema,
waitForFunctionSchema,
scrollIntoViewSchema,
addInitScriptSchema,
keyDownSchema,
keyUpSchema,
insertTextSchema,
multiSelectSchema,
waitForDownloadSchema,
responseBodySchema,
]); ]);
// Parse result type // Parse result type
+67 -1
View File
@@ -386,6 +386,63 @@ export interface BringToFrontCommand extends BaseCommand {
action: 'bringtofront'; 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 // Video recording
export interface VideoStartCommand extends BaseCommand { export interface VideoStartCommand extends BaseCommand {
action: 'video_start'; action: 'video_start';
@@ -760,7 +817,16 @@ export type Command =
| MouseMoveCommand | MouseMoveCommand
| MouseDownCommand | MouseDownCommand
| MouseUpCommand | MouseUpCommand
| BringToFrontCommand; | BringToFrontCommand
| WaitForFunctionCommand
| ScrollIntoViewCommand
| AddInitScriptCommand
| KeyDownCommand
| KeyUpCommand
| InsertTextCommand
| MultiSelectCommand
| WaitForDownloadCommand
| ResponseBodyCommand;
// Response types // Response types
export interface SuccessResponse<T = unknown> { export interface SuccessResponse<T = unknown> {