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:
16
README.md
16
README.md
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
116
scripts/setup-chrome-for-testing.sh
Executable file
116
scripts/setup-chrome-for-testing.sh
Executable 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"
|
||||||
Reference in New Issue
Block a user