more commands
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# veb
|
||||
|
||||
Headless browser automation CLI for agents and humans.
|
||||
Headless browser automation CLI for agents and humans. Near-complete Playwright parity.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -13,70 +13,99 @@ pnpm build
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Open a URL (auto-starts browser daemon)
|
||||
# Navigation
|
||||
veb open https://example.com
|
||||
|
||||
# Click elements
|
||||
# Clicking
|
||||
veb click "#submit-btn"
|
||||
veb click "text=Sign In"
|
||||
veb dblclick "#item"
|
||||
|
||||
# Type into inputs
|
||||
veb type "#email" hello@example.com
|
||||
veb type "#search" "search query"
|
||||
# Form input
|
||||
veb type "#search" "query" # Type character by character
|
||||
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 Tab
|
||||
|
||||
# Wait for things
|
||||
veb wait "#loading" # wait for selector
|
||||
veb wait --text "Welcome" # wait for text
|
||||
veb wait 2000 # wait 2 seconds
|
||||
# Mouse
|
||||
veb hover "#menu"
|
||||
veb focus "#input"
|
||||
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 --full page.png # full page
|
||||
veb screenshot -s "#hero" # specific element
|
||||
veb screenshot --full page.png # Full page
|
||||
veb screenshot -s "#hero" # Specific element
|
||||
veb pdf report.pdf
|
||||
|
||||
# Get accessibility snapshot (great for AI agents)
|
||||
veb snapshot
|
||||
# Content extraction
|
||||
veb snapshot # Accessibility tree (best for agents)
|
||||
veb extract "#main" # Get HTML
|
||||
veb eval "document.title" # Run JavaScript
|
||||
|
||||
# Extract HTML content
|
||||
veb extract "table"
|
||||
veb extract "#main"
|
||||
|
||||
# Evaluate JavaScript
|
||||
veb eval "document.title"
|
||||
veb eval "window.location.href"
|
||||
|
||||
# Scroll the page
|
||||
# Scrolling
|
||||
veb scroll down 500
|
||||
veb scroll up
|
||||
veb scroll -s "#container" down
|
||||
|
||||
# Interact with dropdowns
|
||||
veb select "#country" "US"
|
||||
# Semantic locators (Playwright's recommended approach)
|
||||
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
|
||||
veb hover "#menu"
|
||||
# Frames/iframes
|
||||
veb frame "#iframe" # Switch to iframe
|
||||
veb mainframe # Switch back to main
|
||||
|
||||
# Tab management
|
||||
veb tab new # Open new tab
|
||||
veb tab list # List all tabs
|
||||
veb tab 0 # Switch to tab 0
|
||||
veb tab close # Close current tab
|
||||
veb tab close 1 # Close tab 1
|
||||
# Cookies
|
||||
veb cookies # Get all cookies
|
||||
veb cookies set '[{"name":"session","value":"abc123","domain":".example.com"}]'
|
||||
veb cookies clear
|
||||
|
||||
# Window management
|
||||
veb window new # Open new window
|
||||
# Storage
|
||||
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 agent2 open google.com
|
||||
veb session list # List active sessions
|
||||
VEB_SESSION=agent1 veb eval "document.title"
|
||||
VEB_SESSION=agent1 veb click "#btn"
|
||||
veb session list
|
||||
|
||||
# Close browser (stops daemon)
|
||||
# Close browser
|
||||
veb close
|
||||
```
|
||||
|
||||
@@ -85,85 +114,111 @@ veb close
|
||||
Use `--json` flag for machine-readable output:
|
||||
|
||||
```bash
|
||||
veb open https://example.com --json
|
||||
# {"id":"abc123","success":true,"data":{"url":"https://example.com/","title":"Example Domain"}}
|
||||
|
||||
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
|
||||
|
||||
### Navigation & Interaction
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `open <url>` | Navigate to a URL |
|
||||
| `click <selector>` | Click an element |
|
||||
| `type <selector> <text>` | Type text into an element |
|
||||
| `press <key>` | Press a keyboard key |
|
||||
| `wait <selector\|text\|ms>` | Wait for condition |
|
||||
| `screenshot [path]` | Take a screenshot |
|
||||
| `snapshot` | Get accessibility tree |
|
||||
| `extract <selector>` | Get element HTML |
|
||||
| `open <url>` | Navigate to URL |
|
||||
| `click <selector>` | Click element |
|
||||
| `dblclick <selector>` | Double-click |
|
||||
| `type <selector> <text>` | Type text |
|
||||
| `fill <selector> <value>` | Clear & fill |
|
||||
| `press <key>` | Press key |
|
||||
| `check <selector>` | Check checkbox |
|
||||
| `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 |
|
||||
| `scroll <dir> [amount]` | Scroll page |
|
||||
| `hover <selector>` | Hover over element |
|
||||
| `select <selector> <val>` | Select dropdown option |
|
||||
| `tab new` | Open new tab |
|
||||
| `tab list` | List all tabs |
|
||||
| `tab <index>` | Switch to tab |
|
||||
|
||||
### Browser State
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `cookies` | Get cookies |
|
||||
| `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 |
|
||||
| `window new` | Open new window |
|
||||
| `session` | Show current session |
|
||||
| `session list` | List active sessions |
|
||||
| `window new` | New window |
|
||||
|
||||
### Session & Control
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `wait <sel\|text\|ms>` | Wait for condition |
|
||||
| `session` | Show session |
|
||||
| `session list` | List sessions |
|
||||
| `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
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--session <name>` | Use isolated browser session |
|
||||
| `--json` | Output raw JSON |
|
||||
| `--session <name>` | Use isolated session |
|
||||
| `--json` | JSON output |
|
||||
| `--full, -f` | Full page screenshot |
|
||||
| `--text, -t` | Wait for text |
|
||||
| `--selector, -s` | Target element |
|
||||
| `--debug` | Show debug timing info |
|
||||
| `--help, -h` | Show help |
|
||||
| `--name, -n` | Locator name filter |
|
||||
| `--exact` | Exact text match |
|
||||
| `--text, -t` | Wait for text |
|
||||
| `--debug` | Debug output |
|
||||
|
||||
## Selectors
|
||||
|
||||
veb supports all Playwright selectors:
|
||||
|
||||
- CSS: `#id`, `.class`, `div.container`
|
||||
- Text: `text=Click me`, `"Click me"`
|
||||
- XPath: `xpath=//button`
|
||||
- Role: `role=button[name="Submit"]`
|
||||
```bash
|
||||
# CSS
|
||||
veb click "#id"
|
||||
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
|
||||
|
||||
|
||||
+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 {
|
||||
Command,
|
||||
@@ -6,6 +6,18 @@ import type {
|
||||
NavigateCommand,
|
||||
ClickCommand,
|
||||
TypeCommand,
|
||||
FillCommand,
|
||||
CheckCommand,
|
||||
UncheckCommand,
|
||||
UploadCommand,
|
||||
DoubleClickCommand,
|
||||
FocusCommand,
|
||||
DragCommand,
|
||||
FrameCommand,
|
||||
GetByRoleCommand,
|
||||
GetByTextCommand,
|
||||
GetByLabelCommand,
|
||||
GetByPlaceholderCommand,
|
||||
PressCommand,
|
||||
ScreenshotCommand,
|
||||
EvaluateCommand,
|
||||
@@ -17,6 +29,12 @@ import type {
|
||||
TabSwitchCommand,
|
||||
TabCloseCommand,
|
||||
WindowNewCommand,
|
||||
CookiesSetCommand,
|
||||
StorageGetCommand,
|
||||
StorageSetCommand,
|
||||
StorageClearCommand,
|
||||
DialogCommand,
|
||||
PdfCommand,
|
||||
NavigateData,
|
||||
ScreenshotData,
|
||||
EvaluateData,
|
||||
@@ -50,6 +68,32 @@ export async function executeCommand(
|
||||
return await handleClick(command, browser);
|
||||
case 'type':
|
||||
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':
|
||||
return await handlePress(command, browser);
|
||||
case 'screenshot':
|
||||
@@ -80,6 +124,22 @@ export async function executeCommand(
|
||||
return await handleTabClose(command, browser);
|
||||
case 'window_new':
|
||||
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: {
|
||||
// TypeScript narrows to never here, but we handle it for safety
|
||||
const unknownCommand = command as { id: string; action: string };
|
||||
@@ -372,3 +432,268 @@ async function handleWindowNew(
|
||||
const result = await browser.newWindow(command.viewport);
|
||||
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';
|
||||
|
||||
/**
|
||||
@@ -9,6 +9,8 @@ export class BrowserManager {
|
||||
private contexts: BrowserContext[] = [];
|
||||
private pages: Page[] = [];
|
||||
private activePageIndex: number = 0;
|
||||
private activeFrame: Frame | null = null;
|
||||
private dialogHandler: ((dialog: Dialog) => Promise<void>) | null = null;
|
||||
|
||||
/**
|
||||
* Check if browser is launched
|
||||
@@ -27,6 +29,87 @@ export class BrowserManager {
|
||||
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
|
||||
*/
|
||||
|
||||
+283
-3
@@ -60,18 +60,53 @@ ${c('yellow', 'Usage:')}
|
||||
${c('yellow', 'Commands:')}
|
||||
${c('cyan', 'open')} <url> Open a URL in the browser
|
||||
${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', 'fill')} <selector> <value> Clear and fill input
|
||||
${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', 'screenshot')} [path] Take a screenshot
|
||||
${c('cyan', 'pdf')} <path> Save page as PDF
|
||||
${c('cyan', 'snapshot')} Get accessibility tree (for agents)
|
||||
${c('cyan', 'extract')} <selector> Extract element content
|
||||
${c('cyan', 'eval')} <script> Evaluate JavaScript
|
||||
${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('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('cyan', 'tab new')} Open a new tab
|
||||
${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`));
|
||||
} else if (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');
|
||||
} else if (data.launched) {
|
||||
console.log(c('green', '✓'), 'Browser launched');
|
||||
@@ -204,6 +269,7 @@ async function main(): Promise<void> {
|
||||
const prev = args[i - 1];
|
||||
if (prev === '--selector' || prev === '-s') return false;
|
||||
if (prev === '--session') return false;
|
||||
if (prev === '--name' || prev === '-n') return false;
|
||||
return true;
|
||||
});
|
||||
const command = cleanArgs[0];
|
||||
@@ -215,6 +281,16 @@ async function main(): Promise<void> {
|
||||
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();
|
||||
let cmd: Record<string, unknown>;
|
||||
|
||||
@@ -254,6 +330,80 @@ async function main(): Promise<void> {
|
||||
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': {
|
||||
const key = cleanArgs[1];
|
||||
if (!key) {
|
||||
@@ -357,6 +507,136 @@ async function main(): Promise<void> {
|
||||
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 'quit':
|
||||
case 'exit': {
|
||||
|
||||
+153
@@ -40,6 +40,138 @@ const typeSchema = baseCommandSchema.extend({
|
||||
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({
|
||||
action: z.literal('press'),
|
||||
key: z.string().min(1),
|
||||
@@ -134,6 +266,19 @@ const commandSchema = z.discriminatedUnion('action', [
|
||||
navigateSchema,
|
||||
clickSchema,
|
||||
typeSchema,
|
||||
fillSchema,
|
||||
checkSchema,
|
||||
uncheckSchema,
|
||||
uploadSchema,
|
||||
dblclickSchema,
|
||||
focusSchema,
|
||||
dragSchema,
|
||||
frameSchema,
|
||||
mainframeSchema,
|
||||
getByRoleSchema,
|
||||
getByTextSchema,
|
||||
getByLabelSchema,
|
||||
getByPlaceholderSchema,
|
||||
pressSchema,
|
||||
screenshotSchema,
|
||||
snapshotSchema,
|
||||
@@ -149,6 +294,14 @@ const commandSchema = z.discriminatedUnion('action', [
|
||||
tabSwitchSchema,
|
||||
tabCloseSchema,
|
||||
windowNewSchema,
|
||||
cookiesGetSchema,
|
||||
cookiesSetSchema,
|
||||
cookiesClearSchema,
|
||||
storageGetSchema,
|
||||
storageSetSchema,
|
||||
storageClearSchema,
|
||||
dialogSchema,
|
||||
pdfSchema,
|
||||
]);
|
||||
|
||||
// Parse result type
|
||||
|
||||
+154
-1
@@ -36,6 +36,138 @@ export interface TypeCommand extends BaseCommand {
|
||||
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 {
|
||||
action: 'press';
|
||||
key: string;
|
||||
@@ -127,6 +259,19 @@ export type Command =
|
||||
| NavigateCommand
|
||||
| ClickCommand
|
||||
| TypeCommand
|
||||
| FillCommand
|
||||
| CheckCommand
|
||||
| UncheckCommand
|
||||
| UploadCommand
|
||||
| DoubleClickCommand
|
||||
| FocusCommand
|
||||
| DragCommand
|
||||
| FrameCommand
|
||||
| MainFrameCommand
|
||||
| GetByRoleCommand
|
||||
| GetByTextCommand
|
||||
| GetByLabelCommand
|
||||
| GetByPlaceholderCommand
|
||||
| PressCommand
|
||||
| ScreenshotCommand
|
||||
| SnapshotCommand
|
||||
@@ -141,7 +286,15 @@ export type Command =
|
||||
| TabListCommand
|
||||
| TabSwitchCommand
|
||||
| TabCloseCommand
|
||||
| WindowNewCommand;
|
||||
| WindowNewCommand
|
||||
| CookiesGetCommand
|
||||
| CookiesSetCommand
|
||||
| CookiesClearCommand
|
||||
| StorageGetCommand
|
||||
| StorageSetCommand
|
||||
| StorageClearCommand
|
||||
| DialogCommand
|
||||
| PdfCommand;
|
||||
|
||||
// Response types
|
||||
export interface SuccessResponse<T = unknown> {
|
||||
|
||||
Reference in New Issue
Block a user