food-market/tests/e2e/runner.ts
nns 886cb92f30 test(e2e): сквозной сценарий full-chain «жизнь магазина» + харнес со скриншотами
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>
2026-10-08 02:10:11 +05:00

96 lines
3.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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) })