127 lines
3.5 KiB
TypeScript
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()
|
|
})
|