16 шагов против настоящего стенда: регистрация→справочники→сотрудники/точки→ деньги→3 приёмки→оплаты→смена/продажи/возврат/Z→склад→отчёты→инварианты→откат. Сверяет конкретные числа и инварианты, 0×5xx/0×тостов/0×console.error. Режимы C (temp-орг) и B (Демо на реальном каталоге). Hi-res скриншоты каждого процесса + галерея. Включён в nightly-verify и quality-watchdog (скрипты в $HOME). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
96 lines
3.9 KiB
TypeScript
96 lines
3.9 KiB
TypeScript
/**
|
||
* E2E-runner. Принимает имя сценария, читает YAML, выполняет preconditions
|
||
* и steps, в конце пишет markdown-отчёт.
|
||
*
|
||
* Запуск: pnpm tsx runner.ts <scenario> [--api-only]
|
||
*
|
||
* Сценарий описывает мета-информацию + список step-id. Логика каждого
|
||
* step-а — в `scenarios/<name>.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 <scenario> [--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<string, unknown> = { 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)`)
|
||
}
|
||
|
||
// FULLCHAIN_RUN_ID — чтобы имя отчёта совпадало с папкой скриншотов
|
||
// (reports/full-chain-<RUN_ID>/screenshots/). Если не задан — timestamp.
|
||
const ts = process.env.FULLCHAIN_RUN_ID ?? 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) })
|