From 864d3e60f0797ecef5122356e74617a399c800bc Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 10 Jan 2026 02:13:49 +0100 Subject: [PATCH 1/4] Initial commit with task details Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: https://github.com/link-foundation/browser-commander/issues/19 --- CLAUDE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..96b9ba1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: https://github.com/link-foundation/browser-commander/issues/19 +Your prepared branch: issue-19-bef12b396c1f +Your prepared working directory: /tmp/gh-issue-solver-1768007628261 + +Proceed. From 8b86dd76d2dcad9a183686514e7c6f28c0f78bff Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 10 Jan 2026 02:16:54 +0100 Subject: [PATCH 2/4] fix: include README.md in npm package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copy README.md from repository root to js/ directory before npm publish so the package documentation is visible on npmjs.com. This fixes the issue where the npm package was missing documentation because the package is published from the js/ subdirectory. Fixes #19 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/js.yml | 12 ++++++++++++ .gitignore | 3 +++ js/.changeset/fix-readme-npm-publish.md | 7 +++++++ 3 files changed, 22 insertions(+) create mode 100644 js/.changeset/fix-readme-npm-publish.md diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml index cf810d7..5aef708 100644 --- a/.github/workflows/js.yml +++ b/.github/workflows/js.yml @@ -223,6 +223,12 @@ jobs: id: version run: node scripts/version-and-commit.mjs --mode changeset + - name: Copy README for npm package + # Copy root README.md to js/ directory so npm package includes documentation + if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' + working-directory: . + run: cp README.md js/README.md + - name: Publish to npm # Run if version was committed OR if a previous attempt already committed (for re-runs) if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' @@ -274,6 +280,12 @@ jobs: id: version run: node scripts/version-and-commit.mjs --mode instant --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" + - name: Copy README for npm package + # Copy root README.md to js/ directory so npm package includes documentation + if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' + working-directory: . + run: cp README.md js/README.md + - name: Publish to npm # Run if version was committed OR if a previous attempt already committed (for re-runs) if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' diff --git a/.gitignore b/.gitignore index dc87f5a..47d5faa 100644 --- a/.gitignore +++ b/.gitignore @@ -140,3 +140,6 @@ vite.config.ts.timestamp-* # jscpd reports reports/ + +# js/README.md is copied from root during npm publish +js/README.md diff --git a/js/.changeset/fix-readme-npm-publish.md b/js/.changeset/fix-readme-npm-publish.md new file mode 100644 index 0000000..131a557 --- /dev/null +++ b/js/.changeset/fix-readme-npm-publish.md @@ -0,0 +1,7 @@ +--- +'browser-commander': patch +--- + +Include README.md in npm package + +The npm package was missing the README documentation because the package is published from the js/ subdirectory, but README.md is in the repository root. This fix adds a step in the CI/CD workflow to copy README.md from the repository root to the js/ directory before publishing to npm. From 051b1f71493ce301f442d3bb59284e2299778c8a Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 10 Jan 2026 02:19:17 +0100 Subject: [PATCH 3/4] chore: remove task information file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 96b9ba1..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -Issue to solve: https://github.com/link-foundation/browser-commander/issues/19 -Your prepared branch: issue-19-bef12b396c1f -Your prepared working directory: /tmp/gh-issue-solver-1768007628261 - -Proceed. From f9a7e232165a1ff0c0cab089ef1e8f81f32310a2 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 10 Jan 2026 14:14:44 +0100 Subject: [PATCH 4/4] fix: add language-specific README files for npm and crates.io MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create js/README.md with JavaScript/npm-specific documentation - Create rust/README.md with Rust/Cargo-specific documentation - Update root README.md to be language-agnostic with links to implementations - Remove README copy step from CI workflow (no longer needed) - Remove js/README.md from .gitignore (now tracked in git) This approach provides proper documentation for each package manager: - npm package gets js/README.md directly from the repository - crates.io can use rust/README.md - Root README provides overview linking to both implementations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/js.yml | 12 - .gitignore | 3 - README.md | 303 +-------------------- js/.changeset/fix-readme-npm-publish.md | 8 +- js/README.md | 335 ++++++++++++++++++++++++ rust/README.md | 175 +++++++++++++ 6 files changed, 529 insertions(+), 307 deletions(-) create mode 100644 js/README.md create mode 100644 rust/README.md diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml index 5aef708..cf810d7 100644 --- a/.github/workflows/js.yml +++ b/.github/workflows/js.yml @@ -223,12 +223,6 @@ jobs: id: version run: node scripts/version-and-commit.mjs --mode changeset - - name: Copy README for npm package - # Copy root README.md to js/ directory so npm package includes documentation - if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' - working-directory: . - run: cp README.md js/README.md - - name: Publish to npm # Run if version was committed OR if a previous attempt already committed (for re-runs) if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' @@ -280,12 +274,6 @@ jobs: id: version run: node scripts/version-and-commit.mjs --mode instant --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" - - name: Copy README for npm package - # Copy root README.md to js/ directory so npm package includes documentation - if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' - working-directory: . - run: cp README.md js/README.md - - name: Publish to npm # Run if version was committed OR if a previous attempt already committed (for re-runs) if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' diff --git a/.gitignore b/.gitignore index 47d5faa..dc87f5a 100644 --- a/.gitignore +++ b/.gitignore @@ -140,6 +140,3 @@ vite.config.ts.timestamp-* # jscpd reports reports/ - -# js/README.md is copied from root during npm publish -js/README.md diff --git a/README.md b/README.md index 506451d..9232824 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,13 @@ # Browser Commander -A universal browser automation library that supports both Playwright and Puppeteer with a unified API. The key focus is on **stoppable page triggers** - ensuring automation logic is properly mounted/unmounted during page navigation. +A universal browser automation library with a unified API across multiple browser engines and programming languages. The key focus is on **stoppable page triggers** - ensuring automation logic is properly mounted/unmounted during page navigation. -## Installation +## Available Implementations -```bash -npm install browser-commander -``` - -You'll also need either Playwright or Puppeteer: - -```bash -# With Playwright -npm install playwright - -# Or with Puppeteer -npm install puppeteer -``` +| Language | Package | Status | +|----------|---------|--------| +| JavaScript/TypeScript | [browser-commander](https://www.npmjs.com/package/browser-commander) | [![npm](https://img.shields.io/npm/v/browser-commander)](https://www.npmjs.com/package/browser-commander) | +| Rust | [browser-commander](https://crates.io/crates/browser-commander) | [![crates.io](https://img.shields.io/crates/v/browser-commander)](https://crates.io/crates/browser-commander) | ## Core Concept: Page State Machine @@ -35,103 +26,9 @@ Browser Commander manages the browser as a state machine with two states: **WORKING STATE**: Page is fully loaded (30 seconds of network idle). Page triggers can safely interact with DOM. -## Quick Start - -```javascript -import { - launchBrowser, - makeBrowserCommander, - makeUrlCondition, -} from 'browser-commander'; - -// 1. Launch browser -const { browser, page } = await launchBrowser({ engine: 'playwright' }); - -// 2. Create commander -const commander = makeBrowserCommander({ page, verbose: true }); - -// 3. Register page trigger with condition and action -commander.pageTrigger({ - name: 'example-trigger', - condition: makeUrlCondition('*example.com*'), // matches URLs containing 'example.com' - action: async (ctx) => { - // ctx.commander has all methods, but they throw ActionStoppedError if navigation happens - // ctx.checkStopped() - call in loops to check if should stop - // ctx.abortSignal - use with fetch() for cancellation - // ctx.onCleanup(fn) - register cleanup when action stops - - console.log(`Processing: ${ctx.url}`); - - // Safe iteration - stops if navigation detected - await ctx.forEach(['item1', 'item2'], async (item) => { - await ctx.commander.clickButton({ selector: `[data-id="${item}"]` }); - }); - }, -}); - -// 4. Navigate - action auto-starts when page is ready -await commander.goto({ url: 'https://example.com' }); - -// 5. Cleanup -await commander.destroy(); -await browser.close(); -``` - -## URL Condition Helpers - -The `makeUrlCondition` helper makes it easy to create URL matching conditions: - -```javascript -import { - makeUrlCondition, - allConditions, - anyCondition, - notCondition, -} from 'browser-commander'; - -// Exact URL match -makeUrlCondition('https://example.com/page'); - -// Contains substring (use * wildcards) -makeUrlCondition('*checkout*'); // URL contains 'checkout' -makeUrlCondition('*example.com*'); // URL contains 'example.com' - -// Starts with / ends with -makeUrlCondition('/api/*'); // starts with '/api/' -makeUrlCondition('*.json'); // ends with '.json' - -// Express-style route patterns -makeUrlCondition('/vacancy/:id'); // matches /vacancy/123 -makeUrlCondition('https://hh.ru/vacancy/:vacancyId'); // matches specific domain + path -makeUrlCondition('/user/:userId/profile'); // multiple segments - -// RegExp -makeUrlCondition(/\/product\/\d+/); - -// Custom function (receives full context) -makeUrlCondition((url, ctx) => { - const parsed = new URL(url); - return ( - parsed.pathname.startsWith('/admin') && parsed.searchParams.has('edit') - ); -}); - -// Combine conditions -allConditions( - makeUrlCondition('*example.com*'), - makeUrlCondition('*/checkout*') -); // Both must match - -anyCondition(makeUrlCondition('*/cart*'), makeUrlCondition('*/checkout*')); // Either matches - -notCondition(makeUrlCondition('*/admin*')); // Negation -``` - ## Page Trigger Lifecycle -### The Guarantee - -When navigation is detected: +The library provides a guarantee when navigation is detected: 1. **Action is signaled to stop** (AbortController.abort()) 2. **Wait for action to finish** (up to 10 seconds for graceful cleanup) @@ -143,192 +40,16 @@ This ensures: - Actions can do proper cleanup (clear intervals, save state) - No race conditions between action and navigation -## Action Context API - -When your action is called, it receives a context object with these properties: - -```javascript -commander.pageTrigger({ - name: 'my-trigger', - condition: makeUrlCondition('*/checkout*'), - action: async (ctx) => { - // Current URL - ctx.url; // 'https://example.com/checkout' - - // Trigger name (for debugging) - ctx.triggerName; // 'my-trigger' - - // Check if action should stop - ctx.isStopped(); // Returns true if navigation detected - - // Throw ActionStoppedError if stopped (use in manual loops) - ctx.checkStopped(); - - // AbortSignal - use with fetch() or other cancellable APIs - ctx.abortSignal; - - // Safe wait (throws if stopped during wait) - await ctx.wait(1000); - - // Safe iteration (checks stopped between items) - await ctx.forEach(items, async (item) => { - await ctx.commander.clickButton({ selector: item.selector }); - }); - - // Register cleanup (runs when action stops) - ctx.onCleanup(() => { - console.log('Cleaning up...'); - }); - - // Commander with all methods wrapped to throw on stop - await ctx.commander.fillTextArea({ selector: 'input', text: 'hello' }); - - // Raw commander (use carefully - does not auto-throw) - ctx.rawCommander; - }, -}); -``` - -## API Reference +## Getting Started -### launchBrowser(options) +For installation and usage instructions, see the documentation for your preferred language: -```javascript -const { browser, page } = await launchBrowser({ - engine: 'playwright', // 'playwright' or 'puppeteer' - headless: false, // Run in headless mode - userDataDir: '~/.hh-apply/playwright-data', // Browser profile directory - slowMo: 150, // Slow down operations (ms) - verbose: false, // Enable debug logging - args: ['--no-sandbox', '--disable-setuid-sandbox'], // Custom Chrome args to append -}); -``` - -The `args` option allows passing custom Chrome arguments, which is useful for headless server environments (Docker, CI/CD) that require flags like `--no-sandbox`. - -### makeBrowserCommander(options) - -```javascript -const commander = makeBrowserCommander({ - page, // Required: Playwright/Puppeteer page - verbose: false, // Enable debug logging - enableNetworkTracking: true, // Track HTTP requests - enableNavigationManager: true, // Enable navigation events -}); -``` - -### commander.pageTrigger(config) - -```javascript -const unregister = commander.pageTrigger({ - name: 'trigger-name', // For debugging - condition: (ctx) => boolean, // When to run (receives {url, commander}) - action: async (ctx) => void, // What to do - priority: 0, // Higher runs first -}); -``` - -### commander.goto(options) - -```javascript -await commander.goto({ - url: 'https://example.com', - waitUntil: 'domcontentloaded', // Playwright/Puppeteer option - timeout: 60000, -}); -``` - -### commander.clickButton(options) - -```javascript -await commander.clickButton({ - selector: 'button.submit', - scrollIntoView: true, - waitForNavigation: true, -}); -``` - -### commander.fillTextArea(options) - -```javascript -await commander.fillTextArea({ - selector: 'textarea.message', - text: 'Hello world', - checkEmpty: true, -}); -``` - -### commander.destroy() - -```javascript -await commander.destroy(); // Stop actions, cleanup -``` - -## Best Practices - -### 1. Use ctx.forEach for Loops - -```javascript -// BAD: Won't stop on navigation -for (const item of items) { - await ctx.commander.click({ selector: item }); -} - -// GOOD: Stops immediately on navigation -await ctx.forEach(items, async (item) => { - await ctx.commander.click({ selector: item }); -}); -``` - -### 2. Use ctx.checkStopped for Complex Logic - -```javascript -action: async (ctx) => { - while (hasMorePages) { - ctx.checkStopped(); // Throws if navigation detected - - await processPage(ctx); - hasMorePages = await ctx.commander.isVisible({ selector: '.next' }); - } -}; -``` - -### 3. Register Cleanup for Resources - -```javascript -action: async (ctx) => { - const intervalId = setInterval(updateStatus, 1000); - - ctx.onCleanup(() => { - clearInterval(intervalId); - console.log('Interval cleared'); - }); - - // ... rest of action -}; -``` - -### 4. Use ctx.abortSignal with Fetch - -```javascript -action: async (ctx) => { - const response = await fetch(url, { - signal: ctx.abortSignal, // Cancels on navigation - }); -}; -``` - -## Debugging - -Enable verbose mode for detailed logs: - -```javascript -const commander = makeBrowserCommander({ page, verbose: true }); -``` +- **JavaScript/TypeScript**: See [js/README.md](js/README.md) +- **Rust**: See [rust/README.md](rust/README.md) ## Architecture -See [src/ARCHITECTURE.md](src/ARCHITECTURE.md) for detailed architecture documentation. +See [js/src/ARCHITECTURE.md](js/src/ARCHITECTURE.md) for detailed architecture documentation. ## License diff --git a/js/.changeset/fix-readme-npm-publish.md b/js/.changeset/fix-readme-npm-publish.md index 131a557..0fb0f77 100644 --- a/js/.changeset/fix-readme-npm-publish.md +++ b/js/.changeset/fix-readme-npm-publish.md @@ -4,4 +4,10 @@ Include README.md in npm package -The npm package was missing the README documentation because the package is published from the js/ subdirectory, but README.md is in the repository root. This fix adds a step in the CI/CD workflow to copy README.md from the repository root to the js/ directory before publishing to npm. +Added language-specific README.md files for each implementation: + +- js/README.md: JavaScript/npm-specific documentation with installation and API usage +- rust/README.md: Rust/Cargo-specific documentation +- Root README.md: Common overview linking to both implementations + +The npm package now includes the JavaScript-specific README.md directly from the js/ directory. diff --git a/js/README.md b/js/README.md new file mode 100644 index 0000000..fa54610 --- /dev/null +++ b/js/README.md @@ -0,0 +1,335 @@ +# Browser Commander + +A universal browser automation library for JavaScript/TypeScript that supports both Playwright and Puppeteer with a unified API. The key focus is on **stoppable page triggers** - ensuring automation logic is properly mounted/unmounted during page navigation. + +## Installation + +```bash +npm install browser-commander +``` + +You'll also need either Playwright or Puppeteer: + +```bash +# With Playwright +npm install playwright + +# Or with Puppeteer +npm install puppeteer +``` + +## Core Concept: Page State Machine + +Browser Commander manages the browser as a state machine with two states: + +``` ++------------------+ +------------------+ +| | navigation start | | +| WORKING STATE | -------------------> | LOADING STATE | +| (action runs) | | (wait only) | +| | <----------------- | | ++------------------+ page ready +------------------+ +``` + +**LOADING STATE**: Page is loading. Only waiting/tracking operations are allowed. No automation logic runs. + +**WORKING STATE**: Page is fully loaded (30 seconds of network idle). Page triggers can safely interact with DOM. + +## Quick Start + +```javascript +import { + launchBrowser, + makeBrowserCommander, + makeUrlCondition, +} from 'browser-commander'; + +// 1. Launch browser +const { browser, page } = await launchBrowser({ engine: 'playwright' }); + +// 2. Create commander +const commander = makeBrowserCommander({ page, verbose: true }); + +// 3. Register page trigger with condition and action +commander.pageTrigger({ + name: 'example-trigger', + condition: makeUrlCondition('*example.com*'), // matches URLs containing 'example.com' + action: async (ctx) => { + // ctx.commander has all methods, but they throw ActionStoppedError if navigation happens + // ctx.checkStopped() - call in loops to check if should stop + // ctx.abortSignal - use with fetch() for cancellation + // ctx.onCleanup(fn) - register cleanup when action stops + + console.log(`Processing: ${ctx.url}`); + + // Safe iteration - stops if navigation detected + await ctx.forEach(['item1', 'item2'], async (item) => { + await ctx.commander.clickButton({ selector: `[data-id="${item}"]` }); + }); + }, +}); + +// 4. Navigate - action auto-starts when page is ready +await commander.goto({ url: 'https://example.com' }); + +// 5. Cleanup +await commander.destroy(); +await browser.close(); +``` + +## URL Condition Helpers + +The `makeUrlCondition` helper makes it easy to create URL matching conditions: + +```javascript +import { + makeUrlCondition, + allConditions, + anyCondition, + notCondition, +} from 'browser-commander'; + +// Exact URL match +makeUrlCondition('https://example.com/page'); + +// Contains substring (use * wildcards) +makeUrlCondition('*checkout*'); // URL contains 'checkout' +makeUrlCondition('*example.com*'); // URL contains 'example.com' + +// Starts with / ends with +makeUrlCondition('/api/*'); // starts with '/api/' +makeUrlCondition('*.json'); // ends with '.json' + +// Express-style route patterns +makeUrlCondition('/vacancy/:id'); // matches /vacancy/123 +makeUrlCondition('https://hh.ru/vacancy/:vacancyId'); // matches specific domain + path +makeUrlCondition('/user/:userId/profile'); // multiple segments + +// RegExp +makeUrlCondition(/\/product\/\d+/); + +// Custom function (receives full context) +makeUrlCondition((url, ctx) => { + const parsed = new URL(url); + return ( + parsed.pathname.startsWith('/admin') && parsed.searchParams.has('edit') + ); +}); + +// Combine conditions +allConditions( + makeUrlCondition('*example.com*'), + makeUrlCondition('*/checkout*') +); // Both must match + +anyCondition(makeUrlCondition('*/cart*'), makeUrlCondition('*/checkout*')); // Either matches + +notCondition(makeUrlCondition('*/admin*')); // Negation +``` + +## Page Trigger Lifecycle + +### The Guarantee + +When navigation is detected: + +1. **Action is signaled to stop** (AbortController.abort()) +2. **Wait for action to finish** (up to 10 seconds for graceful cleanup) +3. **Only then start waiting for page load** + +This ensures: + +- No DOM operations on stale/loading pages +- Actions can do proper cleanup (clear intervals, save state) +- No race conditions between action and navigation + +## Action Context API + +When your action is called, it receives a context object with these properties: + +```javascript +commander.pageTrigger({ + name: 'my-trigger', + condition: makeUrlCondition('*/checkout*'), + action: async (ctx) => { + // Current URL + ctx.url; // 'https://example.com/checkout' + + // Trigger name (for debugging) + ctx.triggerName; // 'my-trigger' + + // Check if action should stop + ctx.isStopped(); // Returns true if navigation detected + + // Throw ActionStoppedError if stopped (use in manual loops) + ctx.checkStopped(); + + // AbortSignal - use with fetch() or other cancellable APIs + ctx.abortSignal; + + // Safe wait (throws if stopped during wait) + await ctx.wait(1000); + + // Safe iteration (checks stopped between items) + await ctx.forEach(items, async (item) => { + await ctx.commander.clickButton({ selector: item.selector }); + }); + + // Register cleanup (runs when action stops) + ctx.onCleanup(() => { + console.log('Cleaning up...'); + }); + + // Commander with all methods wrapped to throw on stop + await ctx.commander.fillTextArea({ selector: 'input', text: 'hello' }); + + // Raw commander (use carefully - does not auto-throw) + ctx.rawCommander; + }, +}); +``` + +## API Reference + +### launchBrowser(options) + +```javascript +const { browser, page } = await launchBrowser({ + engine: 'playwright', // 'playwright' or 'puppeteer' + headless: false, // Run in headless mode + userDataDir: '~/.hh-apply/playwright-data', // Browser profile directory + slowMo: 150, // Slow down operations (ms) + verbose: false, // Enable debug logging + args: ['--no-sandbox', '--disable-setuid-sandbox'], // Custom Chrome args to append +}); +``` + +The `args` option allows passing custom Chrome arguments, which is useful for headless server environments (Docker, CI/CD) that require flags like `--no-sandbox`. + +### makeBrowserCommander(options) + +```javascript +const commander = makeBrowserCommander({ + page, // Required: Playwright/Puppeteer page + verbose: false, // Enable debug logging + enableNetworkTracking: true, // Track HTTP requests + enableNavigationManager: true, // Enable navigation events +}); +``` + +### commander.pageTrigger(config) + +```javascript +const unregister = commander.pageTrigger({ + name: 'trigger-name', // For debugging + condition: (ctx) => boolean, // When to run (receives {url, commander}) + action: async (ctx) => void, // What to do + priority: 0, // Higher runs first +}); +``` + +### commander.goto(options) + +```javascript +await commander.goto({ + url: 'https://example.com', + waitUntil: 'domcontentloaded', // Playwright/Puppeteer option + timeout: 60000, +}); +``` + +### commander.clickButton(options) + +```javascript +await commander.clickButton({ + selector: 'button.submit', + scrollIntoView: true, + waitForNavigation: true, +}); +``` + +### commander.fillTextArea(options) + +```javascript +await commander.fillTextArea({ + selector: 'textarea.message', + text: 'Hello world', + checkEmpty: true, +}); +``` + +### commander.destroy() + +```javascript +await commander.destroy(); // Stop actions, cleanup +``` + +## Best Practices + +### 1. Use ctx.forEach for Loops + +```javascript +// BAD: Won't stop on navigation +for (const item of items) { + await ctx.commander.click({ selector: item }); +} + +// GOOD: Stops immediately on navigation +await ctx.forEach(items, async (item) => { + await ctx.commander.click({ selector: item }); +}); +``` + +### 2. Use ctx.checkStopped for Complex Logic + +```javascript +action: async (ctx) => { + while (hasMorePages) { + ctx.checkStopped(); // Throws if navigation detected + + await processPage(ctx); + hasMorePages = await ctx.commander.isVisible({ selector: '.next' }); + } +}; +``` + +### 3. Register Cleanup for Resources + +```javascript +action: async (ctx) => { + const intervalId = setInterval(updateStatus, 1000); + + ctx.onCleanup(() => { + clearInterval(intervalId); + console.log('Interval cleared'); + }); + + // ... rest of action +}; +``` + +### 4. Use ctx.abortSignal with Fetch + +```javascript +action: async (ctx) => { + const response = await fetch(url, { + signal: ctx.abortSignal, // Cancels on navigation + }); +}; +``` + +## Debugging + +Enable verbose mode for detailed logs: + +```javascript +const commander = makeBrowserCommander({ page, verbose: true }); +``` + +## Architecture + +See [src/ARCHITECTURE.md](src/ARCHITECTURE.md) for detailed architecture documentation. + +## License + +[UNLICENSE](../LICENSE) diff --git a/rust/README.md b/rust/README.md new file mode 100644 index 0000000..74bc7f0 --- /dev/null +++ b/rust/README.md @@ -0,0 +1,175 @@ +# Browser Commander + +A Rust library for universal browser automation that provides a unified API for different browser automation engines. The key focus is on **stoppable page triggers** - ensuring automation logic is properly mounted/unmounted during page navigation. + +## Installation + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +browser-commander = "0.4" +tokio = { version = "1.0", features = ["full"] } +``` + +## Core Concept: Page State Machine + +Browser Commander manages the browser as a state machine with two states: + +``` ++------------------+ +------------------+ +| | navigation start | | +| WORKING STATE | -------------------> | LOADING STATE | +| (action runs) | | (wait only) | +| | <----------------- | | ++------------------+ page ready +------------------+ +``` + +**LOADING STATE**: Page is loading. Only waiting/tracking operations are allowed. No automation logic runs. + +**WORKING STATE**: Page is fully loaded (30 seconds of network idle). Page triggers can safely interact with DOM. + +## Quick Start + +```rust +use browser_commander::prelude::*; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Launch a browser with chromiumoxide engine + let options = LaunchOptions::chromiumoxide() + .headless(true); + + let result = launch_browser(options).await?; + println!("Browser launched: {:?}", result.browser.engine); + + // Navigate to a URL + let page = &result.page; + goto(page, "https://example.com", None).await?; + + // Click a button + click_button(page, "button.submit", None).await?; + + // Fill a text field + fill_text_area(page, "input[name='email']", "test@example.com", None).await?; + + Ok(()) +} +``` + +## Features + +- **Unified API** across multiple browser engines +- **Built-in navigation safety handling** +- **Element visibility and scroll management** +- **Click, fill, and other interaction support with verification** +- **Async/await support with Tokio** + +## API Reference + +### Browser Launch + +```rust +use browser_commander::prelude::*; + +// Launch with chromiumoxide (CDP-based) +let options = LaunchOptions::chromiumoxide() + .headless(true) + .user_data_dir("~/.browser-data"); + +let result = launch_browser(options).await?; +``` + +### Navigation + +```rust +// Navigate to URL +goto(&page, "https://example.com", None).await?; + +// Navigate with options +let nav_options = NavigationOptions { + wait_until: WaitUntil::NetworkIdle, + timeout: Some(30000), +}; +goto(&page, "https://example.com", Some(nav_options)).await?; + +// Wait for URL to match condition +wait_for_url_condition(&page, |url| url.contains("success")).await?; +``` + +### Element Interactions + +```rust +// Click a button +click_button(&page, "button.submit", None).await?; + +// Click with options +let click_options = ClickOptions { + scroll_into_view: true, + wait_for_navigation: true, + ..Default::default() +}; +click_button(&page, "button.submit", Some(click_options)).await?; + +// Fill text area +fill_text_area(&page, "textarea.message", "Hello world", None).await?; + +// Scroll element into view +scroll_into_view(&page, ".target-element", None).await?; +``` + +### Element Queries + +```rust +// Check visibility +let visible = is_visible(&page, ".element").await?; + +// Check if enabled +let enabled = is_enabled(&page, "button.submit").await?; + +// Get text content +let text = text_content(&page, ".message").await?; + +// Get attribute value +let href = get_attribute(&page, "a.link", "href").await?; + +// Count matching elements +let count = count(&page, ".item").await?; +``` + +### Utilities + +```rust +// Wait for a duration +wait(1000).await; + +// Get current URL +let url = get_url(&page).await?; + +// Parse URL +let parsed = parse_url("https://example.com/path?query=value")?; + +// Evaluate JavaScript +let result: String = evaluate(&page, "document.title").await?; +``` + +## Modules + +- `core` - Core types and traits (constants, engine adapter, logger) +- `elements` - Element operations (selectors, visibility, content) +- `interactions` - User interactions (click, scroll, fill) +- `browser` - Browser management (launcher, navigation) +- `utilities` - General utilities (URL handling, wait operations) +- `high_level` - High-level DRY utilities + +## Prelude + +For convenience, import everything commonly needed with: + +```rust +use browser_commander::prelude::*; +``` + +## License + +[UNLICENSE](../LICENSE)