import { Before, After, BeforeAll, AfterAll, setDefaultTimeout, } from '@cucumber/cucumber' import { readFileSync } from 'fs' import { spawn, execFileSync, type ChildProcess } from 'child_process' import { resolve, dirname, join } from 'path' import { fileURLToPath } from 'url' import type { AppWorld } from './world.js' import { config } from './types.js' setDefaultTimeout(Math.max(config.responseTimeout, 120_000)) let backendProcess: ChildProcess | undefined let frontendProcess: ChildProcess | undefined async function waitForUrl(url: string, label: string, timeoutMs = 60_000) { const deadline = Date.now() + timeoutMs while (Date.now() < deadline) { try { const res = await fetch(url) if (res.status < 500) return } catch { // not ready yet } await new Promise((r) => setTimeout(r, 500)) } throw new Error(`[e2e] ${label} did not become ready at ${url} within ${timeoutMs}ms`) } function startProcess({ cmd, cwd, env, label, }: { cmd: string cwd: string env: NodeJS.ProcessEnv label: string }) { const p = spawn(cmd, { cwd, env, stdio: 'pipe', shell: true, detached: true, }) p.stderr?.on('data', (d: Buffer) => process.stderr.write(`[${label}] ${d}`)) p.stdout?.on('data', (d: Buffer) => process.stdout.write(`[${label}] ${d}`)) return p } async function stopProcess(p: ChildProcess | undefined) { if (!p?.pid) return try { process.kill(-p.pid, 'SIGTERM') } catch { try { p.kill('SIGTERM') } catch { // ignore } } await new Promise((r) => setTimeout(r, 1_000)) } function killPort(port: number) { try { const out = execFileSync('lsof', ['-ti', `tcp:${port}`], { encoding: 'utf8', }).trim() if (!out) return for (const pid of out.split(/\s+/)) { if (!pid) continue try { process.kill(Number(pid), 'SIGTERM') } catch { // ignore } } } catch { // ignore } } const __dirname = dirname(fileURLToPath(import.meta.url)) const repoRoot = resolve(__dirname, '../../..') const seedFile = join(repoRoot, 'db', 'sqlite-seed.sql') const sqliteDb = join(repoRoot, '.pikku-runtime', 'dev.db') function prepareDatabase() { execFileSync('sqlite3', [sqliteDb], { cwd: repoRoot, input: readFileSync(seedFile), stdio: ['pipe', 'inherit', 'inherit'], }) } BeforeAll(async function () { if (!config.manageServers) return if (process.env.E2E_PREPARE_DB !== '0' && process.env.E2E_PREPARE_DB !== 'false') { prepareDatabase() } killPort(3000) killPort(3001) backendProcess = startProcess({ cmd: process.env.E2E_BACKEND_CMD ?? 'bun run dev:backend', cwd: repoRoot, env: { ...process.env, NODE_ENV: 'test', }, label: 'backend', }) await waitForUrl(`${config.apiUrl}/api/auth/get-session`, 'backend') frontendProcess = startProcess({ cmd: process.env.E2E_FRONTEND_CMD ?? 'bun run dev:frontend', cwd: repoRoot, env: { ...process.env, NODE_ENV: 'test', VITE_API_BASE_URL: config.apiUrl, }, label: 'frontend', }) await waitForUrl(`${config.appUrl}/en`, 'frontend') }) AfterAll(async function () { await stopProcess(frontendProcess) await stopProcess(backendProcess) }) Before(async function (this: AppWorld) { await this.openBrowser() }) After(async function (this: AppWorld) { await this.closeBrowser() })