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

185 lines
5.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
screenshots?: { rel: string; caption: 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}\``)
}
if (step.screenshots && step.screenshots.length > 0) {
lines.push('')
lines.push('Скриншоты процесса:')
lines.push('')
for (const sh of step.screenshots) {
lines.push(`[${sh.caption}](${sh.rel})`)
lines.push('')
lines.push(`![${sh.caption}](${sh.rel})`)
lines.push('')
}
}
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')
}
}