/** * Накопитель отчёта по 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') } }