import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { spawn, spawnSync } from 'node:child_process' import { fileURLToPath } from 'node:url' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const projectRoot = path.resolve(__dirname, '..') const fakePiScript = path.join(__dirname, 'fixtures', 'fake-pi.mjs') const hostHome = os.homedir() const tempRoot = process.env.PIAGENTUI_E2E_TEMP_ROOT ? path.resolve(process.env.PIAGENTUI_E2E_TEMP_ROOT) : fs.mkdtempSync(path.join(os.tmpdir(), 'piagentui-e2e-')) process.env.PIAGENTUI_E2E_TEMP_ROOT = tempRoot const markerDir = path.join(tempRoot, 'markers') const appHome = path.join(tempRoot, 'home') const appData = path.join(appHome, 'AppData', 'Roaming') const localAppData = path.join(appHome, 'AppData', 'Local') const xdgDataHome = path.join(appHome, '.local', 'share') const xdgConfigHome = path.join(appHome, '.config') const xdgCacheHome = path.join(appHome, '.cache') const e2eAgentDir = path.join(tempRoot, 'pi-runtime', 'agent') const e2eSessionDir = path.join(tempRoot, 'pi-runtime', 'agent', 'sessions') for (const dir of [ markerDir, appHome, appData, localAppData, xdgDataHome, xdgConfigHome, xdgCacheHome, e2eAgentDir, e2eSessionDir, ]) { fs.mkdirSync(dir, { recursive: true }) } process.env.PIAGENTUI_E2E_MARKER_DIR = markerDir const appEnv = { ...process.env, HOME: appHome, USERPROFILE: appHome, APPDATA: appData, LOCALAPPDATA: localAppData, XDG_DATA_HOME: xdgDataHome, XDG_CONFIG_HOME: xdgConfigHome, XDG_CACHE_HOME: xdgCacheHome, PIAGENTUI_E2E_NODE: process.execPath, PIAGENTUI_E2E_PI_SCRIPT: fakePiScript, PIAGENTUI_E2E_MARKER_DIR: markerDir, PI_CODING_AGENT_DIR: e2eAgentDir, PI_CODING_AGENT_SESSION_DIR: e2eSessionDir, } let tauriDriver let driverExitExpected = false function appBinaryPath() { if (process.env.PIAGENTUI_E2E_APP) return path.resolve(projectRoot, process.env.PIAGENTUI_E2E_APP) const exe = process.platform === 'win32' ? 'piagentui.exe' : 'piagentui' return path.join(projectRoot, 'src-tauri', 'target', 'debug', exe) } function e2eConfigPath() { return path.join(path.dirname(appBinaryPath()), 'piagentui-e2e.json') } function tauriDriverPath() { if (process.env.TAURI_DRIVER) return process.env.TAURI_DRIVER const exe = process.platform === 'win32' ? 'tauri-driver.exe' : 'tauri-driver' const cargoPath = path.join(hostHome, '.cargo', 'bin', exe) return fs.existsSync(cargoPath) ? cargoPath : exe } function nativeDriverArgs() { const explicit = process.env.TAURI_NATIVE_DRIVER if (explicit) return ['--native-driver', explicit] if (process.platform !== 'win32') return [] const candidates = [ path.join(projectRoot, 'msedgedriver.exe'), path.join(projectRoot, '..', 'msedgedriver.exe'), path.join(process.cwd(), 'msedgedriver.exe'), ] const driver = candidates.find(candidate => fs.existsSync(candidate)) if (driver) console.info(`Using native WebDriver: ${driver}`) return driver ? ['--native-driver', driver] : [] } function run(command, args, options = {}) { const result = spawnSync(command, args, { cwd: projectRoot, stdio: 'inherit', shell: process.platform === 'win32', ...options, }) if (result.status !== 0) { throw new Error(`${command} ${args.join(' ')} failed with status ${result.status}`) } } function cleanup() { driverExitExpected = true tauriDriver?.kill() fs.rmSync(e2eConfigPath(), { force: true }) if (!process.env.PIAGENTUI_E2E_KEEP_TMP) { fs.rmSync(tempRoot, { recursive: true, force: true }) } } export const config = { runner: 'local', hostname: '127.0.0.1', port: 4444, specs: [path.join(projectRoot, 'e2e', 'specs', '**', '*.mjs')], maxInstances: 1, capabilities: [ { browserName: 'wry', maxInstances: 1, 'tauri:options': { application: appBinaryPath(), }, }, ], logLevel: process.env.PIAGENTUI_E2E_LOG_LEVEL ?? 'warn', reporters: ['spec'], framework: 'mocha', mochaOpts: { ui: 'bdd', timeout: 120000, }, waitforTimeout: 15000, connectionRetryTimeout: 120000, connectionRetryCount: 1, onPrepare() { if (!process.env.PIAGENTUI_E2E_SKIP_BUILD) { run('pnpm', ['tauri', 'build', '--debug', '--no-bundle']) } const application = appBinaryPath() if (!fs.existsSync(application)) { throw new Error(`Tauri debug binary not found: ${application}`) } fs.writeFileSync( e2eConfigPath(), JSON.stringify( { node: process.execPath, script: fakePiScript, markerDir, agentDir: e2eAgentDir, }, null, 2, ), 'utf8', ) const driver = tauriDriverPath() tauriDriver = spawn(driver, nativeDriverArgs(), { stdio: ['ignore', process.stdout, process.stderr], env: appEnv, shell: process.platform === 'win32', }) tauriDriver.on('error', error => { console.error('tauri-driver error:', error) process.exit(1) }) tauriDriver.on('exit', code => { if (!driverExitExpected) { console.error('tauri-driver exited unexpectedly with code:', code) process.exit(1) } }) }, async afterTest(_test, _context, result) { if (result.passed) return const artifactsDir = path.join(tempRoot, 'artifacts') fs.mkdirSync(artifactsDir, { recursive: true }) try { const handles = await browser.getWindowHandles() const details = [] for (const handle of handles) { await browser.switchToWindow(handle) details.push({ handle, url: await browser.getUrl(), title: await browser.getTitle() }) } fs.writeFileSync(path.join(artifactsDir, 'windows.json'), JSON.stringify(details, null, 2), 'utf8') fs.writeFileSync(path.join(artifactsDir, 'source.html'), await browser.getPageSource(), 'utf8') const diagnostics = await browser.execute(() => ({ href: window.location.href, webdriver: navigator.webdriver, hasPiBootData: 'PI_BOOT_DATA' in window, piBootData: window.PI_BOOT_DATA ?? null, hasTauriInternals: '__TAURI_INTERNALS__' in window, serviceConfig: window.localStorage.getItem('piagentui-service-config'), })) fs.writeFileSync(path.join(artifactsDir, 'diagnostics.json'), JSON.stringify(diagnostics, null, 2), 'utf8') try { fs.writeFileSync( path.join(artifactsDir, 'browser-logs.json'), JSON.stringify(await browser.getLogs('browser'), null, 2), 'utf8', ) } catch {} await browser.saveScreenshot(path.join(artifactsDir, 'failure.png')) console.error(`E2E failure artifacts: ${artifactsDir}`) } catch (error) { console.error('Failed to write E2E failure artifacts:', error) } }, after() { const marker = path.join(markerDir, 'fake-pi-state.json') if (!fs.existsSync(marker)) { throw new Error(`Fake Pi marker was not written: ${marker}`) } }, onComplete() { cleanup() }, } for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGBREAK']) { process.once(signal, () => { cleanup() process.exit(1) }) }