Files
cli-e2e-mqoahjki/e2e/tests/support/hooks.ts
2026-06-21 23:19:14 +02:00

127 lines
3.5 KiB
TypeScript

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()
})