test(e2e): unify driver onto Appium Chromium attached to CEF CDP (#1696)

This commit is contained in:
Steven Enamakel
2026-05-14 03:10:08 -07:00
committed by GitHub
parent 2672706360
commit 6386854d6c
11 changed files with 848 additions and 443 deletions
+21 -19
View File
@@ -1,12 +1,13 @@
---
name: E2E (Linux) - agent-review
# CEF LIMITATION: Linux E2E via tauri-driver requires WebKitWebDriver
# (webkit2gtk), but this app uses the CEF runtime (tauri-runtime-cef).
# WebKitWebDriver cannot drive a CEF-backed WebView, so Linux CEF E2E is not
# a supported CI gate. Keep this workflow manual/diagnostic until the CEF fork
# adds a ChromeDriver-based automation path or an alternative harness is wired.
# Use macOS/Appium for supported local or manual desktop E2E runs.
# See also: test.yml where the e2e-linux job is commented out for the same reason.
#
# Runs the agent-review spec on Linux via the unified Appium Chromium
# driver attached to CEF's CDP port. Same driver path as `e2e.yml`; this
# workflow exists because agent-review needs a longer timeout and its own
# artifact retention.
#
# Previously: tauri-driver + WebKitWebDriver, which can't drive a
# CEF-backed WebView. That path is dead.
on:
workflow_dispatch:
permissions:
@@ -16,7 +17,7 @@ concurrency:
cancel-in-progress: true
jobs:
e2e-agent-review:
name: E2E agent-review (Linux / tauri-driver)
name: E2E agent-review (Linux / Appium Chromium)
runs-on: ubuntu-22.04
timeout-minutes: 60
steps:
@@ -47,7 +48,7 @@ jobs:
- name: Install Rust (rust-toolchain.toml)
if: steps.gate.outputs.present == 'true'
uses: dtolnay/rust-toolchain@1.93.0
- name: Install system dependencies
- name: Install system dependencies (CEF + Xvfb)
if: steps.gate.outputs.present == 'true'
run: |
sudo apt-get update
@@ -55,8 +56,8 @@ jobs:
libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev \
librsvg2-dev patchelf \
xvfb at-spi2-core dbus-x11 \
webkit2gtk-driver \
libasound2-dev libxdo-dev libxtst-dev libx11-dev libevdev-dev
libasound2-dev libxdo-dev libxtst-dev libx11-dev libevdev-dev \
unzip curl python3
- name: Cargo.lock fingerprint (deps only)
if: steps.gate.outputs.present == 'true'
id: cargo-lock-fingerprint
@@ -76,9 +77,11 @@ jobs:
restore-keys: |
${{ runner.os }}-e2e-agentreview-cargo-
${{ runner.os }}-e2e-cargo-
- name: Install tauri-driver
- name: Install Appium and chromium driver
if: steps.gate.outputs.present == 'true'
run: cargo install tauri-driver --version 2.0.5
run: |
npm install -g appium@3
appium driver install --source=npm appium-chromium-driver
- name: Install JS dependencies
if: steps.gate.outputs.present == 'true'
run: pnpm install --frozen-lockfile
@@ -90,7 +93,6 @@ jobs:
- name: Build E2E app
if: steps.gate.outputs.present == 'true'
run: pnpm --filter openhuman-app test:e2e:build
# Core is linked in-process — no sidecar staging needed.
- name: Run agent-review E2E spec under Xvfb
if: steps.gate.outputs.present == 'true'
run: |
@@ -100,11 +102,10 @@ jobs:
eval "$(dbus-launch --sh-syntax)"
mkdir -p ~/.local/share/applications
export RUST_BACKTRACE=1
cd app
mkdir -p test/e2e/artifacts
if ! bash scripts/e2e-run-spec.sh test/e2e/specs/agent-review.spec.ts agent-review; then
mkdir -p app/test/e2e/artifacts
if ! bash app/scripts/e2e-run-session.sh test/e2e/specs/agent-review.spec.ts agent-review; then
echo "First agent-review run failed; retrying once..."
bash scripts/e2e-run-spec.sh test/e2e/specs/agent-review.spec.ts agent-review-retry
bash app/scripts/e2e-run-session.sh test/e2e/specs/agent-review.spec.ts agent-review-retry
fi
- name: Upload E2E artifacts
if: always() && steps.gate.outputs.present == 'true'
@@ -113,6 +114,7 @@ jobs:
name: e2e-agent-review-artifacts
path: |
app/test/e2e/artifacts/**
/tmp/tauri-driver-e2e-agent-review.log
/tmp/openhuman-e2e-app-*.log
/tmp/appium-e2e-*.log
if-no-files-found: ignore
retention-days: 7
+145 -75
View File
@@ -1,32 +1,35 @@
---
# Dedicated end-to-end test workflow.
# End-to-end desktop test workflow.
#
# Runs the WDIO desktop e2e suite against the mock backend
# (`scripts/mock-api/`) on three OSes in parallel:
# Single unified driver across all three platforms: Appium Chromium driver
# attached to CEF's already-enabled remote-debugging port (:19222). The
# per-OS driver fork (tauri-driver on Linux/Windows, Appium Mac2 on macOS)
# is gone — every platform now runs the same recipe and sees the same DOM.
#
# - linux → containerized in `ghcr.io/tinyhumansai/openhuman_ci:latest`
# (the same image used by `unit-tests` and by
# `e2e/docker-compose.yml`), Xvfb-backed tauri-driver.
# CEF caveat: tauri-driver / webkit2gtk cannot drive the CEF
# WebView's DOM, so this leg runs only specs that don't depend
# on accessibility-tree queries (smoke).
# Recipe per OS:
# 1. Build the CEF app via `pnpm --filter openhuman-app test:e2e:build`.
# 2. Install Appium 3.x + the `chromium` driver.
# 3. Run `app/scripts/e2e-run-session.sh` which:
# - launches the built binary,
# - waits for CDP on :19222,
# - prunes the prewarm `about:blank` target,
# - downloads a chromedriver matching CEF's Chromium version,
# - starts Appium and runs WDIO against `wdio.conf.ts`.
#
# - macos → bare runner, Appium Mac2 driver. Canonical e2e runner.
# Runs smoke + the mega-flow spec (the latter best-effort
# until the BootCheckGate local-mode bypass lands).
# Linux runs CEF inside Xvfb because there is no native display on the
# Actions ubuntu runner.
#
# - windows → bare runner, tauri-driver via WebView2. Smoke only for now;
# Windows hasn't been wired up for the broader spec suite.
# The three jobs are intentionally near-identical — they only differ in
# (a) runner type, (b) container image (linux only), (c) Xvfb wrapping
# (linux only). They are kept as separate jobs (not a matrix with a
# conditional `container:` block) because GitHub Actions does not have
# a clean way to conditionally enable/disable `container:` from matrix
# expansion.
#
# Triggers:
# - pull_request (smoke only on every PR — cheap signal that the bundle
# still builds and launches on every OS).
# - workflow_dispatch with `full=true` to run the entire spec suite on
# macOS (the only platform where the broader specs are designed to pass
# today).
#
# All three legs are `continue-on-error` so a known-flaky platform doesn't
# block the PR — the macOS leg is the source of truth for spec correctness.
# - pull_request (smoke on every PR — cheap signal that the bundle
# still builds and launches on every OS via the unified driver).
# - workflow_dispatch with `full=true` to run the entire spec suite.
name: E2E
on:
@@ -34,7 +37,7 @@ on:
workflow_dispatch:
inputs:
full:
description: Run the full e2e spec suite on macOS (slow; ~30+ min)
description: Run the full e2e spec suite (slow; ~30+ min)
required: false
default: 'false'
type: choice
@@ -49,20 +52,14 @@ concurrency:
cancel-in-progress: true
jobs:
# ---------------------------------------------------------------------------
# Linux — build-only. tauri-driver speaks WebKitWebDriver / webkit2gtk and
# cannot drive the CEF-backed WebView, so we can't run specs against the
# Linux Tauri build yet. We still build the Linux .deb-style debug binary
# so a Linux-breaking dep / config / Rust change still fails the PR.
# Reuses the project's CI image (same one consumed by
# `e2e/docker-compose.yml` and the `unit-tests` job).
# ---------------------------------------------------------------------------
e2e-linux:
name: E2E (Linux build only)
name: E2E (Linux / Appium Chromium)
runs-on: ubuntu-22.04
container:
image: ghcr.io/tinyhumansai/openhuman_ci:latest
timeout-minutes: 45
timeout-minutes: 60
# Linux runs CEF inside Xvfb on a container image — keep it
# non-blocking until we have signal that it's reliable.
continue-on-error: true
steps:
- name: Checkout code
@@ -86,7 +83,7 @@ jobs:
. -> target
app/src-tauri -> target
cache-on-failure: true
key: e2e-linux
key: e2e-linux-unified
- name: Install JS dependencies
run: pnpm install --frozen-lockfile
@@ -96,17 +93,47 @@ jobs:
touch .env
touch app/.env
- name: Build E2E app (Linux debug binary)
- name: Install Appium and chromium driver
run: |
npm install -g appium@3
appium driver install --source=npm appium-chromium-driver
- name: Build E2E app
run: pnpm --filter openhuman-app test:e2e:build
# ---------------------------------------------------------------------------
# macOS — Appium Mac2. Canonical e2e runner.
# ---------------------------------------------------------------------------
- name: Run smoke spec (Xvfb)
run: |
xvfb-run -a --server-args="-screen 0 1280x960x24" \
bash app/scripts/e2e-run-session.sh test/e2e/specs/smoke.spec.ts smoke
- name: Run mega-flow spec (Xvfb, non-blocking)
continue-on-error: true
run: |
xvfb-run -a --server-args="-screen 0 1280x960x24" \
bash app/scripts/e2e-run-session.sh test/e2e/specs/mega-flow.spec.ts mega-flow
- name: Run full e2e suite (workflow_dispatch only)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.full == 'true'
run: |
xvfb-run -a --server-args="-screen 0 1280x960x24" \
bash app/scripts/e2e-run-session.sh
- name: Upload e2e artifacts on failure
if: failure() || cancelled()
uses: actions/upload-artifact@v5
with:
name: e2e-artifacts-linux
path: |
app/test/e2e/artifacts/
${{ runner.temp }}/openhuman-e2e-app-*.log
${{ runner.temp }}/appium-e2e-*.log
if-no-files-found: ignore
retention-days: 7
e2e-macos:
name: E2E (macOS / Appium Mac2)
name: E2E (macOS / Appium Chromium)
runs-on: macos-latest
timeout-minutes: 90
continue-on-error: true
steps:
- name: Checkout code
uses: actions/checkout@v5
@@ -125,6 +152,24 @@ jobs:
- name: Install Rust (rust-toolchain.toml)
uses: dtolnay/rust-toolchain@1.93.0
with:
# macos-latest is arm64, but the vendored tauri-cli's build.rs
# (tauri-bundler/build.rs:134) compiles a CEF helper for
# x86_64-apple-darwin to produce a universal binary, so the
# x86_64 target's libstd must be installed even though the
# final E2E binary is arm64 only.
targets: x86_64-apple-darwin
- name: Verify cargo resolves to real toolchain
# Defensive against rustup-init being shadowed onto cargo's name in
# PATH (observed once where `cargo install --path X` printed
# "rustup-init[EXE]: unexpected argument 'install' found"). Force
# the rustup default to the requested version and print what
# `cargo` actually is.
run: |
rustup default 1.93.0 || true
which cargo
cargo --version
- name: Cache Rust build artifacts
uses: Swatinem/rust-cache@v2
@@ -133,12 +178,10 @@ jobs:
. -> target
app/src-tauri -> target
cache-on-failure: true
key: e2e-macos
- name: Accept Xcode license + first launch
run: |
sudo xcodebuild -license accept
sudo xcodebuild -runFirstLaunch
# v2 suffix invalidates a cache that had a broken cargo binary
# ("error: unexpected argument 'install' found" from rustup-init
# when ensure-tauri-cli ran `cargo install --locked --path`).
key: e2e-macos-unified-v2
- name: Install JS dependencies
run: pnpm install --frozen-lockfile
@@ -148,48 +191,56 @@ jobs:
touch .env
touch app/.env
- name: Install Appium and mac2 driver
- name: Install Appium and chromium driver
run: |
npm install -g appium
appium driver install mac2
npm install -g appium@3
appium driver install --source=npm appium-chromium-driver
- name: Build E2E app bundle
- name: Build E2E app
run: pnpm --filter openhuman-app test:e2e:build
- name: Run smoke spec
run: bash app/scripts/e2e-run-spec.sh test/e2e/specs/smoke.spec.ts smoke
# Adhoc-sign the unsigned debug bundle. macOS now kills processes that
# load dynamic frameworks (the CEF helper apps) from an unsigned bundle
# — the parent OpenHuman process exits within ~2s with an empty log.
# Adhoc signing (-) satisfies the loader without needing a real
# developer-ID cert. `--force --deep` re-signs the embedded helper
# apps and the framework too.
- name: Adhoc-sign the .app bundle
run: |
codesign --force --deep --sign - \
app/src-tauri/target/debug/bundle/macos/OpenHuman.app
codesign --verify --deep --verbose=2 \
app/src-tauri/target/debug/bundle/macos/OpenHuman.app
- name: Run mega-flow spec
- name: Run smoke spec
run: bash app/scripts/e2e-run-session.sh test/e2e/specs/smoke.spec.ts smoke
- name: Run mega-flow spec (non-blocking)
continue-on-error: true
run: bash app/scripts/e2e-run-spec.sh test/e2e/specs/mega-flow.spec.ts mega-flow
run: bash app/scripts/e2e-run-session.sh test/e2e/specs/mega-flow.spec.ts mega-flow
- name: Run full e2e suite (workflow_dispatch only)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.full == 'true'
run: pnpm --filter openhuman-app test:e2e:all:flows
run: bash app/scripts/e2e-run-session.sh
- name: Upload e2e artifacts on failure
if: failure() || cancelled()
uses: actions/upload-artifact@v5
with:
name: e2e-artifacts-macos
path: app/test/e2e/artifacts/
path: |
app/test/e2e/artifacts/
${{ runner.temp }}/openhuman-e2e-app-*.log
${{ runner.temp }}/appium-e2e-*.log
if-no-files-found: ignore
retention-days: 7
# ---------------------------------------------------------------------------
# Windows — build-only. tauri-driver on Windows uses MSEdgeDriver
# (WebView2) which can't drive the CEF-backed WebView the app actually
# ships — observed `session not created: Microsoft Edge failed to start:
# crashed.` when the driver tried to launch Edge for the WebView2 host
# that doesn't exist on our app. Same shape as the Linux block (CEF ↔
# webkit2gtk-driver). Build the .exe so a Windows-breaking dep / config
# / Rust change still fails the PR; skip the spec run until a
# CEF-compatible Windows driver lands.
# ---------------------------------------------------------------------------
e2e-windows:
name: E2E (Windows build only)
name: E2E (Windows / Appium Chromium)
runs-on: windows-latest
timeout-minutes: 60
timeout-minutes: 90
# Windows CEF + chromedriver attach is unverified end-to-end; keep
# non-blocking until we have a green run on a real Windows runner.
continue-on-error: true
steps:
- name: Checkout code
@@ -217,11 +268,7 @@ jobs:
. -> target
app/src-tauri -> target
cache-on-failure: true
# `v2` suffix invalidates the prior cache that ended up pinning
# stale .rmeta artifacts (saw `error[E0061]: sys.refresh_processes`
# referencing a `local_ai/install.rs` line that doesn't exist on
# this branch — cargo cross-referenced an outdated .rmeta).
key: e2e-windows-v2
key: e2e-windows-unified
- name: Install JS dependencies
run: pnpm install --frozen-lockfile
@@ -232,14 +279,37 @@ jobs:
touch .env
touch app/.env
- name: Build E2E app (Windows debug .exe)
- name: Install Appium and chromium driver
shell: bash
run: |
npm install -g appium@3
appium driver install --source=npm appium-chromium-driver
- name: Build E2E app
run: pnpm --filter openhuman-app test:e2e:build
- name: Run smoke spec
shell: bash
run: bash app/scripts/e2e-run-session.sh test/e2e/specs/smoke.spec.ts smoke
- name: Run mega-flow spec (non-blocking)
continue-on-error: true
shell: bash
run: bash app/scripts/e2e-run-session.sh test/e2e/specs/mega-flow.spec.ts mega-flow
- name: Run full e2e suite (workflow_dispatch only)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.full == 'true'
shell: bash
run: bash app/scripts/e2e-run-session.sh
- name: Upload e2e artifacts on failure
if: failure() || cancelled()
uses: actions/upload-artifact@v5
with:
name: e2e-artifacts-windows
path: app/test/e2e/artifacts/
path: |
app/test/e2e/artifacts/
${{ runner.temp }}/openhuman-e2e-app-*.log
${{ runner.temp }}/appium-e2e-*.log
if-no-files-found: ignore
retention-days: 7
+1
View File
@@ -25,3 +25,4 @@ dist-ssr
# ESLint cache
.eslintcache
app/test/e2e/.cache/
+2
View File
@@ -45,6 +45,8 @@
"test:e2e": "pnpm test:e2e:build && pnpm test:e2e:login && pnpm test:e2e:auth",
"test:e2e:all:flows": "bash ./scripts/e2e-run-all-flows.sh",
"test:e2e:all": "pnpm test:e2e:build && pnpm test:e2e:all:flows",
"test:e2e:session": "bash ./scripts/e2e-run-session.sh",
"test:e2e:session:full": "pnpm test:e2e:build && pnpm test:e2e:session",
"test:all": "pnpm test:coverage && pnpm test:rust && pnpm test:e2e",
"rust:check": "cargo check --manifest-path src-tauri/Cargo.toml",
"rust:format": "cargo fmt --manifest-path ../Cargo.toml --all && cargo fmt --manifest-path src-tauri/Cargo.toml --all",
+13 -8
View File
@@ -44,25 +44,30 @@ TAURI_CONFIG_OVERRIDE='{"bundle":{"createUpdaterArtifacts":false}}'
# Tauri CLI maps env CI to --ci and only accepts true|false; some runners set CI=1.
case "${CI:-}" in 1) export CI=true ;; 0) export CI=false ;; esac
# CEF runtime requires the vendored CEF-aware tauri-cli (the stock one produces
# a bundle that panics at startup in cef::library_loader::LibraryLoader::new).
# All other build scripts in app/package.json do `pnpm tauri:ensure` + use
# `cargo tauri build`; the E2E build was the one outlier and we got the panic.
pnpm tauri:ensure
export CEF_PATH="$HOME/Library/Caches/tauri-cef"
OS="$(uname)"
case "$OS" in
Linux)
# Linux: build debug binary only (no bundle needed for tauri-driver)
# Linux: build debug binary only.
echo "Building for Linux (debug binary, no bundle)..."
pnpm exec tauri build -c "$TAURI_CONFIG_OVERRIDE" --debug --no-bundle
cargo tauri build -c "$TAURI_CONFIG_OVERRIDE" --debug --no-bundle -- --bin OpenHuman
;;
Darwin)
# macOS: build .app bundle for Appium Mac2 (wdio.conf points at
# macOS: build .app bundle (wdio.conf points at
# src-tauri/target/debug/bundle/macos/OpenHuman.app).
echo "Building for macOS (.app bundle)..."
pnpm exec tauri build -c "$TAURI_CONFIG_OVERRIDE" --bundles app --debug
cargo tauri build -c "$TAURI_CONFIG_OVERRIDE" --bundles app --debug -- --bin OpenHuman
;;
MINGW*|MSYS*|CYGWIN*|Windows_NT)
# Windows: bare .exe at src-tauri/target/debug/OpenHuman.exe is what
# wdio.conf launches via tauri-driver. NSIS/MSI bundling adds time we
# don't need for the driver path.
# Windows: bare .exe at src-tauri/target/debug/OpenHuman.exe.
echo "Building for Windows (.exe, no bundle)..."
pnpm exec tauri build -c "$TAURI_CONFIG_OVERRIDE" --debug --no-bundle
cargo tauri build -c "$TAURI_CONFIG_OVERRIDE" --debug --no-bundle -- --bin OpenHuman
;;
*)
echo "ERROR: unsupported OS for e2e build: $OS" >&2
+503
View File
@@ -0,0 +1,503 @@
#!/usr/bin/env bash
#
# Unified WDIO E2E runner — one Appium session, one CEF app launch, all specs.
#
# Architecture:
# 1. Build artefacts must exist (run `pnpm test:e2e:build` first).
# 2. Clean cached app data + write a fresh E2E config.toml pointing at the
# shared mock backend.
# 3. Launch the built CEF binary directly (cross-platform).
# 4. Wait for the CEF process to expose CDP on 127.0.0.1:19222.
# 5. Start Appium with the `chromium` driver.
# 6. Run wdio against `test/wdio.conf.ts`, which attaches to the running
# CEF via Appium's Chromium driver. All specs share one session.
# 7. Tear everything down (Appium → CEF → workspace).
#
# Usage:
# ./app/scripts/e2e-run-session.sh # whole suite
# ./app/scripts/e2e-run-session.sh test/e2e/specs/foo.spec.ts # single spec
#
set -euo pipefail
SPEC_ARG="${1:-}"
LOG_SUFFIX="${2:-session}"
E2E_MOCK_PORT="${E2E_MOCK_PORT:-18473}"
CEF_CDP_PORT="${CEF_CDP_PORT:-19222}"
APPIUM_PORT="${APPIUM_PORT:-4723}"
OS="$(uname)"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
REPO_ROOT="$(cd "$APP_DIR/.." && pwd)"
cd "$APP_DIR"
CREATED_TEMP_WORKSPACE=""
APPIUM_PID=""
APP_PID=""
E2E_CONFIG_BACKUP=""
E2E_CONFIG_FILE=""
# ------------------------------------------------------------------------------
# Workspace + config
# ------------------------------------------------------------------------------
if [ -z "${OPENHUMAN_WORKSPACE:-}" ]; then
OPENHUMAN_WORKSPACE="$(mktemp -d)"
CREATED_TEMP_WORKSPACE="$OPENHUMAN_WORKSPACE"
export OPENHUMAN_WORKSPACE
echo "[runner] Using temporary OPENHUMAN_WORKSPACE: $OPENHUMAN_WORKSPACE"
else
echo "[runner] Using OPENHUMAN_WORKSPACE from environment: $OPENHUMAN_WORKSPACE"
fi
if [ "${OPENHUMAN_SERVICE_MOCK:-0}" = "1" ] && [ -z "${OPENHUMAN_SERVICE_MOCK_STATE_FILE:-}" ]; then
OPENHUMAN_SERVICE_MOCK_STATE_FILE="$OPENHUMAN_WORKSPACE/service-mock-state.json"
export OPENHUMAN_SERVICE_MOCK_STATE_FILE
fi
cleanup() {
if [ -n "$APPIUM_PID" ]; then
echo "[runner] Stopping Appium (pid $APPIUM_PID)..."
kill "$APPIUM_PID" 2>/dev/null || true
wait "$APPIUM_PID" 2>/dev/null || true
fi
if [ -n "$APP_PID" ]; then
echo "[runner] Stopping CEF app (pid $APP_PID)..."
kill "$APP_PID" 2>/dev/null || true
wait "$APP_PID" 2>/dev/null || true
fi
if [ -n "$CREATED_TEMP_WORKSPACE" ]; then
rm -rf "$CREATED_TEMP_WORKSPACE"
fi
if [ -n "$E2E_CONFIG_BACKUP" ] && [ -f "$E2E_CONFIG_BACKUP" ]; then
mv "$E2E_CONFIG_BACKUP" "$E2E_CONFIG_FILE"
elif [ -n "$E2E_CONFIG_FILE" ] && [ -f "$E2E_CONFIG_FILE" ]; then
rm -f "$E2E_CONFIG_FILE"
fi
}
trap cleanup EXIT
export VITE_BACKEND_URL="http://127.0.0.1:${E2E_MOCK_PORT}"
export BACKEND_URL="http://127.0.0.1:${E2E_MOCK_PORT}"
export APPIUM_PORT
export CEF_CDP_PORT
echo "[runner] Killing any running OpenHuman instances..."
case "$OS" in
Darwin) pkill -f "OpenHuman" 2>/dev/null || true ;;
Linux) pkill -f "OpenHuman" 2>/dev/null || true ;;
MINGW*|MSYS*|CYGWIN*|Windows_NT)
taskkill //F //IM "OpenHuman.exe" 2>/dev/null || true
;;
esac
sleep 1
echo "[runner] Cleaning cached app data..."
case "$OS" in
Darwin)
rm -rf ~/Library/WebKit/com.openhuman.app
rm -rf ~/Library/Caches/com.openhuman.app
rm -rf "$HOME/Library/Application Support/com.openhuman.app"
rm -rf "$HOME/Library/Saved Application State/com.openhuman.app.savedState"
;;
Linux)
rm -rf "$HOME/.local/share/com.openhuman.app" 2>/dev/null || true
rm -rf "$HOME/.cache/com.openhuman.app" 2>/dev/null || true
rm -rf "$HOME/.config/com.openhuman.app" 2>/dev/null || true
;;
MINGW*|MSYS*|CYGWIN*|Windows_NT)
rm -rf "${APPDATA:-$HOME/AppData/Roaming}/com.openhuman.app" 2>/dev/null || true
rm -rf "${LOCALAPPDATA:-$HOME/AppData/Local}/com.openhuman.app" 2>/dev/null || true
;;
esac
# Mock URL must reach the core sidecar — XCUITest doesn't inherit env,
# and CEF child processes won't either. Pinning via config.toml works
# on every platform.
E2E_CONFIG_DIR="$HOME/.openhuman"
E2E_CONFIG_FILE="$E2E_CONFIG_DIR/config.toml"
mkdir -p "$E2E_CONFIG_DIR"
if [ -f "$E2E_CONFIG_FILE" ]; then
E2E_CONFIG_BACKUP="$E2E_CONFIG_FILE.e2e-backup.$$"
cp "$E2E_CONFIG_FILE" "$E2E_CONFIG_BACKUP"
sed -i.bak '/^api_url[[:space:]]*=/d' "$E2E_CONFIG_FILE" && rm -f "$E2E_CONFIG_FILE.bak"
EXISTING_CONTENT="$(cat "$E2E_CONFIG_FILE")"
printf 'api_url = "http://127.0.0.1:%s"\n%s\n' "${E2E_MOCK_PORT}" "$EXISTING_CONTENT" > "$E2E_CONFIG_FILE"
else
printf 'api_url = "http://127.0.0.1:%s"\n' "${E2E_MOCK_PORT}" > "$E2E_CONFIG_FILE"
fi
echo "[runner] Wrote E2E config.toml with api_url=http://127.0.0.1:${E2E_MOCK_PORT}"
DIST_JS="$(ls dist/assets/index-*.js 2>/dev/null | head -1)"
if [ -z "$DIST_JS" ]; then
echo "ERROR: No frontend bundle found at dist/assets/index-*.js." >&2
echo " Run 'pnpm test:e2e:build' first." >&2
exit 1
fi
if ! grep -q "127.0.0.1:${E2E_MOCK_PORT}" "$DIST_JS"; then
echo "ERROR: frontend bundle does NOT contain mock server URL (127.0.0.1:${E2E_MOCK_PORT})." >&2
echo " Run 'pnpm test:e2e:build' to rebuild." >&2
exit 1
fi
# ------------------------------------------------------------------------------
# Resolve the built CEF binary for this platform
# ------------------------------------------------------------------------------
resolve_app_binary() {
case "$OS" in
Darwin)
for base in \
"$APP_DIR/src-tauri/target/debug/bundle/macos/OpenHuman.app/Contents/MacOS/OpenHuman" \
"$REPO_ROOT/target/debug/bundle/macos/OpenHuman.app/Contents/MacOS/OpenHuman"; do
if [ -x "$base" ]; then echo "$base"; return; fi
done
;;
Linux)
for candidate in \
"$APP_DIR/src-tauri/target/debug/OpenHuman" \
"$REPO_ROOT/target/debug/OpenHuman"; do
if [ -x "$candidate" ]; then echo "$candidate"; return; fi
done
;;
MINGW*|MSYS*|CYGWIN*|Windows_NT)
for candidate in \
"$APP_DIR/src-tauri/target/debug/OpenHuman.exe" \
"$REPO_ROOT/target/debug/OpenHuman.exe"; do
if [ -x "$candidate" ]; then echo "$candidate"; return; fi
done
;;
esac
}
APP_BIN="$(resolve_app_binary)"
if [ -z "${APP_BIN:-}" ] || [ ! -x "$APP_BIN" ]; then
echo "ERROR: built OpenHuman binary not found. Run 'pnpm test:e2e:build' first." >&2
exit 1
fi
# ------------------------------------------------------------------------------
# Ensure a dbus session bus exists on Linux.
#
# The Tauri `single-instance` plugin (used inside OpenHuman) talks to dbus
# via zbus on Linux. If DBUS_SESSION_BUS_ADDRESS is missing or set to
# `disabled:` (which is the openhuman_ci container default), the plugin
# panics during plugin setup:
# panicked at plugins/single-instance/src/platform_impl/linux.rs:57
# Result::unwrap() on Err(Address("unsupported transport 'disabled'"))
# So start a real session bus with `dbus-launch` and inherit its
# DBUS_SESSION_BUS_ADDRESS for the rest of the runner.
# ------------------------------------------------------------------------------
if [ "$OS" = "Linux" ]; then
if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ] || \
printf '%s' "${DBUS_SESSION_BUS_ADDRESS:-}" | grep -q '^disabled'; then
if command -v dbus-launch >/dev/null 2>&1; then
DBUS_LAUNCH_OUT="$(dbus-launch --sh-syntax)"
eval "$DBUS_LAUNCH_OUT"
echo "[runner] Started dbus session bus: $DBUS_SESSION_BUS_ADDRESS"
else
echo "[runner] Warning: dbus-launch not available — single-instance plugin may panic."
fi
fi
fi
# ------------------------------------------------------------------------------
# Make CEF runtime libraries discoverable.
#
# macOS bundles the framework into `OpenHuman.app/Contents/Frameworks/` so the
# OS resolves it automatically. On Linux + Windows we build with --no-bundle
# (faster, no .deb / .msi staging needed for tests), which means the bare
# binary has no co-located libcef.so / libcef.dll. CEF lives in the
# vendored-tauri-cli cache that `ensure-tauri-cli.sh` pinned via CEF_PATH:
# $CEF_PATH/<cef-version>/cef_<os>_<arch>/{libcef.so,libcef.dll,Resources,…}
#
# We find the only versioned directory under CEF_PATH and prepend its CEF
# dist dir to LD_LIBRARY_PATH (Linux) / PATH (Windows). On macOS this is a
# no-op — the .app bundle already self-resolves.
# ------------------------------------------------------------------------------
CEF_PATH="${CEF_PATH:-$HOME/Library/Caches/tauri-cef}"
# Pick exactly one CEF distribution per platform. If two cached versions
# coexist (e.g. after a CEF upgrade) `head -1` silently picks an arbitrary
# one and the binary can load the wrong libcef — Linux/Windows then fail
# only on warmed runners. Error out so the failure is loud.
pick_one_cef_dist() {
local pattern="$1"
local matches
mapfile -t matches < <(find "$CEF_PATH" -mindepth 2 -maxdepth 2 -type d -name "$pattern" 2>/dev/null | sort)
if [ "${#matches[@]}" -gt 1 ]; then
echo "ERROR: multiple CEF distributions found under $CEF_PATH matching $pattern:" >&2
printf ' %s\n' "${matches[@]}" >&2
echo "Set CEF_PATH to a directory containing exactly one cef_* version." >&2
return 1
fi
printf '%s' "${matches[0]:-}"
}
case "$OS" in
Linux)
CEF_DIST_DIR="$(pick_one_cef_dist 'cef_linux_*')" || exit 1
if [ -n "$CEF_DIST_DIR" ] && [ -d "$CEF_DIST_DIR" ]; then
export LD_LIBRARY_PATH="$CEF_DIST_DIR:${LD_LIBRARY_PATH:-}"
echo "[runner] LD_LIBRARY_PATH includes: $CEF_DIST_DIR"
else
echo "[runner] Warning: no CEF Linux distribution found under $CEF_PATH — libcef.so may fail to load."
fi
;;
MINGW*|MSYS*|CYGWIN*|Windows_NT)
CEF_DIST_DIR="$(pick_one_cef_dist 'cef_windows_*')" || exit 1
if [ -n "$CEF_DIST_DIR" ] && [ -d "$CEF_DIST_DIR" ]; then
# Windows uses PATH for DLL resolution. Use the native Windows path form
# so the CEF binary itself can find libcef.dll even though we're running
# under git-bash.
if command -v cygpath >/dev/null 2>&1; then
WIN_CEF_DIST="$(cygpath -w "$CEF_DIST_DIR")"
else
WIN_CEF_DIST="$CEF_DIST_DIR"
fi
export PATH="$CEF_DIST_DIR:$PATH"
echo "[runner] PATH includes: $CEF_DIST_DIR (win: $WIN_CEF_DIST)"
else
echo "[runner] Warning: no CEF Windows distribution found under $CEF_PATH — libcef.dll may fail to load."
fi
;;
esac
# ------------------------------------------------------------------------------
# Launch CEF app — CDP on 19222 is already enabled in lib.rs (see CLAUDE.md).
# ------------------------------------------------------------------------------
LOG_DIR="${RUNNER_TEMP:-${TMPDIR:-/tmp}}"
APP_LOG="$LOG_DIR/openhuman-e2e-app-${LOG_SUFFIX}.log"
APP_ARGS=()
# CEF/Chromium needs extra coaxing in headless / containerized Linux runs:
#
# --no-sandbox crbug.com/638180 — refuses to start as root
# without this. The openhuman_ci docker image
# runs as uid 0.
# --disable-dev-shm-usage docker /dev/shm is often 64 MB; Chromium
# assumes ≥2 GB and crashes mid-startup
# ("Failed global descriptor lookup: 7" in the
# zygote helper).
# --disable-gpu no GPU in the CI container.
# --no-zygote skips the zygote launcher that wants dbus.
#
# Apply only on Linux. macOS/Windows runners are unprivileged users with a
# real display / GPU; leaving the sandbox on there is correct.
case "$OS" in
Linux)
if [ "$(id -u 2>/dev/null || echo 0)" = "0" ]; then
APP_ARGS+=("--no-sandbox")
fi
APP_ARGS+=(
"--disable-dev-shm-usage"
"--disable-gpu"
"--no-zygote"
)
echo "[runner] Linux CEF args: ${APP_ARGS[*]}"
;;
esac
echo "[runner] Launching CEF app: $APP_BIN ${APP_ARGS[*]:-}"
echo "[runner] App logs: $APP_LOG"
# `${APP_ARGS[@]+"${APP_ARGS[@]}"}` is the idiom for expanding a possibly-
# empty array under `set -u`. On macOS APP_ARGS is empty; on Linux it has
# --no-sandbox etc. Without this guard bash errors with "APP_ARGS[@]:
# unbound variable" and the app never launches.
"$APP_BIN" ${APP_ARGS[@]+"${APP_ARGS[@]}"} > "$APP_LOG" 2>&1 &
APP_PID=$!
echo "[runner] Waiting for CDP at http://127.0.0.1:${CEF_CDP_PORT}/json/version ..."
CDP_VERSION_JSON=""
for i in $(seq 1 60); do
CDP_VERSION_JSON="$(curl -sf "http://127.0.0.1:${CEF_CDP_PORT}/json/version" 2>/dev/null || true)"
if [ -n "$CDP_VERSION_JSON" ]; then
echo "[runner] CDP is ready."
break
fi
if ! kill -0 "$APP_PID" 2>/dev/null; then
echo "ERROR: CEF app exited before CDP came up. App log follows:" >&2
echo "----- $APP_LOG -----" >&2
cat "$APP_LOG" >&2 || true
echo "----- end log -----" >&2
exit 1
fi
if [ "$i" -eq 60 ]; then
echo "ERROR: CDP did not come up within 60s. App log follows:" >&2
echo "----- $APP_LOG -----" >&2
cat "$APP_LOG" >&2 || true
echo "----- end log -----" >&2
exit 1
fi
sleep 1
done
# Wait for the main app target (tauri.localhost) to be visible, then close the
# CEF prewarm child-webview slot (about:blank) so chromedriver attaches to the
# real app. Without this, debuggerAddress picks the first page target — which
# is the prewarm — and chromedriver's session is bound to a target CEF may
# garbage-collect mid-test, killing the session with "session terminated".
echo "[runner] Waiting for main app CDP target (tauri.localhost) ..."
MAIN_TARGET_ID=""
for i in $(seq 1 60); do
TARGETS_JSON="$(curl -sf "http://127.0.0.1:${CEF_CDP_PORT}/json/list" 2>/dev/null || true)"
MAIN_TARGET_ID="$(printf '%s' "$TARGETS_JSON" | python3 -c '
import json, sys
try:
data = json.load(sys.stdin)
except Exception:
sys.exit(0)
for t in data:
if t.get("type") == "page" and "tauri.localhost" in (t.get("url") or ""):
print(t.get("id", ""))
break
' 2>/dev/null || true)"
if [ -n "$MAIN_TARGET_ID" ]; then
echo "[runner] Main app target: $MAIN_TARGET_ID"
break
fi
if [ "$i" -eq 60 ]; then
echo "[runner] Warning: main app target never appeared — chromedriver may attach to the wrong target."
break
fi
sleep 1
done
if [ -n "$MAIN_TARGET_ID" ]; then
printf '%s' "$TARGETS_JSON" | python3 -c '
import json, sys
try:
data = json.load(sys.stdin)
except Exception:
sys.exit(0)
for t in data:
if t.get("type") == "page" and (t.get("url") or "").startswith("about:"):
print(t.get("id", ""))
' 2>/dev/null | while read -r STALE_ID; do
if [ -n "$STALE_ID" ]; then
echo "[runner] Closing prewarm/about:blank target: $STALE_ID"
curl -sf "http://127.0.0.1:${CEF_CDP_PORT}/json/close/$STALE_ID" > /dev/null 2>&1 || true
fi
done
fi
# ------------------------------------------------------------------------------
# Resolve a chromedriver whose major matches CEF's bundled Chromium.
#
# chromedriver is strict about same-major-version matching (e.g. cd148 cannot
# talk to Chrome 146). The Appium chromium driver ships its own bundled
# chromedriver but that often drifts ahead of CEF's pinned Chromium.
# Solution: parse Chromium's version from /json/version, then download the
# matching chromedriver from Chrome for Testing into a workspace-local cache.
# We pass the resulting binary to Appium via the `appium:chromedriverExecutable`
# capability (wired in wdio.conf.ts via E2E_CHROMEDRIVER_PATH).
# ------------------------------------------------------------------------------
CHROMIUM_FULL_VERSION="$(echo "$CDP_VERSION_JSON" | sed -n 's/.*"Browser": *"[^/]*\/\([^"]*\)".*/\1/p' | head -1)"
if [ -z "$CHROMIUM_FULL_VERSION" ]; then
CHROMIUM_FULL_VERSION="$(echo "$CDP_VERSION_JSON" | sed -n 's/.*"Browser": *"\([^"]*\)".*/\1/p' | sed 's/^[^/]*\///' | head -1)"
fi
echo "[runner] CEF Chromium version: ${CHROMIUM_FULL_VERSION:-<unknown>}"
case "$OS" in
Darwin)
ARCH="$(uname -m)"
case "$ARCH" in
arm64) CFT_PLATFORM="mac-arm64" ;;
x86_64) CFT_PLATFORM="mac-x64" ;;
*) CFT_PLATFORM="mac-arm64" ;;
esac
CD_EXE_NAME="chromedriver"
;;
Linux)
CFT_PLATFORM="linux64"
CD_EXE_NAME="chromedriver"
;;
MINGW*|MSYS*|CYGWIN*|Windows_NT)
CFT_PLATFORM="win64"
CD_EXE_NAME="chromedriver.exe"
;;
esac
CD_CACHE_DIR="$APP_DIR/test/e2e/.cache/chromedriver/${CHROMIUM_FULL_VERSION:-unknown}-${CFT_PLATFORM}"
CD_BINARY="$CD_CACHE_DIR/chromedriver-${CFT_PLATFORM}/${CD_EXE_NAME}"
if [ -n "$CHROMIUM_FULL_VERSION" ] && [ ! -x "$CD_BINARY" ]; then
CD_URL="https://storage.googleapis.com/chrome-for-testing-public/${CHROMIUM_FULL_VERSION}/${CFT_PLATFORM}/chromedriver-${CFT_PLATFORM}.zip"
echo "[runner] Downloading matching chromedriver: $CD_URL"
mkdir -p "$CD_CACHE_DIR"
CD_ZIP="$CD_CACHE_DIR/chromedriver.zip"
if curl -fSL "$CD_URL" -o "$CD_ZIP"; then
if command -v unzip >/dev/null 2>&1; then
(cd "$CD_CACHE_DIR" && unzip -o -q chromedriver.zip)
else
# The openhuman_ci docker image doesn't ship `unzip`; use Python's
# stdlib zipfile so we don't have to add a system package install.
python3 -c "import sys,zipfile; zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])" \
"$CD_ZIP" "$CD_CACHE_DIR"
fi
chmod +x "$CD_BINARY" 2>/dev/null || true
else
echo "[runner] Warning: chromedriver $CHROMIUM_FULL_VERSION not on Chrome for Testing; falling back to Appium-bundled chromedriver."
fi
fi
if [ -x "$CD_BINARY" ]; then
export E2E_CHROMEDRIVER_PATH="$CD_BINARY"
echo "[runner] Using chromedriver: $E2E_CHROMEDRIVER_PATH"
fi
# ------------------------------------------------------------------------------
# Start Appium (chromium driver)
# ------------------------------------------------------------------------------
# shellcheck source=/dev/null
source "$SCRIPT_DIR/e2e-resolve-node-appium.sh"
# Make sure the chromium driver is installed. `appium driver list --installed`
# exits non-zero on parse errors in some Appium versions, so just attempt the
# install and ignore "already installed" output.
echo "[runner] Ensuring Appium chromium driver is installed..."
"$APPIUM_BIN" driver install --source=npm appium-chromium-driver >/dev/null 2>&1 || true
APPIUM_LOG="$LOG_DIR/appium-e2e-${LOG_SUFFIX}.log"
# Fail fast if something else is already serving on the Appium port. Otherwise
# `curl /status` succeeds against the stale server while our just-launched
# Appium dies with EADDRINUSE — we'd silently drive the wrong instance.
if curl -sf "http://127.0.0.1:$APPIUM_PORT/status" >/dev/null 2>&1; then
echo "ERROR: Appium is already listening on port $APPIUM_PORT. Stop the stale server or set APPIUM_PORT." >&2
exit 1
fi
echo "[runner] Starting Appium on port $APPIUM_PORT"
echo "[runner] Appium logs: $APPIUM_LOG"
"$APPIUM_BIN" --port "$APPIUM_PORT" --relaxed-security > "$APPIUM_LOG" 2>&1 &
APPIUM_PID=$!
for i in $(seq 1 30); do
# If Appium crashed between forks (e.g. unhandled driver-load error), bail
# out instead of polling /status for 30s.
if ! kill -0 "$APPIUM_PID" 2>/dev/null; then
echo "ERROR: Appium exited before becoming ready. Appium log follows:" >&2
echo "----- $APPIUM_LOG -----" >&2
cat "$APPIUM_LOG" >&2 || true
echo "----- end log -----" >&2
exit 1
fi
if curl -sf "http://127.0.0.1:$APPIUM_PORT/status" >/dev/null 2>&1; then
echo "[runner] Appium is ready."
break
fi
if [ "$i" -eq 30 ]; then
echo "ERROR: Appium did not start within 30 seconds." >&2
exit 1
fi
sleep 1
done
# ------------------------------------------------------------------------------
# Run WDIO
# ------------------------------------------------------------------------------
if [ -n "$SPEC_ARG" ]; then
echo "[runner] Running single spec: $SPEC_ARG"
pnpm exec wdio run test/wdio.conf.ts --spec "$SPEC_ARG"
else
echo "[runner] Running full E2E suite (single shared session)..."
pnpm exec wdio run test/wdio.conf.ts
fi
+6 -205
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env bash
#
# Run a single WebDriverIO E2E spec.
# Legacy per-spec runner — kept as a thin shim that delegates to the
# unified session runner. The harness no longer has separate driver paths
# per platform (Mac2 vs tauri-driver); everything runs on Appium Chromium
# driver attached to CEF's CDP port. See e2e-run-session.sh.
#
# - macOS: Appium mac2 driver (started locally, port 4723)
# - Linux: tauri-driver (started locally, port 4444; blocked for default CEF builds)
#
# Usage:
# Usage (unchanged):
# ./app/scripts/e2e-run-spec.sh test/e2e/specs/login-flow.spec.ts [log-suffix]
#
set -euo pipefail
@@ -13,204 +13,5 @@ set -euo pipefail
SPEC="${1:?spec path required}"
LOG_SUFFIX="${2:-$(basename "$SPEC" .spec.ts)}"
E2E_MOCK_PORT="${E2E_MOCK_PORT:-18473}"
OS="$(uname)"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
REPO_ROOT="$(cd "$APP_DIR/.." && pwd)"
cd "$APP_DIR"
CREATED_TEMP_WORKSPACE=""
DRIVER_PID=""
if [ -z "${OPENHUMAN_WORKSPACE:-}" ]; then
OPENHUMAN_WORKSPACE="$(mktemp -d)"
CREATED_TEMP_WORKSPACE="$OPENHUMAN_WORKSPACE"
export OPENHUMAN_WORKSPACE
echo "Using temporary OPENHUMAN_WORKSPACE: $OPENHUMAN_WORKSPACE"
else
echo "Using OPENHUMAN_WORKSPACE from environment: $OPENHUMAN_WORKSPACE"
fi
if [ "${OPENHUMAN_SERVICE_MOCK:-0}" = "1" ] && [ -z "${OPENHUMAN_SERVICE_MOCK_STATE_FILE:-}" ]; then
OPENHUMAN_SERVICE_MOCK_STATE_FILE="$OPENHUMAN_WORKSPACE/service-mock-state.json"
export OPENHUMAN_SERVICE_MOCK_STATE_FILE
echo "Using OPENHUMAN_SERVICE_MOCK_STATE_FILE: $OPENHUMAN_SERVICE_MOCK_STATE_FILE"
fi
cleanup() {
if [ -n "$DRIVER_PID" ]; then
echo "Stopping driver (pid $DRIVER_PID)..."
kill "$DRIVER_PID" 2>/dev/null || true
wait "$DRIVER_PID" 2>/dev/null || true
fi
if [ -n "$CREATED_TEMP_WORKSPACE" ]; then
rm -rf "$CREATED_TEMP_WORKSPACE"
fi
# Restore original config.toml (or remove the E2E one)
if [ -n "${E2E_CONFIG_BACKUP:-}" ] && [ -f "$E2E_CONFIG_BACKUP" ]; then
mv "$E2E_CONFIG_BACKUP" "$E2E_CONFIG_FILE"
echo "Restored original config.toml"
elif [ -n "${E2E_CONFIG_FILE:-}" ] && [ -f "${E2E_CONFIG_FILE:-}" ]; then
rm -f "$E2E_CONFIG_FILE"
echo "Removed E2E config.toml"
fi
}
trap cleanup EXIT
export VITE_BACKEND_URL="http://127.0.0.1:${E2E_MOCK_PORT}"
export BACKEND_URL="http://127.0.0.1:${E2E_MOCK_PORT}"
echo "Killing any running OpenHuman instances..."
if [ "$OS" = "Darwin" ]; then
pkill -f "OpenHuman" 2>/dev/null || true
# Give the process time to exit and release file locks
sleep 1
fi
echo "Cleaning cached app data..."
if [ "$OS" = "Darwin" ]; then
rm -rf ~/Library/WebKit/com.openhuman.app
rm -rf ~/Library/Caches/com.openhuman.app
rm -rf "$HOME/Library/Application Support/com.openhuman.app"
rm -rf "$HOME/Library/Saved Application State/com.openhuman.app.savedState"
else
rm -rf "$HOME/.local/share/com.openhuman.app" 2>/dev/null || true
rm -rf "$HOME/.cache/com.openhuman.app" 2>/dev/null || true
rm -rf "$HOME/.config/com.openhuman.app" 2>/dev/null || true
fi
# Write config.toml into the default ~/.openhuman/ so the core process
# uses the mock server URL. Appium Mac2 launches the .app via XCUITest
# which does NOT inherit shell environment variables, so BACKEND_URL
# never reaches the embedded core process. Writing api_url to the config file
# is the reliable cross-platform approach.
E2E_CONFIG_DIR="$HOME/.openhuman"
E2E_CONFIG_FILE="$E2E_CONFIG_DIR/config.toml"
E2E_CONFIG_BACKUP=""
mkdir -p "$E2E_CONFIG_DIR"
if [ -f "$E2E_CONFIG_FILE" ]; then
E2E_CONFIG_BACKUP="$E2E_CONFIG_FILE.e2e-backup.$$"
cp "$E2E_CONFIG_FILE" "$E2E_CONFIG_BACKUP"
echo "Backed up existing config.toml to $E2E_CONFIG_BACKUP"
# Remove any existing api_url line and prepend the mock URL
sed -i.bak '/^api_url[[:space:]]*=/d' "$E2E_CONFIG_FILE" && rm -f "$E2E_CONFIG_FILE.bak"
EXISTING_CONTENT="$(cat "$E2E_CONFIG_FILE")"
printf 'api_url = "http://127.0.0.1:%s"\n%s\n' "${E2E_MOCK_PORT}" "$EXISTING_CONTENT" > "$E2E_CONFIG_FILE"
else
cat > "$E2E_CONFIG_FILE" <<TOML
api_url = "http://127.0.0.1:${E2E_MOCK_PORT}"
TOML
fi
echo "Wrote E2E config.toml with api_url=http://127.0.0.1:${E2E_MOCK_PORT}"
DIST_JS="$(ls dist/assets/index-*.js 2>/dev/null | head -1)"
if [ -z "$DIST_JS" ]; then
echo "ERROR: No frontend bundle found at dist/assets/index-*.js." >&2
echo " Run 'pnpm test:e2e:build' to build the app before running E2E tests." >&2
exit 1
fi
if ! grep -q "127.0.0.1:${E2E_MOCK_PORT}" "$DIST_JS"; then
echo "ERROR: frontend bundle does NOT contain mock server URL (127.0.0.1:${E2E_MOCK_PORT})." >&2
echo " Run 'pnpm test:e2e:build' to rebuild with the mock URL." >&2
exit 1
fi
if ! grep -q "127.0.0.1:${E2E_MOCK_PORT}" "$DIST_JS"; then
echo "ERROR: frontend bundle does NOT contain mock server URL (127.0.0.1:${E2E_MOCK_PORT})." >&2
echo " Run 'pnpm test:e2e:build' to rebuild with the mock URL." >&2
exit 1
fi
echo "Verified: frontend bundle contains mock server URL."
case "$OS" in
Linux|MINGW*|MSYS*|CYGWIN*|Windows_NT)
# -------------------------------------------------------------------------
# Linux + Windows: start tauri-driver (WebKitWebDriver/Linux,
# MSEdgeDriver/Windows under the hood).
# -------------------------------------------------------------------------
export TAURI_DRIVER_PORT="${TAURI_DRIVER_PORT:-4444}"
case "$OS" in
MINGW*|MSYS*|CYGWIN*|Windows_NT)
DRIVER_LOG="${TMP:-${TEMP:-${TMPDIR:-/tmp}}}/tauri-driver-e2e-${LOG_SUFFIX}.log"
TAURI_DRIVER_NAME="tauri-driver.exe"
;;
*)
DRIVER_LOG="/tmp/tauri-driver-e2e-${LOG_SUFFIX}.log"
TAURI_DRIVER_NAME="tauri-driver"
;;
esac
TAURI_DRIVER_BIN="$(command -v "$TAURI_DRIVER_NAME" 2>/dev/null || true)"
if [ -z "${TAURI_DRIVER_BIN:-}" ] || [ ! -x "$TAURI_DRIVER_BIN" ]; then
# Prefer the POSIX $HOME/.cargo/bin path so `-x` works in git-bash on
# Windows. Only fall back to %USERPROFILE% (converted to a POSIX path
# via cygpath if available) when $HOME doesn't have the binary —
# that covers cases where the runner's HOME is overridden.
TAURI_DRIVER_BIN="$HOME/.cargo/bin/$TAURI_DRIVER_NAME"
if [ ! -x "$TAURI_DRIVER_BIN" ] && [ -n "${USERPROFILE:-}" ]; then
if command -v cygpath >/dev/null 2>&1; then
TAURI_DRIVER_BIN="$(cygpath -u "$USERPROFILE")/.cargo/bin/$TAURI_DRIVER_NAME"
else
TAURI_DRIVER_BIN="$USERPROFILE/.cargo/bin/$TAURI_DRIVER_NAME"
fi
fi
fi
if [ ! -x "$TAURI_DRIVER_BIN" ]; then
echo "ERROR: $TAURI_DRIVER_NAME not found. Install with: cargo install tauri-driver" >&2
exit 1
fi
echo "Starting tauri-driver on port $TAURI_DRIVER_PORT..."
echo " Driver logs: $DRIVER_LOG"
"$TAURI_DRIVER_BIN" --port "$TAURI_DRIVER_PORT" > "$DRIVER_LOG" 2>&1 &
DRIVER_PID=$!
for i in $(seq 1 15); do
if curl -sf "http://127.0.0.1:$TAURI_DRIVER_PORT/status" >/dev/null 2>&1; then
echo "tauri-driver is ready."
break
fi
if [ "$i" -eq 15 ]; then
echo "ERROR: tauri-driver did not start within 15 seconds." >&2
cat "$DRIVER_LOG" >&2
exit 1
fi
sleep 1
done
;;
Darwin)
# -------------------------------------------------------------------------
# macOS: start Appium Mac2.
# -------------------------------------------------------------------------
export APPIUM_PORT="${APPIUM_PORT:-4723}"
# shellcheck source=/dev/null
source "$SCRIPT_DIR/e2e-resolve-node-appium.sh"
APPIUM_LOG="/tmp/appium-e2e-${LOG_SUFFIX}.log"
NODE_VER=$("$NODE24" --version)
echo "Starting Appium on port $APPIUM_PORT (Node $NODE_VER)..."
echo " Appium logs: $APPIUM_LOG"
"$APPIUM_BIN" --port "$APPIUM_PORT" --relaxed-security > "$APPIUM_LOG" 2>&1 &
DRIVER_PID=$!
for i in $(seq 1 30); do
if curl -sf "http://127.0.0.1:$APPIUM_PORT/status" >/dev/null 2>&1; then
echo "Appium is ready."
break
fi
if [ "$i" -eq 30 ]; then
echo "ERROR: Appium did not start within 30 seconds." >&2
exit 1
fi
sleep 1
done
;;
*)
echo "ERROR: unsupported OS for e2e: $OS" >&2
exit 1
;;
esac
echo "Running E2E spec ($SPEC)..."
pnpm exec wdio run test/wdio.conf.ts --spec "$SPEC"
exec "$SCRIPT_DIR/e2e-run-session.sh" "$SPEC" "$LOG_SUFFIX"
+27 -11
View File
@@ -1,25 +1,41 @@
/**
* Cross-platform app lifecycle helpers for E2E tests.
*
* ## Appium Mac2 (macOS)
* XCUITest launches the .app bundle. The app starts with visible:false
* (tray app) — only the menu bar is visible until a deep link shows the window.
* Readiness is detected by polling the accessibility tree element count.
* The harness is unified onto Appium Chromium driver attached to CEF's
* remote-debugging (CDP) port on macOS / Linux / Windows. The session
* exposes the WebView DOM directly — standard CSS selectors, `el.click()`,
* and `browser.execute(...)` all work as in a normal browser session.
*
* ## tauri-driver (Linux)
* tauri-driver launches the debug binary directly and exposes the WebView
* DOM via W3C WebDriver. Readiness is detected by checking document state
* and the presence of the React root element.
* Readiness checks use `document.readyState` + React-root presence;
* the old Mac2 accessibility-tree polling is gone.
*/
import { isTauriDriver } from './platform';
/**
* Wait for the app process to be ready.
* The app starts with a hidden window, so we just wait for the process
* to initialize (driver has already launched it).
*
* The runner script has already launched the CEF binary and confirmed CDP
* is responding on :19222 before WDIO connects, so by the time a spec runs
* we usually just need to give the React root a beat to mount. Specs that
* need a stricter guarantee should call `waitForAppReady` directly.
*/
export async function waitForApp(): Promise<void> {
await browser.pause(5_000);
try {
await waitForAppReady(15_000);
} catch (error) {
// Only swallow genuine readiness timeouts (the error waitForAppReady
// throws when the DOM never settles in the budget). Anything else —
// session terminated, executeScript not supported, the DOM crashed —
// surfaces with full context instead of being hidden behind a blind
// 5s pause.
const message = error instanceof Error ? error.message : String(error);
if (!message.includes('waitForAppReady timed out')) {
throw error;
}
// Fall back to the legacy fixed pause so specs that historically tolerated
// a slow startup don't regress.
await browser.pause(5_000);
}
}
/**
+13 -6
View File
@@ -1,10 +1,20 @@
/**
* Core JSON-RPC for E2E: WebView execute on tauri-driver (Linux), Node fetch on Appium Mac2.
* Core JSON-RPC for E2E.
*
* Always uses the Node-side HTTP path now. The old WebView path used
* `await import('@tauri-apps/api/core')` inside `browser.execute` to fetch
* the RPC URL — that only worked under tauri-driver because Tauri injects
* an import map into the renderer. Under the unified Appium Chromium
* driver (which speaks plain CDP to CEF), the dynamic specifier resolves
* to nothing and the call fails with "Failed to resolve module specifier
* '@tauri-apps/api/core'".
*
* The Node path probes the core's port range, attaches the per-launch
* bearer token written to `${tmpdir}/openhuman-e2e-rpc-token`, and does
* the JSON-RPC over plain HTTP — fully driver-agnostic.
*/
import { callOpenhumanRpcNode } from './core-rpc-node';
import type { RpcCallResult } from './core-rpc-webview';
import { callOpenhumanRpcWebView } from './core-rpc-webview';
import { supportsExecuteScript } from './platform';
export type { RpcCallResult };
@@ -12,8 +22,5 @@ export async function callOpenhumanRpc<T = unknown>(
method: string,
params: Record<string, unknown> = {}
): Promise<RpcCallResult<T>> {
if (supportsExecuteScript()) {
return callOpenhumanRpcWebView<T>(method, params);
}
return callOpenhumanRpcNode<T>(method, params);
}
+17 -36
View File
@@ -1,57 +1,38 @@
/**
* Platform detection utilities for cross-platform E2E tests.
*
* Two automation backends are supported:
* The harness was previously split between Appium Mac2 (accessibility tree)
* and tauri-driver (DOM). It is now unified onto **Appium Chromium driver
* attached to CEF's CDP port** on all three platforms (macOS / Linux /
* Windows). Every session exposes the full DOM and supports W3C
* `executeScript`, so the legacy branching collapses to a single backend.
*
* - **Appium Mac2** (macOS): Drives the `.app` bundle via XCUITest / accessibility
* tree. Elements are XCUIElementType* nodes; clicks require W3C pointer actions
* because accessibility clicks don't propagate to WKWebView DOM handlers.
*
* - **tauri-driver** (Linux): WebDriver server shipped by the Tauri project.
* Exposes the WebView DOM directly — standard CSS selectors and `el.click()`
* work as in a normal browser session.
* These functions are kept (rather than deleted) as a shim so the ~40
* existing specs that branch on `isTauriDriver()` / `isMac2()` still pass:
* they always take the DOM-capable code path now.
*/
/**
* Returns `true` when the session is driven by tauri-driver (Linux E2E).
*
* tauri-driver does not set `platformName` or `appium:automationName`, so the
* absence of Mac2 markers is the signal. We also check `process.platform` as
* a secondary indicator.
* Always true — the unified Chromium-driver session exposes the WebView DOM
* exactly like the old tauri-driver path did. Specs that gated DOM work
* behind `if (isTauriDriver())` now run that work on every platform.
*/
export function isTauriDriver(): boolean {
if (typeof browser === 'undefined') return process.platform === 'linux';
const caps = browser.capabilities as Record<string, unknown>;
const automation = String(
caps['appium:automationName'] ?? caps['automationName'] ?? ''
).toLowerCase();
// Appium Mac2 always sets automationName to 'mac2'
if (automation === 'mac2' || automation.includes('mac2')) return false;
const platform = String(caps.platformName ?? caps['appium:platformName'] ?? '').toLowerCase();
// If platformName is 'mac' it's Appium on macOS even without automationName
if (platform === 'mac') return false;
return true;
}
/**
* Returns `true` when the session is driven by Appium Mac2 (macOS E2E).
* Always false. The Mac2 accessibility-tree backend is retired; macOS now
* speaks CDP just like Linux/Windows.
*/
export function isMac2(): boolean {
return !isTauriDriver();
return false;
}
/**
* Returns `true` when the WebDriver session supports W3C Execute Script
* for running JS inside the WebView.
*
* - tauri-driver: YES (full W3C WebDriver compliance)
* - Appium Mac2: NO (only supports `macos: *` extension commands)
* Always true — Chromium driver is a full W3C WebDriver and supports
* `browser.execute(...)` on every platform.
*/
export function supportsExecuteScript(): boolean {
return isTauriDriver();
return true;
}
+100 -83
View File
@@ -1,116 +1,133 @@
import type { Options } from '@wdio/types';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { captureFailureArtifacts } from './e2e/helpers/artifacts';
/**
* Unified WDIO config — single Appium Chromium-driver session that attaches
* to the running CEF app over its remote-debugging port (CDP).
*
* One automation backend on every platform:
*
* macOS / Linux / Windows → Appium Chromium driver → CEF :19222
*
* The runner script (`scripts/e2e-run-session.sh`) is responsible for:
* 1. Launching the built CEF app binary.
* 2. Waiting until `http://127.0.0.1:19222/json/version` responds (CDP up).
* 3. Starting Appium with the `chromium` driver installed.
* 4. Invoking `wdio` against this config.
*
* WDIO creates ONE session per worker. With `maxInstances: 1` and no
* cross-spec teardown, all specs run sequentially in the same session,
* against the same app process — no restart cost between spec files.
* Tests are intentionally order-dependent: state from spec N flows into
* spec N+1. Each spec is responsible for any reset it requires.
*/
const configDir = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(configDir, '..');
const repoRoot = path.resolve(projectRoot, '..');
const tsconfigE2ePath = path.join(projectRoot, 'test', 'tsconfig.e2e.json');
const testSpecsPath = path.join(projectRoot, 'test', 'e2e', 'specs', '**', '*.spec.ts');
/**
* Resolve the path to the built Tauri application.
*
* - macOS: .app bundle for Appium Mac2
* - Linux: debug binary for tauri-driver
* - Windows: .exe for tauri-driver
*/
function getAppPath(): string {
const bundleBases = [
path.join(projectRoot, 'src-tauri', 'target', 'debug', 'bundle'),
path.join(repoRoot, 'target', 'debug', 'bundle'),
];
const APPIUM_PORT = parseInt(process.env.APPIUM_PORT || '4723', 10);
const CEF_CDP_HOST = process.env.CEF_CDP_HOST || '127.0.0.1';
const CEF_CDP_PORT = parseInt(process.env.CEF_CDP_PORT || '19222', 10);
switch (process.platform) {
case 'darwin': {
for (const base of bundleBases) {
const appPath = path.join(base, 'macos', 'OpenHuman.app');
if (fs.existsSync(appPath)) {
return appPath;
}
}
return path.join(bundleBases[0], 'macos', 'OpenHuman.app');
}
case 'win32':
return path.join(projectRoot, 'src-tauri', 'target', 'debug', 'OpenHuman.exe');
case 'linux': {
// tauri-driver launches the binary directly (not a bundle).
// Prefer the Tauri build output (src-tauri/target) over the repo-root
// target/ which may contain a stale core-only binary.
const candidates = [
path.join(projectRoot, 'src-tauri', 'target', 'debug', 'OpenHuman'),
path.join(repoRoot, 'target', 'debug', 'OpenHuman'),
];
for (const candidate of candidates) {
if (fs.existsSync(candidate)) return candidate;
}
return candidates[0];
}
default:
throw new Error(`Unsupported platform: ${process.platform}`);
}
// appium-chromium-driver advertises support for platformName ∈ {windows, mac, linux}
// (not "chromium" — that's only the automationName). Pick the actual OS so the
// capability negotiation succeeds.
function platformNameForHost(): 'mac' | 'linux' | 'windows' {
if (process.platform === 'darwin') return 'mac';
if (process.platform === 'win32') return 'windows';
return 'linux';
}
/**
* Build capabilities for the current platform.
*
* - Linux: tauri-driver (W3C WebDriver, port 4444)
* - Windows: tauri-driver (W3C WebDriver, port 4444) — same as Linux,
* launches the bare .exe rather than a bundle.
* - macOS: Appium Mac2 (XCUITest, port 4723)
*/
function getPlatformCapabilities(): Record<string, unknown>[] {
if (process.platform === 'linux' || process.platform === 'win32') {
return [{ 'tauri:options': { application: getAppPath() } }];
}
// macOS: Appium Mac2
return [
{
platformName: 'mac',
'appium:automationName': 'Mac2',
'appium:app': getAppPath(),
'appium:showServerLogs': true,
},
];
}
/** Port for the automation driver: tauri-driver (4444) or Appium (4723). */
const isTauriDriverHost = process.platform === 'linux' || process.platform === 'win32';
const driverPort = isTauriDriverHost
? parseInt(process.env.TAURI_DRIVER_PORT || '4444', 10)
: parseInt(process.env.APPIUM_PORT || '4723', 10);
export const config: Options.Testrunner & Record<string, unknown> = {
runner: 'local',
hostname: '127.0.0.1',
port: driverPort,
port: APPIUM_PORT,
path: '/',
specs: [testSpecsPath],
rootDir: projectRoot,
maxInstances: 1, // Tauri apps are single-instance
capabilities: getPlatformCapabilities(),
// Single session — Tauri+CEF is one app instance.
maxInstances: 1,
capabilities: [
{
platformName: platformNameForHost(),
'appium:automationName': 'Chromium',
// The runner downloads a chromedriver whose major matches CEF's
// bundled Chromium and exports its path here. If unset, Appium falls
// back to its bundled chromedriver — which usually drifts ahead of
// CEF and produces a "ChromeDriver only supports Chrome version N"
// session-creation error.
//
// Appium chromium driver names this capability `executable` (see
// appium-chromium-driver/build/lib/desired-caps.js), not the more
// common Chrome-driver name `chromedriverExecutable`.
...(process.env.E2E_CHROMEDRIVER_PATH
? { 'appium:executable': process.env.E2E_CHROMEDRIVER_PATH }
: {}),
'goog:chromeOptions': {
// Attach to the already-running CEF process. chromedriver will not
// try to launch its own Chrome — it picks the first page target
// exposed at this address (which is the main OpenHuman webview).
debuggerAddress: `${CEF_CDP_HOST}:${CEF_CDP_PORT}`,
},
},
],
logLevel: 'warn',
bail: 0,
waitforTimeout: 10_000,
// Linux tauri-driver can take longer to establish the initial session on
// loaded CI runners; keep macOS defaults while giving Linux more headroom.
connectionRetryTimeout: isTauriDriverHost ? 240_000 : 120_000,
connectionRetryCount: isTauriDriverHost ? 5 : 3,
// No appium/tauri-driver service — driver is started externally via scripts.
connectionRetryTimeout: 120_000,
connectionRetryCount: 3,
framework: 'mocha',
reporters: ['spec'],
mochaOpts: {
ui: 'bdd',
timeout: 120_000, // Billing/settings tests need extra time for API polling
// Billing/settings flows poll on real timers; keep the generous budget.
timeout: 120_000,
},
autoCompileOpts: { tsNodeOpts: { project: tsconfigE2ePath } },
/**
* Always capture screenshot + page source on failure so agents can
* inspect what the app looked like the moment the assertion failed.
* After the chromedriver session attaches, switch the active window to
* the main OpenHuman app webview.
*
* CEF exposes multiple CDP page targets:
* - `about:blank` — the CEF prewarm hot-loaded child-webview slot
* (see CEF_PREWARM_LABEL in src-tauri/src/lib.rs).
* - `OpenHuman` @ `http://tauri.localhost/#/` — the main React app.
*
* `debuggerAddress` makes chromedriver attach to the *first* page target,
* which is `about:blank`. Without this switch, every spec ends up looking
* at an empty document. We pick the first window whose URL contains
* `tauri.localhost`, falling back to the first non-`about:blank`.
*/
before: async function () {
const handles = await browser.getWindowHandles();
let target: string | null = null;
for (const handle of handles) {
await browser.switchToWindow(handle);
const url = await browser.getUrl();
if (url.includes('tauri.localhost')) {
target = handle;
break;
}
}
if (!target) {
for (const handle of handles) {
await browser.switchToWindow(handle);
const url = await browser.getUrl();
if (!url.startsWith('about:')) {
target = handle;
break;
}
}
}
if (target) {
await browser.switchToWindow(target);
}
},
afterTest: async function (
test: { title: string; parent?: string },
_context: unknown,