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>
185 lines
5.5 KiB
TypeScript
185 lines
5.5 KiB
TypeScript
/**
|
||
* Накопитель отчёта по 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(``)
|
||
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')
|
||
}
|
||
}
|