mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
test(e2e): unify driver onto Appium Chromium attached to CEF CDP (#1696)
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -25,3 +25,4 @@ dist-ssr
|
||||
|
||||
# ESLint cache
|
||||
.eslintcache
|
||||
app/test/e2e/.cache/
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+503
@@ -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
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user