From 81cba42c8d8e1231ae278e0ca9e36a80018507b4 Mon Sep 17 00:00:00 2001 From: "Dhanji R. Prasanna" Date: Mon, 15 Dec 2025 17:02:30 +1100 Subject: [PATCH] Add Chrome for Testing support for reliable WebDriver automation - Add setup script (scripts/setup-chrome-for-testing.sh) that downloads matching Chrome and ChromeDriver versions from Google's CDN - Add chrome_binary config option to specify custom Chrome binary path - Update ChromeDriver to support custom binary via with_port_headless_and_binary() - Update README with Chrome for Testing setup instructions - Update config.example.toml with chrome_binary documentation Chrome for Testing is Google's dedicated browser for automated testing that guarantees version compatibility with ChromeDriver, avoiding the common 'version mismatch' errors when Chrome auto-updates. --- README.md | 16 ++- config.example.toml | 6 + .../src/webdriver/chrome.rs | 16 +++ crates/g3-config/src/lib.rs | 5 + crates/g3-core/src/lib.rs | 9 +- scripts/setup-chrome-for-testing.sh | 116 ++++++++++++++++++ 6 files changed, 165 insertions(+), 3 deletions(-) create mode 100755 scripts/setup-chrome-for-testing.sh diff --git a/README.md b/README.md index d2c84d8..5dd16cc 100644 --- a/README.md +++ b/README.md @@ -273,11 +273,25 @@ g3 --webdriver g3 --webdriver --safari ``` -**Chrome Headless Setup**: Install ChromeDriver: +**Chrome Setup Options**: + +*Option 1: Use Chrome for Testing (Recommended)* - Guarantees version compatibility: +```bash +./scripts/setup-chrome-for-testing.sh +``` +Then add to your `~/.config/g3/config.toml`: +```toml +[webdriver] +chrome_binary = "/Users/yourname/.chrome-for-testing/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing" +``` + +*Option 2: Use system Chrome* - Requires matching ChromeDriver version: - macOS: `brew install chromedriver` - Linux: `apt install chromium-chromedriver` - Or download from: https://chromedriver.chromium.org/downloads +**Note**: If you see "ChromeDriver version doesn't match Chrome version" errors, use Option 1 (Chrome for Testing) which bundles matching versions. + ## macOS Accessibility API Tools G3 includes support for controlling macOS applications via the Accessibility API, allowing you to automate native macOS apps. diff --git a/config.example.toml b/config.example.toml index 05234cb..e2f70c0 100644 --- a/config.example.toml +++ b/config.example.toml @@ -110,6 +110,12 @@ chrome_port = 9515 # Safari opens a visible browser window # Chrome headless runs in the background without a visible window browser = "chrome-headless" +# Optional: Path to Chrome binary (e.g., Chrome for Testing) +# If not set, ChromeDriver will use the default Chrome installation +# Use this to avoid version mismatch issues between Chrome and ChromeDriver +# Run: ./scripts/setup-chrome-for-testing.sh to install matching versions +# chrome_binary = "/Users/yourname/.chrome-for-testing/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing" +# chrome_binary = "/Users/yourname/.chrome-for-testing/chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing" [macax] enabled = false diff --git a/crates/g3-computer-control/src/webdriver/chrome.rs b/crates/g3-computer-control/src/webdriver/chrome.rs index 8ec26f7..5e706e9 100644 --- a/crates/g3-computer-control/src/webdriver/chrome.rs +++ b/crates/g3-computer-control/src/webdriver/chrome.rs @@ -19,8 +19,18 @@ impl ChromeDriver { Self::with_port_headless(9515).await } + /// Create a new ChromeDriver instance with Chrome for Testing binary + pub async fn new_headless_with_binary(chrome_binary: &str) -> Result { + Self::with_port_headless_and_binary(9515, Some(chrome_binary)).await + } + /// Create a new ChromeDriver instance with a custom port in headless mode pub async fn with_port_headless(port: u16) -> Result { + Self::with_port_headless_and_binary(port, None).await + } + + /// Create a new ChromeDriver instance with a custom port and optional Chrome binary path + pub async fn with_port_headless_and_binary(port: u16, chrome_binary: Option<&str>) -> Result { let url = format!("http://localhost:{}", port); let mut caps = serde_json::Map::new(); @@ -41,6 +51,12 @@ impl ChromeDriver { Value::String("--window-size=1920,1080".to_string()), ]), ); + + // If a custom Chrome binary is specified, use it + if let Some(binary) = chrome_binary { + chrome_options.insert("binary".to_string(), Value::String(binary.to_string())); + } + caps.insert( "goog:chromeOptions".to_string(), Value::Object(chrome_options), diff --git a/crates/g3-config/src/lib.rs b/crates/g3-config/src/lib.rs index 5845d1e..004f3af 100644 --- a/crates/g3-config/src/lib.rs +++ b/crates/g3-config/src/lib.rs @@ -132,6 +132,10 @@ pub struct WebDriverConfig { #[serde(default)] pub chrome_port: u16, #[serde(default)] + /// Optional path to Chrome binary (e.g., Chrome for Testing) + /// If not set, ChromeDriver will use the default Chrome installation + pub chrome_binary: Option, + #[serde(default)] pub browser: WebDriverBrowser, } @@ -152,6 +156,7 @@ impl Default for WebDriverConfig { enabled: true, safari_port: 4444, chrome_port: 9515, + chrome_binary: None, browser: WebDriverBrowser::ChromeHeadless, } } diff --git a/crates/g3-core/src/lib.rs b/crates/g3-core/src/lib.rs index 09c99f1..69a702a 100644 --- a/crates/g3-core/src/lib.rs +++ b/crates/g3-core/src/lib.rs @@ -5605,8 +5605,13 @@ impl Agent { // Wait before each attempt (200ms between retries, total max ~2s) tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; - // Try to connect to ChromeDriver in headless mode - match g3_computer_control::ChromeDriver::with_port_headless(port).await { + // Try to connect to ChromeDriver in headless mode (with optional custom binary) + let driver_result = match &self.config.webdriver.chrome_binary { + Some(binary) => g3_computer_control::ChromeDriver::with_port_headless_and_binary(port, Some(binary)).await, + None => g3_computer_control::ChromeDriver::with_port_headless(port).await, + }; + + match driver_result { Ok(driver) => { let session = std::sync::Arc::new(tokio::sync::Mutex::new(WebDriverSession::Chrome(driver))); *self.webdriver_session.write().await = Some(session); diff --git a/scripts/setup-chrome-for-testing.sh b/scripts/setup-chrome-for-testing.sh new file mode 100755 index 0000000..7925ef6 --- /dev/null +++ b/scripts/setup-chrome-for-testing.sh @@ -0,0 +1,116 @@ +#!/bin/bash +# Setup Chrome for Testing with matching ChromeDriver +# This ensures version compatibility for WebDriver automation + +set -e + +# Configuration +INSTALL_DIR="${HOME}/.chrome-for-testing" +BIN_DIR="${HOME}/.local/bin" + +# Detect architecture +ARCH=$(uname -m) +if [ "$ARCH" = "arm64" ]; then + PLATFORM="mac-arm64" +elif [ "$ARCH" = "x86_64" ]; then + PLATFORM="mac-x64" +else + echo "❌ Unsupported architecture: $ARCH" + exit 1 +fi + +echo "🔍 Detecting platform: $PLATFORM" + +# Get latest stable version info +echo "📡 Fetching latest Chrome for Testing version..." +VERSION_JSON=$(curl -s 'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json') +VERSION=$(echo "$VERSION_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['channels']['Stable']['version'])") + +echo "📦 Latest stable version: $VERSION" + +# Get download URLs +CHROME_URL=$(echo "$VERSION_JSON" | python3 -c " +import json,sys +data = json.load(sys.stdin) +for d in data['channels']['Stable']['downloads']['chrome']: + if d['platform'] == '$PLATFORM': + print(d['url']) + break +") + +CHROMEDRIVER_URL=$(echo "$VERSION_JSON" | python3 -c " +import json,sys +data = json.load(sys.stdin) +for d in data['channels']['Stable']['downloads']['chromedriver']: + if d['platform'] == '$PLATFORM': + print(d['url']) + break +") + +# Create directories +mkdir -p "$INSTALL_DIR" +mkdir -p "$BIN_DIR" + +# Download and extract Chrome for Testing +echo "⬇️ Downloading Chrome for Testing..." +cd "$INSTALL_DIR" +curl -L -o chrome.zip "$CHROME_URL" +unzip -q -o chrome.zip +rm chrome.zip + +# The extracted folder name varies by platform +CHROME_APP_DIR="chrome-$PLATFORM" +if [ -d "$CHROME_APP_DIR" ]; then + echo "✅ Chrome for Testing installed to: $INSTALL_DIR/$CHROME_APP_DIR" +else + echo "❌ Chrome extraction failed" + exit 1 +fi + +# Download and extract ChromeDriver +echo "⬇️ Downloading ChromeDriver..." +curl -L -o chromedriver.zip "$CHROMEDRIVER_URL" +unzip -q -o chromedriver.zip +rm chromedriver.zip + +CHROMEDRIVER_DIR="chromedriver-$PLATFORM" +if [ -f "$CHROMEDRIVER_DIR/chromedriver" ]; then + # Create symlink in bin directory + ln -sf "$INSTALL_DIR/$CHROMEDRIVER_DIR/chromedriver" "$BIN_DIR/chromedriver-for-testing" + chmod +x "$INSTALL_DIR/$CHROMEDRIVER_DIR/chromedriver" + echo "✅ ChromeDriver installed and linked to: $BIN_DIR/chromedriver-for-testing" +else + echo "❌ ChromeDriver extraction failed" + exit 1 +fi + +# Create a wrapper script that uses Chrome for Testing +cat > "$BIN_DIR/chrome-for-testing" << 'EOF' +#!/bin/bash +INSTALL_DIR="${HOME}/.chrome-for-testing" +ARCH=$(uname -m) +if [ "$ARCH" = "arm64" ]; then + PLATFORM="mac-arm64" +else + PLATFORM="mac-x64" +fi +exec "$INSTALL_DIR/chrome-$PLATFORM/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing" "$@" +EOF +chmod +x "$BIN_DIR/chrome-for-testing" + +echo "" +echo "✅ Setup complete!" +echo "" +echo "Installed versions:" +echo " Chrome for Testing: $VERSION" +echo " ChromeDriver: $VERSION" +echo "" +echo "Binaries:" +echo " Chrome: $BIN_DIR/chrome-for-testing" +echo " ChromeDriver: $BIN_DIR/chromedriver-for-testing" +echo "" +echo "To use with g3, make sure $BIN_DIR is in your PATH:" +echo " export PATH=\"$BIN_DIR:\$PATH\"" +echo "" +echo "Or add to your shell profile (~/.zshrc or ~/.bashrc):" +echo " echo 'export PATH=\"$BIN_DIR:\$PATH\"' >> ~/.zshrc"