chore: perauset customer project

This commit is contained in:
e2e
2026-07-11 08:35:17 +02:00
commit 37eea09bf0
293 changed files with 28412 additions and 0 deletions

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