more
This commit is contained in:
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user