chore: perauset customer project
This commit is contained in:
136
e2e/tests/support/browser-hooks.ts
Normal file
136
e2e/tests/support/browser-hooks.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { BeforeAll, AfterAll, Before, After, setDefaultTimeout } from '@cucumber/cucumber'
|
||||
import { spawn, spawnSync, type ChildProcess } from 'child_process'
|
||||
import { rmSync, mkdirSync } from 'fs'
|
||||
import { resolve, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { browserConfig } from './browser-config.js'
|
||||
import type { BrowserWorld } from './browser-world.js'
|
||||
|
||||
setDefaultTimeout(browserConfig.timeout)
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = resolve(__dirname, '../../..')
|
||||
|
||||
// Provision the isolated e2e db with `pikku db reset` (migrations + seed);
|
||||
// PIKKU_SQLITE_DB points createConfig at this file.
|
||||
function provisionDb(dbFile: string): void {
|
||||
const res = spawnSync('npx', ['pikku', 'db', 'reset'], {
|
||||
cwd: repoRoot,
|
||||
env: { ...process.env, PIKKU_SQLITE_DB: dbFile },
|
||||
encoding: 'utf8',
|
||||
})
|
||||
if (res.status !== 0) {
|
||||
throw new Error(
|
||||
`pikku db reset failed (${res.status}):\n${res.stdout ?? ''}\n${res.stderr ?? ''}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let backendProcess: ChildProcess | undefined
|
||||
let frontendProcess: ChildProcess | undefined
|
||||
|
||||
async function waitForServer(url: string, label: string, timeoutMs = 90_000) {
|
||||
const deadline = Date.now() + timeoutMs
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const res = await fetch(url)
|
||||
if (res.ok || res.status < 500) {
|
||||
console.log(`[${label}] ready on ${url}`)
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// not ready yet
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 500))
|
||||
}
|
||||
throw new Error(`[browser-e2e] ${label} not ready within ${timeoutMs / 1000}s on ${url}`)
|
||||
}
|
||||
|
||||
function startProcess(cmd: string, cwd: string, env: Record<string, string>, 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
|
||||
}
|
||||
|
||||
function stopProcess(p: ChildProcess | undefined) {
|
||||
if (!p?.pid) return
|
||||
try {
|
||||
process.kill(-p.pid, 'SIGTERM')
|
||||
} catch {
|
||||
try {
|
||||
p.kill('SIGTERM')
|
||||
} catch {
|
||||
// already dead
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BeforeAll(async function () {
|
||||
const backendPort = new URL(browserConfig.backendUrl).port || '6002'
|
||||
|
||||
// Fresh SQLite file per run, provisioned by `pikku db reset`.
|
||||
for (const suffix of ['', '-wal', '-shm']) {
|
||||
rmSync(`${browserConfig.dbFile}${suffix}`, { force: true })
|
||||
}
|
||||
mkdirSync(dirname(browserConfig.dbFile), { recursive: true })
|
||||
provisionDb(browserConfig.dbFile)
|
||||
|
||||
console.log('[browser-e2e] starting backend…')
|
||||
backendProcess = startProcess(
|
||||
'npx tsx backend/bin/start-e2e.ts',
|
||||
repoRoot,
|
||||
{
|
||||
...(process.env as Record<string, string>),
|
||||
PORT: backendPort,
|
||||
E2E_DB_FILE: browserConfig.dbFile,
|
||||
// 32+ chars keeps better-auth quiet; BETTER_AUTH_URL = the app origin so
|
||||
// better-auth trusts the browser's Origin (requests arrive via the vite
|
||||
// same-origin proxy).
|
||||
BETTER_AUTH_SECRET: 'e2e-browser-secret-32-characters-long',
|
||||
BETTER_AUTH_URL: browserConfig.appUrl,
|
||||
AUTH_SECRET: 'e2e-browser-secret-32-characters-long',
|
||||
},
|
||||
'backend'
|
||||
)
|
||||
await waitForServer(`${browserConfig.backendUrl}/health-check`, 'backend')
|
||||
// Admin (admin@perauset.org / "test") comes from db/sqlite-seed.sql via
|
||||
// `pikku db reset`.
|
||||
|
||||
console.log('[browser-e2e] starting frontend…')
|
||||
frontendProcess = startProcess(
|
||||
'yarn dev',
|
||||
resolve(repoRoot, 'apps/app'),
|
||||
{
|
||||
...(process.env as Record<string, string>),
|
||||
// Absolute, app-origin API base: SSR-safe and keeps browser calls
|
||||
// same-origin (no CORS). The vite proxy maps /api/auth → backend/api/auth
|
||||
// and /api/* → backend root.
|
||||
VITE_API_URL: `${browserConfig.appUrl}/api`,
|
||||
VITE_BACKEND_URL: browserConfig.backendUrl,
|
||||
},
|
||||
'frontend'
|
||||
)
|
||||
await waitForServer(`${browserConfig.appUrl}/login`, 'frontend')
|
||||
})
|
||||
|
||||
AfterAll(async function () {
|
||||
stopProcess(backendProcess)
|
||||
stopProcess(frontendProcess)
|
||||
backendProcess = undefined
|
||||
frontendProcess = undefined
|
||||
})
|
||||
|
||||
Before(async function (this: BrowserWorld) {
|
||||
await this.openBrowser()
|
||||
})
|
||||
|
||||
After(async function (this: BrowserWorld, scenario) {
|
||||
if (scenario.result?.status === 'FAILED' && this.page) {
|
||||
const safe = scenario.pickle.name.replace(/[^a-z0-9]+/gi, '-').toLowerCase()
|
||||
await this.page
|
||||
.screenshot({ path: `/tmp/e2e-fail-${safe}.png`, fullPage: true })
|
||||
.catch(() => {})
|
||||
}
|
||||
await this.closeBrowser()
|
||||
})
|
||||
Reference in New Issue
Block a user