/** * E2E-runner. Принимает имя сценария, читает YAML, выполняет preconditions * и steps, в конце пишет markdown-отчёт. * * Запуск: pnpm tsx runner.ts [--api-only] * * Сценарий описывает мета-информацию + список step-id. Логика каждого * step-а — в `scenarios/.steps.ts`. Это гибрид DSL + код: YAML * декларативный, но конкретные проверки (Playwright assertions / axios calls) * хранятся как функции, потому что для всех 12 шагов full-cycle описать * сложные UI-сценарии чисто декларативно неоправданно. */ import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import { load as parseYaml } from 'js-yaml' import { Report } from './lib/report.js' import { resetTenantData } from './lib/db.js' import { login } from './lib/api.js' interface Scenario { name: string description?: string preconditions?: { reset_db?: boolean; smoke_login_super_admin?: boolean } steps: { id: string; title: string }[] } async function main() { const args = process.argv.slice(2) const name = args.find((a) => !a.startsWith('--')) if (!name) { console.error('Usage: tsx runner.ts [--api-only]') process.exit(2) } const apiOnly = args.includes('--api-only') const scenarioPath = resolve(import.meta.dirname, 'scenarios', `${name}.yml`) const scenario = parseYaml(readFileSync(scenarioPath, 'utf8')) as Scenario const stepsModule = await import(resolve(import.meta.dirname, 'scenarios', `${name}.steps.ts`)) const report = new Report(scenario.name) // Preconditions if (scenario.preconditions?.reset_db) { console.error('[preflight] resetting tenant data...') resetTenantData() } if (scenario.preconditions?.smoke_login_super_admin !== false) { try { const sa = await login('admin@food-market.local', 'Admin12345!') if (!sa.roles.includes('SuperAdmin')) { throw new Error(`SuperAdmin not in roles: ${sa.roles}`) } console.error(`[preflight] SuperAdmin login OK (sub=${sa.email})`) } catch (e) { console.error('[preflight] SuperAdmin login FAILED — abort', e) process.exit(3) } } // Контекст пробрасываем между steps — accumulator для ID'ов созданных сущностей. const ctx: Record = { apiOnly } for (const stepDecl of scenario.steps) { const handler = stepsModule[stepDecl.id] if (typeof handler !== 'function') { const step = report.startStep(stepDecl.id, stepDecl.title) step.status = 'skip' step.notes.push(`Handler ${stepDecl.id} не найден в ${name}.steps.ts`) console.error(`[${stepDecl.id}] SKIP (no handler)`) continue } const t0 = Date.now() const step = report.startStep(stepDecl.id, stepDecl.title) console.error(`[${stepDecl.id}] ${stepDecl.title}...`) try { await handler({ ctx, step, report }) } catch (e) { step.status = 'fail' step.notes.push(`Exception: ${(e as Error).message}`) console.error(`[${stepDecl.id}] FAIL: ${(e as Error).message}`) } report.finishStep(step, t0) console.error(`[${stepDecl.id}] ${step.status} (${step.durationMs}ms)`) } const ts = new Date().toISOString().replace(/[:.]/g, '-') const reportPath = resolve(import.meta.dirname, 'reports', `${name}-${ts}.md`) report.finalize(reportPath) console.error(`[done] report → ${reportPath}`) console.error(report.toMarkdown().split('\n').slice(0, 20).join('\n')) } main().catch((e) => { console.error(e); process.exit(1) })