food-market/tests/e2e/runner.ts
nns 7bb941259a
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 1m20s
CI / Web (React + Vite) (push) Successful in 42s
feat(e2e): infrastructure + first full-cycle scenario + baseline report
Декларативные 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 добавим следующим коммитом, не блокирует
получение полезного отчёта.
2026-05-08 00:05:52 +05:00

94 lines
3.7 KiB
TypeScript
Raw 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)`)
}
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) })