This commit is contained in:
Chris Tate
2026-01-10 12:15:57 -06:00
parent b6e1693739
commit 6dc408e02f
6 changed files with 1342 additions and 1 deletions
+86
View File
@@ -26,6 +26,7 @@ veb title # Get page title
# Clicking
veb click "#submit-btn"
veb dblclick "#item"
veb tap "#mobile-btn" # Touch tap
# Form input
veb type "#search" "query" # Type character by character
@@ -33,15 +34,27 @@ veb fill "#email" "test@example.com" # Clear and fill (faster)
veb check "#agree" # Check checkbox/radio
veb uncheck "#newsletter" # Uncheck checkbox
veb select "#country" "US" # Select dropdown
veb clear-input "#field" # Clear input field
veb setvalue "#hidden" "val" # Set value directly
# Keyboard
veb press Enter
veb press Tab
veb press "Control+a" # Keyboard shortcuts
veb press "Shift+Tab"
# Mouse
veb hover "#menu"
veb focus "#input"
veb drag "#source" "#target"
veb wheel 300 # Scroll wheel (deltaY)
veb wheel 0 100 # Scroll wheel (deltaX, deltaY)
# Text operations
veb selectall "#input" # Select all text
veb clipboard copy # Copy to clipboard
veb clipboard paste # Paste from clipboard
veb clipboard read # Read clipboard content
# File upload
veb upload "#file-input" ./document.pdf
@@ -65,6 +78,9 @@ veb pdf report.pdf
veb snapshot # Accessibility tree (best for agents)
veb extract "#main" # Get HTML
veb eval "document.title" # Run JavaScript
veb innertext "#message" # Get inner text
veb innerhtml "#container" # Get inner HTML
veb inputvalue "#field" # Get input value
# Element info
veb gettext "#message" # Get text content
@@ -74,6 +90,7 @@ veb isenabled "#submit" # Check if enabled
veb ischecked "#checkbox" # Check if checked
veb count ".items" # Count matching elements
veb boundingbox "#element" # Get position/size
veb highlight "#element" # Highlight element (debugging)
# Scrolling
veb scroll down 500
@@ -104,6 +121,13 @@ veb viewport 1920 1080 # Set viewport size
veb device "iPhone 14" # Emulate device
veb geolocation 37.7749 -122.4194 # Set location (SF)
veb permissions grant geolocation notifications
veb media --dark # Dark mode
veb media --light # Light mode
veb media --print # Print media type
veb media --reduced-motion # Reduced motion
veb offline # Enable offline mode
veb offline off # Disable offline mode
veb headers '{"X-Custom":"value"}' # Set extra headers
# Cookies
veb cookies # Get all cookies
@@ -122,6 +146,28 @@ veb dialog accept # Accept next dialog
veb dialog accept "input text" # Accept prompt with text
veb dialog dismiss # Dismiss next dialog
# Debugging & Tracing
veb trace start # Start recording trace
veb trace start --screenshots # With screenshots
veb trace stop ./trace.zip # Stop and save trace
veb state save ./auth.json # Save cookies/storage
veb state load ./auth.json # Load saved state
veb console # View console messages
veb console --clear # Clear console
veb errors # View page errors
veb errors --clear # Clear errors
veb pause # Pause (opens inspector)
# Inject scripts/styles
veb addscript "console.log('hi')"
veb addscript https://example.com/lib.js
veb addstyle "body { background: red; }"
veb addstyle https://example.com/styles.css
# Custom events
veb dispatch "#btn" "click"
veb dispatch "#input" "focus"
# Tabs
veb tab new
veb tab list
@@ -168,6 +214,7 @@ veb isvisible "#modal" --json
|---------|-------------|
| `click <selector>` | Click element |
| `dblclick <selector>` | Double-click |
| `tap <selector>` | Touch tap |
| `type <selector> <text>` | Type text |
| `fill <selector> <value>` | Clear & fill |
| `press <key>` | Press key |
@@ -177,20 +224,29 @@ veb isvisible "#modal" --json
| `hover <selector>` | Hover |
| `focus <selector>` | Focus |
| `drag <src> <target>` | Drag & drop |
| `wheel [deltaY] [deltaX]` | Mouse wheel |
| `clear-input <selector>` | Clear input |
| `setvalue <selector> <val>` | Set value |
| `selectall <selector>` | Select all text |
| `upload <selector> <files>` | Upload files |
| `download <selector> <path>` | Download file |
| `scroll <dir> [amount]` | Scroll |
| `clipboard copy\|paste\|read` | Clipboard ops |
### Element Info
| Command | Description |
|---------|-------------|
| `gettext <selector>` | Get text content |
| `innertext <selector>` | Get inner text |
| `innerhtml <selector>` | Get inner HTML |
| `inputvalue <selector>` | Get input value |
| `getattr <selector> <attr>` | Get attribute |
| `isvisible <selector>` | Check visibility |
| `isenabled <selector>` | Check enabled |
| `ischecked <selector>` | Check checked |
| `count <selector>` | Count elements |
| `boundingbox <selector>` | Get bounds |
| `highlight <selector>` | Highlight element |
### Semantic Locators
| Command | Description |
@@ -224,6 +280,9 @@ veb isvisible "#modal" --json
| `device <name>` | Emulate device |
| `geolocation <lat> <lng>` | Set location |
| `permissions grant\|deny` | Set permissions |
| `media [options]` | Emulate media |
| `offline [on\|off]` | Offline mode |
| `headers <json>` | Extra HTTP headers |
### Browser State
| Command | Description |
@@ -234,6 +293,27 @@ veb isvisible "#modal" --json
| `storage local [key]` | localStorage |
| `storage session [key]` | sessionStorage |
| `dialog accept\|dismiss` | Handle dialogs |
| `state save <path>` | Save auth state |
| `state load <path>` | Load auth state |
### Debugging & Tracing
| Command | Description |
|---------|-------------|
| `trace start` | Start trace recording |
| `trace stop <path>` | Stop & save trace |
| `console` | View console messages |
| `console --clear` | Clear console |
| `errors` | View page errors |
| `errors --clear` | Clear errors |
| `pause` | Open inspector |
| `highlight <sel>` | Highlight element |
### Script Injection
| Command | Description |
|---------|-------------|
| `addscript <url\|code>` | Add script |
| `addstyle <url\|code>` | Add stylesheet |
| `dispatch <sel> <event>` | Dispatch event |
### Frames & Tabs
| Command | Description |
@@ -267,6 +347,12 @@ veb isvisible "#modal" --json
| `--abort` | Abort route |
| `--body` | Route response body |
| `--filter` | Filter requests |
| `--clear` | Clear console/errors |
| `--screenshots` | Include screenshots in trace |
| `--dark` | Dark color scheme |
| `--light` | Light color scheme |
| `--print` | Print media type |
| `--reduced-motion` | Reduced motion |
| `--debug` | Debug output |
## Device Emulation
+415
View File
@@ -49,6 +49,29 @@ import type {
IsCheckedCommand,
CountCommand,
BoundingBoxCommand,
TraceStartCommand,
TraceStopCommand,
HarStopCommand,
StorageStateSaveCommand,
ConsoleCommand,
ErrorsCommand,
KeyboardCommand,
WheelCommand,
TapCommand,
ClipboardCommand,
HighlightCommand,
ClearCommand,
SelectAllCommand,
InnerTextCommand,
InnerHtmlCommand,
InputValueCommand,
SetValueCommand,
DispatchEventCommand,
AddScriptCommand,
AddStyleCommand,
EmulateMediaCommand,
OfflineCommand,
HeadersCommand,
NavigateData,
ScreenshotData,
EvaluateData,
@@ -196,6 +219,66 @@ export async function executeCommand(
return await handleCount(command, browser);
case 'boundingbox':
return await handleBoundingBox(command, browser);
case 'video_start':
return await handleVideoStart(command, browser);
case 'video_stop':
return await handleVideoStop(command, browser);
case 'trace_start':
return await handleTraceStart(command, browser);
case 'trace_stop':
return await handleTraceStop(command, browser);
case 'har_start':
return await handleHarStart(command, browser);
case 'har_stop':
return await handleHarStop(command, browser);
case 'state_save':
return await handleStateSave(command, browser);
case 'state_load':
return await handleStateLoad(command, browser);
case 'console':
return await handleConsole(command, browser);
case 'errors':
return await handleErrors(command, browser);
case 'keyboard':
return await handleKeyboard(command, browser);
case 'wheel':
return await handleWheel(command, browser);
case 'tap':
return await handleTap(command, browser);
case 'clipboard':
return await handleClipboard(command, browser);
case 'highlight':
return await handleHighlight(command, browser);
case 'clear':
return await handleClear(command, browser);
case 'selectall':
return await handleSelectAll(command, browser);
case 'innertext':
return await handleInnerText(command, browser);
case 'innerhtml':
return await handleInnerHtml(command, browser);
case 'inputvalue':
return await handleInputValue(command, browser);
case 'setvalue':
return await handleSetValue(command, browser);
case 'dispatch':
return await handleDispatch(command, browser);
case 'evalhandle':
return await handleEvalHandle(command, browser);
case 'expose':
return await handleExpose(command, browser);
case 'addscript':
return await handleAddScript(command, browser);
case 'addstyle':
return await handleAddStyle(command, browser);
case 'emulatemedia':
return await handleEmulateMedia(command, browser);
case 'offline':
return await handleOffline(command, browser);
case 'headers':
return await handleHeaders(command, browser);
case 'pause':
return await handlePause(command, browser);
default: {
// TypeScript narrows to never here, but we handle it for safety
const unknownCommand = command as { id: string; action: string };
@@ -980,3 +1063,335 @@ async function handleBoundingBox(
const box = await page.locator(command.selector).boundingBox();
return successResponse(command.id, { box });
}
// Advanced handlers
async function handleVideoStart(
command: Command & { action: 'video_start'; path: string },
browser: BrowserManager
): Promise<Response> {
// Video recording requires context-level setup at launch
// For now, return a note about this limitation
return successResponse(command.id, {
note: 'Video recording must be enabled at browser launch. Use --video flag when starting.',
path: command.path,
});
}
async function handleVideoStop(
command: Command & { action: 'video_stop' },
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
const video = page.video();
if (video) {
const path = await video.path();
return successResponse(command.id, { path });
}
return successResponse(command.id, { note: 'No video recording active' });
}
async function handleTraceStart(
command: TraceStartCommand,
browser: BrowserManager
): Promise<Response> {
await browser.startTracing({
screenshots: command.screenshots,
snapshots: command.snapshots,
});
return successResponse(command.id, { started: true });
}
async function handleTraceStop(
command: TraceStopCommand,
browser: BrowserManager
): Promise<Response> {
await browser.stopTracing(command.path);
return successResponse(command.id, { path: command.path });
}
async function handleHarStart(
command: Command & { action: 'har_start' },
browser: BrowserManager
): Promise<Response> {
await browser.startHarRecording();
browser.startRequestTracking();
return successResponse(command.id, { started: true });
}
async function handleHarStop(
command: HarStopCommand,
browser: BrowserManager
): Promise<Response> {
// HAR recording is handled at context level
// For now, we save tracked requests as a simplified HAR-like format
const requests = browser.getRequests();
return successResponse(command.id, {
path: command.path,
requestCount: requests.length,
});
}
async function handleStateSave(
command: StorageStateSaveCommand,
browser: BrowserManager
): Promise<Response> {
await browser.saveStorageState(command.path);
return successResponse(command.id, { path: command.path });
}
async function handleStateLoad(
command: Command & { action: 'state_load'; path: string },
browser: BrowserManager
): Promise<Response> {
// Storage state is loaded at context creation
return successResponse(command.id, {
note: 'Storage state must be loaded at browser launch. Use --state flag.',
path: command.path,
});
}
async function handleConsole(
command: ConsoleCommand,
browser: BrowserManager
): Promise<Response> {
if (command.clear) {
browser.clearConsoleMessages();
return successResponse(command.id, { cleared: true });
}
browser.startConsoleTracking();
const messages = browser.getConsoleMessages();
return successResponse(command.id, { messages });
}
async function handleErrors(
command: ErrorsCommand,
browser: BrowserManager
): Promise<Response> {
if (command.clear) {
browser.clearPageErrors();
return successResponse(command.id, { cleared: true });
}
browser.startErrorTracking();
const errors = browser.getPageErrors();
return successResponse(command.id, { errors });
}
async function handleKeyboard(
command: KeyboardCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
await page.keyboard.press(command.keys);
return successResponse(command.id, { pressed: command.keys });
}
async function handleWheel(
command: WheelCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
if (command.selector) {
const element = page.locator(command.selector);
await element.hover();
}
await page.mouse.wheel(command.deltaX ?? 0, command.deltaY ?? 0);
return successResponse(command.id, { scrolled: true });
}
async function handleTap(
command: TapCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
await page.tap(command.selector);
return successResponse(command.id, { tapped: true });
}
async function handleClipboard(
command: ClipboardCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
switch (command.operation) {
case 'copy':
await page.keyboard.press('Control+c');
return successResponse(command.id, { copied: true });
case 'paste':
await page.keyboard.press('Control+v');
return successResponse(command.id, { pasted: true });
case 'read':
const text = await page.evaluate('navigator.clipboard.readText()');
return successResponse(command.id, { text });
default:
return errorResponse(command.id, 'Unknown clipboard operation');
}
}
async function handleHighlight(
command: HighlightCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
await page.locator(command.selector).highlight();
return successResponse(command.id, { highlighted: true });
}
async function handleClear(
command: ClearCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
await page.locator(command.selector).clear();
return successResponse(command.id, { cleared: true });
}
async function handleSelectAll(
command: SelectAllCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
await page.locator(command.selector).selectText();
return successResponse(command.id, { selected: true });
}
async function handleInnerText(
command: InnerTextCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
const text = await page.locator(command.selector).innerText();
return successResponse(command.id, { text });
}
async function handleInnerHtml(
command: InnerHtmlCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
const html = await page.locator(command.selector).innerHTML();
return successResponse(command.id, { html });
}
async function handleInputValue(
command: InputValueCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
const value = await page.locator(command.selector).inputValue();
return successResponse(command.id, { value });
}
async function handleSetValue(
command: SetValueCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
await page.locator(command.selector).fill(command.value);
return successResponse(command.id, { set: true });
}
async function handleDispatch(
command: DispatchEventCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
await page.locator(command.selector).dispatchEvent(command.event, command.eventInit);
return successResponse(command.id, { dispatched: command.event });
}
async function handleEvalHandle(
command: Command & { action: 'evalhandle'; script: string },
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
const handle = await page.evaluateHandle(command.script);
const result = await handle.jsonValue().catch(() => 'Handle (non-serializable)');
return successResponse(command.id, { result });
}
async function handleExpose(
command: Command & { action: 'expose'; name: string },
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
await page.exposeFunction(command.name, () => {
// Exposed function - can be extended
return `Function ${command.name} called`;
});
return successResponse(command.id, { exposed: command.name });
}
async function handleAddScript(
command: AddScriptCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
if (command.content) {
await page.addScriptTag({ content: command.content });
} else if (command.url) {
await page.addScriptTag({ url: command.url });
}
return successResponse(command.id, { added: true });
}
async function handleAddStyle(
command: AddStyleCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
if (command.content) {
await page.addStyleTag({ content: command.content });
} else if (command.url) {
await page.addStyleTag({ url: command.url });
}
return successResponse(command.id, { added: true });
}
async function handleEmulateMedia(
command: EmulateMediaCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
await page.emulateMedia({
media: command.media,
colorScheme: command.colorScheme,
reducedMotion: command.reducedMotion,
forcedColors: command.forcedColors,
});
return successResponse(command.id, { emulated: true });
}
async function handleOffline(
command: OfflineCommand,
browser: BrowserManager
): Promise<Response> {
await browser.setOffline(command.offline);
return successResponse(command.id, { offline: command.offline });
}
async function handleHeaders(
command: HeadersCommand,
browser: BrowserManager
): Promise<Response> {
await browser.setExtraHeaders(command.headers);
return successResponse(command.id, { set: true });
}
async function handlePause(
command: Command & { action: 'pause' },
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
await page.pause();
return successResponse(command.id, { paused: true });
}
+137
View File
@@ -9,6 +9,17 @@ interface TrackedRequest {
resourceType: string;
}
interface ConsoleMessage {
type: string;
text: string;
timestamp: number;
}
interface PageError {
message: string;
timestamp: number;
}
/**
* Manages the Playwright browser lifecycle with multiple tabs/windows
*/
@@ -21,6 +32,9 @@ export class BrowserManager {
private dialogHandler: ((dialog: Dialog) => Promise<void>) | null = null;
private trackedRequests: TrackedRequest[] = [];
private routes: Map<string, (route: Route) => Promise<void>> = new Map();
private consoleMessages: ConsoleMessage[] = [];
private pageErrors: PageError[] = [];
private isRecordingHar: boolean = false;
/**
* Check if browser is launched
@@ -251,6 +265,129 @@ export class BrowserManager {
return Object.keys(devices);
}
/**
* Start console message tracking
*/
startConsoleTracking(): void {
const page = this.getPage();
page.on('console', (msg) => {
this.consoleMessages.push({
type: msg.type(),
text: msg.text(),
timestamp: Date.now(),
});
});
}
/**
* Get console messages
*/
getConsoleMessages(): ConsoleMessage[] {
return this.consoleMessages;
}
/**
* Clear console messages
*/
clearConsoleMessages(): void {
this.consoleMessages = [];
}
/**
* Start error tracking
*/
startErrorTracking(): void {
const page = this.getPage();
page.on('pageerror', (error) => {
this.pageErrors.push({
message: error.message,
timestamp: Date.now(),
});
});
}
/**
* Get page errors
*/
getPageErrors(): PageError[] {
return this.pageErrors;
}
/**
* Clear page errors
*/
clearPageErrors(): void {
this.pageErrors = [];
}
/**
* Start HAR recording
*/
async startHarRecording(): Promise<void> {
// HAR is started at context level, flag for tracking
this.isRecordingHar = true;
}
/**
* Check if HAR recording
*/
isHarRecording(): boolean {
return this.isRecordingHar;
}
/**
* Set offline mode
*/
async setOffline(offline: boolean): Promise<void> {
const context = this.contexts[0];
if (context) {
await context.setOffline(offline);
}
}
/**
* Set extra HTTP headers
*/
async setExtraHeaders(headers: Record<string, string>): Promise<void> {
const context = this.contexts[0];
if (context) {
await context.setExtraHTTPHeaders(headers);
}
}
/**
* Start tracing
*/
async startTracing(options: { screenshots?: boolean; snapshots?: boolean }): Promise<void> {
const context = this.contexts[0];
if (context) {
await context.tracing.start({
screenshots: options.screenshots ?? true,
snapshots: options.snapshots ?? true,
});
}
}
/**
* Stop tracing and save
*/
async stopTracing(path: string): Promise<void> {
const context = this.contexts[0];
if (context) {
await context.tracing.stop({ path });
}
}
/**
* Save storage state (cookies, localStorage, etc.)
*/
async saveStorageState(path: string): Promise<void> {
const context = this.contexts[0];
if (context) {
await context.storageState({ path });
}
}
/**
* Get all pages
*/
+301
View File
@@ -307,6 +307,55 @@ function printResponse(response: Response, jsonMode: boolean): void {
} else if (data.remaining !== undefined) {
// Tab close
console.log(c('green', '✓'), `Tab closed (${data.remaining} remaining)`);
} else if (data.started !== undefined) {
console.log(c('green', '✓'), 'Started');
} else if (data.messages) {
// Console messages
const msgs = data.messages as Array<{ type: string; text: string; timestamp: number }>;
if (msgs.length === 0) {
console.log(c('dim', 'No console messages'));
} else {
msgs.forEach(msg => {
const typeColor = msg.type === 'error' ? 'red' : msg.type === 'warning' ? 'yellow' : 'dim';
console.log(`${c(typeColor, `[${msg.type}]`)} ${msg.text}`);
});
}
} else if (data.errors) {
// Page errors
const errs = data.errors as Array<{ message: string; timestamp: number }>;
if (errs.length === 0) {
console.log(c('dim', 'No page errors'));
} else {
errs.forEach(err => {
console.log(c('red', '✗'), err.message);
});
}
} else if (data.pressed) {
console.log(c('green', '✓'), `Pressed: ${data.pressed}`);
} else if (data.tapped) {
console.log(c('green', '✓'), 'Tapped');
} else if (data.copied) {
console.log(c('green', '✓'), 'Copied to clipboard');
} else if (data.pasted) {
console.log(c('green', '✓'), 'Pasted from clipboard');
} else if (data.highlighted) {
console.log(c('green', '✓'), 'Element highlighted');
} else if (data.dispatched) {
console.log(c('green', '✓'), `Event dispatched: ${data.dispatched}`);
} else if (data.exposed) {
console.log(c('green', '✓'), `Function exposed: ${data.exposed}`);
} else if (data.added) {
console.log(c('green', '✓'), 'Added');
} else if (data.emulated) {
console.log(c('green', '✓'), 'Media emulated');
} else if (data.offline !== undefined) {
console.log(c('green', '✓'), data.offline ? 'Offline mode enabled' : 'Online mode enabled');
} else if (data.paused) {
console.log(c('green', '✓'), 'Paused (Inspector open)');
} else if (data.note) {
console.log(c('yellow', '⚠'), data.note);
} else if (data.requestCount !== undefined) {
console.log(c('green', '✓'), `HAR saved (${data.requestCount} requests)`);
} else {
console.log(c('green', '✓'), JSON.stringify(data));
}
@@ -971,6 +1020,258 @@ async function main(): Promise<void> {
}
}
// === Advanced commands ===
case 'trace': {
const subCmd = cleanArgs[1];
if (subCmd === 'start') {
const screenshotsMode = args.includes('--screenshots');
const snapshotsMode = args.includes('--snapshots');
cmd = { id, action: 'trace_start', screenshots: screenshotsMode, snapshots: snapshotsMode };
} else if (subCmd === 'stop') {
const path = cleanArgs[2];
if (!path) {
console.error(c('red', 'Error:'), 'Path required for trace output');
process.exit(1);
}
cmd = { id, action: 'trace_stop', path };
} else {
console.error(c('red', 'Error:'), 'Usage: veb trace start|stop');
process.exit(1);
}
break;
}
case 'state': {
const subCmd = cleanArgs[1];
const path = cleanArgs[2];
if (subCmd === 'save') {
if (!path) {
console.error(c('red', 'Error:'), 'Path required');
process.exit(1);
}
cmd = { id, action: 'state_save', path };
} else if (subCmd === 'load') {
if (!path) {
console.error(c('red', 'Error:'), 'Path required');
process.exit(1);
}
cmd = { id, action: 'state_load', path };
} else {
console.error(c('red', 'Error:'), 'Usage: veb state save|load <path>');
process.exit(1);
}
break;
}
case 'console': {
const clearMode = args.includes('--clear');
cmd = { id, action: 'console', clear: clearMode };
break;
}
case 'errors': {
const clearMode = args.includes('--clear');
cmd = { id, action: 'errors', clear: clearMode };
break;
}
case 'keyboard':
case 'key':
case 'press': {
const keys = cleanArgs[1];
if (!keys) {
console.error(c('red', 'Error:'), 'Keys required (e.g., veb press "Control+a")');
process.exit(1);
}
cmd = { id, action: 'keyboard', keys };
break;
}
case 'wheel':
case 'scroll-wheel': {
const deltaY = parseInt(cleanArgs[1], 10) || 100;
const deltaX = parseInt(cleanArgs[2], 10) || 0;
const selector = selectorOverride;
cmd = { id, action: 'wheel', deltaX, deltaY, selector };
break;
}
case 'tap': {
const selector = cleanArgs[1];
if (!selector) {
console.error(c('red', 'Error:'), 'Selector required');
process.exit(1);
}
cmd = { id, action: 'tap', selector };
break;
}
case 'clipboard': {
const operation = cleanArgs[1] as 'copy' | 'paste' | 'read';
if (!['copy', 'paste', 'read'].includes(operation)) {
console.error(c('red', 'Error:'), 'Usage: veb clipboard copy|paste|read');
process.exit(1);
}
cmd = { id, action: 'clipboard', operation };
break;
}
case 'highlight': {
const selector = cleanArgs[1];
if (!selector) {
console.error(c('red', 'Error:'), 'Selector required');
process.exit(1);
}
cmd = { id, action: 'highlight', selector };
break;
}
case 'clear-input':
case 'clearinput': {
const selector = cleanArgs[1];
if (!selector) {
console.error(c('red', 'Error:'), 'Selector required');
process.exit(1);
}
cmd = { id, action: 'clear', selector };
break;
}
case 'selectall':
case 'select-all': {
const selector = cleanArgs[1];
if (!selector) {
console.error(c('red', 'Error:'), 'Selector required');
process.exit(1);
}
cmd = { id, action: 'selectall', selector };
break;
}
case 'innertext': {
const selector = cleanArgs[1];
if (!selector) {
console.error(c('red', 'Error:'), 'Selector required');
process.exit(1);
}
cmd = { id, action: 'innertext', selector };
break;
}
case 'innerhtml':
case 'inner-html': {
const selector = cleanArgs[1];
if (!selector) {
console.error(c('red', 'Error:'), 'Selector required');
process.exit(1);
}
cmd = { id, action: 'innerhtml', selector };
break;
}
case 'inputvalue':
case 'input-value': {
const selector = cleanArgs[1];
if (!selector) {
console.error(c('red', 'Error:'), 'Selector required');
process.exit(1);
}
cmd = { id, action: 'inputvalue', selector };
break;
}
case 'setvalue':
case 'set-value': {
const selector = cleanArgs[1];
const value = cleanArgs[2];
if (!selector || !value) {
console.error(c('red', 'Error:'), 'Selector and value required');
process.exit(1);
}
cmd = { id, action: 'setvalue', selector, value };
break;
}
case 'dispatch': {
const selector = cleanArgs[1];
const event = cleanArgs[2];
if (!selector || !event) {
console.error(c('red', 'Error:'), 'Selector and event required');
process.exit(1);
}
cmd = { id, action: 'dispatch', selector, event };
break;
}
case 'addscript':
case 'add-script': {
const urlOrContent = cleanArgs[1];
if (!urlOrContent) {
console.error(c('red', 'Error:'), 'URL or content required');
process.exit(1);
}
if (urlOrContent.startsWith('http')) {
cmd = { id, action: 'addscript', url: urlOrContent };
} else {
cmd = { id, action: 'addscript', content: urlOrContent };
}
break;
}
case 'addstyle':
case 'add-style': {
const urlOrContent = cleanArgs[1];
if (!urlOrContent) {
console.error(c('red', 'Error:'), 'URL or content required');
process.exit(1);
}
if (urlOrContent.startsWith('http')) {
cmd = { id, action: 'addstyle', url: urlOrContent };
} else {
cmd = { id, action: 'addstyle', content: urlOrContent };
}
break;
}
case 'media': {
const colorScheme = args.includes('--dark') ? 'dark' : args.includes('--light') ? 'light' : undefined;
const media = args.includes('--print') ? 'print' : args.includes('--screen') ? 'screen' : undefined;
const reducedMotion = args.includes('--reduced-motion') ? 'reduce' : undefined;
cmd = { id, action: 'emulatemedia', colorScheme, media, reducedMotion };
break;
}
case 'offline': {
const offlineMode = cleanArgs[1] !== 'false' && cleanArgs[1] !== 'off';
cmd = { id, action: 'offline', offline: offlineMode };
break;
}
case 'headers': {
const headersJson = cleanArgs[1];
if (!headersJson) {
console.error(c('red', 'Error:'), 'Headers JSON required');
process.exit(1);
}
try {
const headers = JSON.parse(headersJson);
cmd = { id, action: 'headers', headers };
} catch {
console.error(c('red', 'Error:'), 'Invalid JSON for headers');
process.exit(1);
}
break;
}
case 'pause': {
cmd = { id, action: 'pause' };
break;
}
default:
console.error(c('red', 'Error:'), `Unknown command: ${command}`);
console.error(c('dim', 'Run veb --help for usage'));
+189
View File
@@ -286,6 +286,165 @@ const boundingBoxSchema = baseCommandSchema.extend({
selector: z.string().min(1),
});
const videoStartSchema = baseCommandSchema.extend({
action: z.literal('video_start'),
path: z.string().min(1),
});
const videoStopSchema = baseCommandSchema.extend({
action: z.literal('video_stop'),
});
const traceStartSchema = baseCommandSchema.extend({
action: z.literal('trace_start'),
screenshots: z.boolean().optional(),
snapshots: z.boolean().optional(),
});
const traceStopSchema = baseCommandSchema.extend({
action: z.literal('trace_stop'),
path: z.string().min(1),
});
const harStartSchema = baseCommandSchema.extend({
action: z.literal('har_start'),
});
const harStopSchema = baseCommandSchema.extend({
action: z.literal('har_stop'),
path: z.string().min(1),
});
const stateSaveSchema = baseCommandSchema.extend({
action: z.literal('state_save'),
path: z.string().min(1),
});
const stateLoadSchema = baseCommandSchema.extend({
action: z.literal('state_load'),
path: z.string().min(1),
});
const consoleSchema = baseCommandSchema.extend({
action: z.literal('console'),
clear: z.boolean().optional(),
});
const errorsSchema = baseCommandSchema.extend({
action: z.literal('errors'),
clear: z.boolean().optional(),
});
const keyboardSchema = baseCommandSchema.extend({
action: z.literal('keyboard'),
keys: z.string().min(1),
});
const wheelSchema = baseCommandSchema.extend({
action: z.literal('wheel'),
deltaX: z.number().optional(),
deltaY: z.number().optional(),
selector: z.string().optional(),
});
const tapSchema = baseCommandSchema.extend({
action: z.literal('tap'),
selector: z.string().min(1),
});
const clipboardSchema = baseCommandSchema.extend({
action: z.literal('clipboard'),
operation: z.enum(['copy', 'paste', 'read']),
text: z.string().optional(),
});
const highlightSchema = baseCommandSchema.extend({
action: z.literal('highlight'),
selector: z.string().min(1),
});
const clearSchema = baseCommandSchema.extend({
action: z.literal('clear'),
selector: z.string().min(1),
});
const selectAllSchema = baseCommandSchema.extend({
action: z.literal('selectall'),
selector: z.string().min(1),
});
const innerTextSchema = baseCommandSchema.extend({
action: z.literal('innertext'),
selector: z.string().min(1),
});
const innerHtmlSchema = baseCommandSchema.extend({
action: z.literal('innerhtml'),
selector: z.string().min(1),
});
const inputValueSchema = baseCommandSchema.extend({
action: z.literal('inputvalue'),
selector: z.string().min(1),
});
const setValueSchema = baseCommandSchema.extend({
action: z.literal('setvalue'),
selector: z.string().min(1),
value: z.string(),
});
const dispatchSchema = baseCommandSchema.extend({
action: z.literal('dispatch'),
selector: z.string().min(1),
event: z.string().min(1),
eventInit: z.record(z.unknown()).optional(),
});
const evalHandleSchema = baseCommandSchema.extend({
action: z.literal('evalhandle'),
script: z.string().min(1),
});
const exposeSchema = baseCommandSchema.extend({
action: z.literal('expose'),
name: z.string().min(1),
});
const addScriptSchema = baseCommandSchema.extend({
action: z.literal('addscript'),
content: z.string().optional(),
url: z.string().optional(),
});
const addStyleSchema = baseCommandSchema.extend({
action: z.literal('addstyle'),
content: z.string().optional(),
url: z.string().optional(),
});
const emulateMediaSchema = baseCommandSchema.extend({
action: z.literal('emulatemedia'),
media: z.enum(['screen', 'print']).nullable().optional(),
colorScheme: z.enum(['light', 'dark', 'no-preference']).nullable().optional(),
reducedMotion: z.enum(['reduce', 'no-preference']).nullable().optional(),
forcedColors: z.enum(['active', 'none']).nullable().optional(),
});
const offlineSchema = baseCommandSchema.extend({
action: z.literal('offline'),
offline: z.boolean(),
});
const headersSchema = baseCommandSchema.extend({
action: z.literal('headers'),
headers: z.record(z.string()),
});
const pauseSchema = baseCommandSchema.extend({
action: z.literal('pause'),
});
const pressSchema = baseCommandSchema.extend({
action: z.literal('press'),
key: z.string().min(1),
@@ -437,6 +596,36 @@ const commandSchema = z.discriminatedUnion('action', [
isCheckedSchema,
countSchema,
boundingBoxSchema,
videoStartSchema,
videoStopSchema,
traceStartSchema,
traceStopSchema,
harStartSchema,
harStopSchema,
stateSaveSchema,
stateLoadSchema,
consoleSchema,
errorsSchema,
keyboardSchema,
wheelSchema,
tapSchema,
clipboardSchema,
highlightSchema,
clearSchema,
selectAllSchema,
innerTextSchema,
innerHtmlSchema,
inputValueSchema,
setValueSchema,
dispatchSchema,
evalHandleSchema,
exposeSchema,
addScriptSchema,
addStyleSchema,
emulateMediaSchema,
offlineSchema,
headersSchema,
pauseSchema,
]);
// Parse result type
+214 -1
View File
@@ -294,6 +294,189 @@ export interface BoundingBoxCommand extends BaseCommand {
selector: string;
}
// Video recording
export interface VideoStartCommand extends BaseCommand {
action: 'video_start';
path: string;
}
export interface VideoStopCommand extends BaseCommand {
action: 'video_stop';
}
// Tracing
export interface TraceStartCommand extends BaseCommand {
action: 'trace_start';
screenshots?: boolean;
snapshots?: boolean;
}
export interface TraceStopCommand extends BaseCommand {
action: 'trace_stop';
path: string;
}
// HAR recording
export interface HarStartCommand extends BaseCommand {
action: 'har_start';
}
export interface HarStopCommand extends BaseCommand {
action: 'har_stop';
path: string;
}
// Storage state (auth persistence)
export interface StorageStateSaveCommand extends BaseCommand {
action: 'state_save';
path: string;
}
export interface StorageStateLoadCommand extends BaseCommand {
action: 'state_load';
path: string;
}
// Console logs
export interface ConsoleCommand extends BaseCommand {
action: 'console';
clear?: boolean;
}
// Page errors
export interface ErrorsCommand extends BaseCommand {
action: 'errors';
clear?: boolean;
}
// Keyboard shortcuts
export interface KeyboardCommand extends BaseCommand {
action: 'keyboard';
keys: string; // e.g., "Control+a", "Shift+Tab"
}
// Mouse wheel
export interface WheelCommand extends BaseCommand {
action: 'wheel';
deltaX?: number;
deltaY?: number;
selector?: string;
}
// Touch events
export interface TapCommand extends BaseCommand {
action: 'tap';
selector: string;
}
// Clipboard
export interface ClipboardCommand extends BaseCommand {
action: 'clipboard';
operation: 'copy' | 'paste' | 'read';
text?: string;
}
// Highlight element (for debugging)
export interface HighlightCommand extends BaseCommand {
action: 'highlight';
selector: string;
}
// Clear input
export interface ClearCommand extends BaseCommand {
action: 'clear';
selector: string;
}
// Select all text
export interface SelectAllCommand extends BaseCommand {
action: 'selectall';
selector: string;
}
// Inner text vs text content
export interface InnerTextCommand extends BaseCommand {
action: 'innertext';
selector: string;
}
export interface InnerHtmlCommand extends BaseCommand {
action: 'innerhtml';
selector: string;
}
// Input value
export interface InputValueCommand extends BaseCommand {
action: 'inputvalue';
selector: string;
}
// Set input value directly (without events)
export interface SetValueCommand extends BaseCommand {
action: 'setvalue';
selector: string;
value: string;
}
// Dispatch event
export interface DispatchEventCommand extends BaseCommand {
action: 'dispatch';
selector: string;
event: string;
eventInit?: Record<string, unknown>;
}
// Evaluate handle (for complex JS)
export interface EvaluateHandleCommand extends BaseCommand {
action: 'evalhandle';
script: string;
}
// Expose function
export interface ExposeFunctionCommand extends BaseCommand {
action: 'expose';
name: string;
}
// Add script/style tag
export interface AddScriptCommand extends BaseCommand {
action: 'addscript';
content?: string;
url?: string;
}
export interface AddStyleCommand extends BaseCommand {
action: 'addstyle';
content?: string;
url?: string;
}
// Emulate media
export interface EmulateMediaCommand extends BaseCommand {
action: 'emulatemedia';
media?: 'screen' | 'print' | null;
colorScheme?: 'light' | 'dark' | 'no-preference' | null;
reducedMotion?: 'reduce' | 'no-preference' | null;
forcedColors?: 'active' | 'none' | null;
}
// Set offline mode
export interface OfflineCommand extends BaseCommand {
action: 'offline';
offline: boolean;
}
// Set extra HTTP headers
export interface HeadersCommand extends BaseCommand {
action: 'headers';
headers: Record<string, string>;
}
// Pause execution (for debugging)
export interface PauseCommand extends BaseCommand {
action: 'pause';
}
export interface PressCommand extends BaseCommand {
action: 'press';
key: string;
@@ -441,7 +624,37 @@ export type Command =
| IsEnabledCommand
| IsCheckedCommand
| CountCommand
| BoundingBoxCommand;
| BoundingBoxCommand
| VideoStartCommand
| VideoStopCommand
| TraceStartCommand
| TraceStopCommand
| HarStartCommand
| HarStopCommand
| StorageStateSaveCommand
| StorageStateLoadCommand
| ConsoleCommand
| ErrorsCommand
| KeyboardCommand
| WheelCommand
| TapCommand
| ClipboardCommand
| HighlightCommand
| ClearCommand
| SelectAllCommand
| InnerTextCommand
| InnerHtmlCommand
| InputValueCommand
| SetValueCommand
| DispatchEventCommand
| EvaluateHandleCommand
| ExposeFunctionCommand
| AddScriptCommand
| AddStyleCommand
| EmulateMediaCommand
| OfflineCommand
| HeadersCommand
| PauseCommand;
// Response types
export interface SuccessResponse<T = unknown> {