more
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# veb
|
# veb
|
||||||
|
|
||||||
Headless browser automation CLI for agents and humans. Near-complete Playwright parity.
|
Headless browser automation CLI for agents and humans. Full Playwright parity.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -15,6 +15,13 @@ pnpm build
|
|||||||
```bash
|
```bash
|
||||||
# Navigation
|
# Navigation
|
||||||
veb open https://example.com
|
veb open https://example.com
|
||||||
|
veb back # Go back
|
||||||
|
veb forward # Go forward
|
||||||
|
veb reload # Reload page
|
||||||
|
|
||||||
|
# Page info
|
||||||
|
veb url # Get current URL
|
||||||
|
veb title # Get page title
|
||||||
|
|
||||||
# Clicking
|
# Clicking
|
||||||
veb click "#submit-btn"
|
veb click "#submit-btn"
|
||||||
@@ -40,6 +47,9 @@ veb drag "#source" "#target"
|
|||||||
veb upload "#file-input" ./document.pdf
|
veb upload "#file-input" ./document.pdf
|
||||||
veb upload "#files" ./a.png ./b.png
|
veb upload "#files" ./a.png ./b.png
|
||||||
|
|
||||||
|
# Downloads
|
||||||
|
veb download "#download-btn" ./file.zip
|
||||||
|
|
||||||
# Waiting
|
# Waiting
|
||||||
veb wait "#loading" # Wait for selector
|
veb wait "#loading" # Wait for selector
|
||||||
veb wait --text "Welcome" # Wait for text
|
veb wait --text "Welcome" # Wait for text
|
||||||
@@ -56,6 +66,15 @@ veb snapshot # Accessibility tree (best for agents)
|
|||||||
veb extract "#main" # Get HTML
|
veb extract "#main" # Get HTML
|
||||||
veb eval "document.title" # Run JavaScript
|
veb eval "document.title" # Run JavaScript
|
||||||
|
|
||||||
|
# Element info
|
||||||
|
veb gettext "#message" # Get text content
|
||||||
|
veb getattr "#link" "href" # Get attribute
|
||||||
|
veb isvisible "#modal" # Check visibility
|
||||||
|
veb isenabled "#submit" # Check if enabled
|
||||||
|
veb ischecked "#checkbox" # Check if checked
|
||||||
|
veb count ".items" # Count matching elements
|
||||||
|
veb boundingbox "#element" # Get position/size
|
||||||
|
|
||||||
# Scrolling
|
# Scrolling
|
||||||
veb scroll down 500
|
veb scroll down 500
|
||||||
veb scroll up
|
veb scroll up
|
||||||
@@ -73,6 +92,19 @@ veb placeholder "Search..." fill "query"
|
|||||||
veb frame "#iframe" # Switch to iframe
|
veb frame "#iframe" # Switch to iframe
|
||||||
veb mainframe # Switch back to main
|
veb mainframe # Switch back to main
|
||||||
|
|
||||||
|
# Network interception
|
||||||
|
veb route "**/*.png" --abort # Block images
|
||||||
|
veb route "**/api/*" --body '{"mock":true}' # Mock API
|
||||||
|
veb unroute # Remove all routes
|
||||||
|
veb requests # View tracked requests
|
||||||
|
veb requests --filter "api" # Filter requests
|
||||||
|
|
||||||
|
# Browser settings
|
||||||
|
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
|
||||||
|
|
||||||
# Cookies
|
# Cookies
|
||||||
veb cookies # Get all cookies
|
veb cookies # Get all cookies
|
||||||
veb cookies set '[{"name":"session","value":"abc123","domain":".example.com"}]'
|
veb cookies set '[{"name":"session","value":"abc123","domain":".example.com"}]'
|
||||||
@@ -116,14 +148,24 @@ Use `--json` flag for machine-readable output:
|
|||||||
```bash
|
```bash
|
||||||
veb snapshot --json
|
veb snapshot --json
|
||||||
veb eval "document.title" --json
|
veb eval "document.title" --json
|
||||||
|
veb isvisible "#modal" --json
|
||||||
```
|
```
|
||||||
|
|
||||||
## Commands Reference
|
## All Commands
|
||||||
|
|
||||||
### Navigation & Interaction
|
### Navigation
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `open <url>` | Navigate to URL |
|
| `open <url>` | Navigate to URL |
|
||||||
|
| `back` | Go back |
|
||||||
|
| `forward` | Go forward |
|
||||||
|
| `reload` | Reload page |
|
||||||
|
| `url` | Get current URL |
|
||||||
|
| `title` | Get page title |
|
||||||
|
|
||||||
|
### Interaction
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
| `click <selector>` | Click element |
|
| `click <selector>` | Click element |
|
||||||
| `dblclick <selector>` | Double-click |
|
| `dblclick <selector>` | Double-click |
|
||||||
| `type <selector> <text>` | Type text |
|
| `type <selector> <text>` | Type text |
|
||||||
@@ -136,8 +178,20 @@ veb eval "document.title" --json
|
|||||||
| `focus <selector>` | Focus |
|
| `focus <selector>` | Focus |
|
||||||
| `drag <src> <target>` | Drag & drop |
|
| `drag <src> <target>` | Drag & drop |
|
||||||
| `upload <selector> <files>` | Upload files |
|
| `upload <selector> <files>` | Upload files |
|
||||||
|
| `download <selector> <path>` | Download file |
|
||||||
| `scroll <dir> [amount]` | Scroll |
|
| `scroll <dir> [amount]` | Scroll |
|
||||||
|
|
||||||
|
### Element Info
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `gettext <selector>` | Get text content |
|
||||||
|
| `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 |
|
||||||
|
|
||||||
### Semantic Locators
|
### Semantic Locators
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
@@ -154,6 +208,22 @@ veb eval "document.title" --json
|
|||||||
| `snapshot` | Accessibility tree |
|
| `snapshot` | Accessibility tree |
|
||||||
| `extract <selector>` | Get HTML |
|
| `extract <selector>` | Get HTML |
|
||||||
| `eval <script>` | Run JavaScript |
|
| `eval <script>` | Run JavaScript |
|
||||||
|
| `wait <sel\|text\|ms>` | Wait |
|
||||||
|
|
||||||
|
### Network
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `route <url> [options]` | Intercept requests |
|
||||||
|
| `unroute [url]` | Remove routes |
|
||||||
|
| `requests [--filter]` | View requests |
|
||||||
|
|
||||||
|
### Browser Settings
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `viewport <w> <h>` | Set viewport |
|
||||||
|
| `device <name>` | Emulate device |
|
||||||
|
| `geolocation <lat> <lng>` | Set location |
|
||||||
|
| `permissions grant\|deny` | Set permissions |
|
||||||
|
|
||||||
### Browser State
|
### Browser State
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
@@ -161,11 +231,9 @@ veb eval "document.title" --json
|
|||||||
| `cookies` | Get cookies |
|
| `cookies` | Get cookies |
|
||||||
| `cookies set <json>` | Set cookies |
|
| `cookies set <json>` | Set cookies |
|
||||||
| `cookies clear` | Clear cookies |
|
| `cookies clear` | Clear cookies |
|
||||||
| `storage local [key]` | Get localStorage |
|
| `storage local [key]` | localStorage |
|
||||||
| `storage local set <k> <v>` | Set localStorage |
|
| `storage session [key]` | sessionStorage |
|
||||||
| `storage local clear` | Clear localStorage |
|
| `dialog accept\|dismiss` | Handle dialogs |
|
||||||
| `dialog accept [text]` | Accept dialog |
|
|
||||||
| `dialog dismiss` | Dismiss dialog |
|
|
||||||
|
|
||||||
### Frames & Tabs
|
### Frames & Tabs
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
@@ -181,7 +249,6 @@ veb eval "document.title" --json
|
|||||||
### Session & Control
|
### Session & Control
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `wait <sel\|text\|ms>` | Wait for condition |
|
|
||||||
| `session` | Show session |
|
| `session` | Show session |
|
||||||
| `session list` | List sessions |
|
| `session list` | List sessions |
|
||||||
| `close` | Close browser |
|
| `close` | Close browser |
|
||||||
@@ -197,8 +264,49 @@ veb eval "document.title" --json
|
|||||||
| `--name, -n` | Locator name filter |
|
| `--name, -n` | Locator name filter |
|
||||||
| `--exact` | Exact text match |
|
| `--exact` | Exact text match |
|
||||||
| `--text, -t` | Wait for text |
|
| `--text, -t` | Wait for text |
|
||||||
|
| `--abort` | Abort route |
|
||||||
|
| `--body` | Route response body |
|
||||||
|
| `--filter` | Filter requests |
|
||||||
| `--debug` | Debug output |
|
| `--debug` | Debug output |
|
||||||
|
|
||||||
|
## Device Emulation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mobile devices
|
||||||
|
veb device "iPhone 14"
|
||||||
|
veb device "iPhone 14 Pro Max"
|
||||||
|
veb device "Pixel 7"
|
||||||
|
veb device "Galaxy S23"
|
||||||
|
|
||||||
|
# Tablets
|
||||||
|
veb device "iPad Pro 11"
|
||||||
|
veb device "Galaxy Tab S8"
|
||||||
|
|
||||||
|
# Desktop
|
||||||
|
veb viewport 1920 1080
|
||||||
|
veb viewport 2560 1440
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sessions
|
||||||
|
|
||||||
|
Sessions allow multiple agents to use veb simultaneously without interfering:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using --session flag
|
||||||
|
veb --session agent1 open https://site-a.com
|
||||||
|
veb --session agent2 open https://site-b.com
|
||||||
|
|
||||||
|
# Using environment variable
|
||||||
|
export VEB_SESSION=agent1
|
||||||
|
veb open https://example.com
|
||||||
|
|
||||||
|
# List all running sessions
|
||||||
|
veb session list
|
||||||
|
|
||||||
|
# Close a specific session
|
||||||
|
veb --session agent1 close
|
||||||
|
```
|
||||||
|
|
||||||
## Selectors
|
## Selectors
|
||||||
|
|
||||||
veb supports all Playwright selectors:
|
veb supports all Playwright selectors:
|
||||||
|
|||||||
+283
@@ -35,6 +35,20 @@ import type {
|
|||||||
StorageClearCommand,
|
StorageClearCommand,
|
||||||
DialogCommand,
|
DialogCommand,
|
||||||
PdfCommand,
|
PdfCommand,
|
||||||
|
RouteCommand,
|
||||||
|
RequestsCommand,
|
||||||
|
DownloadCommand,
|
||||||
|
GeolocationCommand,
|
||||||
|
PermissionsCommand,
|
||||||
|
ViewportCommand,
|
||||||
|
DeviceCommand,
|
||||||
|
GetAttributeCommand,
|
||||||
|
GetTextCommand,
|
||||||
|
IsVisibleCommand,
|
||||||
|
IsEnabledCommand,
|
||||||
|
IsCheckedCommand,
|
||||||
|
CountCommand,
|
||||||
|
BoundingBoxCommand,
|
||||||
NavigateData,
|
NavigateData,
|
||||||
ScreenshotData,
|
ScreenshotData,
|
||||||
EvaluateData,
|
EvaluateData,
|
||||||
@@ -140,6 +154,48 @@ export async function executeCommand(
|
|||||||
return await handleDialog(command, browser);
|
return await handleDialog(command, browser);
|
||||||
case 'pdf':
|
case 'pdf':
|
||||||
return await handlePdf(command, browser);
|
return await handlePdf(command, browser);
|
||||||
|
case 'route':
|
||||||
|
return await handleRoute(command, browser);
|
||||||
|
case 'unroute':
|
||||||
|
return await handleUnroute(command, browser);
|
||||||
|
case 'requests':
|
||||||
|
return await handleRequests(command, browser);
|
||||||
|
case 'download':
|
||||||
|
return await handleDownload(command, browser);
|
||||||
|
case 'geolocation':
|
||||||
|
return await handleGeolocation(command, browser);
|
||||||
|
case 'permissions':
|
||||||
|
return await handlePermissions(command, browser);
|
||||||
|
case 'viewport':
|
||||||
|
return await handleViewport(command, browser);
|
||||||
|
case 'useragent':
|
||||||
|
return await handleUserAgent(command, browser);
|
||||||
|
case 'device':
|
||||||
|
return await handleDevice(command, browser);
|
||||||
|
case 'back':
|
||||||
|
return await handleBack(command, browser);
|
||||||
|
case 'forward':
|
||||||
|
return await handleForward(command, browser);
|
||||||
|
case 'reload':
|
||||||
|
return await handleReload(command, browser);
|
||||||
|
case 'url':
|
||||||
|
return await handleUrl(command, browser);
|
||||||
|
case 'title':
|
||||||
|
return await handleTitle(command, browser);
|
||||||
|
case 'getattribute':
|
||||||
|
return await handleGetAttribute(command, browser);
|
||||||
|
case 'gettext':
|
||||||
|
return await handleGetText(command, browser);
|
||||||
|
case 'isvisible':
|
||||||
|
return await handleIsVisible(command, browser);
|
||||||
|
case 'isenabled':
|
||||||
|
return await handleIsEnabled(command, browser);
|
||||||
|
case 'ischecked':
|
||||||
|
return await handleIsChecked(command, browser);
|
||||||
|
case 'count':
|
||||||
|
return await handleCount(command, browser);
|
||||||
|
case 'boundingbox':
|
||||||
|
return await handleBoundingBox(command, browser);
|
||||||
default: {
|
default: {
|
||||||
// TypeScript narrows to never here, but we handle it for safety
|
// TypeScript narrows to never here, but we handle it for safety
|
||||||
const unknownCommand = command as { id: string; action: string };
|
const unknownCommand = command as { id: string; action: string };
|
||||||
@@ -697,3 +753,230 @@ async function handlePdf(
|
|||||||
});
|
});
|
||||||
return successResponse(command.id, { path: command.path });
|
return successResponse(command.id, { path: command.path });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Network & Request handlers
|
||||||
|
|
||||||
|
async function handleRoute(
|
||||||
|
command: RouteCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
await browser.addRoute(command.url, {
|
||||||
|
response: command.response,
|
||||||
|
abort: command.abort,
|
||||||
|
});
|
||||||
|
return successResponse(command.id, { routed: command.url });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUnroute(
|
||||||
|
command: Command & { action: 'unroute'; url?: string },
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
await browser.removeRoute(command.url);
|
||||||
|
return successResponse(command.id, { unrouted: command.url ?? 'all' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRequests(
|
||||||
|
command: RequestsCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
if (command.clear) {
|
||||||
|
browser.clearRequests();
|
||||||
|
return successResponse(command.id, { cleared: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start tracking if not already
|
||||||
|
browser.startRequestTracking();
|
||||||
|
|
||||||
|
const requests = browser.getRequests(command.filter);
|
||||||
|
return successResponse(command.id, { requests });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDownload(
|
||||||
|
command: DownloadCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
|
||||||
|
const [download] = await Promise.all([
|
||||||
|
page.waitForEvent('download'),
|
||||||
|
page.click(command.selector),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await download.saveAs(command.path);
|
||||||
|
return successResponse(command.id, {
|
||||||
|
path: command.path,
|
||||||
|
suggestedFilename: download.suggestedFilename(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGeolocation(
|
||||||
|
command: GeolocationCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
await browser.setGeolocation(command.latitude, command.longitude, command.accuracy);
|
||||||
|
return successResponse(command.id, {
|
||||||
|
latitude: command.latitude,
|
||||||
|
longitude: command.longitude,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePermissions(
|
||||||
|
command: PermissionsCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
await browser.setPermissions(command.permissions, command.grant);
|
||||||
|
return successResponse(command.id, {
|
||||||
|
permissions: command.permissions,
|
||||||
|
granted: command.grant,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleViewport(
|
||||||
|
command: ViewportCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
await browser.setViewport(command.width, command.height);
|
||||||
|
return successResponse(command.id, {
|
||||||
|
width: command.width,
|
||||||
|
height: command.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUserAgent(
|
||||||
|
command: Command & { action: 'useragent'; userAgent: string },
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
const context = page.context();
|
||||||
|
// Note: Can't change user agent after context is created, but we can for new pages
|
||||||
|
return successResponse(command.id, {
|
||||||
|
note: 'User agent can only be set at launch time. Use device command instead.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDevice(
|
||||||
|
command: DeviceCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const device = browser.getDevice(command.device);
|
||||||
|
if (!device) {
|
||||||
|
const available = browser.listDevices().slice(0, 10).join(', ');
|
||||||
|
throw new Error(`Unknown device: ${command.device}. Available: ${available}...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply device viewport
|
||||||
|
await browser.setViewport(device.viewport.width, device.viewport.height);
|
||||||
|
|
||||||
|
return successResponse(command.id, {
|
||||||
|
device: command.device,
|
||||||
|
viewport: device.viewport,
|
||||||
|
userAgent: device.userAgent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleBack(
|
||||||
|
command: Command & { action: 'back' },
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
await page.goBack();
|
||||||
|
return successResponse(command.id, { url: page.url() });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleForward(
|
||||||
|
command: Command & { action: 'forward' },
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
await page.goForward();
|
||||||
|
return successResponse(command.id, { url: page.url() });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleReload(
|
||||||
|
command: Command & { action: 'reload' },
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
await page.reload();
|
||||||
|
return successResponse(command.id, { url: page.url() });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUrl(
|
||||||
|
command: Command & { action: 'url' },
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
return successResponse(command.id, { url: page.url() });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTitle(
|
||||||
|
command: Command & { action: 'title' },
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
const title = await page.title();
|
||||||
|
return successResponse(command.id, { title });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGetAttribute(
|
||||||
|
command: GetAttributeCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
const value = await page.getAttribute(command.selector, command.attribute);
|
||||||
|
return successResponse(command.id, { attribute: command.attribute, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGetText(
|
||||||
|
command: GetTextCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
const text = await page.textContent(command.selector);
|
||||||
|
return successResponse(command.id, { text });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleIsVisible(
|
||||||
|
command: IsVisibleCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
const visible = await page.isVisible(command.selector);
|
||||||
|
return successResponse(command.id, { visible });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleIsEnabled(
|
||||||
|
command: IsEnabledCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
const enabled = await page.isEnabled(command.selector);
|
||||||
|
return successResponse(command.id, { enabled });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleIsChecked(
|
||||||
|
command: IsCheckedCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
const checked = await page.isChecked(command.selector);
|
||||||
|
return successResponse(command.id, { checked });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCount(
|
||||||
|
command: CountCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
const count = await page.locator(command.selector).count();
|
||||||
|
return successResponse(command.id, { count });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleBoundingBox(
|
||||||
|
command: BoundingBoxCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
const box = await page.locator(command.selector).boundingBox();
|
||||||
|
return successResponse(command.id, { box });
|
||||||
|
}
|
||||||
|
|||||||
+142
-1
@@ -1,6 +1,14 @@
|
|||||||
import { chromium, firefox, webkit, type Browser, type BrowserContext, type Page, type Frame, type Dialog } from 'playwright';
|
import { chromium, firefox, webkit, devices, type Browser, type BrowserContext, type Page, type Frame, type Dialog, type Request, type Route } from 'playwright';
|
||||||
import type { LaunchCommand } from './types.js';
|
import type { LaunchCommand } from './types.js';
|
||||||
|
|
||||||
|
interface TrackedRequest {
|
||||||
|
url: string;
|
||||||
|
method: string;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
timestamp: number;
|
||||||
|
resourceType: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages the Playwright browser lifecycle with multiple tabs/windows
|
* Manages the Playwright browser lifecycle with multiple tabs/windows
|
||||||
*/
|
*/
|
||||||
@@ -11,6 +19,8 @@ export class BrowserManager {
|
|||||||
private activePageIndex: number = 0;
|
private activePageIndex: number = 0;
|
||||||
private activeFrame: Frame | null = null;
|
private activeFrame: Frame | null = null;
|
||||||
private dialogHandler: ((dialog: Dialog) => Promise<void>) | null = null;
|
private dialogHandler: ((dialog: Dialog) => Promise<void>) | null = null;
|
||||||
|
private trackedRequests: TrackedRequest[] = [];
|
||||||
|
private routes: Map<string, (route: Route) => Promise<void>> = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if browser is launched
|
* Check if browser is launched
|
||||||
@@ -110,6 +120,137 @@ export class BrowserManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start tracking requests
|
||||||
|
*/
|
||||||
|
startRequestTracking(): void {
|
||||||
|
const page = this.getPage();
|
||||||
|
page.on('request', (request: Request) => {
|
||||||
|
this.trackedRequests.push({
|
||||||
|
url: request.url(),
|
||||||
|
method: request.method(),
|
||||||
|
headers: request.headers(),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
resourceType: request.resourceType(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tracked requests
|
||||||
|
*/
|
||||||
|
getRequests(filter?: string): TrackedRequest[] {
|
||||||
|
if (filter) {
|
||||||
|
return this.trackedRequests.filter(r => r.url.includes(filter));
|
||||||
|
}
|
||||||
|
return this.trackedRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear tracked requests
|
||||||
|
*/
|
||||||
|
clearRequests(): void {
|
||||||
|
this.trackedRequests = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a route to intercept requests
|
||||||
|
*/
|
||||||
|
async addRoute(
|
||||||
|
url: string,
|
||||||
|
options: {
|
||||||
|
response?: { status?: number; body?: string; contentType?: string; headers?: Record<string, string> };
|
||||||
|
abort?: boolean;
|
||||||
|
}
|
||||||
|
): Promise<void> {
|
||||||
|
const page = this.getPage();
|
||||||
|
|
||||||
|
const handler = async (route: Route) => {
|
||||||
|
if (options.abort) {
|
||||||
|
await route.abort();
|
||||||
|
} else if (options.response) {
|
||||||
|
await route.fulfill({
|
||||||
|
status: options.response.status ?? 200,
|
||||||
|
body: options.response.body ?? '',
|
||||||
|
contentType: options.response.contentType ?? 'text/plain',
|
||||||
|
headers: options.response.headers,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await route.continue();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.routes.set(url, handler);
|
||||||
|
await page.route(url, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a route
|
||||||
|
*/
|
||||||
|
async removeRoute(url?: string): Promise<void> {
|
||||||
|
const page = this.getPage();
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
const handler = this.routes.get(url);
|
||||||
|
if (handler) {
|
||||||
|
await page.unroute(url, handler);
|
||||||
|
this.routes.delete(url);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove all routes
|
||||||
|
for (const [routeUrl, handler] of this.routes) {
|
||||||
|
await page.unroute(routeUrl, handler);
|
||||||
|
}
|
||||||
|
this.routes.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set geolocation
|
||||||
|
*/
|
||||||
|
async setGeolocation(latitude: number, longitude: number, accuracy?: number): Promise<void> {
|
||||||
|
const context = this.contexts[0];
|
||||||
|
if (context) {
|
||||||
|
await context.setGeolocation({ latitude, longitude, accuracy });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set permissions
|
||||||
|
*/
|
||||||
|
async setPermissions(permissions: string[], grant: boolean): Promise<void> {
|
||||||
|
const context = this.contexts[0];
|
||||||
|
if (context) {
|
||||||
|
if (grant) {
|
||||||
|
await context.grantPermissions(permissions);
|
||||||
|
} else {
|
||||||
|
await context.clearPermissions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set viewport
|
||||||
|
*/
|
||||||
|
async setViewport(width: number, height: number): Promise<void> {
|
||||||
|
const page = this.getPage();
|
||||||
|
await page.setViewportSize({ width, height });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get device descriptor
|
||||||
|
*/
|
||||||
|
getDevice(deviceName: string): typeof devices[keyof typeof devices] | undefined {
|
||||||
|
return devices[deviceName as keyof typeof devices];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List available devices
|
||||||
|
*/
|
||||||
|
listDevices(): string[] {
|
||||||
|
return Object.keys(devices);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all pages
|
* Get all pages
|
||||||
*/
|
*/
|
||||||
|
|||||||
+267
@@ -107,6 +107,37 @@ ${c('yellow', 'Dialog Commands:')}
|
|||||||
${c('cyan', 'dialog accept')} [text] Accept next dialog
|
${c('cyan', 'dialog accept')} [text] Accept next dialog
|
||||||
${c('cyan', 'dialog dismiss')} Dismiss next dialog
|
${c('cyan', 'dialog dismiss')} Dismiss next dialog
|
||||||
|
|
||||||
|
${c('yellow', 'Navigation:')}
|
||||||
|
${c('cyan', 'back')} Go back
|
||||||
|
${c('cyan', 'forward')} Go forward
|
||||||
|
${c('cyan', 'reload')} Reload page
|
||||||
|
${c('cyan', 'url')} Get current URL
|
||||||
|
${c('cyan', 'title')} Get page title
|
||||||
|
|
||||||
|
${c('yellow', 'Element Info:')}
|
||||||
|
${c('cyan', 'gettext')} <selector> Get element text
|
||||||
|
${c('cyan', 'getattr')} <selector> <attr> Get attribute value
|
||||||
|
${c('cyan', 'isvisible')} <selector> Check if visible
|
||||||
|
${c('cyan', 'isenabled')} <selector> Check if enabled
|
||||||
|
${c('cyan', 'ischecked')} <selector> Check if checked
|
||||||
|
${c('cyan', 'count')} <selector> Count matching elements
|
||||||
|
${c('cyan', 'boundingbox')} <selector> Get element bounds
|
||||||
|
|
||||||
|
${c('yellow', 'Network:')}
|
||||||
|
${c('cyan', 'route')} <url> [--abort] Intercept requests
|
||||||
|
${c('cyan', 'route')} <url> --body <json> Mock response
|
||||||
|
${c('cyan', 'unroute')} [url] Remove route(s)
|
||||||
|
${c('cyan', 'requests')} [--filter url] Get tracked requests
|
||||||
|
|
||||||
|
${c('yellow', 'Browser Settings:')}
|
||||||
|
${c('cyan', 'viewport')} <width> <height> Set viewport size
|
||||||
|
${c('cyan', 'device')} <name> Emulate device
|
||||||
|
${c('cyan', 'geolocation')} <lat> <lng> Set location
|
||||||
|
${c('cyan', 'permissions')} grant|deny <...> Set permissions
|
||||||
|
|
||||||
|
${c('yellow', 'Downloads:')}
|
||||||
|
${c('cyan', 'download')} <selector> <path> Download file
|
||||||
|
|
||||||
${c('yellow', 'Tab/Window Commands:')}
|
${c('yellow', 'Tab/Window Commands:')}
|
||||||
${c('cyan', 'tab new')} Open a new tab
|
${c('cyan', 'tab new')} Open a new tab
|
||||||
${c('cyan', 'tab list')} List all open tabs
|
${c('cyan', 'tab list')} List all open tabs
|
||||||
@@ -164,6 +195,10 @@ function printResponse(response: Response, jsonMode: boolean): void {
|
|||||||
if (data.url && data.title) {
|
if (data.url && data.title) {
|
||||||
console.log(c('green', '✓'), c('bold', data.title as string));
|
console.log(c('green', '✓'), c('bold', data.title as string));
|
||||||
console.log(c('dim', ` ${data.url}`));
|
console.log(c('dim', ` ${data.url}`));
|
||||||
|
} else if (data.url && !data.title) {
|
||||||
|
console.log(data.url);
|
||||||
|
} else if (data.title && !data.url) {
|
||||||
|
console.log(data.title);
|
||||||
} else if (data.html) {
|
} else if (data.html) {
|
||||||
console.log(data.html);
|
console.log(data.html);
|
||||||
} else if (data.snapshot) {
|
} else if (data.snapshot) {
|
||||||
@@ -210,6 +245,46 @@ function printResponse(response: Response, jsonMode: boolean): void {
|
|||||||
console.log(c('green', '✓'), `Uploaded ${files.length} file(s)`);
|
console.log(c('green', '✓'), `Uploaded ${files.length} file(s)`);
|
||||||
} else if (data.handler) {
|
} else if (data.handler) {
|
||||||
console.log(c('green', '✓'), `Dialog handler set to ${data.response}`);
|
console.log(c('green', '✓'), `Dialog handler set to ${data.response}`);
|
||||||
|
} else if (data.text !== undefined) {
|
||||||
|
console.log(data.text ?? c('dim', 'null'));
|
||||||
|
} else if (data.attribute !== undefined) {
|
||||||
|
console.log(data.value ?? c('dim', 'null'));
|
||||||
|
} else if (data.visible !== undefined) {
|
||||||
|
console.log(data.visible ? c('green', 'true') : c('red', 'false'));
|
||||||
|
} else if (data.enabled !== undefined) {
|
||||||
|
console.log(data.enabled ? c('green', 'true') : c('red', 'false'));
|
||||||
|
} else if (data.checked !== undefined) {
|
||||||
|
console.log(data.checked ? c('green', 'true') : c('red', 'false'));
|
||||||
|
} else if (data.count !== undefined) {
|
||||||
|
console.log(data.count);
|
||||||
|
} else if (data.box) {
|
||||||
|
const box = data.box as { x: number; y: number; width: number; height: number };
|
||||||
|
console.log(`x: ${box.x}, y: ${box.y}, width: ${box.width}, height: ${box.height}`);
|
||||||
|
} else if (data.requests) {
|
||||||
|
const reqs = data.requests as Array<{ url: string; method: string; resourceType: string }>;
|
||||||
|
if (reqs.length === 0) {
|
||||||
|
console.log(c('dim', 'No requests tracked'));
|
||||||
|
} else {
|
||||||
|
reqs.forEach(req => {
|
||||||
|
console.log(`${c('cyan', req.method)} ${req.url}`);
|
||||||
|
console.log(c('dim', ` ${req.resourceType}`));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (data.routed) {
|
||||||
|
console.log(c('green', '✓'), `Route added: ${data.routed}`);
|
||||||
|
} else if (data.unrouted) {
|
||||||
|
console.log(c('green', '✓'), `Route removed: ${data.unrouted}`);
|
||||||
|
} else if (data.device) {
|
||||||
|
const vp = data.viewport as { width: number; height: number };
|
||||||
|
console.log(c('green', '✓'), `Emulating ${data.device}`);
|
||||||
|
console.log(c('dim', ` Viewport: ${vp.width}x${vp.height}`));
|
||||||
|
} else if (data.latitude !== undefined) {
|
||||||
|
console.log(c('green', '✓'), `Location set to ${data.latitude}, ${data.longitude}`);
|
||||||
|
} else if (data.width !== undefined && data.height !== undefined) {
|
||||||
|
console.log(c('green', '✓'), `Viewport: ${data.width}x${data.height}`);
|
||||||
|
} else if (data.suggestedFilename) {
|
||||||
|
console.log(c('green', '✓'), `Downloaded: ${data.suggestedFilename}`);
|
||||||
|
console.log(c('dim', ` Saved to: ${data.path}`));
|
||||||
} else if (data.clicked || data.typed || data.pressed || data.hovered || data.scrolled || data.selected || data.waited || data.filled || data.checked || data.unchecked || data.focused || data.dragged || data.switched || data.set || data.cleared) {
|
} else if (data.clicked || data.typed || data.pressed || data.hovered || data.scrolled || data.selected || data.waited || data.filled || data.checked || data.unchecked || data.focused || data.dragged || data.switched || data.set || data.cleared) {
|
||||||
console.log(c('green', '✓'), 'Done');
|
console.log(c('green', '✓'), 'Done');
|
||||||
} else if (data.launched) {
|
} else if (data.launched) {
|
||||||
@@ -637,6 +712,198 @@ async function main(): Promise<void> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'back': {
|
||||||
|
cmd = { id, action: 'back' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'forward': {
|
||||||
|
cmd = { id, action: 'forward' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'reload': {
|
||||||
|
cmd = { id, action: 'reload' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'url': {
|
||||||
|
cmd = { id, action: 'url' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'title': {
|
||||||
|
cmd = { id, action: 'title' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'gettext': {
|
||||||
|
const selector = cleanArgs[1];
|
||||||
|
if (!selector) {
|
||||||
|
console.error(c('red', 'Error:'), 'Selector required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'gettext', selector };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'getattr':
|
||||||
|
case 'getattribute': {
|
||||||
|
const selector = cleanArgs[1];
|
||||||
|
const attribute = cleanArgs[2];
|
||||||
|
if (!selector || !attribute) {
|
||||||
|
console.error(c('red', 'Error:'), 'Selector and attribute required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'getattribute', selector, attribute };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'isvisible': {
|
||||||
|
const selector = cleanArgs[1];
|
||||||
|
if (!selector) {
|
||||||
|
console.error(c('red', 'Error:'), 'Selector required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'isvisible', selector };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'isenabled': {
|
||||||
|
const selector = cleanArgs[1];
|
||||||
|
if (!selector) {
|
||||||
|
console.error(c('red', 'Error:'), 'Selector required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'isenabled', selector };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ischecked': {
|
||||||
|
const selector = cleanArgs[1];
|
||||||
|
if (!selector) {
|
||||||
|
console.error(c('red', 'Error:'), 'Selector required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'ischecked', selector };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'count': {
|
||||||
|
const selector = cleanArgs[1];
|
||||||
|
if (!selector) {
|
||||||
|
console.error(c('red', 'Error:'), 'Selector required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'count', selector };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'boundingbox':
|
||||||
|
case 'bbox': {
|
||||||
|
const selector = cleanArgs[1];
|
||||||
|
if (!selector) {
|
||||||
|
console.error(c('red', 'Error:'), 'Selector required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'boundingbox', selector };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'route': {
|
||||||
|
const url = cleanArgs[1];
|
||||||
|
if (!url) {
|
||||||
|
console.error(c('red', 'Error:'), 'URL pattern required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const abortMode = args.includes('--abort');
|
||||||
|
const bodyIdx = args.findIndex(a => a === '--body');
|
||||||
|
let response: { status?: number; body?: string; contentType?: string } | undefined;
|
||||||
|
|
||||||
|
if (bodyIdx !== -1 && args[bodyIdx + 1]) {
|
||||||
|
try {
|
||||||
|
const body = args[bodyIdx + 1];
|
||||||
|
response = { body, contentType: 'application/json' };
|
||||||
|
} catch {
|
||||||
|
response = { body: args[bodyIdx + 1] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = { id, action: 'route', url, abort: abortMode, response };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'unroute': {
|
||||||
|
const url = cleanArgs[1];
|
||||||
|
cmd = { id, action: 'unroute', url };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'requests': {
|
||||||
|
const clearMode = args.includes('--clear');
|
||||||
|
const filterIdx = args.findIndex(a => a === '--filter');
|
||||||
|
const filter = filterIdx !== -1 ? args[filterIdx + 1] : undefined;
|
||||||
|
|
||||||
|
cmd = { id, action: 'requests', clear: clearMode, filter };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'viewport': {
|
||||||
|
const width = parseInt(cleanArgs[1], 10);
|
||||||
|
const height = parseInt(cleanArgs[2], 10);
|
||||||
|
if (isNaN(width) || isNaN(height)) {
|
||||||
|
console.error(c('red', 'Error:'), 'Width and height required (e.g., veb viewport 1920 1080)');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'viewport', width, height };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'device': {
|
||||||
|
const device = cleanArgs[1];
|
||||||
|
if (!device) {
|
||||||
|
console.error(c('red', 'Error:'), 'Device name required (e.g., veb device "iPhone 14")');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'device', device };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'geolocation':
|
||||||
|
case 'geo': {
|
||||||
|
const lat = parseFloat(cleanArgs[1]);
|
||||||
|
const lng = parseFloat(cleanArgs[2]);
|
||||||
|
if (isNaN(lat) || isNaN(lng)) {
|
||||||
|
console.error(c('red', 'Error:'), 'Latitude and longitude required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'geolocation', latitude: lat, longitude: lng };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'permissions': {
|
||||||
|
const grantOrDeny = cleanArgs[1];
|
||||||
|
const perms = cleanArgs.slice(2);
|
||||||
|
if ((grantOrDeny !== 'grant' && grantOrDeny !== 'deny') || perms.length === 0) {
|
||||||
|
console.error(c('red', 'Error:'), 'Usage: veb permissions grant|deny <permission...>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'permissions', permissions: perms, grant: grantOrDeny === 'grant' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'download': {
|
||||||
|
const selector = cleanArgs[1];
|
||||||
|
const downloadPath = cleanArgs[2];
|
||||||
|
if (!selector || !downloadPath) {
|
||||||
|
console.error(c('red', 'Error:'), 'Selector and path required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'download', selector, path: downloadPath };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'close':
|
case 'close':
|
||||||
case 'quit':
|
case 'quit':
|
||||||
case 'exit': {
|
case 'exit': {
|
||||||
|
|||||||
+135
@@ -172,6 +172,120 @@ const pdfSchema = baseCommandSchema.extend({
|
|||||||
format: z.enum(['Letter', 'Legal', 'Tabloid', 'Ledger', 'A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6']).optional(),
|
format: z.enum(['Letter', 'Legal', 'Tabloid', 'Ledger', 'A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6']).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const routeSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('route'),
|
||||||
|
url: z.string().min(1),
|
||||||
|
response: z.object({
|
||||||
|
status: z.number().optional(),
|
||||||
|
body: z.string().optional(),
|
||||||
|
contentType: z.string().optional(),
|
||||||
|
headers: z.record(z.string()).optional(),
|
||||||
|
}).optional(),
|
||||||
|
abort: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const unrouteSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('unroute'),
|
||||||
|
url: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestsSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('requests'),
|
||||||
|
filter: z.string().optional(),
|
||||||
|
clear: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('download'),
|
||||||
|
selector: z.string().min(1),
|
||||||
|
path: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const geolocationSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('geolocation'),
|
||||||
|
latitude: z.number(),
|
||||||
|
longitude: z.number(),
|
||||||
|
accuracy: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const permissionsSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('permissions'),
|
||||||
|
permissions: z.array(z.string()),
|
||||||
|
grant: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const viewportSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('viewport'),
|
||||||
|
width: z.number().positive(),
|
||||||
|
height: z.number().positive(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const userAgentSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('useragent'),
|
||||||
|
userAgent: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const deviceSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('device'),
|
||||||
|
device: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const backSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('back'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const forwardSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('forward'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const reloadSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('reload'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const urlSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('url'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const titleSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('title'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getAttributeSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('getattribute'),
|
||||||
|
selector: z.string().min(1),
|
||||||
|
attribute: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getTextSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('gettext'),
|
||||||
|
selector: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const isVisibleSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('isvisible'),
|
||||||
|
selector: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const isEnabledSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('isenabled'),
|
||||||
|
selector: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const isCheckedSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('ischecked'),
|
||||||
|
selector: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const countSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('count'),
|
||||||
|
selector: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const boundingBoxSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('boundingbox'),
|
||||||
|
selector: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
const pressSchema = baseCommandSchema.extend({
|
const pressSchema = baseCommandSchema.extend({
|
||||||
action: z.literal('press'),
|
action: z.literal('press'),
|
||||||
key: z.string().min(1),
|
key: z.string().min(1),
|
||||||
@@ -302,6 +416,27 @@ const commandSchema = z.discriminatedUnion('action', [
|
|||||||
storageClearSchema,
|
storageClearSchema,
|
||||||
dialogSchema,
|
dialogSchema,
|
||||||
pdfSchema,
|
pdfSchema,
|
||||||
|
routeSchema,
|
||||||
|
unrouteSchema,
|
||||||
|
requestsSchema,
|
||||||
|
downloadSchema,
|
||||||
|
geolocationSchema,
|
||||||
|
permissionsSchema,
|
||||||
|
viewportSchema,
|
||||||
|
userAgentSchema,
|
||||||
|
deviceSchema,
|
||||||
|
backSchema,
|
||||||
|
forwardSchema,
|
||||||
|
reloadSchema,
|
||||||
|
urlSchema,
|
||||||
|
titleSchema,
|
||||||
|
getAttributeSchema,
|
||||||
|
getTextSchema,
|
||||||
|
isVisibleSchema,
|
||||||
|
isEnabledSchema,
|
||||||
|
isCheckedSchema,
|
||||||
|
countSchema,
|
||||||
|
boundingBoxSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Parse result type
|
// Parse result type
|
||||||
|
|||||||
+148
-1
@@ -168,6 +168,132 @@ export interface PdfCommand extends BaseCommand {
|
|||||||
format?: 'Letter' | 'Legal' | 'Tabloid' | 'Ledger' | 'A0' | 'A1' | 'A2' | 'A3' | 'A4' | 'A5' | 'A6';
|
format?: 'Letter' | 'Legal' | 'Tabloid' | 'Ledger' | 'A0' | 'A1' | 'A2' | 'A3' | 'A4' | 'A5' | 'A6';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Network interception
|
||||||
|
export interface RouteCommand extends BaseCommand {
|
||||||
|
action: 'route';
|
||||||
|
url: string;
|
||||||
|
response?: {
|
||||||
|
status?: number;
|
||||||
|
body?: string;
|
||||||
|
contentType?: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
};
|
||||||
|
abort?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnrouteCommand extends BaseCommand {
|
||||||
|
action: 'unroute';
|
||||||
|
url?: string; // If not provided, remove all routes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request inspection
|
||||||
|
export interface RequestsCommand extends BaseCommand {
|
||||||
|
action: 'requests';
|
||||||
|
filter?: string; // URL pattern to filter
|
||||||
|
clear?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download handling
|
||||||
|
export interface DownloadCommand extends BaseCommand {
|
||||||
|
action: 'download';
|
||||||
|
selector: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Geolocation
|
||||||
|
export interface GeolocationCommand extends BaseCommand {
|
||||||
|
action: 'geolocation';
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
accuracy?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
export interface PermissionsCommand extends BaseCommand {
|
||||||
|
action: 'permissions';
|
||||||
|
permissions: string[];
|
||||||
|
grant: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Viewport
|
||||||
|
export interface ViewportCommand extends BaseCommand {
|
||||||
|
action: 'viewport';
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// User agent
|
||||||
|
export interface UserAgentCommand extends BaseCommand {
|
||||||
|
action: 'useragent';
|
||||||
|
userAgent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emulate device
|
||||||
|
export interface DeviceCommand extends BaseCommand {
|
||||||
|
action: 'device';
|
||||||
|
device: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go back/forward
|
||||||
|
export interface BackCommand extends BaseCommand {
|
||||||
|
action: 'back';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ForwardCommand extends BaseCommand {
|
||||||
|
action: 'forward';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReloadCommand extends BaseCommand {
|
||||||
|
action: 'reload';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get URL/Title
|
||||||
|
export interface UrlCommand extends BaseCommand {
|
||||||
|
action: 'url';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TitleCommand extends BaseCommand {
|
||||||
|
action: 'title';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attribute/Property/Text
|
||||||
|
export interface GetAttributeCommand extends BaseCommand {
|
||||||
|
action: 'getattribute';
|
||||||
|
selector: string;
|
||||||
|
attribute: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetTextCommand extends BaseCommand {
|
||||||
|
action: 'gettext';
|
||||||
|
selector: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IsVisibleCommand extends BaseCommand {
|
||||||
|
action: 'isvisible';
|
||||||
|
selector: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IsEnabledCommand extends BaseCommand {
|
||||||
|
action: 'isenabled';
|
||||||
|
selector: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IsCheckedCommand extends BaseCommand {
|
||||||
|
action: 'ischecked';
|
||||||
|
selector: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CountCommand extends BaseCommand {
|
||||||
|
action: 'count';
|
||||||
|
selector: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounding box
|
||||||
|
export interface BoundingBoxCommand extends BaseCommand {
|
||||||
|
action: 'boundingbox';
|
||||||
|
selector: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PressCommand extends BaseCommand {
|
export interface PressCommand extends BaseCommand {
|
||||||
action: 'press';
|
action: 'press';
|
||||||
key: string;
|
key: string;
|
||||||
@@ -294,7 +420,28 @@ export type Command =
|
|||||||
| StorageSetCommand
|
| StorageSetCommand
|
||||||
| StorageClearCommand
|
| StorageClearCommand
|
||||||
| DialogCommand
|
| DialogCommand
|
||||||
| PdfCommand;
|
| PdfCommand
|
||||||
|
| RouteCommand
|
||||||
|
| UnrouteCommand
|
||||||
|
| RequestsCommand
|
||||||
|
| DownloadCommand
|
||||||
|
| GeolocationCommand
|
||||||
|
| PermissionsCommand
|
||||||
|
| ViewportCommand
|
||||||
|
| UserAgentCommand
|
||||||
|
| DeviceCommand
|
||||||
|
| BackCommand
|
||||||
|
| ForwardCommand
|
||||||
|
| ReloadCommand
|
||||||
|
| UrlCommand
|
||||||
|
| TitleCommand
|
||||||
|
| GetAttributeCommand
|
||||||
|
| GetTextCommand
|
||||||
|
| IsVisibleCommand
|
||||||
|
| IsEnabledCommand
|
||||||
|
| IsCheckedCommand
|
||||||
|
| CountCommand
|
||||||
|
| BoundingBoxCommand;
|
||||||
|
|
||||||
// Response types
|
// Response types
|
||||||
export interface SuccessResponse<T = unknown> {
|
export interface SuccessResponse<T = unknown> {
|
||||||
|
|||||||
Reference in New Issue
Block a user