feat: unified E2E testing engine — 205 tests, dual-stack support

Infrastructure:
- adapters/: base, tauri (port 9750), electrobun (port 9761 + PTY daemon)
- helpers/: 120+ centralized selectors, reusable actions, custom assertions
- wdio.shared.conf.js + stack-specific configs

18 unified specs (205 tests):
splash(6) smoke(15) settings(19) terminal(14) agent(15) search(12)
files(15) comms(10) tasks(10) theme(12) groups(12) keyboard(8)
notifications(10) diagnostics(8) status-bar(12) context(9)
worktree(8) llm-judged(10)

Daemon: --stack tauri|electrobun|both flag
Scripts: test:e2e:tauri, test:e2e:electrobun, test:e2e:both
This commit is contained in:
Hibryda 2026-03-22 05:27:36 +01:00
parent 1995f03682
commit 77b9ce9f62
31 changed files with 3547 additions and 344 deletions

View file

@ -0,0 +1,150 @@
/**
* WebDriverIO config for Tauri stack E2E tests.
*
* Extends shared config with tauri-driver lifecycle and capabilities.
* Port: 9750 (per project convention).
*/
import { spawn, execSync } from 'node:child_process';
import { createConnection } from 'node:net';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { rmSync, existsSync } from 'node:fs';
import { createTestFixture } from './infra/fixtures.ts';
import { sharedConfig } from './wdio.shared.conf.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = resolve(__dirname, '../..');
const tauriBinary = resolve(projectRoot, 'target/debug/agent-orchestrator');
let tauriDriver;
const fixture = createTestFixture('agor-e2e-tauri');
process.env.AGOR_TEST = '1';
process.env.AGOR_TEST_DATA_DIR = fixture.dataDir;
process.env.AGOR_TEST_CONFIG_DIR = fixture.configDir;
const TAURI_DRIVER_PORT = 9750;
console.log(`[tauri] Test fixture at ${fixture.rootDir}`);
export const config = {
...sharedConfig,
port: TAURI_DRIVER_PORT,
capabilities: [{
'wdio:enforceWebDriverClassic': true,
'tauri:options': {
application: tauriBinary,
env: fixture.env,
},
}],
onPrepare() {
// Kill stale tauri-driver on our port
try {
const pids = execSync(`lsof -ti:${TAURI_DRIVER_PORT} 2>/dev/null`, { encoding: 'utf8' }).trim();
if (pids) {
console.log(`Killing stale process(es) on port ${TAURI_DRIVER_PORT}: ${pids}`);
execSync(`kill ${pids} 2>/dev/null`);
}
} catch { /* no process — good */ }
// Verify devUrl port is free
const DEV_URL_PORT = 9710;
try {
const devPids = execSync(`lsof -ti:${DEV_URL_PORT} 2>/dev/null`, { encoding: 'utf8' }).trim();
if (devPids) {
throw new Error(
`Port ${DEV_URL_PORT} (Tauri devUrl) in use by PIDs: ${devPids}. ` +
`Stop that process or use a release build.`
);
}
} catch (e) {
if (e.message.includes(`Port ${DEV_URL_PORT}`)) throw e;
}
if (!existsSync(tauriBinary)) {
if (process.env.SKIP_BUILD) {
throw new Error(`Binary not found at ${tauriBinary}. Build first or unset SKIP_BUILD.`);
}
}
if (process.env.SKIP_BUILD) {
if (!existsSync(resolve(projectRoot, 'dist/index.html'))) {
console.log('Frontend dist/ missing — building...');
execSync('npm run build', { cwd: projectRoot, stdio: 'inherit' });
}
return Promise.resolve();
}
return new Promise((resolveHook, reject) => {
console.log('Building frontend...');
try {
execSync('npm run build', { cwd: projectRoot, stdio: 'inherit' });
} catch (e) {
reject(new Error(`Frontend build failed: ${e.message}`));
return;
}
console.log('Building Tauri debug binary...');
const build = spawn('cargo', ['tauri', 'build', '--debug', '--no-bundle'], {
cwd: projectRoot,
stdio: 'inherit',
});
build.on('close', (code) => code === 0 ? resolveHook() : reject(new Error(`Build failed (exit ${code})`)));
build.on('error', reject);
});
},
beforeSession() {
return new Promise((res, reject) => {
const preCheck = createConnection({ port: TAURI_DRIVER_PORT, host: 'localhost' }, () => {
preCheck.destroy();
reject(new Error(`Port ${TAURI_DRIVER_PORT} already in use.`));
});
preCheck.on('error', () => {
preCheck.destroy();
tauriDriver = spawn('tauri-driver', ['--port', String(TAURI_DRIVER_PORT)], {
stdio: ['ignore', 'pipe', 'pipe'],
});
tauriDriver.on('error', (err) => {
reject(new Error(`tauri-driver failed: ${err.message}. Install: cargo install tauri-driver`));
});
const deadline = Date.now() + 10_000;
function probe() {
if (Date.now() > deadline) { reject(new Error('tauri-driver not ready within 10s')); return; }
const sock = createConnection({ port: TAURI_DRIVER_PORT, host: 'localhost' }, () => {
sock.destroy();
console.log(`tauri-driver ready on port ${TAURI_DRIVER_PORT}`);
res();
});
sock.on('error', () => { sock.destroy(); setTimeout(probe, 200); });
}
setTimeout(probe, 300);
});
});
},
async before() {
await browser.waitUntil(
async () => {
const title = await browser.getTitle();
const hasEl = await browser.execute(() =>
document.querySelector('[data-testid="status-bar"]') !== null
|| document.querySelector('.project-grid') !== null
|| document.querySelector('.settings-panel') !== null
);
return hasEl || title.toLowerCase().includes('agor') || title.toLowerCase().includes('orchestrator');
},
{ timeout: 15_000, interval: 500, timeoutMsg: 'Wrong app — not Agent Orchestrator' },
);
console.log('[tauri] App identity verified.');
},
afterSession() {
if (tauriDriver) { tauriDriver.kill(); tauriDriver = null; }
try { rmSync(fixture.rootDir, { recursive: true, force: true }); } catch { /* best-effort */ }
},
};