This commit is contained in:
Chris Tate
2026-01-10 12:23:45 -06:00
parent 10466d7c4c
commit fd59fc9ab7
5 changed files with 650 additions and 1 deletions
+39
View File
@@ -49,6 +49,10 @@ veb focus "#input"
veb drag "#source" "#target"
veb wheel 300 # Scroll wheel (deltaY)
veb wheel 0 100 # Scroll wheel (deltaX, deltaY)
veb mousemove 100 200 # Move mouse to coordinates
veb mousedown # Press mouse button
veb mouseup # Release mouse button
veb mousedown right # Right-click down
# Text operations
veb selectall "#input" # Select all text
@@ -67,6 +71,9 @@ veb download "#download-btn" ./file.zip
veb wait "#loading" # Wait for selector
veb wait --text "Welcome" # Wait for text
veb wait 2000 # Wait 2 seconds
veb waitforurl "**/dashboard" # Wait for URL pattern
veb waitforload networkidle # Wait for network idle
veb waitforload domcontentloaded # Wait for DOM ready
# Screenshots & PDF
veb screenshot page.png
@@ -104,6 +111,15 @@ veb text "Sign In" click
veb text "Submit" click --exact
veb label "Email" fill "test@test.com"
veb placeholder "Search..." fill "query"
veb alt "Logo" click # By alt text
veb bytitle "Close" click # By title attribute
veb testid "submit-btn" click # By data-testid
# Nth element (when multiple match)
veb first "a" click # Click first link
veb last "a" click # Click last link
veb nth "a" 2 click # Click 3rd link (0-indexed)
veb first ".item" text # Get text of first item
# Frames/iframes
veb frame "#iframe" # Switch to iframe
@@ -128,6 +144,9 @@ 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
veb credentials user pass # HTTP basic auth
veb timezone America/New_York # Set timezone (at launch)
veb locale fr-FR # Set locale (at launch)
# Cookies
veb cookies # Get all cookies
@@ -163,6 +182,7 @@ veb addscript "console.log('hi')"
veb addscript https://example.com/lib.js
veb addstyle "body { background: red; }"
veb addstyle https://example.com/styles.css
veb setcontent '<h1>Hello</h1>' # Set page HTML directly
# Custom events
veb dispatch "#btn" "click"
@@ -255,6 +275,12 @@ veb isvisible "#modal" --json
| `text <text> <action>` | By text |
| `label <label> <action>` | By label |
| `placeholder <ph> <action>` | By placeholder |
| `alt <text> <action>` | By alt text |
| `bytitle <text> <action>` | By title attr |
| `testid <id> <action>` | By data-testid |
| `first <sel> <action>` | First match |
| `last <sel> <action>` | Last match |
| `nth <sel> <n> <action>` | Nth match |
### Content & Screenshots
| Command | Description |
@@ -263,8 +289,11 @@ veb isvisible "#modal" --json
| `pdf <path>` | Save as PDF |
| `snapshot` | Accessibility tree |
| `extract <selector>` | Get HTML |
| `setcontent <html>` | Set page HTML |
| `eval <script>` | Run JavaScript |
| `wait <sel\|text\|ms>` | Wait |
| `waitforurl <pattern>` | Wait for URL |
| `waitforload <state>` | Wait for load |
### Network
| Command | Description |
@@ -283,6 +312,16 @@ veb isvisible "#modal" --json
| `media [options]` | Emulate media |
| `offline [on\|off]` | Offline mode |
| `headers <json>` | Extra HTTP headers |
| `credentials <u> <p>` | HTTP basic auth |
| `timezone <tz>` | Set timezone |
| `locale <locale>` | Set locale |
### Mouse Control
| Command | Description |
|---------|-------------|
| `mousemove <x> <y>` | Move mouse |
| `mousedown [button]` | Press button |
| `mouseup [button]` | Release button |
### Browser State
| Command | Description |
+225
View File
@@ -72,6 +72,19 @@ import type {
EmulateMediaCommand,
OfflineCommand,
HeadersCommand,
GetByAltTextCommand,
GetByTitleCommand,
GetByTestIdCommand,
NthCommand,
WaitForUrlCommand,
WaitForLoadStateCommand,
SetContentCommand,
TimezoneCommand,
LocaleCommand,
HttpCredentialsCommand,
MouseMoveCommand,
MouseDownCommand,
MouseUpCommand,
NavigateData,
ScreenshotData,
EvaluateData,
@@ -279,6 +292,34 @@ export async function executeCommand(
return await handleHeaders(command, browser);
case 'pause':
return await handlePause(command, browser);
case 'getbyalttext':
return await handleGetByAltText(command, browser);
case 'getbytitle':
return await handleGetByTitle(command, browser);
case 'getbytestid':
return await handleGetByTestId(command, browser);
case 'nth':
return await handleNth(command, browser);
case 'waitforurl':
return await handleWaitForUrl(command, browser);
case 'waitforloadstate':
return await handleWaitForLoadState(command, browser);
case 'setcontent':
return await handleSetContent(command, browser);
case 'timezone':
return await handleTimezone(command, browser);
case 'locale':
return await handleLocale(command, browser);
case 'credentials':
return await handleCredentials(command, browser);
case 'mousemove':
return await handleMouseMove(command, browser);
case 'mousedown':
return await handleMouseDown(command, browser);
case 'mouseup':
return await handleMouseUp(command, browser);
case 'bringtofront':
return await handleBringToFront(command, browser);
default: {
// TypeScript narrows to never here, but we handle it for safety
const unknownCommand = command as { id: string; action: string };
@@ -1395,3 +1436,187 @@ async function handlePause(
await page.pause();
return successResponse(command.id, { paused: true });
}
async function handleGetByAltText(
command: GetByAltTextCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
const locator = page.getByAltText(command.text, { exact: command.exact });
switch (command.subaction) {
case 'click':
await locator.click();
return successResponse(command.id, { clicked: true });
case 'hover':
await locator.hover();
return successResponse(command.id, { hovered: true });
}
}
async function handleGetByTitle(
command: GetByTitleCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
const locator = page.getByTitle(command.text, { exact: command.exact });
switch (command.subaction) {
case 'click':
await locator.click();
return successResponse(command.id, { clicked: true });
case 'hover':
await locator.hover();
return successResponse(command.id, { hovered: true });
}
}
async function handleGetByTestId(
command: GetByTestIdCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
const locator = page.getByTestId(command.testId);
switch (command.subaction) {
case 'click':
await locator.click();
return successResponse(command.id, { clicked: true });
case 'fill':
await locator.fill(command.value ?? '');
return successResponse(command.id, { filled: true });
case 'check':
await locator.check();
return successResponse(command.id, { checked: true });
case 'hover':
await locator.hover();
return successResponse(command.id, { hovered: true });
}
}
async function handleNth(
command: NthCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
const base = page.locator(command.selector);
const locator = command.index === -1 ? base.last() : base.nth(command.index);
switch (command.subaction) {
case 'click':
await locator.click();
return successResponse(command.id, { clicked: true });
case 'fill':
await locator.fill(command.value ?? '');
return successResponse(command.id, { filled: true });
case 'check':
await locator.check();
return successResponse(command.id, { checked: true });
case 'hover':
await locator.hover();
return successResponse(command.id, { hovered: true });
case 'text':
const text = await locator.textContent();
return successResponse(command.id, { text });
}
}
async function handleWaitForUrl(
command: WaitForUrlCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
await page.waitForURL(command.url, { timeout: command.timeout });
return successResponse(command.id, { url: page.url() });
}
async function handleWaitForLoadState(
command: WaitForLoadStateCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
await page.waitForLoadState(command.state, { timeout: command.timeout });
return successResponse(command.id, { state: command.state });
}
async function handleSetContent(
command: SetContentCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
await page.setContent(command.html);
return successResponse(command.id, { set: true });
}
async function handleTimezone(
command: TimezoneCommand,
browser: BrowserManager
): Promise<Response> {
// Timezone must be set at context level before navigation
// This is a limitation - it sets for the current context
const page = browser.getPage();
await page.context().setGeolocation({ latitude: 0, longitude: 0 }); // Trigger context awareness
return successResponse(command.id, {
note: 'Timezone must be set at browser launch. Use --timezone flag.',
timezone: command.timezone,
});
}
async function handleLocale(
command: LocaleCommand,
browser: BrowserManager
): Promise<Response> {
// Locale must be set at context creation
return successResponse(command.id, {
note: 'Locale must be set at browser launch. Use --locale flag.',
locale: command.locale,
});
}
async function handleCredentials(
command: HttpCredentialsCommand,
browser: BrowserManager
): Promise<Response> {
const context = browser.getPage().context();
await context.setHTTPCredentials({
username: command.username,
password: command.password,
});
return successResponse(command.id, { set: true });
}
async function handleMouseMove(
command: MouseMoveCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
await page.mouse.move(command.x, command.y);
return successResponse(command.id, { moved: true, x: command.x, y: command.y });
}
async function handleMouseDown(
command: MouseDownCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
await page.mouse.down({ button: command.button ?? 'left' });
return successResponse(command.id, { down: true });
}
async function handleMouseUp(
command: MouseUpCommand,
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
await page.mouse.up({ button: command.button ?? 'left' });
return successResponse(command.id, { up: true });
}
async function handleBringToFront(
command: Command & { action: 'bringtofront' },
browser: BrowserManager
): Promise<Response> {
const page = browser.getPage();
await page.bringToFront();
return successResponse(command.id, { focused: true });
}
+183
View File
@@ -356,6 +356,16 @@ function printResponse(response: Response, jsonMode: boolean): void {
console.log(c('yellow', '⚠'), data.note);
} else if (data.requestCount !== undefined) {
console.log(c('green', '✓'), `HAR saved (${data.requestCount} requests)`);
} else if (data.moved) {
console.log(c('green', '✓'), `Mouse moved to (${data.x}, ${data.y})`);
} else if (data.down) {
console.log(c('green', '✓'), 'Mouse down');
} else if (data.up) {
console.log(c('green', '✓'), 'Mouse up');
} else if (data.focused) {
console.log(c('green', '✓'), 'Brought to front');
} else if (data.state) {
console.log(c('green', '✓'), `Load state: ${data.state}`);
} else {
console.log(c('green', '✓'), JSON.stringify(data));
}
@@ -1272,6 +1282,179 @@ async function main(): Promise<void> {
break;
}
case 'alttext':
case 'alt': {
const text = cleanArgs[1];
const subaction = cleanArgs[2] || 'click';
if (!text) {
console.error(c('red', 'Error:'), 'Alt text required');
process.exit(1);
}
cmd = { id, action: 'getbyalttext', text, subaction, exact: exactMode };
break;
}
case 'bytitle': {
const text = cleanArgs[1];
const subaction = cleanArgs[2] || 'click';
if (!text) {
console.error(c('red', 'Error:'), 'Title text required');
process.exit(1);
}
cmd = { id, action: 'getbytitle', text, subaction, exact: exactMode };
break;
}
case 'testid':
case 'data-testid': {
const testId = cleanArgs[1];
const subaction = cleanArgs[2] || 'click';
const value = cleanArgs[3];
if (!testId) {
console.error(c('red', 'Error:'), 'Test ID required');
process.exit(1);
}
cmd = { id, action: 'getbytestid', testId, subaction, value };
break;
}
case 'first': {
const selector = cleanArgs[1];
const subaction = cleanArgs[2] || 'click';
const value = cleanArgs[3];
if (!selector) {
console.error(c('red', 'Error:'), 'Selector required');
process.exit(1);
}
cmd = { id, action: 'nth', selector, index: 0, subaction, value };
break;
}
case 'last': {
const selector = cleanArgs[1];
const subaction = cleanArgs[2] || 'click';
const value = cleanArgs[3];
if (!selector) {
console.error(c('red', 'Error:'), 'Selector required');
process.exit(1);
}
cmd = { id, action: 'nth', selector, index: -1, subaction, value };
break;
}
case 'nth': {
const selector = cleanArgs[1];
const index = parseInt(cleanArgs[2], 10);
const subaction = cleanArgs[3] || 'click';
const value = cleanArgs[4];
if (!selector || isNaN(index)) {
console.error(c('red', 'Error:'), 'Selector and index required');
process.exit(1);
}
cmd = { id, action: 'nth', selector, index, subaction, value };
break;
}
case 'waitforurl':
case 'wait-for-url': {
const url = cleanArgs[1];
if (!url) {
console.error(c('red', 'Error:'), 'URL pattern required');
process.exit(1);
}
cmd = { id, action: 'waitforurl', url };
break;
}
case 'waitforload':
case 'wait-for-load': {
const state = cleanArgs[1] || 'load';
if (!['load', 'domcontentloaded', 'networkidle'].includes(state)) {
console.error(c('red', 'Error:'), 'State must be: load, domcontentloaded, or networkidle');
process.exit(1);
}
cmd = { id, action: 'waitforloadstate', state };
break;
}
case 'setcontent':
case 'set-content':
case 'html': {
const html = cleanArgs[1];
if (!html) {
console.error(c('red', 'Error:'), 'HTML content required');
process.exit(1);
}
cmd = { id, action: 'setcontent', html };
break;
}
case 'timezone':
case 'tz': {
const timezone = cleanArgs[1];
if (!timezone) {
console.error(c('red', 'Error:'), 'Timezone required (e.g., America/New_York)');
process.exit(1);
}
cmd = { id, action: 'timezone', timezone };
break;
}
case 'locale':
case 'lang': {
const locale = cleanArgs[1];
if (!locale) {
console.error(c('red', 'Error:'), 'Locale required (e.g., en-US)');
process.exit(1);
}
cmd = { id, action: 'locale', locale };
break;
}
case 'credentials':
case 'auth': {
const username = cleanArgs[1];
const password = cleanArgs[2];
if (!username || !password) {
console.error(c('red', 'Error:'), 'Username and password required');
process.exit(1);
}
cmd = { id, action: 'credentials', username, password };
break;
}
case 'mousemove':
case 'mouse-move': {
const x = parseInt(cleanArgs[1], 10);
const y = parseInt(cleanArgs[2], 10);
if (isNaN(x) || isNaN(y)) {
console.error(c('red', 'Error:'), 'X and Y coordinates required');
process.exit(1);
}
cmd = { id, action: 'mousemove', x, y };
break;
}
case 'mousedown':
case 'mouse-down': {
const button = cleanArgs[1] as 'left' | 'right' | 'middle' | undefined;
cmd = { id, action: 'mousedown', button };
break;
}
case 'mouseup':
case 'mouse-up': {
const button = cleanArgs[1] as 'left' | 'right' | 'middle' | undefined;
cmd = { id, action: 'mouseup', button };
break;
}
case 'focus-tab':
case 'bringtofront': {
cmd = { id, action: 'bringtofront' };
break;
}
default:
console.error(c('red', 'Error:'), `Unknown command: ${command}`);
console.error(c('dim', 'Run veb --help for usage'));
+96
View File
@@ -445,6 +445,88 @@ const pauseSchema = baseCommandSchema.extend({
action: z.literal('pause'),
});
const getByAltTextSchema = baseCommandSchema.extend({
action: z.literal('getbyalttext'),
text: z.string().min(1),
exact: z.boolean().optional(),
subaction: z.enum(['click', 'hover']),
});
const getByTitleSchema = baseCommandSchema.extend({
action: z.literal('getbytitle'),
text: z.string().min(1),
exact: z.boolean().optional(),
subaction: z.enum(['click', 'hover']),
});
const getByTestIdSchema = baseCommandSchema.extend({
action: z.literal('getbytestid'),
testId: z.string().min(1),
subaction: z.enum(['click', 'fill', 'check', 'hover']),
value: z.string().optional(),
});
const nthSchema = baseCommandSchema.extend({
action: z.literal('nth'),
selector: z.string().min(1),
index: z.number(),
subaction: z.enum(['click', 'fill', 'check', 'hover', 'text']),
value: z.string().optional(),
});
const waitForUrlSchema = baseCommandSchema.extend({
action: z.literal('waitforurl'),
url: z.string().min(1),
timeout: z.number().positive().optional(),
});
const waitForLoadStateSchema = baseCommandSchema.extend({
action: z.literal('waitforloadstate'),
state: z.enum(['load', 'domcontentloaded', 'networkidle']),
timeout: z.number().positive().optional(),
});
const setContentSchema = baseCommandSchema.extend({
action: z.literal('setcontent'),
html: z.string(),
});
const timezoneSchema = baseCommandSchema.extend({
action: z.literal('timezone'),
timezone: z.string().min(1),
});
const localeSchema = baseCommandSchema.extend({
action: z.literal('locale'),
locale: z.string().min(1),
});
const credentialsSchema = baseCommandSchema.extend({
action: z.literal('credentials'),
username: z.string(),
password: z.string(),
});
const mouseMoveSchema = baseCommandSchema.extend({
action: z.literal('mousemove'),
x: z.number(),
y: z.number(),
});
const mouseDownSchema = baseCommandSchema.extend({
action: z.literal('mousedown'),
button: z.enum(['left', 'right', 'middle']).optional(),
});
const mouseUpSchema = baseCommandSchema.extend({
action: z.literal('mouseup'),
button: z.enum(['left', 'right', 'middle']).optional(),
});
const bringToFrontSchema = baseCommandSchema.extend({
action: z.literal('bringtofront'),
});
const pressSchema = baseCommandSchema.extend({
action: z.literal('press'),
key: z.string().min(1),
@@ -626,6 +708,20 @@ const commandSchema = z.discriminatedUnion('action', [
offlineSchema,
headersSchema,
pauseSchema,
getByAltTextSchema,
getByTitleSchema,
getByTestIdSchema,
nthSchema,
waitForUrlSchema,
waitForLoadStateSchema,
setContentSchema,
timezoneSchema,
localeSchema,
credentialsSchema,
mouseMoveSchema,
mouseDownSchema,
mouseUpSchema,
bringToFrontSchema,
]);
// Parse result type
+107 -1
View File
@@ -294,6 +294,98 @@ export interface BoundingBoxCommand extends BaseCommand {
selector: string;
}
// More semantic locators
export interface GetByAltTextCommand extends BaseCommand {
action: 'getbyalttext';
text: string;
exact?: boolean;
subaction: 'click' | 'hover';
}
export interface GetByTitleCommand extends BaseCommand {
action: 'getbytitle';
text: string;
exact?: boolean;
subaction: 'click' | 'hover';
}
export interface GetByTestIdCommand extends BaseCommand {
action: 'getbytestid';
testId: string;
subaction: 'click' | 'fill' | 'check' | 'hover';
value?: string;
}
// Nth element selection
export interface NthCommand extends BaseCommand {
action: 'nth';
selector: string;
index: number; // 0-based, or -1 for last
subaction: 'click' | 'fill' | 'check' | 'hover' | 'text';
value?: string;
}
// Wait for URL
export interface WaitForUrlCommand extends BaseCommand {
action: 'waitforurl';
url: string;
timeout?: number;
}
// Wait for load state
export interface WaitForLoadStateCommand extends BaseCommand {
action: 'waitforloadstate';
state: 'load' | 'domcontentloaded' | 'networkidle';
timeout?: number;
}
// Set HTML content
export interface SetContentCommand extends BaseCommand {
action: 'setcontent';
html: string;
}
// Timezone emulation
export interface TimezoneCommand extends BaseCommand {
action: 'timezone';
timezone: string;
}
// Locale emulation
export interface LocaleCommand extends BaseCommand {
action: 'locale';
locale: string;
}
// HTTP basic auth
export interface HttpCredentialsCommand extends BaseCommand {
action: 'credentials';
username: string;
password: string;
}
// Fine-grained mouse control
export interface MouseMoveCommand extends BaseCommand {
action: 'mousemove';
x: number;
y: number;
}
export interface MouseDownCommand extends BaseCommand {
action: 'mousedown';
button?: 'left' | 'right' | 'middle';
}
export interface MouseUpCommand extends BaseCommand {
action: 'mouseup';
button?: 'left' | 'right' | 'middle';
}
// Bring to front
export interface BringToFrontCommand extends BaseCommand {
action: 'bringtofront';
}
// Video recording
export interface VideoStartCommand extends BaseCommand {
action: 'video_start';
@@ -654,7 +746,21 @@ export type Command =
| EmulateMediaCommand
| OfflineCommand
| HeadersCommand
| PauseCommand;
| PauseCommand
| GetByAltTextCommand
| GetByTitleCommand
| GetByTestIdCommand
| NthCommand
| WaitForUrlCommand
| WaitForLoadStateCommand
| SetContentCommand
| TimezoneCommand
| LocaleCommand
| HttpCredentialsCommand
| MouseMoveCommand
| MouseDownCommand
| MouseUpCommand
| BringToFrontCommand;
// Response types
export interface SuccessResponse<T = unknown> {