From 6dc408e02f68956b71cc8e55f34eb9a47cceff73 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 10 Jan 2026 12:15:57 -0600 Subject: [PATCH] more --- README.md | 86 ++++++++++ src/actions.ts | 415 ++++++++++++++++++++++++++++++++++++++++++++++++ src/browser.ts | 137 ++++++++++++++++ src/index.ts | 301 +++++++++++++++++++++++++++++++++++ src/protocol.ts | 189 ++++++++++++++++++++++ src/types.ts | 215 ++++++++++++++++++++++++- 6 files changed, 1342 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c629a7e..3f1d2ad 100644 --- a/README.md +++ b/README.md @@ -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 ` | Click element | | `dblclick ` | Double-click | +| `tap ` | Touch tap | | `type ` | Type text | | `fill ` | Clear & fill | | `press ` | Press key | @@ -177,20 +224,29 @@ veb isvisible "#modal" --json | `hover ` | Hover | | `focus ` | Focus | | `drag ` | Drag & drop | +| `wheel [deltaY] [deltaX]` | Mouse wheel | +| `clear-input ` | Clear input | +| `setvalue ` | Set value | +| `selectall ` | Select all text | | `upload ` | Upload files | | `download ` | Download file | | `scroll [amount]` | Scroll | +| `clipboard copy\|paste\|read` | Clipboard ops | ### Element Info | Command | Description | |---------|-------------| | `gettext ` | Get text content | +| `innertext ` | Get inner text | +| `innerhtml ` | Get inner HTML | +| `inputvalue ` | Get input value | | `getattr ` | Get attribute | | `isvisible ` | Check visibility | | `isenabled ` | Check enabled | | `ischecked ` | Check checked | | `count ` | Count elements | | `boundingbox ` | Get bounds | +| `highlight ` | Highlight element | ### Semantic Locators | Command | Description | @@ -224,6 +280,9 @@ veb isvisible "#modal" --json | `device ` | Emulate device | | `geolocation ` | Set location | | `permissions grant\|deny` | Set permissions | +| `media [options]` | Emulate media | +| `offline [on\|off]` | Offline mode | +| `headers ` | 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 ` | Save auth state | +| `state load ` | Load auth state | + +### Debugging & Tracing +| Command | Description | +|---------|-------------| +| `trace start` | Start trace recording | +| `trace stop ` | Stop & save trace | +| `console` | View console messages | +| `console --clear` | Clear console | +| `errors` | View page errors | +| `errors --clear` | Clear errors | +| `pause` | Open inspector | +| `highlight ` | Highlight element | + +### Script Injection +| Command | Description | +|---------|-------------| +| `addscript ` | Add script | +| `addstyle ` | Add stylesheet | +| `dispatch ` | 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 diff --git a/src/actions.ts b/src/actions.ts index 94057a9..3adecb4 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -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 { + // 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 { + 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 { + await browser.startTracing({ + screenshots: command.screenshots, + snapshots: command.snapshots, + }); + return successResponse(command.id, { started: true }); +} + +async function handleTraceStop( + command: TraceStopCommand, + browser: BrowserManager +): Promise { + await browser.stopTracing(command.path); + return successResponse(command.id, { path: command.path }); +} + +async function handleHarStart( + command: Command & { action: 'har_start' }, + browser: BrowserManager +): Promise { + await browser.startHarRecording(); + browser.startRequestTracking(); + return successResponse(command.id, { started: true }); +} + +async function handleHarStop( + command: HarStopCommand, + browser: BrowserManager +): Promise { + // 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 { + 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 { + // 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 { + 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 { + 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 { + 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 { + 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 { + const page = browser.getPage(); + await page.tap(command.selector); + return successResponse(command.id, { tapped: true }); +} + +async function handleClipboard( + command: ClipboardCommand, + browser: BrowserManager +): Promise { + 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 { + const page = browser.getPage(); + await page.locator(command.selector).highlight(); + return successResponse(command.id, { highlighted: true }); +} + +async function handleClear( + command: ClearCommand, + browser: BrowserManager +): Promise { + const page = browser.getPage(); + await page.locator(command.selector).clear(); + return successResponse(command.id, { cleared: true }); +} + +async function handleSelectAll( + command: SelectAllCommand, + browser: BrowserManager +): Promise { + const page = browser.getPage(); + await page.locator(command.selector).selectText(); + return successResponse(command.id, { selected: true }); +} + +async function handleInnerText( + command: InnerTextCommand, + browser: BrowserManager +): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + await browser.setOffline(command.offline); + return successResponse(command.id, { offline: command.offline }); +} + +async function handleHeaders( + command: HeadersCommand, + browser: BrowserManager +): Promise { + await browser.setExtraHeaders(command.headers); + return successResponse(command.id, { set: true }); +} + +async function handlePause( + command: Command & { action: 'pause' }, + browser: BrowserManager +): Promise { + const page = browser.getPage(); + await page.pause(); + return successResponse(command.id, { paused: true }); +} diff --git a/src/browser.ts b/src/browser.ts index ca7ad3a..cec0689 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -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) | null = null; private trackedRequests: TrackedRequest[] = []; private routes: Map Promise> = 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 { + // 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 { + const context = this.contexts[0]; + if (context) { + await context.setOffline(offline); + } + } + + /** + * Set extra HTTP headers + */ + async setExtraHeaders(headers: Record): Promise { + const context = this.contexts[0]; + if (context) { + await context.setExtraHTTPHeaders(headers); + } + } + + /** + * Start tracing + */ + async startTracing(options: { screenshots?: boolean; snapshots?: boolean }): Promise { + 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 { + const context = this.contexts[0]; + if (context) { + await context.tracing.stop({ path }); + } + } + + /** + * Save storage state (cookies, localStorage, etc.) + */ + async saveStorageState(path: string): Promise { + const context = this.contexts[0]; + if (context) { + await context.storageState({ path }); + } + } + /** * Get all pages */ diff --git a/src/index.ts b/src/index.ts index f251eba..61d642a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 { } } + // === 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 '); + 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')); diff --git a/src/protocol.ts b/src/protocol.ts index a15e35e..b751e47 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -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 diff --git a/src/types.ts b/src/types.ts index 0657350..2fd8b71 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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; +} + +// 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; +} + +// 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 {