import { spawn, spawnSync } from 'node:child_process' import { existsSync, rmSync } from 'node:fs' import { resolve } from 'node:path' import process from 'node:process' const repoRoot = process.cwd() const backendReadyFile = resolve(repoRoot, '.context/playwright/backend.ready') const backendReadyTimeoutMs = Number(process.env.PLAYWRIGHT_BACKEND_TIMEOUT_MS || '360000') const playwrightArgs = process.argv.slice(2) function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)) } function formatChildExit(name: string, child: ReturnType) { if (child.exitCode !== null) return `${name} exited before readiness with code ${child.exitCode}` if (child.signalCode !== null) return `${name} exited before readiness from signal ${child.signalCode}` return null } function stopChildProcess(child: ReturnType | null, signal: NodeJS.Signals) { if (!child || child.exitCode !== null || child.signalCode !== null) return child.kill(signal) } async function waitForBackend(timeoutMs: number, backend: ReturnType) { const startedAt = Date.now() while (Date.now() - startedAt < timeoutMs) { const exitMessage = formatChildExit('Playwright backend', backend) if (exitMessage) throw new Error(exitMessage) if (existsSync(backendReadyFile)) return await sleep(1000) } throw new Error(`Timed out waiting for Playwright backend readiness marker at ${backendReadyFile}`) } function stopExistingPlaywrightBackend() { spawnSync('pkill', ['-f', 'supabase-functions.playwright.env'], { cwd: repoRoot, stdio: 'ignore', env: process.env, }) } stopExistingPlaywrightBackend() rmSync(backendReadyFile, { force: true }) const backend = spawn('bun', ['scripts/serve-backend-playwright.ts'], { cwd: repoRoot, stdio: 'inherit', env: { ...process.env, PLAYWRIGHT_READY_FILE: backendReadyFile, }, }) const signalHandlers = new Map void>() let playwright: ReturnType | null = null let relayingSignal = false function removeSignalHandlers() { for (const [signal, handler] of signalHandlers) process.off(signal, handler) } function forwardSignal(signal: NodeJS.Signals) { const handler = () => { if (relayingSignal) return relayingSignal = true stopChildProcess(playwright, signal) stopChildProcess(backend, signal) removeSignalHandlers() process.kill(process.pid, signal) } signalHandlers.set(signal, handler) process.on(signal, handler) } forwardSignal('SIGINT') forwardSignal('SIGTERM') try { await waitForBackend(backendReadyTimeoutMs, backend) playwright = spawn('bunx', ['playwright', 'test', ...playwrightArgs], { cwd: repoRoot, stdio: 'inherit', env: { ...process.env, SKIP_BACKEND_START: 'true', }, }) const exitCode = await new Promise((resolve) => { playwright.on('exit', (code, signal) => { if (signal) { stopChildProcess(backend, signal) removeSignalHandlers() process.kill(process.pid, signal) return } resolve(code ?? 1) }) }) stopChildProcess(backend, 'SIGTERM') process.exit(exitCode) } catch (error) { stopChildProcess(playwright, 'SIGTERM') stopChildProcess(backend, 'SIGTERM') throw error } finally { removeSignalHandlers() }