Декларативные end-to-end сценарии в tests/e2e/. YAML описывает шаги, TypeScript-handler — конкретные API/UI/DB-проверки. Отчёт в Markdown. Структура: - runner.ts : entry, парсит YAML, прогоняет steps, пишет report - run.sh : pnpm install + tsx - lib/api.ts : axios + login() (через /connect/token + /api/me) - lib/db.ts : docker exec psql, resetTenantData(), countRows() - lib/report.ts : Markdown-аккумулятор (steps + bugs + ux + gap + perf) - scenarios/full-cycle.yml : 12 шагов - scenarios/full-cycle.steps.ts : handlers (один на шаг) - README.md : как добавить новый сценарий reset_db в preconditions: - TRUNCATE tenant-таблиц CASCADE - AspNet*/users — оставляем только admin@food-market.local - OpenIddict tokens — все valid → revoked - Реестр products + системные справочники + миграции + platform_settings — НЕ трогаем Запуск: tests/e2e/run.sh full-cycle [--api-only] Первый прогон (--api-only, baseline в reports/full-cycle-2026-05-07-baseline.md): - 8 ✓ / 1 ✗ / 3 ◯ из 12. - Critical bug: Cashier видит /api/organization/employees через API (нет [Authorize(Roles="Admin")] на List endpoint). - High: при CreateOrg через SuperAdmin не сидируются tenant-units — пустой каталог измерений у новой org (DevDataSeeder.SeedTenantReferencesAsync должен вызываться, но не вызывается). - Logic gaps: реестр products tenant-scoped и новая org стартует с пустым каталогом; SuperAdmin /organizations не валидирует ФЛК телефона; Cashier не получает Identity-роль "Cashier" при создании через /employees. UI-шаги (Playwright) в этом коммите не покрыты — runner работает в --api-only режиме. UI-extension добавим следующим коммитом, не блокирует получение полезного отчёта.
94 lines
3.7 KiB
TypeScript
94 lines
3.7 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)`)
|
||
}
|
||
|
||
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) })
|