food-market/tests/e2e/lib/report.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

173 lines
5 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-сценарию. Markdown с секциями step-by-step.
* Каждый step фиксирует API/UI checks, статус (pass/fail/warn), время,
* и в конце runner добавляет summary + категоризованные рекомендации.
*/
import { writeFileSync, mkdirSync } from 'node:fs'
import { dirname } from 'node:path'
export type StepStatus = 'pass' | 'fail' | 'warn' | 'skip'
export interface CheckResult {
kind: 'api' | 'ui' | 'db'
description: string
ok: boolean
detail?: string
}
export interface Step {
id: string
title: string
status: StepStatus
checks: CheckResult[]
durationMs: number
notes: string[]
screenshot?: string
}
export interface Bug {
step: string
severity: 'critical' | 'high' | 'medium' | 'low'
title: string
detail: string
fix?: string
}
export class Report {
scenario: string
startedAt = new Date()
steps: Step[] = []
bugs: Bug[] = []
uxNotes: string[] = []
logicGaps: string[] = []
perfNotes: string[] = []
constructor(scenario: string) {
this.scenario = scenario
}
startStep(id: string, title: string): Step {
const step: Step = { id, title, status: 'pass', checks: [], durationMs: 0, notes: [] }
this.steps.push(step)
return step
}
finishStep(step: Step, t0: number) {
step.durationMs = Date.now() - t0
if (step.checks.some((c) => !c.ok)) step.status = 'fail'
}
bug(b: Bug) { this.bugs.push(b) }
ux(s: string) { this.uxNotes.push(s) }
gap(s: string) { this.logicGaps.push(s) }
perf(s: string) { this.perfNotes.push(s) }
finalize(filePath: string) {
mkdirSync(dirname(filePath), { recursive: true })
writeFileSync(filePath, this.toMarkdown(), 'utf8')
}
private icon(s: StepStatus): string {
switch (s) {
case 'pass': return '✓'
case 'fail': return '✗'
case 'warn': return '⚠'
case 'skip': return '◯'
}
}
toMarkdown(): string {
const passed = this.steps.filter((s) => s.status === 'pass').length
const failed = this.steps.filter((s) => s.status === 'fail').length
const warns = this.steps.filter((s) => s.status === 'warn').length
const skipped = this.steps.filter((s) => s.status === 'skip').length
const lines: string[] = []
lines.push(`# E2E report: ${this.scenario}`)
lines.push('')
lines.push(`Запущен: ${this.startedAt.toISOString()}`)
lines.push(`Длительность: ${(this.steps.reduce((a, s) => a + s.durationMs, 0) / 1000).toFixed(1)}с`)
lines.push('')
lines.push(`**Итог:** ${passed} ✓ / ${failed} ✗ / ${warns} ⚠ / ${skipped} ◯ (всего ${this.steps.length})`)
lines.push('')
for (const step of this.steps) {
lines.push(`## ${this.icon(step.status)} Step ${step.id}: ${step.title}`)
lines.push('')
lines.push(`Длительность: ${step.durationMs}мс`)
if (step.checks.length > 0) {
lines.push('')
lines.push('| Тип | Проверка | Результат |')
lines.push('|---|---|---|')
for (const c of step.checks) {
const cell = c.detail ? `${c.detail.replace(/\|/g, '\\|').slice(0, 200)}` : ''
lines.push(`| ${c.kind} | ${c.description} | ${c.ok ? '✓' : '✗'} ${cell} |`)
}
}
if (step.notes.length > 0) {
lines.push('')
for (const n of step.notes) lines.push(`> ${n}`)
}
if (step.screenshot) {
lines.push('')
lines.push(`Скриншот: \`${step.screenshot}\``)
}
lines.push('')
}
lines.push('## Summary')
lines.push('')
lines.push(`- Passed: ${passed}`)
lines.push(`- Failed: ${failed}`)
lines.push(`- Warnings: ${warns}`)
lines.push(`- Skipped: ${skipped}`)
lines.push('')
if (this.bugs.length > 0) {
lines.push('## Critical bugs')
lines.push('')
const order: Bug['severity'][] = ['critical', 'high', 'medium', 'low']
for (const sev of order) {
const list = this.bugs.filter((b) => b.severity === sev)
if (list.length === 0) continue
lines.push(`### ${sev.toUpperCase()}`)
lines.push('')
for (const b of list) {
lines.push(`- **[${b.step}] ${b.title}**`)
lines.push(` - ${b.detail}`)
if (b.fix) lines.push(` - Fix: ${b.fix}`)
}
lines.push('')
}
} else {
lines.push('## Critical bugs')
lines.push('')
lines.push('Нет.')
lines.push('')
}
if (this.uxNotes.length > 0) {
lines.push('## UX recommendations')
lines.push('')
for (const n of this.uxNotes) lines.push(`- ${n}`)
lines.push('')
}
if (this.logicGaps.length > 0) {
lines.push('## Logic gaps')
lines.push('')
for (const n of this.logicGaps) lines.push(`- ${n}`)
lines.push('')
}
if (this.perfNotes.length > 0) {
lines.push('## Performance observations')
lines.push('')
for (const n of this.perfNotes) lines.push(`- ${n}`)
lines.push('')
}
return lines.join('\n')
}
}