more commands
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# veb
|
# veb
|
||||||
|
|
||||||
Headless browser automation CLI for agents and humans.
|
Headless browser automation CLI for agents and humans. Near-complete Playwright parity.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -13,70 +13,99 @@ pnpm build
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Open a URL (auto-starts browser daemon)
|
# Navigation
|
||||||
veb open https://example.com
|
veb open https://example.com
|
||||||
|
|
||||||
# Click elements
|
# Clicking
|
||||||
veb click "#submit-btn"
|
veb click "#submit-btn"
|
||||||
veb click "text=Sign In"
|
veb dblclick "#item"
|
||||||
|
|
||||||
# Type into inputs
|
# Form input
|
||||||
veb type "#email" hello@example.com
|
veb type "#search" "query" # Type character by character
|
||||||
veb type "#search" "search query"
|
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
|
||||||
|
|
||||||
# Press keyboard keys
|
# Keyboard
|
||||||
veb press Enter
|
veb press Enter
|
||||||
veb press Tab
|
veb press Tab
|
||||||
|
|
||||||
# Wait for things
|
# Mouse
|
||||||
veb wait "#loading" # wait for selector
|
veb hover "#menu"
|
||||||
veb wait --text "Welcome" # wait for text
|
veb focus "#input"
|
||||||
veb wait 2000 # wait 2 seconds
|
veb drag "#source" "#target"
|
||||||
|
|
||||||
# Take screenshots
|
# File upload
|
||||||
|
veb upload "#file-input" ./document.pdf
|
||||||
|
veb upload "#files" ./a.png ./b.png
|
||||||
|
|
||||||
|
# Waiting
|
||||||
|
veb wait "#loading" # Wait for selector
|
||||||
|
veb wait --text "Welcome" # Wait for text
|
||||||
|
veb wait 2000 # Wait 2 seconds
|
||||||
|
|
||||||
|
# Screenshots & PDF
|
||||||
veb screenshot page.png
|
veb screenshot page.png
|
||||||
veb screenshot --full page.png # full page
|
veb screenshot --full page.png # Full page
|
||||||
veb screenshot -s "#hero" # specific element
|
veb screenshot -s "#hero" # Specific element
|
||||||
|
veb pdf report.pdf
|
||||||
|
|
||||||
# Get accessibility snapshot (great for AI agents)
|
# Content extraction
|
||||||
veb snapshot
|
veb snapshot # Accessibility tree (best for agents)
|
||||||
|
veb extract "#main" # Get HTML
|
||||||
|
veb eval "document.title" # Run JavaScript
|
||||||
|
|
||||||
# Extract HTML content
|
# Scrolling
|
||||||
veb extract "table"
|
|
||||||
veb extract "#main"
|
|
||||||
|
|
||||||
# Evaluate JavaScript
|
|
||||||
veb eval "document.title"
|
|
||||||
veb eval "window.location.href"
|
|
||||||
|
|
||||||
# Scroll the page
|
|
||||||
veb scroll down 500
|
veb scroll down 500
|
||||||
veb scroll up
|
veb scroll up
|
||||||
veb scroll -s "#container" down
|
veb scroll -s "#container" down
|
||||||
|
|
||||||
# Interact with dropdowns
|
# Semantic locators (Playwright's recommended approach)
|
||||||
veb select "#country" "US"
|
veb role button click --name "Submit"
|
||||||
|
veb role textbox fill "hello" --name "Email"
|
||||||
|
veb text "Sign In" click
|
||||||
|
veb text "Submit" click --exact
|
||||||
|
veb label "Email" fill "test@test.com"
|
||||||
|
veb placeholder "Search..." fill "query"
|
||||||
|
|
||||||
# Hover over elements
|
# Frames/iframes
|
||||||
veb hover "#menu"
|
veb frame "#iframe" # Switch to iframe
|
||||||
|
veb mainframe # Switch back to main
|
||||||
|
|
||||||
# Tab management
|
# Cookies
|
||||||
veb tab new # Open new tab
|
veb cookies # Get all cookies
|
||||||
veb tab list # List all tabs
|
veb cookies set '[{"name":"session","value":"abc123","domain":".example.com"}]'
|
||||||
veb tab 0 # Switch to tab 0
|
veb cookies clear
|
||||||
veb tab close # Close current tab
|
|
||||||
veb tab close 1 # Close tab 1
|
|
||||||
|
|
||||||
# Window management
|
# Storage
|
||||||
veb window new # Open new window
|
veb storage local # Get all localStorage
|
||||||
|
veb storage local myKey # Get specific key
|
||||||
|
veb storage local set key value # Set value
|
||||||
|
veb storage local clear # Clear localStorage
|
||||||
|
veb storage session # sessionStorage (same commands)
|
||||||
|
|
||||||
# Session management (isolate multiple agents)
|
# Dialogs (alerts, confirms, prompts)
|
||||||
|
veb dialog accept # Accept next dialog
|
||||||
|
veb dialog accept "input text" # Accept prompt with text
|
||||||
|
veb dialog dismiss # Dismiss next dialog
|
||||||
|
|
||||||
|
# Tabs
|
||||||
|
veb tab new
|
||||||
|
veb tab list
|
||||||
|
veb tab 0 # Switch to tab
|
||||||
|
veb tab close
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
veb window new
|
||||||
|
|
||||||
|
# Sessions (isolate multiple agents)
|
||||||
veb --session agent1 open example.com
|
veb --session agent1 open example.com
|
||||||
veb --session agent2 open google.com
|
veb --session agent2 open google.com
|
||||||
veb session list # List active sessions
|
VEB_SESSION=agent1 veb click "#btn"
|
||||||
VEB_SESSION=agent1 veb eval "document.title"
|
veb session list
|
||||||
|
|
||||||
# Close browser (stops daemon)
|
# Close browser
|
||||||
veb close
|
veb close
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -85,85 +114,111 @@ veb close
|
|||||||
Use `--json` flag for machine-readable output:
|
Use `--json` flag for machine-readable output:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
veb open https://example.com --json
|
|
||||||
# {"id":"abc123","success":true,"data":{"url":"https://example.com/","title":"Example Domain"}}
|
|
||||||
|
|
||||||
veb snapshot --json
|
veb snapshot --json
|
||||||
# {"id":"def456","success":true,"data":{"snapshot":"..."}}
|
veb eval "document.title" --json
|
||||||
```
|
```
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
veb runs a background daemon that keeps the browser open between commands. The first command automatically starts the daemon. Use `veb close` to shut it down.
|
|
||||||
|
|
||||||
## Commands Reference
|
## Commands Reference
|
||||||
|
|
||||||
|
### Navigation & Interaction
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `open <url>` | Navigate to a URL |
|
| `open <url>` | Navigate to URL |
|
||||||
| `click <selector>` | Click an element |
|
| `click <selector>` | Click element |
|
||||||
| `type <selector> <text>` | Type text into an element |
|
| `dblclick <selector>` | Double-click |
|
||||||
| `press <key>` | Press a keyboard key |
|
| `type <selector> <text>` | Type text |
|
||||||
| `wait <selector\|text\|ms>` | Wait for condition |
|
| `fill <selector> <value>` | Clear & fill |
|
||||||
| `screenshot [path]` | Take a screenshot |
|
| `press <key>` | Press key |
|
||||||
| `snapshot` | Get accessibility tree |
|
| `check <selector>` | Check checkbox |
|
||||||
| `extract <selector>` | Get element HTML |
|
| `uncheck <selector>` | Uncheck |
|
||||||
|
| `select <selector> <value>` | Select option |
|
||||||
|
| `hover <selector>` | Hover |
|
||||||
|
| `focus <selector>` | Focus |
|
||||||
|
| `drag <src> <target>` | Drag & drop |
|
||||||
|
| `upload <selector> <files>` | Upload files |
|
||||||
|
| `scroll <dir> [amount]` | Scroll |
|
||||||
|
|
||||||
|
### Semantic Locators
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `role <role> <action>` | By ARIA role |
|
||||||
|
| `text <text> <action>` | By text |
|
||||||
|
| `label <label> <action>` | By label |
|
||||||
|
| `placeholder <ph> <action>` | By placeholder |
|
||||||
|
|
||||||
|
### Content & Screenshots
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `screenshot [path]` | Screenshot |
|
||||||
|
| `pdf <path>` | Save as PDF |
|
||||||
|
| `snapshot` | Accessibility tree |
|
||||||
|
| `extract <selector>` | Get HTML |
|
||||||
| `eval <script>` | Run JavaScript |
|
| `eval <script>` | Run JavaScript |
|
||||||
| `scroll <dir> [amount]` | Scroll page |
|
|
||||||
| `hover <selector>` | Hover over element |
|
### Browser State
|
||||||
| `select <selector> <val>` | Select dropdown option |
|
| Command | Description |
|
||||||
| `tab new` | Open new tab |
|
|---------|-------------|
|
||||||
| `tab list` | List all tabs |
|
| `cookies` | Get cookies |
|
||||||
| `tab <index>` | Switch to tab |
|
| `cookies set <json>` | Set cookies |
|
||||||
|
| `cookies clear` | Clear cookies |
|
||||||
|
| `storage local [key]` | Get localStorage |
|
||||||
|
| `storage local set <k> <v>` | Set localStorage |
|
||||||
|
| `storage local clear` | Clear localStorage |
|
||||||
|
| `dialog accept [text]` | Accept dialog |
|
||||||
|
| `dialog dismiss` | Dismiss dialog |
|
||||||
|
|
||||||
|
### Frames & Tabs
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `frame <selector>` | Switch to frame |
|
||||||
|
| `mainframe` | Back to main |
|
||||||
|
| `tab new` | New tab |
|
||||||
|
| `tab list` | List tabs |
|
||||||
|
| `tab <index>` | Switch tab |
|
||||||
| `tab close [index]` | Close tab |
|
| `tab close [index]` | Close tab |
|
||||||
| `window new` | Open new window |
|
| `window new` | New window |
|
||||||
| `session` | Show current session |
|
|
||||||
| `session list` | List active sessions |
|
### Session & Control
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `wait <sel\|text\|ms>` | Wait for condition |
|
||||||
|
| `session` | Show session |
|
||||||
|
| `session list` | List sessions |
|
||||||
| `close` | Close browser |
|
| `close` | Close browser |
|
||||||
|
|
||||||
## Sessions
|
|
||||||
|
|
||||||
Sessions allow multiple agents to use veb simultaneously without interfering with each other. Each session runs its own isolated browser instance.
|
|
||||||
|
|
||||||
```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
|
|
||||||
veb click "#button"
|
|
||||||
|
|
||||||
# List all running sessions
|
|
||||||
veb session list
|
|
||||||
|
|
||||||
# Close a specific session
|
|
||||||
veb --session agent1 close
|
|
||||||
```
|
|
||||||
|
|
||||||
Sessions are identified by name. If no session is specified, the "default" session is used.
|
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `--session <name>` | Use isolated browser session |
|
| `--session <name>` | Use isolated session |
|
||||||
| `--json` | Output raw JSON |
|
| `--json` | JSON output |
|
||||||
| `--full, -f` | Full page screenshot |
|
| `--full, -f` | Full page screenshot |
|
||||||
| `--text, -t` | Wait for text |
|
|
||||||
| `--selector, -s` | Target element |
|
| `--selector, -s` | Target element |
|
||||||
| `--debug` | Show debug timing info |
|
| `--name, -n` | Locator name filter |
|
||||||
| `--help, -h` | Show help |
|
| `--exact` | Exact text match |
|
||||||
|
| `--text, -t` | Wait for text |
|
||||||
|
| `--debug` | Debug output |
|
||||||
|
|
||||||
## Selectors
|
## Selectors
|
||||||
|
|
||||||
veb supports all Playwright selectors:
|
veb supports all Playwright selectors:
|
||||||
|
|
||||||
- CSS: `#id`, `.class`, `div.container`
|
```bash
|
||||||
- Text: `text=Click me`, `"Click me"`
|
# CSS
|
||||||
- XPath: `xpath=//button`
|
veb click "#id"
|
||||||
- Role: `role=button[name="Submit"]`
|
veb click ".class"
|
||||||
|
veb click "div.container > button"
|
||||||
|
|
||||||
|
# Text
|
||||||
|
veb click "text=Click me"
|
||||||
|
|
||||||
|
# XPath
|
||||||
|
veb click "xpath=//button[@type='submit']"
|
||||||
|
|
||||||
|
# Semantic (recommended)
|
||||||
|
veb role button click --name "Submit"
|
||||||
|
veb label "Email" fill "test@test.com"
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+326
-1
@@ -1,4 +1,4 @@
|
|||||||
import type { Page } from 'playwright';
|
import type { Page, Frame } from 'playwright';
|
||||||
import type { BrowserManager } from './browser.js';
|
import type { BrowserManager } from './browser.js';
|
||||||
import type {
|
import type {
|
||||||
Command,
|
Command,
|
||||||
@@ -6,6 +6,18 @@ import type {
|
|||||||
NavigateCommand,
|
NavigateCommand,
|
||||||
ClickCommand,
|
ClickCommand,
|
||||||
TypeCommand,
|
TypeCommand,
|
||||||
|
FillCommand,
|
||||||
|
CheckCommand,
|
||||||
|
UncheckCommand,
|
||||||
|
UploadCommand,
|
||||||
|
DoubleClickCommand,
|
||||||
|
FocusCommand,
|
||||||
|
DragCommand,
|
||||||
|
FrameCommand,
|
||||||
|
GetByRoleCommand,
|
||||||
|
GetByTextCommand,
|
||||||
|
GetByLabelCommand,
|
||||||
|
GetByPlaceholderCommand,
|
||||||
PressCommand,
|
PressCommand,
|
||||||
ScreenshotCommand,
|
ScreenshotCommand,
|
||||||
EvaluateCommand,
|
EvaluateCommand,
|
||||||
@@ -17,6 +29,12 @@ import type {
|
|||||||
TabSwitchCommand,
|
TabSwitchCommand,
|
||||||
TabCloseCommand,
|
TabCloseCommand,
|
||||||
WindowNewCommand,
|
WindowNewCommand,
|
||||||
|
CookiesSetCommand,
|
||||||
|
StorageGetCommand,
|
||||||
|
StorageSetCommand,
|
||||||
|
StorageClearCommand,
|
||||||
|
DialogCommand,
|
||||||
|
PdfCommand,
|
||||||
NavigateData,
|
NavigateData,
|
||||||
ScreenshotData,
|
ScreenshotData,
|
||||||
EvaluateData,
|
EvaluateData,
|
||||||
@@ -50,6 +68,32 @@ export async function executeCommand(
|
|||||||
return await handleClick(command, browser);
|
return await handleClick(command, browser);
|
||||||
case 'type':
|
case 'type':
|
||||||
return await handleType(command, browser);
|
return await handleType(command, browser);
|
||||||
|
case 'fill':
|
||||||
|
return await handleFill(command, browser);
|
||||||
|
case 'check':
|
||||||
|
return await handleCheck(command, browser);
|
||||||
|
case 'uncheck':
|
||||||
|
return await handleUncheck(command, browser);
|
||||||
|
case 'upload':
|
||||||
|
return await handleUpload(command, browser);
|
||||||
|
case 'dblclick':
|
||||||
|
return await handleDoubleClick(command, browser);
|
||||||
|
case 'focus':
|
||||||
|
return await handleFocus(command, browser);
|
||||||
|
case 'drag':
|
||||||
|
return await handleDrag(command, browser);
|
||||||
|
case 'frame':
|
||||||
|
return await handleFrame(command, browser);
|
||||||
|
case 'mainframe':
|
||||||
|
return await handleMainFrame(command, browser);
|
||||||
|
case 'getbyrole':
|
||||||
|
return await handleGetByRole(command, browser);
|
||||||
|
case 'getbytext':
|
||||||
|
return await handleGetByText(command, browser);
|
||||||
|
case 'getbylabel':
|
||||||
|
return await handleGetByLabel(command, browser);
|
||||||
|
case 'getbyplaceholder':
|
||||||
|
return await handleGetByPlaceholder(command, browser);
|
||||||
case 'press':
|
case 'press':
|
||||||
return await handlePress(command, browser);
|
return await handlePress(command, browser);
|
||||||
case 'screenshot':
|
case 'screenshot':
|
||||||
@@ -80,6 +124,22 @@ export async function executeCommand(
|
|||||||
return await handleTabClose(command, browser);
|
return await handleTabClose(command, browser);
|
||||||
case 'window_new':
|
case 'window_new':
|
||||||
return await handleWindowNew(command, browser);
|
return await handleWindowNew(command, browser);
|
||||||
|
case 'cookies_get':
|
||||||
|
return await handleCookiesGet(command, browser);
|
||||||
|
case 'cookies_set':
|
||||||
|
return await handleCookiesSet(command, browser);
|
||||||
|
case 'cookies_clear':
|
||||||
|
return await handleCookiesClear(command, browser);
|
||||||
|
case 'storage_get':
|
||||||
|
return await handleStorageGet(command, browser);
|
||||||
|
case 'storage_set':
|
||||||
|
return await handleStorageSet(command, browser);
|
||||||
|
case 'storage_clear':
|
||||||
|
return await handleStorageClear(command, browser);
|
||||||
|
case 'dialog':
|
||||||
|
return await handleDialog(command, browser);
|
||||||
|
case 'pdf':
|
||||||
|
return await handlePdf(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 };
|
||||||
@@ -372,3 +432,268 @@ async function handleWindowNew(
|
|||||||
const result = await browser.newWindow(command.viewport);
|
const result = await browser.newWindow(command.viewport);
|
||||||
return successResponse(command.id, result);
|
return successResponse(command.id, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New handlers for enhanced Playwright parity
|
||||||
|
|
||||||
|
async function handleFill(
|
||||||
|
command: FillCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const frame = browser.getFrame();
|
||||||
|
await frame.fill(command.selector, command.value);
|
||||||
|
return successResponse(command.id, { filled: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCheck(
|
||||||
|
command: CheckCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const frame = browser.getFrame();
|
||||||
|
await frame.check(command.selector);
|
||||||
|
return successResponse(command.id, { checked: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUncheck(
|
||||||
|
command: UncheckCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const frame = browser.getFrame();
|
||||||
|
await frame.uncheck(command.selector);
|
||||||
|
return successResponse(command.id, { unchecked: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpload(
|
||||||
|
command: UploadCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const frame = browser.getFrame();
|
||||||
|
const files = Array.isArray(command.files) ? command.files : [command.files];
|
||||||
|
await frame.setInputFiles(command.selector, files);
|
||||||
|
return successResponse(command.id, { uploaded: files });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDoubleClick(
|
||||||
|
command: DoubleClickCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const frame = browser.getFrame();
|
||||||
|
await frame.dblclick(command.selector);
|
||||||
|
return successResponse(command.id, { clicked: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFocus(
|
||||||
|
command: FocusCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const frame = browser.getFrame();
|
||||||
|
await frame.focus(command.selector);
|
||||||
|
return successResponse(command.id, { focused: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDrag(
|
||||||
|
command: DragCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const frame = browser.getFrame();
|
||||||
|
await frame.dragAndDrop(command.source, command.target);
|
||||||
|
return successResponse(command.id, { dragged: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFrame(
|
||||||
|
command: FrameCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
await browser.switchToFrame({
|
||||||
|
selector: command.selector,
|
||||||
|
name: command.name,
|
||||||
|
url: command.url,
|
||||||
|
});
|
||||||
|
return successResponse(command.id, { switched: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMainFrame(
|
||||||
|
command: Command & { action: 'mainframe' },
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
browser.switchToMainFrame();
|
||||||
|
return successResponse(command.id, { switched: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGetByRole(
|
||||||
|
command: GetByRoleCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
const locator = page.getByRole(command.role as any, { name: command.name });
|
||||||
|
|
||||||
|
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 handleGetByText(
|
||||||
|
command: GetByTextCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
const locator = page.getByText(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 handleGetByLabel(
|
||||||
|
command: GetByLabelCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
const locator = page.getByLabel(command.label);
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGetByPlaceholder(
|
||||||
|
command: GetByPlaceholderCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
const locator = page.getByPlaceholder(command.placeholder);
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCookiesGet(
|
||||||
|
command: Command & { action: 'cookies_get'; urls?: string[] },
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
const context = page.context();
|
||||||
|
const cookies = await context.cookies(command.urls);
|
||||||
|
return successResponse(command.id, { cookies });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCookiesSet(
|
||||||
|
command: CookiesSetCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
const context = page.context();
|
||||||
|
await context.addCookies(command.cookies);
|
||||||
|
return successResponse(command.id, { set: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCookiesClear(
|
||||||
|
command: Command & { action: 'cookies_clear' },
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
const context = page.context();
|
||||||
|
await context.clearCookies();
|
||||||
|
return successResponse(command.id, { cleared: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStorageGet(
|
||||||
|
command: StorageGetCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage';
|
||||||
|
|
||||||
|
if (command.key) {
|
||||||
|
const value = await page.evaluate(
|
||||||
|
`${storageType}.getItem(${JSON.stringify(command.key)})`
|
||||||
|
);
|
||||||
|
return successResponse(command.id, { key: command.key, value });
|
||||||
|
} else {
|
||||||
|
const data = await page.evaluate(`
|
||||||
|
(() => {
|
||||||
|
const storage = ${storageType};
|
||||||
|
const result = {};
|
||||||
|
for (let i = 0; i < storage.length; i++) {
|
||||||
|
const key = storage.key(i);
|
||||||
|
if (key) result[key] = storage.getItem(key);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
})()
|
||||||
|
`);
|
||||||
|
return successResponse(command.id, { data });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStorageSet(
|
||||||
|
command: StorageSetCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage';
|
||||||
|
|
||||||
|
await page.evaluate(
|
||||||
|
`${storageType}.setItem(${JSON.stringify(command.key)}, ${JSON.stringify(command.value)})`
|
||||||
|
);
|
||||||
|
return successResponse(command.id, { set: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStorageClear(
|
||||||
|
command: StorageClearCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage';
|
||||||
|
|
||||||
|
await page.evaluate(`${storageType}.clear()`);
|
||||||
|
return successResponse(command.id, { cleared: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDialog(
|
||||||
|
command: DialogCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
browser.setDialogHandler(command.response, command.promptText);
|
||||||
|
return successResponse(command.id, { handler: 'set', response: command.response });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePdf(
|
||||||
|
command: PdfCommand,
|
||||||
|
browser: BrowserManager
|
||||||
|
): Promise<Response> {
|
||||||
|
const page = browser.getPage();
|
||||||
|
await page.pdf({
|
||||||
|
path: command.path,
|
||||||
|
format: command.format ?? 'Letter',
|
||||||
|
});
|
||||||
|
return successResponse(command.id, { path: command.path });
|
||||||
|
}
|
||||||
|
|||||||
+84
-1
@@ -1,4 +1,4 @@
|
|||||||
import { chromium, firefox, webkit, type Browser, type BrowserContext, type Page } from 'playwright';
|
import { chromium, firefox, webkit, type Browser, type BrowserContext, type Page, type Frame, type Dialog } from 'playwright';
|
||||||
import type { LaunchCommand } from './types.js';
|
import type { LaunchCommand } from './types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -9,6 +9,8 @@ export class BrowserManager {
|
|||||||
private contexts: BrowserContext[] = [];
|
private contexts: BrowserContext[] = [];
|
||||||
private pages: Page[] = [];
|
private pages: Page[] = [];
|
||||||
private activePageIndex: number = 0;
|
private activePageIndex: number = 0;
|
||||||
|
private activeFrame: Frame | null = null;
|
||||||
|
private dialogHandler: ((dialog: Dialog) => Promise<void>) | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if browser is launched
|
* Check if browser is launched
|
||||||
@@ -27,6 +29,87 @@ export class BrowserManager {
|
|||||||
return this.pages[this.activePageIndex];
|
return this.pages[this.activePageIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current frame (or page's main frame if no frame is selected)
|
||||||
|
*/
|
||||||
|
getFrame(): Frame {
|
||||||
|
if (this.activeFrame) {
|
||||||
|
return this.activeFrame;
|
||||||
|
}
|
||||||
|
return this.getPage().mainFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to a frame by selector, name, or URL
|
||||||
|
*/
|
||||||
|
async switchToFrame(options: { selector?: string; name?: string; url?: string }): Promise<void> {
|
||||||
|
const page = this.getPage();
|
||||||
|
|
||||||
|
if (options.selector) {
|
||||||
|
const frameElement = await page.$(options.selector);
|
||||||
|
if (!frameElement) {
|
||||||
|
throw new Error(`Frame not found: ${options.selector}`);
|
||||||
|
}
|
||||||
|
const frame = await frameElement.contentFrame();
|
||||||
|
if (!frame) {
|
||||||
|
throw new Error(`Element is not a frame: ${options.selector}`);
|
||||||
|
}
|
||||||
|
this.activeFrame = frame;
|
||||||
|
} else if (options.name) {
|
||||||
|
const frame = page.frame({ name: options.name });
|
||||||
|
if (!frame) {
|
||||||
|
throw new Error(`Frame not found with name: ${options.name}`);
|
||||||
|
}
|
||||||
|
this.activeFrame = frame;
|
||||||
|
} else if (options.url) {
|
||||||
|
const frame = page.frame({ url: options.url });
|
||||||
|
if (!frame) {
|
||||||
|
throw new Error(`Frame not found with URL: ${options.url}`);
|
||||||
|
}
|
||||||
|
this.activeFrame = frame;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch back to main frame
|
||||||
|
*/
|
||||||
|
switchToMainFrame(): void {
|
||||||
|
this.activeFrame = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up dialog handler
|
||||||
|
*/
|
||||||
|
setDialogHandler(response: 'accept' | 'dismiss', promptText?: string): void {
|
||||||
|
const page = this.getPage();
|
||||||
|
|
||||||
|
// Remove existing handler if any
|
||||||
|
if (this.dialogHandler) {
|
||||||
|
page.removeListener('dialog', this.dialogHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dialogHandler = async (dialog: Dialog) => {
|
||||||
|
if (response === 'accept') {
|
||||||
|
await dialog.accept(promptText);
|
||||||
|
} else {
|
||||||
|
await dialog.dismiss();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
page.on('dialog', this.dialogHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear dialog handler
|
||||||
|
*/
|
||||||
|
clearDialogHandler(): void {
|
||||||
|
if (this.dialogHandler) {
|
||||||
|
const page = this.getPage();
|
||||||
|
page.removeListener('dialog', this.dialogHandler);
|
||||||
|
this.dialogHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all pages
|
* Get all pages
|
||||||
*/
|
*/
|
||||||
|
|||||||
+283
-3
@@ -60,18 +60,53 @@ ${c('yellow', 'Usage:')}
|
|||||||
${c('yellow', 'Commands:')}
|
${c('yellow', 'Commands:')}
|
||||||
${c('cyan', 'open')} <url> Open a URL in the browser
|
${c('cyan', 'open')} <url> Open a URL in the browser
|
||||||
${c('cyan', 'click')} <selector> Click an element
|
${c('cyan', 'click')} <selector> Click an element
|
||||||
|
${c('cyan', 'dblclick')} <selector> Double-click an element
|
||||||
${c('cyan', 'type')} <selector> <text> Type text into an element
|
${c('cyan', 'type')} <selector> <text> Type text into an element
|
||||||
|
${c('cyan', 'fill')} <selector> <value> Clear and fill input
|
||||||
${c('cyan', 'press')} <key> Press a keyboard key
|
${c('cyan', 'press')} <key> Press a keyboard key
|
||||||
|
${c('cyan', 'check')} <selector> Check a checkbox/radio
|
||||||
|
${c('cyan', 'uncheck')} <selector> Uncheck a checkbox
|
||||||
|
${c('cyan', 'select')} <selector> <value> Select dropdown option
|
||||||
|
${c('cyan', 'hover')} <selector> Hover over an element
|
||||||
|
${c('cyan', 'focus')} <selector> Focus an element
|
||||||
|
${c('cyan', 'drag')} <source> <target> Drag and drop
|
||||||
|
${c('cyan', 'upload')} <selector> <file...> Upload files
|
||||||
${c('cyan', 'wait')} <selector|text|ms> Wait for element, text, or duration
|
${c('cyan', 'wait')} <selector|text|ms> Wait for element, text, or duration
|
||||||
${c('cyan', 'screenshot')} [path] Take a screenshot
|
${c('cyan', 'screenshot')} [path] Take a screenshot
|
||||||
|
${c('cyan', 'pdf')} <path> Save page as PDF
|
||||||
${c('cyan', 'snapshot')} Get accessibility tree (for agents)
|
${c('cyan', 'snapshot')} Get accessibility tree (for agents)
|
||||||
${c('cyan', 'extract')} <selector> Extract element content
|
${c('cyan', 'extract')} <selector> Extract element content
|
||||||
${c('cyan', 'eval')} <script> Evaluate JavaScript
|
${c('cyan', 'eval')} <script> Evaluate JavaScript
|
||||||
${c('cyan', 'scroll')} <direction> [amount] Scroll the page
|
${c('cyan', 'scroll')} <direction> [amount] Scroll the page
|
||||||
${c('cyan', 'hover')} <selector> Hover over an element
|
|
||||||
${c('cyan', 'select')} <selector> <value> Select dropdown option
|
|
||||||
${c('cyan', 'close')} Close browser and stop daemon
|
${c('cyan', 'close')} Close browser and stop daemon
|
||||||
|
|
||||||
|
${c('yellow', 'Locator Commands:')}
|
||||||
|
${c('cyan', 'role')} <role> click|fill|check Find by ARIA role
|
||||||
|
${c('cyan', 'text')} <text> click|hover Find by text content
|
||||||
|
${c('cyan', 'label')} <label> click|fill Find by label
|
||||||
|
${c('cyan', 'placeholder')} <ph> click|fill Find by placeholder
|
||||||
|
|
||||||
|
${c('yellow', 'Frame Commands:')}
|
||||||
|
${c('cyan', 'frame')} <selector> Switch to iframe
|
||||||
|
${c('cyan', 'mainframe')} Switch back to main frame
|
||||||
|
|
||||||
|
${c('yellow', 'Cookie Commands:')}
|
||||||
|
${c('cyan', 'cookies')} Get all cookies
|
||||||
|
${c('cyan', 'cookies set')} <json> Set cookies
|
||||||
|
${c('cyan', 'cookies clear')} Clear all cookies
|
||||||
|
|
||||||
|
${c('yellow', 'Storage Commands:')}
|
||||||
|
${c('cyan', 'storage local')} [key] Get localStorage
|
||||||
|
${c('cyan', 'storage local set')} <k> <v> Set localStorage
|
||||||
|
${c('cyan', 'storage local clear')} Clear localStorage
|
||||||
|
${c('cyan', 'storage session')} [key] Get sessionStorage
|
||||||
|
${c('cyan', 'storage session set')} <k> <v> Set sessionStorage
|
||||||
|
${c('cyan', 'storage session clear')} Clear sessionStorage
|
||||||
|
|
||||||
|
${c('yellow', 'Dialog Commands:')}
|
||||||
|
${c('cyan', 'dialog accept')} [text] Accept next dialog
|
||||||
|
${c('cyan', 'dialog dismiss')} Dismiss next dialog
|
||||||
|
|
||||||
${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
|
||||||
@@ -145,7 +180,37 @@ function printResponse(response: Response, jsonMode: boolean): void {
|
|||||||
console.log(c('dim', ` ${(data.base64 as string).length} bytes`));
|
console.log(c('dim', ` ${(data.base64 as string).length} bytes`));
|
||||||
} else if (data.path) {
|
} else if (data.path) {
|
||||||
console.log(c('green', '✓'), `Saved to ${data.path}`);
|
console.log(c('green', '✓'), `Saved to ${data.path}`);
|
||||||
} else if (data.clicked || data.typed || data.pressed || data.hovered || data.scrolled || data.selected || data.waited) {
|
} else if (data.cookies) {
|
||||||
|
// Cookies get
|
||||||
|
const cookies = data.cookies as Array<{ name: string; value: string; domain?: string }>;
|
||||||
|
if (cookies.length === 0) {
|
||||||
|
console.log(c('dim', 'No cookies'));
|
||||||
|
} else {
|
||||||
|
cookies.forEach(cookie => {
|
||||||
|
console.log(`${c('cyan', cookie.name)}: ${cookie.value}`);
|
||||||
|
if (cookie.domain) console.log(c('dim', ` domain: ${cookie.domain}`));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (data.data) {
|
||||||
|
// Storage get (all)
|
||||||
|
const storage = data.data as Record<string, string>;
|
||||||
|
const keys = Object.keys(storage);
|
||||||
|
if (keys.length === 0) {
|
||||||
|
console.log(c('dim', 'Empty storage'));
|
||||||
|
} else {
|
||||||
|
keys.forEach(key => {
|
||||||
|
console.log(`${c('cyan', key)}: ${storage[key]}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (data.value !== undefined && data.key) {
|
||||||
|
// Storage get (single key)
|
||||||
|
console.log(data.value ?? c('dim', 'null'));
|
||||||
|
} else if (data.uploaded) {
|
||||||
|
const files = data.uploaded as string[];
|
||||||
|
console.log(c('green', '✓'), `Uploaded ${files.length} file(s)`);
|
||||||
|
} else if (data.handler) {
|
||||||
|
console.log(c('green', '✓'), `Dialog handler set to ${data.response}`);
|
||||||
|
} 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) {
|
||||||
console.log(c('green', '✓'), 'Browser launched');
|
console.log(c('green', '✓'), 'Browser launched');
|
||||||
@@ -204,6 +269,7 @@ async function main(): Promise<void> {
|
|||||||
const prev = args[i - 1];
|
const prev = args[i - 1];
|
||||||
if (prev === '--selector' || prev === '-s') return false;
|
if (prev === '--selector' || prev === '-s') return false;
|
||||||
if (prev === '--session') return false;
|
if (prev === '--session') return false;
|
||||||
|
if (prev === '--name' || prev === '-n') return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
const command = cleanArgs[0];
|
const command = cleanArgs[0];
|
||||||
@@ -215,6 +281,16 @@ async function main(): Promise<void> {
|
|||||||
selectorOverride = args[sIdx + 1];
|
selectorOverride = args[sIdx + 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find --name value (for locator commands)
|
||||||
|
let nameOverride: string | undefined;
|
||||||
|
const nIdx = args.findIndex(a => a === '--name' || a === '-n');
|
||||||
|
if (nIdx !== -1 && args[nIdx + 1]) {
|
||||||
|
nameOverride = args[nIdx + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find --exact flag
|
||||||
|
const exactMode = args.includes('--exact');
|
||||||
|
|
||||||
const id = genId();
|
const id = genId();
|
||||||
let cmd: Record<string, unknown>;
|
let cmd: Record<string, unknown>;
|
||||||
|
|
||||||
@@ -254,6 +330,80 @@ async function main(): Promise<void> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'fill': {
|
||||||
|
const selector = cleanArgs[1];
|
||||||
|
const value = cleanArgs.slice(2).join(' ');
|
||||||
|
if (!selector || value === undefined) {
|
||||||
|
console.error(c('red', 'Error:'), 'Selector and value required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'fill', selector, value };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'check': {
|
||||||
|
const selector = cleanArgs[1];
|
||||||
|
if (!selector) {
|
||||||
|
console.error(c('red', 'Error:'), 'Selector required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'check', selector };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'uncheck': {
|
||||||
|
const selector = cleanArgs[1];
|
||||||
|
if (!selector) {
|
||||||
|
console.error(c('red', 'Error:'), 'Selector required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'uncheck', selector };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'dblclick':
|
||||||
|
case 'doubleclick': {
|
||||||
|
const selector = cleanArgs[1];
|
||||||
|
if (!selector) {
|
||||||
|
console.error(c('red', 'Error:'), 'Selector required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'dblclick', selector };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'focus': {
|
||||||
|
const selector = cleanArgs[1];
|
||||||
|
if (!selector) {
|
||||||
|
console.error(c('red', 'Error:'), 'Selector required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'focus', selector };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'drag': {
|
||||||
|
const source = cleanArgs[1];
|
||||||
|
const target = cleanArgs[2];
|
||||||
|
if (!source || !target) {
|
||||||
|
console.error(c('red', 'Error:'), 'Source and target selectors required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'drag', source, target };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'upload': {
|
||||||
|
const selector = cleanArgs[1];
|
||||||
|
const files = cleanArgs.slice(2);
|
||||||
|
if (!selector || files.length === 0) {
|
||||||
|
console.error(c('red', 'Error:'), 'Selector and file(s) required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'upload', selector, files };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'press': {
|
case 'press': {
|
||||||
const key = cleanArgs[1];
|
const key = cleanArgs[1];
|
||||||
if (!key) {
|
if (!key) {
|
||||||
@@ -357,6 +507,136 @@ async function main(): Promise<void> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'pdf': {
|
||||||
|
const pdfPath = cleanArgs[1];
|
||||||
|
if (!pdfPath) {
|
||||||
|
console.error(c('red', 'Error:'), 'Path required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'pdf', path: pdfPath };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'frame': {
|
||||||
|
const selector = cleanArgs[1];
|
||||||
|
if (!selector) {
|
||||||
|
console.error(c('red', 'Error:'), 'Frame selector required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'frame', selector };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'mainframe': {
|
||||||
|
cmd = { id, action: 'mainframe' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'role': {
|
||||||
|
const role = cleanArgs[1];
|
||||||
|
const subaction = cleanArgs[2] as 'click' | 'fill' | 'check' | 'hover';
|
||||||
|
const value = cleanArgs[3];
|
||||||
|
if (!role || !subaction) {
|
||||||
|
console.error(c('red', 'Error:'), 'Role and action required (e.g., veb role button click --name "Submit")');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'getbyrole', role, name: nameOverride, subaction, value };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'text': {
|
||||||
|
const text = cleanArgs[1];
|
||||||
|
const subaction = cleanArgs[2] as 'click' | 'hover';
|
||||||
|
if (!text || !subaction) {
|
||||||
|
console.error(c('red', 'Error:'), 'Text and action required (e.g., veb text "Submit" click)');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'getbytext', text, exact: exactMode, subaction };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'label': {
|
||||||
|
const label = cleanArgs[1];
|
||||||
|
const subaction = cleanArgs[2] as 'click' | 'fill' | 'check';
|
||||||
|
const value = cleanArgs[3];
|
||||||
|
if (!label || !subaction) {
|
||||||
|
console.error(c('red', 'Error:'), 'Label and action required (e.g., veb label "Email" fill "test@test.com")');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'getbylabel', label, subaction, value };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'placeholder': {
|
||||||
|
const placeholder = cleanArgs[1];
|
||||||
|
const subaction = cleanArgs[2] as 'click' | 'fill';
|
||||||
|
const value = cleanArgs[3];
|
||||||
|
if (!placeholder || !subaction) {
|
||||||
|
console.error(c('red', 'Error:'), 'Placeholder and action required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'getbyplaceholder', placeholder, subaction, value };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'cookies': {
|
||||||
|
const subCmd = cleanArgs[1];
|
||||||
|
|
||||||
|
if (subCmd === 'set') {
|
||||||
|
const jsonStr = cleanArgs.slice(2).join(' ');
|
||||||
|
try {
|
||||||
|
const cookies = JSON.parse(jsonStr);
|
||||||
|
cmd = { id, action: 'cookies_set', cookies: Array.isArray(cookies) ? cookies : [cookies] };
|
||||||
|
} catch {
|
||||||
|
console.error(c('red', 'Error:'), 'Invalid JSON for cookies');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} else if (subCmd === 'clear') {
|
||||||
|
cmd = { id, action: 'cookies_clear' };
|
||||||
|
} else {
|
||||||
|
cmd = { id, action: 'cookies_get' };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'storage': {
|
||||||
|
const storageType = cleanArgs[1]; // 'local' or 'session'
|
||||||
|
const subCmd = cleanArgs[2];
|
||||||
|
|
||||||
|
if (storageType !== 'local' && storageType !== 'session') {
|
||||||
|
console.error(c('red', 'Error:'), 'Storage type must be "local" or "session"');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subCmd === 'set') {
|
||||||
|
const key = cleanArgs[3];
|
||||||
|
const value = cleanArgs.slice(4).join(' ');
|
||||||
|
if (!key) {
|
||||||
|
console.error(c('red', 'Error:'), 'Key required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'storage_set', type: storageType, key, value };
|
||||||
|
} else if (subCmd === 'clear') {
|
||||||
|
cmd = { id, action: 'storage_clear', type: storageType };
|
||||||
|
} else {
|
||||||
|
// Get - subCmd might be a key or undefined
|
||||||
|
cmd = { id, action: 'storage_get', type: storageType, key: subCmd };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'dialog': {
|
||||||
|
const response = cleanArgs[1];
|
||||||
|
const promptText = cleanArgs[2];
|
||||||
|
|
||||||
|
if (response !== 'accept' && response !== 'dismiss') {
|
||||||
|
console.error(c('red', 'Error:'), 'Dialog response must be "accept" or "dismiss"');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
cmd = { id, action: 'dialog', response, promptText };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'close':
|
case 'close':
|
||||||
case 'quit':
|
case 'quit':
|
||||||
case 'exit': {
|
case 'exit': {
|
||||||
|
|||||||
+153
@@ -40,6 +40,138 @@ const typeSchema = baseCommandSchema.extend({
|
|||||||
clear: z.boolean().optional(),
|
clear: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fillSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('fill'),
|
||||||
|
selector: z.string().min(1),
|
||||||
|
value: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('check'),
|
||||||
|
selector: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const uncheckSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('uncheck'),
|
||||||
|
selector: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('upload'),
|
||||||
|
selector: z.string().min(1),
|
||||||
|
files: z.union([z.string(), z.array(z.string())]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const dblclickSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('dblclick'),
|
||||||
|
selector: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const focusSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('focus'),
|
||||||
|
selector: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const dragSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('drag'),
|
||||||
|
source: z.string().min(1),
|
||||||
|
target: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const frameSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('frame'),
|
||||||
|
selector: z.string().min(1).optional(),
|
||||||
|
name: z.string().optional(),
|
||||||
|
url: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mainframeSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('mainframe'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getByRoleSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('getbyrole'),
|
||||||
|
role: z.string().min(1),
|
||||||
|
name: z.string().optional(),
|
||||||
|
subaction: z.enum(['click', 'fill', 'check', 'hover']),
|
||||||
|
value: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getByTextSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('getbytext'),
|
||||||
|
text: z.string().min(1),
|
||||||
|
exact: z.boolean().optional(),
|
||||||
|
subaction: z.enum(['click', 'hover']),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getByLabelSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('getbylabel'),
|
||||||
|
label: z.string().min(1),
|
||||||
|
subaction: z.enum(['click', 'fill', 'check']),
|
||||||
|
value: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getByPlaceholderSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('getbyplaceholder'),
|
||||||
|
placeholder: z.string().min(1),
|
||||||
|
subaction: z.enum(['click', 'fill']),
|
||||||
|
value: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const cookiesGetSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('cookies_get'),
|
||||||
|
urls: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const cookiesSetSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('cookies_set'),
|
||||||
|
cookies: z.array(z.object({
|
||||||
|
name: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
url: z.string().optional(),
|
||||||
|
domain: z.string().optional(),
|
||||||
|
path: z.string().optional(),
|
||||||
|
expires: z.number().optional(),
|
||||||
|
httpOnly: z.boolean().optional(),
|
||||||
|
secure: z.boolean().optional(),
|
||||||
|
sameSite: z.enum(['Strict', 'Lax', 'None']).optional(),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
const cookiesClearSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('cookies_clear'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const storageGetSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('storage_get'),
|
||||||
|
key: z.string().optional(),
|
||||||
|
type: z.enum(['local', 'session']),
|
||||||
|
});
|
||||||
|
|
||||||
|
const storageSetSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('storage_set'),
|
||||||
|
key: z.string().min(1),
|
||||||
|
value: z.string(),
|
||||||
|
type: z.enum(['local', 'session']),
|
||||||
|
});
|
||||||
|
|
||||||
|
const storageClearSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('storage_clear'),
|
||||||
|
type: z.enum(['local', 'session']),
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialogSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('dialog'),
|
||||||
|
response: z.enum(['accept', 'dismiss']),
|
||||||
|
promptText: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfSchema = baseCommandSchema.extend({
|
||||||
|
action: z.literal('pdf'),
|
||||||
|
path: z.string().min(1),
|
||||||
|
format: z.enum(['Letter', 'Legal', 'Tabloid', 'Ledger', 'A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6']).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
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),
|
||||||
@@ -134,6 +266,19 @@ const commandSchema = z.discriminatedUnion('action', [
|
|||||||
navigateSchema,
|
navigateSchema,
|
||||||
clickSchema,
|
clickSchema,
|
||||||
typeSchema,
|
typeSchema,
|
||||||
|
fillSchema,
|
||||||
|
checkSchema,
|
||||||
|
uncheckSchema,
|
||||||
|
uploadSchema,
|
||||||
|
dblclickSchema,
|
||||||
|
focusSchema,
|
||||||
|
dragSchema,
|
||||||
|
frameSchema,
|
||||||
|
mainframeSchema,
|
||||||
|
getByRoleSchema,
|
||||||
|
getByTextSchema,
|
||||||
|
getByLabelSchema,
|
||||||
|
getByPlaceholderSchema,
|
||||||
pressSchema,
|
pressSchema,
|
||||||
screenshotSchema,
|
screenshotSchema,
|
||||||
snapshotSchema,
|
snapshotSchema,
|
||||||
@@ -149,6 +294,14 @@ const commandSchema = z.discriminatedUnion('action', [
|
|||||||
tabSwitchSchema,
|
tabSwitchSchema,
|
||||||
tabCloseSchema,
|
tabCloseSchema,
|
||||||
windowNewSchema,
|
windowNewSchema,
|
||||||
|
cookiesGetSchema,
|
||||||
|
cookiesSetSchema,
|
||||||
|
cookiesClearSchema,
|
||||||
|
storageGetSchema,
|
||||||
|
storageSetSchema,
|
||||||
|
storageClearSchema,
|
||||||
|
dialogSchema,
|
||||||
|
pdfSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Parse result type
|
// Parse result type
|
||||||
|
|||||||
+154
-1
@@ -36,6 +36,138 @@ export interface TypeCommand extends BaseCommand {
|
|||||||
clear?: boolean;
|
clear?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FillCommand extends BaseCommand {
|
||||||
|
action: 'fill';
|
||||||
|
selector: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckCommand extends BaseCommand {
|
||||||
|
action: 'check';
|
||||||
|
selector: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UncheckCommand extends BaseCommand {
|
||||||
|
action: 'uncheck';
|
||||||
|
selector: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadCommand extends BaseCommand {
|
||||||
|
action: 'upload';
|
||||||
|
selector: string;
|
||||||
|
files: string | string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DoubleClickCommand extends BaseCommand {
|
||||||
|
action: 'dblclick';
|
||||||
|
selector: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FocusCommand extends BaseCommand {
|
||||||
|
action: 'focus';
|
||||||
|
selector: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DragCommand extends BaseCommand {
|
||||||
|
action: 'drag';
|
||||||
|
source: string;
|
||||||
|
target: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FrameCommand extends BaseCommand {
|
||||||
|
action: 'frame';
|
||||||
|
selector?: string;
|
||||||
|
name?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MainFrameCommand extends BaseCommand {
|
||||||
|
action: 'mainframe';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetByRoleCommand extends BaseCommand {
|
||||||
|
action: 'getbyrole';
|
||||||
|
role: string;
|
||||||
|
name?: string;
|
||||||
|
subaction: 'click' | 'fill' | 'check' | 'hover';
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetByTextCommand extends BaseCommand {
|
||||||
|
action: 'getbytext';
|
||||||
|
text: string;
|
||||||
|
exact?: boolean;
|
||||||
|
subaction: 'click' | 'hover';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetByLabelCommand extends BaseCommand {
|
||||||
|
action: 'getbylabel';
|
||||||
|
label: string;
|
||||||
|
subaction: 'click' | 'fill' | 'check';
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetByPlaceholderCommand extends BaseCommand {
|
||||||
|
action: 'getbyplaceholder';
|
||||||
|
placeholder: string;
|
||||||
|
subaction: 'click' | 'fill';
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CookiesGetCommand extends BaseCommand {
|
||||||
|
action: 'cookies_get';
|
||||||
|
urls?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CookiesSetCommand extends BaseCommand {
|
||||||
|
action: 'cookies_set';
|
||||||
|
cookies: Array<{
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
url?: string;
|
||||||
|
domain?: string;
|
||||||
|
path?: string;
|
||||||
|
expires?: number;
|
||||||
|
httpOnly?: boolean;
|
||||||
|
secure?: boolean;
|
||||||
|
sameSite?: 'Strict' | 'Lax' | 'None';
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CookiesClearCommand extends BaseCommand {
|
||||||
|
action: 'cookies_clear';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StorageGetCommand extends BaseCommand {
|
||||||
|
action: 'storage_get';
|
||||||
|
key?: string;
|
||||||
|
type: 'local' | 'session';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StorageSetCommand extends BaseCommand {
|
||||||
|
action: 'storage_set';
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
type: 'local' | 'session';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StorageClearCommand extends BaseCommand {
|
||||||
|
action: 'storage_clear';
|
||||||
|
type: 'local' | 'session';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DialogCommand extends BaseCommand {
|
||||||
|
action: 'dialog';
|
||||||
|
response: 'accept' | 'dismiss';
|
||||||
|
promptText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PdfCommand extends BaseCommand {
|
||||||
|
action: 'pdf';
|
||||||
|
path: string;
|
||||||
|
format?: 'Letter' | 'Legal' | 'Tabloid' | 'Ledger' | 'A0' | 'A1' | 'A2' | 'A3' | 'A4' | 'A5' | 'A6';
|
||||||
|
}
|
||||||
|
|
||||||
export interface PressCommand extends BaseCommand {
|
export interface PressCommand extends BaseCommand {
|
||||||
action: 'press';
|
action: 'press';
|
||||||
key: string;
|
key: string;
|
||||||
@@ -127,6 +259,19 @@ export type Command =
|
|||||||
| NavigateCommand
|
| NavigateCommand
|
||||||
| ClickCommand
|
| ClickCommand
|
||||||
| TypeCommand
|
| TypeCommand
|
||||||
|
| FillCommand
|
||||||
|
| CheckCommand
|
||||||
|
| UncheckCommand
|
||||||
|
| UploadCommand
|
||||||
|
| DoubleClickCommand
|
||||||
|
| FocusCommand
|
||||||
|
| DragCommand
|
||||||
|
| FrameCommand
|
||||||
|
| MainFrameCommand
|
||||||
|
| GetByRoleCommand
|
||||||
|
| GetByTextCommand
|
||||||
|
| GetByLabelCommand
|
||||||
|
| GetByPlaceholderCommand
|
||||||
| PressCommand
|
| PressCommand
|
||||||
| ScreenshotCommand
|
| ScreenshotCommand
|
||||||
| SnapshotCommand
|
| SnapshotCommand
|
||||||
@@ -141,7 +286,15 @@ export type Command =
|
|||||||
| TabListCommand
|
| TabListCommand
|
||||||
| TabSwitchCommand
|
| TabSwitchCommand
|
||||||
| TabCloseCommand
|
| TabCloseCommand
|
||||||
| WindowNewCommand;
|
| WindowNewCommand
|
||||||
|
| CookiesGetCommand
|
||||||
|
| CookiesSetCommand
|
||||||
|
| CookiesClearCommand
|
||||||
|
| StorageGetCommand
|
||||||
|
| StorageSetCommand
|
||||||
|
| StorageClearCommand
|
||||||
|
| DialogCommand
|
||||||
|
| PdfCommand;
|
||||||
|
|
||||||
// Response types
|
// Response types
|
||||||
export interface SuccessResponse<T = unknown> {
|
export interface SuccessResponse<T = unknown> {
|
||||||
|
|||||||
Reference in New Issue
Block a user