diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..0e7350c --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,78 @@ +# E2E Testing — CEF Mode + +## Overview + +The Electrobun build supports two rendering backends: + +| Backend | Use case | E2E automation | WebGL | xterm limit | +|---------|----------|---------------|-------|-------------| +| **WebKitGTK** (default) | Production | None (no WebDriver) | No | 4 instances | +| **CEF** (opt-in) | Dev/test | CDP via port 9222 | Yes | Unlimited | + +Production always ships with WebKitGTK — lighter footprint, no Chromium dependency, system-native rendering. CEF mode is for development and CI testing only. + +## Enabling CEF Mode + +Set the `AGOR_CEF` environment variable before building or running: + +```bash +# Build with CEF +AGOR_CEF=1 electrobun build --env=dev + +# Dev mode with CEF +AGOR_CEF=1 electrobun dev +``` + +This does three things in `electrobun.config.ts`: +1. Sets `bundleCEF: true` — downloads and bundles CEF libraries +2. Sets `defaultRenderer: "cef"` — uses CEF instead of WebKitGTK +3. Adds `chromiumFlags` — enables `--remote-debugging-port=9222` and `--remote-allow-origins=*` + +## Running E2E Tests + +```bash +# Run Electrobun E2E tests (sets AGOR_CEF=1 automatically) +npm run test:e2e:electrobun + +# Run both Tauri and Electrobun E2E suites +npm run test:e2e:both + +# Full test suite including E2E +npm run test:all:e2e +``` + +The test runner (`wdio.electrobun.conf.js`): +1. Creates an isolated test fixture (temp dirs, test groups.json) +2. Builds the app if no binary exists +3. Launches the app with `AGOR_CEF=1` and `AGOR_TEST=1` +4. Waits for CDP port 9222 to respond +5. Connects WebDriverIO via `automationProtocol: 'devtools'` +6. Runs all shared spec files +7. Kills the app and cleans up fixtures + +## CI Setup + +```yaml +- name: E2E (Electrobun + CEF) + run: xvfb-run AGOR_CEF=1 npm run test:e2e:electrobun + env: + AGOR_TEST: '1' +``` + +CEF requires a display server. Use `xvfb-run` on headless CI. + +## Port Assignments + +| Port | Service | +|------|---------| +| 9222 | CDP remote debugging (CEF) | +| 9760 | Vite dev server (HMR) | +| 9761 | Reserved (was WebKitWebDriver) | + +## Troubleshooting + +**"CDP port 9222 not ready after 30000ms"** — The app failed to start or CEF was not bundled. Check that the build used `AGOR_CEF=1`. Look for `[LAUNCHER] ERROR: CEF libraries found but LD_PRELOAD not set` in stderr. + +**"No binary found"** — The test runner falls back to `electrobun dev`, which builds and launches in one step. This is slower but works without a pre-built binary. + +**Tests pass with CEF but fail with WebKitGTK** — Expected. WebKitGTK has no WebDriver/CDP support in Electrobun. E2E tests only run against CEF. Manual testing covers WebKitGTK. diff --git a/package.json b/package.json index 3f7888d..2f88dca 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "test:cargo": "cd src-tauri && cargo test", "test:e2e": "wdio run tests/e2e/wdio.conf.js", "test:e2e:tauri": "wdio run tests/e2e/wdio.tauri.conf.js", - "test:e2e:electrobun": "wdio run tests/e2e/wdio.electrobun.conf.js", + "test:e2e:electrobun": "AGOR_CEF=1 wdio run tests/e2e/wdio.electrobun.conf.js", "test:e2e:both": "npm run test:e2e:tauri && npm run test:e2e:electrobun", "test:all": "bash scripts/test-all.sh", "test:all:e2e": "bash scripts/test-all.sh --e2e", diff --git a/tests/e2e/helpers/actions.ts b/tests/e2e/helpers/actions.ts index 420c4e1..da006f4 100644 --- a/tests/e2e/helpers/actions.ts +++ b/tests/e2e/helpers/actions.ts @@ -8,6 +8,26 @@ import { browser } from '@wdio/globals'; import * as S from './selectors.ts'; +/** + * Wait for a TCP port to accept connections. Used by CDP-based E2E configs + * to wait for the debugging port before connecting WebDriverIO. + * + * Polls `http://localhost:{port}/json` (CDP discovery endpoint) every 500ms. + */ +export async function waitForPort(port: number, timeout: number): Promise { + const start = Date.now(); + while (Date.now() - start < timeout) { + try { + const res = await fetch(`http://localhost:${port}/json`); + if (res.ok) return; + } catch { + // Port not ready yet — retry + } + await new Promise(r => setTimeout(r, 500)); + } + throw new Error(`CDP port ${port} not ready after ${timeout}ms`); +} + /** Open settings panel via gear icon click */ export async function openSettings(): Promise { // Try clicking settings button — may need multiple attempts on WebKitGTK diff --git a/tests/e2e/wdio.electrobun.conf.js b/tests/e2e/wdio.electrobun.conf.js index a470ee7..87b8e77 100644 --- a/tests/e2e/wdio.electrobun.conf.js +++ b/tests/e2e/wdio.electrobun.conf.js @@ -1,77 +1,119 @@ /** * WebDriverIO config for Electrobun stack E2E tests. * - * Extends shared config with WebKitWebDriver lifecycle and optional - * PTY daemon management. Port: 9761 (per project convention). + * Uses CDP (Chrome DevTools Protocol) via CEF mode for reliable E2E automation. + * Electrobun must be built/run with AGOR_CEF=1 to bundle CEF and expose + * --remote-debugging-port=9222 (configured in electrobun.config.ts). + * + * Port conventions: + * 9222 — CDP debugging port (CEF) + * 9760 — Vite dev server (HMR) + * 9761 — (reserved, was WebKitWebDriver) */ -import { execSync } from 'node:child_process'; +import { execSync, spawn } from 'node:child_process'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { existsSync, rmSync } from 'node:fs'; import { sharedConfig } from './wdio.shared.conf.js'; +import { waitForPort } from './helpers/actions.ts'; const __dirname = dirname(fileURLToPath(import.meta.url)); const projectRoot = resolve(__dirname, '../..'); const electrobunRoot = resolve(projectRoot, 'ui-electrobun'); +const CDP_PORT = 9222; + // Use the Electrobun fixture generator (different groups.json format) let fixture; try { const { createTestFixture } = await import('../../ui-electrobun/tests/e2e/fixtures.ts'); - fixture = createTestFixture('agor-ebun-unified'); + fixture = createTestFixture('agor-ebun-cdp'); } catch { - // Fall back to the Tauri fixture generator if Electrobun fixtures not available const { createTestFixture } = await import('./infra/fixtures.ts'); - fixture = createTestFixture('agor-ebun-unified'); + fixture = createTestFixture('agor-ebun-cdp'); } process.env.AGOR_TEST = '1'; +process.env.AGOR_CEF = '1'; process.env.AGOR_TEST_DATA_DIR = fixture.dataDir; process.env.AGOR_TEST_CONFIG_DIR = fixture.configDir; -const WEBDRIVER_PORT = 9761; +console.log(`[electrobun-cdp] Test fixture at ${fixture.rootDir ?? fixture.configDir}`); -console.log(`[electrobun] Test fixture at ${fixture.rootDir ?? fixture.configDir}`); +let appProcess; export const config = { ...sharedConfig, - port: WEBDRIVER_PORT, + // Use devtools protocol (CDP) instead of WebDriver + automationProtocol: 'devtools', capabilities: [{ - 'wdio:enforceWebDriverClassic': true, - browserName: 'webkit', + browserName: 'chromium', + 'goog:chromeOptions': { + debuggerAddress: `localhost:${CDP_PORT}`, + }, }], onPrepare() { - // Try multiple binary paths (dev vs canary vs production) + // Find existing binary or build const candidates = [ resolve(electrobunRoot, 'build/dev-linux-x64/AgentOrchestrator-dev/AgentOrchestrator-dev'), resolve(electrobunRoot, 'build/Agent Orchestrator'), resolve(electrobunRoot, 'build/AgentOrchestrator'), ]; - const electrobunBinary = candidates.find(p => existsSync(p)); + let electrobunBinary = candidates.find(p => existsSync(p)); if (!electrobunBinary && !process.env.SKIP_BUILD) { - console.log('Building Electrobun...'); + console.log('[electrobun-cdp] Building with CEF...'); try { execSync('npx vite build', { cwd: electrobunRoot, stdio: 'inherit' }); - // electrobun build may not be available — skip if missing - try { execSync('electrobun build --env=dev', { cwd: electrobunRoot, stdio: 'inherit' }); } catch {} + execSync('electrobun build --env=dev', { cwd: electrobunRoot, stdio: 'inherit' }); } catch (e) { - console.warn('Build failed:', e.message); + console.warn('[electrobun-cdp] Build failed:', e.message); } + electrobunBinary = candidates.find(p => existsSync(p)); } - const finalBinary = candidates.find(p => existsSync(p)); - if (!finalBinary) { - console.warn('Electrobun binary not found — tests will use WebKitWebDriver with dev server'); + if (!electrobunBinary) { + // Fall back to `electrobun dev` which builds + launches in one step + console.log('[electrobun-cdp] No binary found, launching via electrobun dev...'); + appProcess = spawn('electrobun', ['dev'], { + cwd: electrobunRoot, + env: { + ...process.env, + AGOR_CEF: '1', + AGOR_TEST: '1', + AGOR_TEST_DATA_DIR: fixture.dataDir, + AGOR_TEST_CONFIG_DIR: fixture.configDir, + }, + stdio: 'pipe', + }); + } else { + console.log(`[electrobun-cdp] Launching binary: ${electrobunBinary}`); + appProcess = spawn(electrobunBinary, [], { + env: { + ...process.env, + AGOR_CEF: '1', + AGOR_TEST: '1', + AGOR_TEST_DATA_DIR: fixture.dataDir, + AGOR_TEST_CONFIG_DIR: fixture.configDir, + }, + stdio: 'pipe', + }); } + + appProcess.stdout?.on('data', (d) => process.stdout.write(`[app] ${d}`)); + appProcess.stderr?.on('data', (d) => process.stderr.write(`[app] ${d}`)); + appProcess.on('exit', (code) => console.log(`[electrobun-cdp] App exited with code ${code}`)); + + // Wait for CDP port to become available + return waitForPort(CDP_PORT, 30_000); }, async before() { - // Wait for Electrobun app to load + // Wait for Electrobun app to render await browser.waitUntil( async () => { const hasEl = await browser.execute(() => @@ -83,10 +125,16 @@ export const config = { }, { timeout: 20_000, interval: 500, timeoutMsg: 'Electrobun app did not load in 20s' }, ); - console.log('[electrobun] App loaded.'); + console.log('[electrobun-cdp] App loaded.'); }, - afterSession() { + onComplete() { + if (appProcess) { + console.log('[electrobun-cdp] Stopping app...'); + appProcess.kill('SIGTERM'); + appProcess = undefined; + } + const cleanup = fixture.cleanup ?? (() => { try { if (fixture.rootDir) rmSync(fixture.rootDir, { recursive: true, force: true }); diff --git a/ui-electrobun/electrobun.config.ts b/ui-electrobun/electrobun.config.ts index 012c481..e19d39a 100644 --- a/ui-electrobun/electrobun.config.ts +++ b/ui-electrobun/electrobun.config.ts @@ -1,5 +1,15 @@ import type { ElectrobunConfig } from "electrobun"; +// CEF mode: opt-in via AGOR_CEF=1. Used for dev/test (E2E automation via CDP, +// WebGL support, unlimited xterm instances). Production uses WebKitGTK (lighter, +// no Chromium dependency, system-native). +const useCEF = process.env.AGOR_CEF === "1"; + +// When CEF is active, enable remote debugging for CDP-based E2E automation. +const cefFlags: Record | undefined = useCEF + ? { "remote-debugging-port": "9222", "remote-allow-origins": "*" } + : undefined; + export default { app: { name: "Agent Orchestrator", @@ -16,16 +26,22 @@ export default { }, watchIgnore: ["dist/**"], mac: { - bundleCEF: false, + bundleCEF: useCEF, bundleWGPU: true, + ...(useCEF && { defaultRenderer: "cef" as const }), + ...(cefFlags && { chromiumFlags: cefFlags }), }, linux: { - bundleCEF: false, + bundleCEF: useCEF, bundleWGPU: true, + ...(useCEF && { defaultRenderer: "cef" as const }), + ...(cefFlags && { chromiumFlags: cefFlags }), }, win: { - bundleCEF: false, + bundleCEF: useCEF, bundleWGPU: true, + ...(useCEF && { defaultRenderer: "cef" as const }), + ...(cefFlags && { chromiumFlags: cefFlags }), }, }, } satisfies ElectrobunConfig;