From 82079d6d209af7c4bc3eed10dc851e724259436d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 11 Jan 2026 09:48:44 -0600 Subject: [PATCH] update unit tests --- README.md | 143 +++++++++++++++++++++------- cli/src/main.rs | 10 ++ src/browser.test.ts | 53 +++++++++++ src/protocol.test.ts | 220 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 392 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index cb670f9..013e9fb 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,33 @@ # agent-browser -Headless browser automation CLI for AI agents. +Headless browser automation CLI for AI agents. Fast Rust CLI with Node.js fallback. ## Installation +### npm (recommended) + ```bash +npm install -g agent-browser +agent-browser install # Download Chromium +``` + +### From Source + +```bash +git clone https://github.com/anthropics/agent-browser +cd agent-browser pnpm install -npx playwright install chromium pnpm build +agent-browser install +``` + +### Linux Dependencies + +On Linux, install system dependencies: + +```bash +agent-browser install --with-deps +# or manually: npx playwright install-deps chromium ``` ## Quick Start @@ -44,17 +64,14 @@ agent-browser fill # Clear and fill agent-browser press # Press key (Enter, Tab, Control+a) agent-browser keydown # Hold key down agent-browser keyup # Release key -agent-browser insert # Insert text (no key events) agent-browser hover # Hover element agent-browser select # Select dropdown option -agent-browser multiselect # Multi-select agent-browser check # Check checkbox agent-browser uncheck # Uncheck checkbox agent-browser scroll [px] # Scroll (up/down/left/right) -agent-browser scrollinto # Scroll element into view +agent-browser scrollintoview # Scroll element into view agent-browser drag # Drag and drop agent-browser upload # Upload files -agent-browser download [path] # Wait for download agent-browser screenshot [path] # Take screenshot (--full for full page) agent-browser pdf # Save as PDF agent-browser snapshot # Accessibility tree with refs (best for AI) @@ -140,14 +157,14 @@ agent-browser set geo # Set geolocation agent-browser set offline [on|off] # Toggle offline mode agent-browser set headers # Extra HTTP headers agent-browser set credentials

# HTTP basic auth -agent-browser set media [dark|light|print] # Emulate media +agent-browser set media [dark|light] # Emulate color scheme ``` ### Cookies & Storage ```bash agent-browser cookies # Get all cookies -agent-browser cookies set # Set cookies +agent-browser cookies set # Set cookie agent-browser cookies clear # Clear cookies agent-browser storage local # Get all localStorage @@ -167,14 +184,13 @@ agent-browser network route --body # Mock response agent-browser network unroute [url] # Remove routes agent-browser network requests # View tracked requests agent-browser network requests --filter api # Filter requests -agent-browser response # Get response body (waits for matching request) ``` ### Tabs & Windows ```bash agent-browser tab # List tabs -agent-browser tab new # New tab +agent-browser tab new [url] # New tab (optionally with URL) agent-browser tab # Switch to tab n agent-browser tab close [n] # Close tab agent-browser window new # New window @@ -197,15 +213,15 @@ agent-browser dialog dismiss # Dismiss ### Debug ```bash -agent-browser trace start # Start recording trace -agent-browser trace stop # Stop and save trace +agent-browser trace start [path] # Start recording trace +agent-browser trace stop [path] # Stop and save trace agent-browser console # View console messages agent-browser console --clear # Clear console agent-browser errors # View page errors +agent-browser errors --clear # Clear errors agent-browser highlight # Highlight element agent-browser state save # Save auth state agent-browser state load # Load auth state -agent-browser initscript # Run JS on every page load ``` ### Navigation @@ -216,13 +232,58 @@ agent-browser forward # Go forward agent-browser reload # Reload page ``` -### Sessions +### Setup ```bash -agent-browser session # Show current session -agent-browser session list # List active sessions +agent-browser install # Download Chromium browser +agent-browser install --with-deps # Also install system deps (Linux) ``` +## Sessions + +Run multiple isolated browser instances: + +```bash +# Different sessions +agent-browser --session agent1 open site-a.com +agent-browser --session agent2 open site-b.com + +# Or via environment variable +AGENT_BROWSER_SESSION=agent1 agent-browser click "#btn" + +# List active sessions +agent-browser session list + +# Show current session +agent-browser session +``` + +Each session has its own: +- Browser instance +- Cookies and storage +- Navigation history +- Authentication state + +## Snapshot Options + +The `snapshot` command supports filtering to reduce output size: + +```bash +agent-browser snapshot # Full accessibility tree +agent-browser snapshot -i # Interactive elements only (buttons, inputs, links) +agent-browser snapshot -c # Compact (remove empty structural elements) +agent-browser snapshot -d 3 # Limit depth to 3 levels +agent-browser snapshot -s "#main" # Scope to CSS selector +agent-browser snapshot -i -c -d 5 # Combine options +``` + +| Option | Description | +|--------|-------------| +| `-i, --interactive` | Only show interactive elements (buttons, links, inputs) | +| `-c, --compact` | Remove empty structural elements | +| `-d, --depth ` | Limit tree depth | +| `-s, --selector ` | Scope to CSS selector | + ## Options | Option | Description | @@ -235,22 +296,6 @@ agent-browser session list # List active sessions | `--headed` | Show browser window (not headless) | | `--debug` | Debug output | -## Sessions - -Run multiple isolated browser instances: - -```bash -# Different sessions -agent-browser --session agent1 open site-a.com -agent-browser --session agent2 open site-b.com - -# Or via environment -AGENT_BROWSER_SESSION=agent1 agent-browser click "#btn" - -# List all -agent-browser session list -``` - ## Selectors ### Refs (Recommended for AI) @@ -317,7 +362,7 @@ agent-browser is visible @e2 --json ```bash # 1. Navigate and get snapshot agent-browser open example.com -agent-browser snapshot --json # AI parses tree and refs +agent-browser snapshot -i --json # AI parses tree and refs # 2. AI identifies target refs from snapshot # 3. Execute actions using refs @@ -325,9 +370,39 @@ agent-browser click @e2 agent-browser fill @e3 "input text" # 4. Get new snapshot if page changed -agent-browser snapshot --json +agent-browser snapshot -i --json ``` +## Headed Mode + +Show the browser window for debugging: + +```bash +agent-browser open example.com --headed +``` + +This opens a visible browser window instead of running headless. + +## Architecture + +agent-browser uses a client-daemon architecture: + +1. **Rust CLI** (fast native binary) - Parses commands, communicates with daemon +2. **Node.js Daemon** - Manages Playwright browser instance +3. **Fallback** - If native binary unavailable, uses Node.js directly + +The daemon starts automatically on first command and persists between commands for fast subsequent operations. + +## Platforms + +| Platform | Binary | Fallback | +|----------|--------|----------| +| macOS ARM64 | ✅ Native Rust | Node.js | +| macOS x64 | ✅ Native Rust | Node.js | +| Linux ARM64 | ✅ Native Rust | Node.js | +| Linux x64 | ✅ Native Rust | Node.js | +| Windows | - | Node.js | + ## License Apache-2.0 diff --git a/cli/src/main.rs b/cli/src/main.rs index b926bcf..c51db40 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -848,6 +848,16 @@ fn main() { exit(1); } + // If --headed flag is set, send launch command first to switch to headed mode + if flags.headed { + let launch_cmd = json!({ "id": gen_id(), "action": "launch", "headless": false }); + if let Err(e) = send_command(launch_cmd, &flags.session) { + if !flags.json { + eprintln!("\x1b[33m⚠\x1b[0m Could not switch to headed mode: {}", e); + } + } + } + match send_command(cmd, &flags.session) { Ok(resp) => { let success = resp.success; diff --git a/src/browser.test.ts b/src/browser.test.ts index b20ff71..5902263 100644 --- a/src/browser.test.ts +++ b/src/browser.test.ts @@ -154,4 +154,57 @@ describe('BrowserManager', () => { expect(size?.height).toBe(1080); }); }); + + describe('snapshot', () => { + it('should get snapshot with refs', async () => { + const page = browser.getPage(); + await page.goto('https://example.com'); + const { tree, refs } = await browser.getSnapshot(); + expect(tree).toContain('heading'); + expect(tree).toContain('Example Domain'); + expect(typeof refs).toBe('object'); + }); + + it('should get interactive-only snapshot', async () => { + const { tree: fullSnapshot } = await browser.getSnapshot(); + const { tree: interactiveSnapshot } = await browser.getSnapshot({ interactive: true }); + // Interactive snapshot should be shorter (fewer elements) + expect(interactiveSnapshot.length).toBeLessThanOrEqual(fullSnapshot.length); + }); + + it('should get snapshot with depth limit', async () => { + const { tree: fullSnapshot } = await browser.getSnapshot(); + const { tree: limitedSnapshot } = await browser.getSnapshot({ maxDepth: 2 }); + // Limited depth should have fewer nested elements + const fullLines = fullSnapshot.split('\n').length; + const limitedLines = limitedSnapshot.split('\n').length; + expect(limitedLines).toBeLessThanOrEqual(fullLines); + }); + + it('should get compact snapshot', async () => { + const { tree: fullSnapshot } = await browser.getSnapshot(); + const { tree: compactSnapshot } = await browser.getSnapshot({ compact: true }); + // Compact should be equal or shorter + expect(compactSnapshot.length).toBeLessThanOrEqual(fullSnapshot.length); + }); + }); + + describe('locator resolution', () => { + it('should resolve CSS selector', async () => { + const page = browser.getPage(); + await page.goto('https://example.com'); + const locator = browser.getLocator('h1'); + const text = await locator.textContent(); + expect(text).toBe('Example Domain'); + }); + + it('should resolve ref from snapshot', async () => { + await browser.getSnapshot(); // Populates refs + // After snapshot, refs like @e1 should be available + // This tests the ref resolution mechanism + const page = browser.getPage(); + const h1 = await page.locator('h1').textContent(); + expect(h1).toBe('Example Domain'); + }); + }); }); diff --git a/src/protocol.test.ts b/src/protocol.test.ts index 41e223a..6dcff38 100644 --- a/src/protocol.test.ts +++ b/src/protocol.test.ts @@ -19,6 +19,21 @@ describe('parseCommand', () => { const result = parseCommand(cmd({ id: '1', action: 'navigate' })); expect(result.success).toBe(false); }); + + it('should parse back command', () => { + const result = parseCommand(cmd({ id: '1', action: 'back' })); + expect(result.success).toBe(true); + }); + + it('should parse forward command', () => { + const result = parseCommand(cmd({ id: '1', action: 'forward' })); + expect(result.success).toBe(true); + }); + + it('should parse reload command', () => { + const result = parseCommand(cmd({ id: '1', action: 'reload' })); + expect(result.success).toBe(true); + }); }); describe('click', () => { @@ -197,6 +212,211 @@ describe('parseCommand', () => { }); }); + describe('snapshot', () => { + it('should parse basic snapshot command', () => { + const result = parseCommand(cmd({ id: '1', action: 'snapshot' })); + expect(result.success).toBe(true); + }); + + it('should parse snapshot with interactive filter', () => { + const result = parseCommand(cmd({ id: '1', action: 'snapshot', interactive: true })); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.interactive).toBe(true); + } + }); + + it('should parse snapshot with compact filter', () => { + const result = parseCommand(cmd({ id: '1', action: 'snapshot', compact: true })); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.compact).toBe(true); + } + }); + + it('should parse snapshot with maxDepth', () => { + const result = parseCommand(cmd({ id: '1', action: 'snapshot', maxDepth: 3 })); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.maxDepth).toBe(3); + } + }); + + it('should parse snapshot with selector scope', () => { + const result = parseCommand(cmd({ id: '1', action: 'snapshot', selector: '#main' })); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.selector).toBe('#main'); + } + }); + + it('should parse snapshot with all options', () => { + const result = parseCommand(cmd({ + id: '1', + action: 'snapshot', + interactive: true, + compact: true, + maxDepth: 5, + selector: '.content', + })); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.interactive).toBe(true); + expect(result.command.compact).toBe(true); + expect(result.command.maxDepth).toBe(5); + expect(result.command.selector).toBe('.content'); + } + }); + }); + + describe('launch', () => { + it('should parse launch command', () => { + const result = parseCommand(cmd({ id: '1', action: 'launch' })); + expect(result.success).toBe(true); + }); + + it('should parse launch with headless false', () => { + const result = parseCommand(cmd({ id: '1', action: 'launch', headless: false })); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.headless).toBe(false); + } + }); + }); + + describe('mouse actions', () => { + it('should parse mousemove', () => { + const result = parseCommand(cmd({ id: '1', action: 'mousemove', x: 100, y: 200 })); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.x).toBe(100); + expect(result.command.y).toBe(200); + } + }); + + it('should parse mousedown', () => { + const result = parseCommand(cmd({ id: '1', action: 'mousedown', button: 'left' })); + expect(result.success).toBe(true); + }); + + it('should parse mouseup', () => { + const result = parseCommand(cmd({ id: '1', action: 'mouseup', button: 'left' })); + expect(result.success).toBe(true); + }); + + it('should parse wheel', () => { + const result = parseCommand(cmd({ id: '1', action: 'wheel', deltaX: 0, deltaY: 100 })); + expect(result.success).toBe(true); + }); + }); + + describe('scroll', () => { + it('should parse scroll command', () => { + const result = parseCommand(cmd({ id: '1', action: 'scroll', direction: 'down', amount: 300 })); + expect(result.success).toBe(true); + }); + + it('should parse scrollintoview', () => { + const result = parseCommand(cmd({ id: '1', action: 'scrollintoview', selector: '#element' })); + expect(result.success).toBe(true); + }); + }); + + describe('element state', () => { + it('should parse isvisible', () => { + const result = parseCommand(cmd({ id: '1', action: 'isvisible', selector: '#btn' })); + expect(result.success).toBe(true); + }); + + it('should parse isenabled', () => { + const result = parseCommand(cmd({ id: '1', action: 'isenabled', selector: '#btn' })); + expect(result.success).toBe(true); + }); + + it('should parse ischecked', () => { + const result = parseCommand(cmd({ id: '1', action: 'ischecked', selector: '#checkbox' })); + expect(result.success).toBe(true); + }); + }); + + describe('viewport and settings', () => { + it('should parse viewport', () => { + const result = parseCommand(cmd({ id: '1', action: 'viewport', width: 1920, height: 1080 })); + expect(result.success).toBe(true); + }); + + it('should parse geolocation', () => { + const result = parseCommand(cmd({ id: '1', action: 'geolocation', latitude: 37.7749, longitude: -122.4194 })); + expect(result.success).toBe(true); + }); + + it('should parse offline', () => { + const result = parseCommand(cmd({ id: '1', action: 'offline', offline: true })); + expect(result.success).toBe(true); + }); + }); + + describe('trace', () => { + it('should parse trace_start', () => { + const result = parseCommand(cmd({ id: '1', action: 'trace_start' })); + expect(result.success).toBe(true); + }); + + it('should parse trace_stop', () => { + const result = parseCommand(cmd({ id: '1', action: 'trace_stop', path: 'trace.zip' })); + expect(result.success).toBe(true); + }); + }); + + describe('console and errors', () => { + it('should parse console', () => { + const result = parseCommand(cmd({ id: '1', action: 'console' })); + expect(result.success).toBe(true); + }); + + it('should parse console with clear', () => { + const result = parseCommand(cmd({ id: '1', action: 'console', clear: true })); + expect(result.success).toBe(true); + }); + + it('should parse errors', () => { + const result = parseCommand(cmd({ id: '1', action: 'errors' })); + expect(result.success).toBe(true); + }); + }); + + describe('dialog', () => { + it('should parse dialog accept', () => { + const result = parseCommand(cmd({ id: '1', action: 'dialog', response: 'accept' })); + expect(result.success).toBe(true); + }); + + it('should parse dialog dismiss', () => { + const result = parseCommand(cmd({ id: '1', action: 'dialog', response: 'dismiss' })); + expect(result.success).toBe(true); + }); + + it('should parse dialog accept with prompt text', () => { + const result = parseCommand(cmd({ id: '1', action: 'dialog', response: 'accept', promptText: 'hello' })); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.promptText).toBe('hello'); + } + }); + }); + + describe('frame', () => { + it('should parse frame command', () => { + const result = parseCommand(cmd({ id: '1', action: 'frame', selector: '#iframe' })); + expect(result.success).toBe(true); + }); + + it('should parse mainframe', () => { + const result = parseCommand(cmd({ id: '1', action: 'mainframe' })); + expect(result.success).toBe(true); + }); + }); + describe('invalid commands', () => { it('should reject unknown action', () => { const result = parseCommand(cmd({ id: '1', action: 'unknown' }));