food-market/tests/e2e/tools/build-coverage.ts
nns 3f416e71ad test(uat): измеримое покрытие — экстрактор элементов из кода + браузерный прогон
- tools/extract-controls.ts: статически из web/src/pages достаёт все интерактивные
  элементы каждого экрана (поиск/фильтры/сортировки/пагинация/экспорт/тумблеры/
  действия) → docs/uat-coverage.expected.json (36 экранов, 195 элементов; приоритеты).
- scenarios/stage-ui-uat-coverage.spec.ts: читает expected, в браузере задействует
  каждый элемент (фиксирует запрос с параметром/действие), пишет reports/uat-covered.json.
- tools/build-coverage.ts: сводит → docs/uat-coverage.{json,html} с % по экрану/
  приоритету/общим (цель P1–P3 ≥95%, P4 ≥70%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:27:31 +05:00

88 lines
5.5 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.

/**
* Сводит экстракт элементов (docs/uat-coverage.expected.json) и результат браузерного
* прогона (reports/uat-covered.json) → docs/uat-coverage.json + docs/uat-coverage.html
* с процентами покрытия по экрану, по приоритету и общим. Цель: P1–P3 ≥95%, P4 ≥70%.
*/
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const HERE = dirname(fileURLToPath(import.meta.url))
const EXP = resolve(HERE, '../../../docs/uat-coverage.expected.json')
const COV = resolve(HERE, '../reports/uat-covered.json')
const OUTJSON = resolve(HERE, '../../../docs/uat-coverage.json')
const OUTHTML = resolve(HERE, '../../../docs/uat-coverage.html')
const exp = JSON.parse(readFileSync(EXP, 'utf8'))
const cov = existsSync(COV) ? JSON.parse(readFileSync(COV, 'utf8')).covered : {}
const rows: any[] = []
const byPrio: Record<string, { total: number; done: number }> = {}
let total = 0, done = 0
for (const s of exp.screens) {
const c = cov[s.route] ?? {}
const els = s.elements.map((e: any) => {
const hit = c[e.key]?.covered === true
if (hit) done++
total++
;(byPrio[s.priority] ??= { total: 0, done: 0 }).total++
if (hit) byPrio[s.priority].done++
return { ...e, covered: hit, how: c[e.key]?.how ?? '' }
})
const scDone = els.filter((e: any) => e.covered).length
rows.push({ ...s, elements: els, pct: s.elements.length ? Math.round((scDone / s.elements.length) * 100) : 100, done: scDone })
}
const pct = (d: number, t: number) => (t ? Math.round((d / t) * 100) : 100)
const summary = {
overall: pct(done, total), total, done,
byPriority: Object.fromEntries(Object.entries(byPrio).map(([k, v]) => [k, { pct: pct(v.done, v.total), ...v }])),
targets: { P1: 95, P2: 95, P3: 95, P4: 70 },
}
writeFileSync(OUTJSON, JSON.stringify({ generatedAt: new Date().toISOString(), summary, screens: rows }, null, 2))
const p13 = ['P1', 'P2', 'P3'].reduce((a, k) => ({ total: a.total + (byPrio[k]?.total ?? 0), done: a.done + (byPrio[k]?.done ?? 0) }), { total: 0, done: 0 })
const badge = (p: number, target: number) => `<b style="color:${p >= target ? '#16a34a' : '#dc2626'}">${p}%</b>`
const html = `<!doctype html><meta charset="utf-8"><title>UAT coverage — ALL MARKET</title>
<style>
body{font:14px system-ui,Segoe UI,Roboto,sans-serif;margin:0;background:#f8fafc;color:#0f172a;padding:24px}
h1{font-size:20px} .muted{color:#64748b}
.cards{display:flex;gap:16px;flex-wrap:wrap;margin:16px 0}
.card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:14px 18px;min-width:150px}
.card .n{font-size:26px;font-weight:700}
table{border-collapse:collapse;width:100%;background:#fff;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden}
th,td{padding:7px 10px;border-bottom:1px solid #eef2f7;text-align:left;vertical-align:top}
th{background:#f1f5f9;font-size:12px;text-transform:uppercase;letter-spacing:.03em;color:#475569}
.ok{color:#16a34a} .no{color:#dc2626}
.chip{display:inline-block;font-size:11px;padding:1px 7px;border-radius:999px;background:#eef2f7;margin:1px}
.bar{height:8px;border-radius:4px;background:#e2e8f0;overflow:hidden;min-width:90px}
.bar>i{display:block;height:100%}
code{background:#f1f5f9;padding:0 4px;border-radius:4px}
</style>
<h1>UAT coverage — покрытие каждого элемента каждого экрана</h1>
<div class="muted">Сгенерировано ${new Date().toISOString()} · элементы извлечены из кода (web/src/pages), покрытие — браузерный прогон на стенде.</div>
<div class="cards">
<div class="card"><div class="muted">Всего элементов</div><div class="n">${total}</div></div>
<div class="card"><div class="muted">Покрыто</div><div class="n">${done}</div></div>
<div class="card"><div class="muted">Общее</div><div class="n">${summary.overall}%</div></div>
<div class="card"><div class="muted">P1–P3 (цель 95%)</div><div class="n">${badge(pct(p13.done, p13.total), 95)}</div><div class="muted">${p13.done}/${p13.total}</div></div>
<div class="card"><div class="muted">P4 (цель 70%)</div><div class="n">${badge(summary.byPriority.P4?.pct ?? 0, 70)}</div><div class="muted">${summary.byPriority.P4?.done ?? 0}/${summary.byPriority.P4?.total ?? 0}</div></div>
</div>
<table>
<thead><tr><th>Пр.</th><th>Экран</th><th>%</th><th></th><th>Элементы (зелёные покрыты)</th></tr></thead>
<tbody>
${rows.map((s) => `<tr>
<td>${s.priority}</td>
<td><b>${s.title}</b><br><span class="muted">${s.route}</span></td>
<td>${badge(s.pct, s.priority === 'P4' ? 70 : 95)}<br><span class="muted">${s.done}/${s.elements.length}</span></td>
<td><div class="bar"><i style="width:${s.pct}%;background:${s.pct >= (s.priority === 'P4' ? 70 : 95) ? '#16a34a' : '#f59e0b'}"></i></div></td>
<td>${s.elements.map((e: any) => `<span class="chip" style="background:${e.covered ? '#dcfce7' : '#fee2e2'};color:${e.covered ? '#166534' : '#991b1b'}" title="${(e.how || '').replace(/"/g, '')}">${e.covered ? '✓' : '✗'} ${e.key}</span>`).join(' ')}</td>
</tr>`).join('\n')}
</tbody>
</table>`
writeFileSync(OUTHTML, html)
console.log(`[coverage] overall ${summary.overall}% (${done}/${total})`)
for (const [k, v] of Object.entries(summary.byPriority)) console.log(` ${k}: ${(v as any).pct}% (${(v as any).done}/${(v as any).total})`)
console.log(` P1-P3: ${pct(p13.done, p13.total)}% (${p13.done}/${p13.total}) · цель 95`)
console.log(`→ ${OUTHTML}`)