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.
This commit is contained in:
Dhanji R. Prasanna
2025-12-15 17:02:30 +11:00
parent d142cdfffe
commit 81cba42c8d
6 changed files with 165 additions and 3 deletions

View File

@@ -273,11 +273,25 @@ g3 --webdriver
g3 --webdriver --safari 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` - macOS: `brew install chromedriver`
- Linux: `apt install chromium-chromedriver` - Linux: `apt install chromium-chromedriver`
- Or download from: https://chromedriver.chromium.org/downloads - 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 ## macOS Accessibility API Tools
G3 includes support for controlling macOS applications via the Accessibility API, allowing you to automate native macOS apps. G3 includes support for controlling macOS applications via the Accessibility API, allowing you to automate native macOS apps.

View File

@@ -110,6 +110,12 @@ chrome_port = 9515
# Safari opens a visible browser window # Safari opens a visible browser window
# Chrome headless runs in the background without a visible window # Chrome headless runs in the background without a visible window
browser = "chrome-headless" 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] [macax]
enabled = false enabled = false

View File

@@ -19,8 +19,18 @@ impl ChromeDriver {
Self::with_port_headless(9515).await 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> {
Self::with_port_headless_and_binary(9515, Some(chrome_binary)).await
}
/// Create a new ChromeDriver instance with a custom port in headless mode /// Create a new ChromeDriver instance with a custom port in headless mode
pub async fn with_port_headless(port: u16) -> Result<Self> { pub async fn with_port_headless(port: u16) -> Result<Self> {
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<Self> {
let url = format!("http://localhost:{}", port); let url = format!("http://localhost:{}", port);
let mut caps = serde_json::Map::new(); let mut caps = serde_json::Map::new();
@@ -41,6 +51,12 @@ impl ChromeDriver {
Value::String("--window-size=1920,1080".to_string()), 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( caps.insert(
"goog:chromeOptions".to_string(), "goog:chromeOptions".to_string(),
Value::Object(chrome_options), Value::Object(chrome_options),

View File

@@ -132,6 +132,10 @@ pub struct WebDriverConfig {
#[serde(default)] #[serde(default)]
pub chrome_port: u16, pub chrome_port: u16,
#[serde(default)] #[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<String>,
#[serde(default)]
pub browser: WebDriverBrowser, pub browser: WebDriverBrowser,
} }
@@ -152,6 +156,7 @@ impl Default for WebDriverConfig {
enabled: true, enabled: true,
safari_port: 4444, safari_port: 4444,
chrome_port: 9515, chrome_port: 9515,
chrome_binary: None,
browser: WebDriverBrowser::ChromeHeadless, browser: WebDriverBrowser::ChromeHeadless,
} }
} }

View File

@@ -5605,8 +5605,13 @@ impl<W: UiWriter> Agent<W> {
// Wait before each attempt (200ms between retries, total max ~2s) // Wait before each attempt (200ms between retries, total max ~2s)
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
// Try to connect to ChromeDriver in headless mode // Try to connect to ChromeDriver in headless mode (with optional custom binary)
match g3_computer_control::ChromeDriver::with_port_headless(port).await { 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) => { Ok(driver) => {
let session = std::sync::Arc::new(tokio::sync::Mutex::new(WebDriverSession::Chrome(driver))); let session = std::sync::Arc::new(tokio::sync::Mutex::new(WebDriverSession::Chrome(driver)));
*self.webdriver_session.write().await = Some(session); *self.webdriver_session.write().await = Some(session);

View File

@@ -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"