- 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>
88 lines
5.5 KiB
TypeScript
88 lines
5.5 KiB
TypeScript
/**
|
||
* Сводит экстракт элементов (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}`)
|