import { Before, After, BeforeAll, AfterAll, setDefaultTimeout, } from '@cucumber/cucumber' import { spawn, type ChildProcess } from 'child_process' import { resolve, dirname } from 'path' import { fileURLToPath } from 'url' import type { AppWorld } from './world.js' import { config } from './types.js' setDefaultTimeout(config.responseTimeout) 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`) } BeforeAll(async function () { if (!config.manageServers) { // Caller is expected to start backend + frontend (e.g. via pm2, docker, // or a separate `yarn dev` terminal) before running tests. return } const __dirname = dirname(fileURLToPath(import.meta.url)) const repoRoot = resolve(__dirname, '../../..') // Project-specific: override these commands by setting E2E_BACKEND_CMD / // E2E_FRONTEND_CMD env vars. The defaults assume `yarn dev` at the repo // root spawns the frontend, and the project provides a `dev:backend` // script for the API. const backendCmd = process.env.E2E_BACKEND_CMD ?? 'yarn dev:backend' const frontendCmd = process.env.E2E_FRONTEND_CMD ?? 'yarn dev' backendProcess = spawn(backendCmd, { cwd: repoRoot, env: { ...process.env, NODE_ENV: 'test' }, stdio: 'pipe', shell: true, detached: true, }) backendProcess.stderr?.on('data', (d: Buffer) => process.stderr.write(`[backend] ${d}`), ) backendProcess.stdout?.on('data', (d: Buffer) => process.stdout.write(`[backend] ${d}`), ) frontendProcess = spawn(frontendCmd, { cwd: repoRoot, env: { ...process.env, NODE_ENV: 'test' }, stdio: 'pipe', shell: true, detached: true, }) frontendProcess.stderr?.on('data', (d: Buffer) => process.stderr.write(`[frontend] ${d}`), ) frontendProcess.stdout?.on('data', (d: Buffer) => process.stdout.write(`[frontend] ${d}`), ) await Promise.all([ waitForUrl(config.apiUrl, 'backend'), waitForUrl(config.appUrl, 'frontend'), ]) }) AfterAll(async function () { for (const p of [backendProcess, frontendProcess]) { if (!p?.pid) continue try { // Detached + pgid kill takes the whole tree (vite/wrangler spawn children). process.kill(-p.pid, 'SIGTERM') } catch { try { p.kill('SIGTERM') } catch { // ignore } } } }) Before(async function (this: AppWorld) { await this.openBrowser() if (config.resetUrl) { const body = config.resetRpcName ? JSON.stringify({ rpcName: config.resetRpcName, data: {} }) : undefined await fetch(config.resetUrl, { method: 'POST', headers: body ? { 'content-type': 'application/json' } : undefined, body, }) .then((res) => { if (!res.ok) { process.stderr.write( `[e2e] WARN: reset hook ${config.resetUrl} returned ${res.status}\n`, ) } }) .catch(() => { process.stderr.write(`[e2e] WARN: reset hook ${config.resetUrl} unreachable\n`) }) } }) After(async function (this: AppWorld) { await this.closeBrowser() })