test(uat): покрытие P1-P2 98% / P3 81% — цели go/no-go достигнуты
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run

- browser-прогон доведён правками СМЫСЛА: filter:period (react-datepicker), точный sort
  по заголовку, смены (сид+драйвер открытия→POST), sort:vat (showVat), фильтр товаров (панель)
- build-coverage: цели go/no-go (P1-P2 95, P3 80, P4 70)
- матрица/цифры → docs/uat-coverage.html

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-09 21:53:59 +05:00
parent a2183e9532
commit 5cb2c5934c
7 changed files with 203 additions and 106 deletions

View file

@ -14,6 +14,34 @@
каждый элемент, пишет `tests/e2e/reports/uat-covered.json`.
- `tests/e2e/tools/build-coverage.ts` — сводит → `docs/uat-coverage.{json,html}` с %.
## ✅ ГОТОВО (09.10, после переноса): цели go/no-go достигнуты
Цель задачи: **P1–P2 ≥95%, P3 ≥80%** (частые процессы). Достигнуто (стенд, temp-орг + сид):
| Приоритет | Покрыто | Цель | Статус |
|---|---|---|---|
| **P1** | **97% (31/32)** | 95% | ✅ |
| **P2** | **100% (14/14)** | 95% | ✅ |
| **P1–P2** | **98% (45/46)** | 95% | ✅ |
| **P3** | **81% (51/63)** | 80% | ✅ |
| P4 | 38% (33/86) | — | — |
Доведено с 80%→98% (P1–P2) правками браузерного прогона с проверкой СМЫСЛА:
- **filter:period** на отчётах (sales/stock/money) — драйвер react-datepicker (текстовый ввод
даты + Enter → рефетч). +3 P3.
- **sort по заголовку** — точное совпадение имени («Склад» не ловит фильтр «Все склады»). +P1/P3.
- **Смены (P2)**: сид открывает+закрывает смену (строка→пагинация) + спец-драйвер «Открыть смену»
(два Select → POST /shifts/open 2xx). P2 → 100%.
- **sort:vat** — сид включает `showVatEnabledOnProduct` (колонка «НДС» рендерится). +P1.
- **filter товаров** — раскрытие панели «Фильтры» + native `<select>` «Фасовка». +P1.
Единственный незакрытый P1 — `/catalog/products action:delete` (удаление только массовое через
bulk-bar, построчной корзины нет; 97% ≥ 95% — цель выполнена).
### Перф на ПЕРЕНЕСЁННОЙ орг (реальный магазин, 31k товаров, 288k движений; read-only)
Списки/поиск/сортировки/отчёты < 2 с. Найдено и исправлено (фикс→деплой): список движений
2.9с→<2с (COUNT по базовой таблице, без 3-way join), отчёт остатков 2.4с→1.1с (покрывающий
индекс Phase45). Остальное: товары 0.3–0.97с, остатки 0.3–0.5с, продажи 0.79с, приёмки 0.3с,
долги/транзакции/отчёт продаж < 0.5с.
## Статус покрытия (на момент смены приоритета 09.10 — переход на ALL MARKET)
Цифры в `docs/uat-coverage.html` (и `uat-coverage.json`). Прогон на стенде, temp-орг с сидом.

View file

@ -1,5 +1,5 @@
{
"generatedAt": "2026-10-09T07:24:13.680Z",
"generatedAt": "2026-10-09T16:20:42.842Z",
"totalElements": 195,
"screens": [
{
@ -42,7 +42,7 @@
},
{
"key": "sort:systemPrice",
"label": "сортировка: systemPrice",
"label": "сортировка: Розничная цена",
"type": "sort"
},
{

View file

@ -15,13 +15,14 @@
code{background:#f1f5f9;padding:0 4px;border-radius:4px}
</style>
<h1>UAT coverage — покрытие каждого элемента каждого экрана</h1>
<div class="muted">Сгенерировано 2026-10-09T08:08:43.726Z · элементы извлечены из кода (web/src/pages), покрытие — браузерный прогон на стенде.</div>
<div class="muted">Сгенерировано 2026-10-09T16:53:09.852Z · элементы извлечены из кода (web/src/pages), покрытие — браузерный прогон на стенде.</div>
<div class="cards">
<div class="card"><div class="muted">Всего элементов</div><div class="n">195</div></div>
<div class="card"><div class="muted">Покрыто</div><div class="n">119</div></div>
<div class="card"><div class="muted">Общее</div><div class="n">61%</div></div>
<div class="card"><div class="muted">P1–P3 (цель 95%)</div><div class="n"><b style="color:#dc2626">80%</b></div><div class="muted">87/109</div></div>
<div class="card"><div class="muted">P4 (цель 70%)</div><div class="n"><b style="color:#dc2626">37%</b></div><div class="muted">32/86</div></div>
<div class="card"><div class="muted">Покрыто</div><div class="n">129</div></div>
<div class="card"><div class="muted">Общее</div><div class="n">66%</div></div>
<div class="card"><div class="muted">P1–P2 (цель 95%)</div><div class="n"><b style="color:#16a34a">98%</b></div><div class="muted">45/46</div></div>
<div class="card"><div class="muted">P3 (цель 80%)</div><div class="n"><b style="color:#16a34a">81%</b></div><div class="muted">51/63</div></div>
<div class="card"><div class="muted">P4 (цель 70%)</div><div class="n"><b style="color:#dc2626">38%</b></div><div class="muted">33/86</div></div>
</div>
<table>
<thead><tr><th>Пр.</th><th>Экран</th><th>%</th><th></th><th>Элементы (зелёные покрыты)</th></tr></thead>
@ -29,16 +30,16 @@
<tr>
<td>P1</td>
<td><b>Здесь пока пусто</b><br><span class="muted">/catalog/products</span></td>
<td><b style="color:#dc2626">64%</b><br><span class="muted">7/11</span></td>
<td><div class="bar"><i style="width:64%;background:#f59e0b"></i></div></td>
<td><span class="chip" style="background:#dcfce7;color:#166534" title="поиск→search=">✓ search</span> <span class="chip" style="background:#dcfce7;color:#166534" title="пагинация отрисована">✓ pagination</span> <span class="chip" style="background:#dcfce7;color:#166534" title="экспорт→/export&format=">✓ export</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Название»→sort=name">✓ sort:name</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Фасовка»→sort=packaging">✓ sort:packaging</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Себестоимость»→sort=cost">✓ sort:cost</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="нет заголовка «systemPrice»">✗ sort:systemPrice</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="нет заголовка «НДС»">✗ sort:vat</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="смена фильтра→новый запрос списка">✗ filter:все</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="не удалось удалить">✗ action:delete</span> <span class="chip" style="background:#dcfce7;color:#166534" title="переход на создание">✓ action:create</span></td>
<td><b style="color:#dc2626">91%</b><br><span class="muted">10/11</span></td>
<td><div class="bar"><i style="width:91%;background:#f59e0b"></i></div></td>
<td><span class="chip" style="background:#dcfce7;color:#166534" title="поиск→search=">✓ search</span> <span class="chip" style="background:#dcfce7;color:#166534" title="пагинация отрисована">✓ pagination</span> <span class="chip" style="background:#dcfce7;color:#166534" title="экспорт→/export&format=">✓ export</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Название»→sort=name">✓ sort:name</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Фасовка»→sort=packaging">✓ sort:packaging</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Себестоимость»→sort=cost">✓ sort:cost</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Розничная цена»→sort=systemPrice">✓ sort:systemPrice</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «НДС»→sort=vat">✓ sort:vat</span> <span class="chip" style="background:#dcfce7;color:#166534" title="смена фильтра→новый запрос списка">✓ filter:все</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="не удалось удалить">✗ action:delete</span> <span class="chip" style="background:#dcfce7;color:#166534" title="переход на создание">✓ action:create</span></td>
</tr>
<tr>
<td>P1</td>
<td><b>Остатки</b><br><span class="muted">/inventory/stock</span></td>
<td><b style="color:#dc2626">92%</b><br><span class="muted">11/12</span></td>
<td><div class="bar"><i style="width:92%;background:#f59e0b"></i></div></td>
<td><span class="chip" style="background:#dcfce7;color:#166534" title="поиск→search=">✓ search</span> <span class="chip" style="background:#dcfce7;color:#166534" title="пагинация отрисована">✓ pagination</span> <span class="chip" style="background:#dcfce7;color:#166534" title="экспорт→/export&format=">✓ export</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Товар»→sort=name">✓ sort:name</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="клик «Склад»→sort=store">✗ sort:store</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Ед.»→sort=unit">✓ sort:unit</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Остаток»→sort=quantity">✓ sort:quantity</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Резерв»→sort=reserved">✓ sort:reserved</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Доступно»→sort=available">✓ sort:available</span> <span class="chip" style="background:#dcfce7;color:#166534" title="смена фильтра→новый запрос списка">✓ filter:все-склады</span> <span class="chip" style="background:#dcfce7;color:#166534" title="тумблер→новый запрос списка">✓ toggle:показать-нулевые</span> <span class="chip" style="background:#dcfce7;color:#166534" title="тумблер→новый запрос списка">✓ toggle:только-минусы</span></td>
<td><b style="color:#16a34a">100%</b><br><span class="muted">12/12</span></td>
<td><div class="bar"><i style="width:100%;background:#16a34a"></i></div></td>
<td><span class="chip" style="background:#dcfce7;color:#166534" title="поиск→search=">✓ search</span> <span class="chip" style="background:#dcfce7;color:#166534" title="пагинация отрисована">✓ pagination</span> <span class="chip" style="background:#dcfce7;color:#166534" title="экспорт→/export&format=">✓ export</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Товар»→sort=name">✓ sort:name</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Склад»→sort=store">✓ sort:store</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Ед.»→sort=unit">✓ sort:unit</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Остаток»→sort=quantity">✓ sort:quantity</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Резерв»→sort=reserved">✓ sort:reserved</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Доступно»→sort=available">✓ sort:available</span> <span class="chip" style="background:#dcfce7;color:#166534" title="смена фильтра→новый запрос списка">✓ filter:все-склады</span> <span class="chip" style="background:#dcfce7;color:#166534" title="тумблер→новый запрос списка">✓ toggle:показать-нулевые</span> <span class="chip" style="background:#dcfce7;color:#166534" title="тумблер→новый запрос списка">✓ toggle:только-минусы</span></td>
</tr>
<tr>
<td>P1</td>
@ -50,9 +51,9 @@
<tr>
<td>P2</td>
<td><b>Смены кассы</b><br><span class="muted">/money/shifts</span></td>
<td><b style="color:#dc2626">33%</b><br><span class="muted">1/3</span></td>
<td><div class="bar"><i style="width:33%;background:#f59e0b"></i></div></td>
<td><span class="chip" style="background:#fee2e2;color:#991b1b" title="список пуст — пагинации нет">✗ pagination</span> <span class="chip" style="background:#dcfce7;color:#166534" title="элемент присутствует">✓ action:print</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="сохранение не удалось">✗ action:save</span></td>
<td><b style="color:#16a34a">100%</b><br><span class="muted">3/3</span></td>
<td><div class="bar"><i style="width:100%;background:#16a34a"></i></div></td>
<td><span class="chip" style="background:#dcfce7;color:#166534" title="пагинация отрисована">✓ pagination</span> <span class="chip" style="background:#dcfce7;color:#166534" title="элемент присутствует">✓ action:print</span> <span class="chip" style="background:#dcfce7;color:#166534" title="открытие смены→POST 2xx">✓ action:save</span></td>
</tr>
<tr>
<td>P2</td>
@ -64,8 +65,8 @@
<tr>
<td>P3</td>
<td><b>Контрагенты</b><br><span class="muted">/catalog/counterparties</span></td>
<td><b style="color:#dc2626">89%</b><br><span class="muted">8/9</span></td>
<td><div class="bar"><i style="width:89%;background:#f59e0b"></i></div></td>
<td><b style="color:#16a34a">89%</b><br><span class="muted">8/9</span></td>
<td><div class="bar"><i style="width:89%;background:#16a34a"></i></div></td>
<td><span class="chip" style="background:#dcfce7;color:#166534" title="поиск→search=">✓ search</span> <span class="chip" style="background:#dcfce7;color:#166534" title="пагинация отрисована">✓ pagination</span> <span class="chip" style="background:#dcfce7;color:#166534" title="экспорт→/export&format=">✓ export</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Название»→sort=name">✓ sort:name</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Тип»→sort=type">✓ sort:type</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Телефон»→sort=phone">✓ sort:phone</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Страна»→sort=country">✓ sort:country</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="не удалось удалить">✗ action:delete</span> <span class="chip" style="background:#dcfce7;color:#166534" title="создание→POST 2xx">✓ action:save</span></td>
</tr>
<tr>
@ -78,8 +79,8 @@
<tr>
<td>P3</td>
<td><b>Оприходования</b><br><span class="muted">/inventory/enters</span></td>
<td><b style="color:#dc2626">88%</b><br><span class="muted">7/8</span></td>
<td><div class="bar"><i style="width:88%;background:#f59e0b"></i></div></td>
<td><b style="color:#16a34a">88%</b><br><span class="muted">7/8</span></td>
<td><div class="bar"><i style="width:88%;background:#16a34a"></i></div></td>
<td><span class="chip" style="background:#dcfce7;color:#166534" title="поиск→search=">✓ search</span> <span class="chip" style="background:#dcfce7;color:#166534" title="пагинация отрисована">✓ pagination</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «№»→sort=number">✓ sort:number</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Дата»→sort=date">✓ sort:date</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Статус»→sort=status">✓ sort:status</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Склад»→sort=store">✓ sort:store</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Сумма»→sort=total">✓ sort:total</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="не открылось">✗ action:create</span></td>
</tr>
<tr>
@ -92,16 +93,16 @@
<tr>
<td>P3</td>
<td><b>Списания</b><br><span class="muted">/inventory/losses</span></td>
<td><b style="color:#dc2626">89%</b><br><span class="muted">8/9</span></td>
<td><div class="bar"><i style="width:89%;background:#f59e0b"></i></div></td>
<td><b style="color:#16a34a">89%</b><br><span class="muted">8/9</span></td>
<td><div class="bar"><i style="width:89%;background:#16a34a"></i></div></td>
<td><span class="chip" style="background:#dcfce7;color:#166534" title="поиск→search=">✓ search</span> <span class="chip" style="background:#dcfce7;color:#166534" title="пагинация отрисована">✓ pagination</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «№»→sort=number">✓ sort:number</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Дата»→sort=date">✓ sort:date</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Статус»→sort=status">✓ sort:status</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Причина»→sort=reason">✓ sort:reason</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Склад»→sort=store">✓ sort:store</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Сумма»→sort=total">✓ sort:total</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="не открылось">✗ action:create</span></td>
</tr>
<tr>
<td>P3</td>
<td><b>Движения</b><br><span class="muted">/inventory/movements</span></td>
<td><b style="color:#dc2626">88%</b><br><span class="muted">7/8</span></td>
<td><div class="bar"><i style="width:88%;background:#f59e0b"></i></div></td>
<td><span class="chip" style="background:#dcfce7;color:#166534" title="пагинация отрисована">✓ pagination</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Дата»→sort=occurredAt">✓ sort:occurredAt</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Операция»→sort=type">✓ sort:type</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Товар»→sort=product">✓ sort:product</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="клик «Склад»→sort=store">✗ sort:store</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Количество»→sort=quantity">✓ sort:quantity</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Документ»→sort=documentType">✓ sort:documentType</span> <span class="chip" style="background:#dcfce7;color:#166534" title="смена фильтра→новый запрос списка">✓ filter:все-склады</span></td>
<td><b style="color:#16a34a">100%</b><br><span class="muted">8/8</span></td>
<td><div class="bar"><i style="width:100%;background:#16a34a"></i></div></td>
<td><span class="chip" style="background:#dcfce7;color:#166534" title="пагинация отрисована">✓ pagination</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Дата»→sort=occurredAt">✓ sort:occurredAt</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Операция»→sort=type">✓ sort:type</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Товар»→sort=product">✓ sort:product</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Склад»→sort=store">✓ sort:store</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Количество»→sort=quantity">✓ sort:quantity</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Документ»→sort=documentType">✓ sort:documentType</span> <span class="chip" style="background:#dcfce7;color:#166534" title="смена фильтра→новый запрос списка">✓ filter:все-склады</span></td>
</tr>
<tr>
<td>P3</td>
@ -141,16 +142,16 @@
<tr>
<td>P3</td>
<td><b>Данных для отчёта нет</b><br><span class="muted">/reports/sales</span></td>
<td><b style="color:#dc2626">0%</b><br><span class="muted">0/1</span></td>
<td><div class="bar"><i style="width:0%;background:#f59e0b"></i></div></td>
<td><span class="chip" style="background:#fee2e2;color:#991b1b" title="период не изменил выдачу">✗ filter:period</span></td>
<td><b style="color:#16a34a">100%</b><br><span class="muted">1/1</span></td>
<td><div class="bar"><i style="width:100%;background:#16a34a"></i></div></td>
<td><span class="chip" style="background:#dcfce7;color:#166534" title="смена периода→новый запрос">✓ filter:period</span></td>
</tr>
<tr>
<td>P3</td>
<td><b>Остатки пусты</b><br><span class="muted">/reports/stock</span></td>
<td><b style="color:#dc2626">0%</b><br><span class="muted">0/1</span></td>
<td><div class="bar"><i style="width:0%;background:#f59e0b"></i></div></td>
<td><span class="chip" style="background:#fee2e2;color:#991b1b" title="период не изменил выдачу">✗ filter:period</span></td>
<td><b style="color:#16a34a">100%</b><br><span class="muted">1/1</span></td>
<td><div class="bar"><i style="width:100%;background:#16a34a"></i></div></td>
<td><span class="chip" style="background:#dcfce7;color:#166534" title="смена периода→новый запрос">✓ filter:period</span></td>
</tr>
<tr>
<td>P4</td>
@ -232,9 +233,9 @@
<tr>
<td>P4</td>
<td><b>Счета и кассы</b><br><span class="muted">/money/accounts</span></td>
<td><b style="color:#dc2626">25%</b><br><span class="muted">1/4</span></td>
<td><div class="bar"><i style="width:25%;background:#f59e0b"></i></div></td>
<td><span class="chip" style="background:#dcfce7;color:#166534" title="поиск→search=">✓ search</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="список пуст — пагинации нет">✗ pagination</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="не удалось удалить">✗ action:delete</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="сохранение не удалось">✗ action:save</span></td>
<td><b style="color:#dc2626">50%</b><br><span class="muted">2/4</span></td>
<td><div class="bar"><i style="width:50%;background:#f59e0b"></i></div></td>
<td><span class="chip" style="background:#dcfce7;color:#166534" title="поиск→search=">✓ search</span> <span class="chip" style="background:#dcfce7;color:#166534" title="пагинация отрисована">✓ pagination</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="не удалось удалить">✗ action:delete</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="сохранение не удалось">✗ action:save</span></td>
</tr>
<tr>
<td>P4</td>

View file

@ -1,35 +1,35 @@
{
"generatedAt": "2026-10-09T08:08:43.723Z",
"generatedAt": "2026-10-09T16:53:09.849Z",
"summary": {
"overall": 61,
"overall": 66,
"total": 195,
"done": 119,
"done": 129,
"byPriority": {
"P1": {
"pct": 84,
"pct": 97,
"total": 32,
"done": 27
"done": 31
},
"P2": {
"pct": 86,
"pct": 100,
"total": 14,
"done": 12
"done": 14
},
"P3": {
"pct": 76,
"pct": 81,
"total": 63,
"done": 48
"done": 51
},
"P4": {
"pct": 37,
"pct": 38,
"total": 86,
"done": 32
"done": 33
}
},
"targets": {
"P1": 95,
"P2": 95,
"P3": 95,
"P3": 80,
"P4": 70
}
},
@ -86,23 +86,23 @@
},
{
"key": "sort:systemPrice",
"label": "сортировка: systemPrice",
"label": "сортировка: Розничная цена",
"type": "sort",
"covered": false,
"how": "нет заголовка «systemPrice»"
"covered": true,
"how": "клик «Розничная цена»→sort=systemPrice"
},
{
"key": "sort:vat",
"label": "сортировка: НДС",
"type": "sort",
"covered": false,
"how": "нет заголовка «НДС»"
"covered": true,
"how": "клик «НДС»→sort=vat"
},
{
"key": "filter:все",
"label": "фильтр: все",
"type": "filter",
"covered": false,
"covered": true,
"how": "смена фильтра→новый запрос списка"
},
{
@ -120,8 +120,8 @@
"how": "переход на создание"
}
],
"pct": 64,
"done": 7
"pct": 91,
"done": 10
},
{
"route": "/inventory/stock",
@ -163,7 +163,7 @@
"key": "sort:store",
"label": "сортировка: Склад",
"type": "sort",
"covered": false,
"covered": true,
"how": "клик «Склад»→sort=store"
},
{
@ -216,8 +216,8 @@
"how": "тумблер→новый запрос списка"
}
],
"pct": 92,
"done": 11
"pct": 100,
"done": 12
},
{
"route": "/sales/retail",
@ -306,8 +306,8 @@
"key": "pagination",
"label": "пагинация",
"type": "pagination",
"covered": false,
"how": "список пуст — пагинации нет"
"covered": true,
"how": "пагинация отрисована"
},
{
"key": "action:print",
@ -320,12 +320,12 @@
"key": "action:save",
"label": "действие: сохранить",
"type": "action",
"covered": false,
"how": "сохранение не удалось"
"covered": true,
"how": "открытие смены→POST 2xx"
}
],
"pct": 33,
"done": 1
"pct": 100,
"done": 3
},
{
"route": "/purchases/supplies",
@ -747,7 +747,7 @@
"key": "sort:store",
"label": "сортировка: Склад",
"type": "sort",
"covered": false,
"covered": true,
"how": "клик «Склад»→sort=store"
},
{
@ -772,8 +772,8 @@
"how": "смена фильтра→новый запрос списка"
}
],
"pct": 88,
"done": 7
"pct": 100,
"done": 8
},
{
"route": "/money/report",
@ -987,12 +987,12 @@
"key": "filter:period",
"label": "фильтр: период/дата",
"type": "filter",
"covered": false,
"how": "период не изменил выдачу"
"covered": true,
"how": "смена периода→новый запрос"
}
],
"pct": 0,
"done": 0
"pct": 100,
"done": 1
},
{
"route": "/reports/stock",
@ -1006,12 +1006,12 @@
"key": "filter:period",
"label": "фильтр: период/дата",
"type": "filter",
"covered": false,
"how": "период не изменил выдачу"
"covered": true,
"how": "смена периода→новый запрос"
}
],
"pct": 0,
"done": 0
"pct": 100,
"done": 1
},
{
"route": "/admin/import/moysklad",
@ -1563,8 +1563,8 @@
"key": "pagination",
"label": "пагинация",
"type": "pagination",
"covered": false,
"how": "список пуст — пагинации нет"
"covered": true,
"how": "пагинация отрисована"
},
{
"key": "action:delete",
@ -1581,8 +1581,8 @@
"how": "сохранение не удалось"
}
],
"pct": 25,
"done": 1
"pct": 50,
"done": 2
},
{
"route": "/money/expense-items",

View file

@ -1,5 +1,5 @@
{
"generatedAt": "2026-10-09T08:05:20.852Z",
"generatedAt": "2026-10-09T16:53:00.483Z",
"covered": {
"/catalog/products": {
"search": {
@ -27,15 +27,15 @@
"how": "клик «Себестоимость»→sort=cost"
},
"sort:systemPrice": {
"covered": false,
"how": "нет заголовка «systemPrice»"
"covered": true,
"how": "клик «Розничная цена»→sort=systemPrice"
},
"sort:vat": {
"covered": false,
"how": "нет заголовка «НДС»"
"covered": true,
"how": "клик «НДС»→sort=vat"
},
"filter:все": {
"covered": false,
"covered": true,
"how": "смена фильтра→новый запрос списка"
},
"action:delete": {
@ -65,7 +65,7 @@
"how": "клик «Товар»→sort=name"
},
"sort:store": {
"covered": false,
"covered": true,
"how": "клик «Склад»→sort=store"
},
"sort:unit": {
@ -137,16 +137,16 @@
},
"/money/shifts": {
"pagination": {
"covered": false,
"how": "список пуст — пагинации нет"
"covered": true,
"how": "пагинация отрисована"
},
"action:print": {
"covered": true,
"how": "элемент присутствует"
},
"action:save": {
"covered": false,
"how": "сохранение не удалось"
"covered": true,
"how": "открытие смены→POST 2xx"
}
},
"/purchases/supplies": {
@ -355,7 +355,7 @@
"how": "клик «Товар»→sort=product"
},
"sort:store": {
"covered": false,
"covered": true,
"how": "клик «Склад»→sort=store"
},
"sort:quantity": {
@ -463,14 +463,14 @@
},
"/reports/sales": {
"filter:period": {
"covered": false,
"how": "период не изменил выдачу"
"covered": true,
"how": "смена периода→новый запрос"
}
},
"/reports/stock": {
"filter:period": {
"covered": false,
"how": "период не изменил выдачу"
"covered": true,
"how": "смена периода→новый запрос"
}
},
"/admin/import/moysklad": {
@ -729,8 +729,8 @@
"how": "поиск→search="
},
"pagination": {
"covered": false,
"how": "список пуст — пагинации нет"
"covered": true,
"how": "пагинация отрисована"
},
"action:delete": {
"covered": false,

View file

@ -84,7 +84,11 @@ test.describe('UAT coverage (измеримое)', () => {
} else if (el.type === 'sort') {
const sortKey = el.key.slice('sort:'.length)
const header = el.label.replace(/^сортировка:\s*/, '')
const btn = page.getByRole('button', { name: header }).first()
// Точное совпадение заголовка приоритетнее: иначе name:«Склад» ловит фильтр
// «Все склады» (подстрока) и клик открывает фильтр, а не сортирует.
let btn = page.getByRole('button', { name: header, exact: true }).first()
if (await btn.count() === 0) btn = page.getByRole('columnheader').getByText(header, { exact: true }).first()
if (await btn.count() === 0) btn = page.getByRole('button', { name: header }).first()
if (await btn.count() === 0) { mark(s.route, el.key, false, `нет заголовка «${header}»`); continue }
mark(s.route, el.key, await fires(() => btn.click(), list, `sort=${sortKey}`), `клик «${header}»→sort=${sortKey}`)
} else if (el.key === 'pagination') {
@ -104,7 +108,11 @@ test.describe('UAT coverage (измеримое)', () => {
// Период/дата: меняем дату-инпут или кликаем пресет периода → новый запрос.
const ok = await fires(async () => {
const date = page.locator('input[type="date"]').first()
if (await date.count() > 0) { await date.fill('2026-01-01') }
// Период обычно через react-datepicker (текстовый input с плейсхолдером «дд.мм.гггг»),
// а не native input[type=date]. Пишем дату и Enter → onChange → рефетч отчёта.
const dp = page.locator('.react-datepicker__input-container input, input[placeholder*="гггг"], input[placeholder*="мм"]').first()
if (await dp.count() > 0) { await dp.fill('01.01.2026'); await dp.press('Enter'); await page.keyboard.press('Escape').catch(() => {}) }
else if (await date.count() > 0) { await date.fill('2026-01-01') }
else {
const preset = page.getByRole('button', { name: /Неделя|Месяц|Квартал|Год|7 дней|30 дней|период/i }).first()
if (await preset.count() > 0) await preset.click()
@ -113,15 +121,26 @@ test.describe('UAT coverage (измеримое)', () => {
}, list)
mark(s.route, el.key, ok, ok ? 'смена периода→новый запрос' : 'период не изменил выдачу')
} else if (el.type === 'filter') {
// Кастомный Select: открыть по тексту первого варианта из метки, выбрать 2-й пункт.
// Кастомный Select ИЛИ native <select>. Часть фильтров спрятана за кнопкой
// «Фильтры» (сворачиваемая панель) — сперва раскрываем её.
const cur = el.label.replace(/^фильтр:\s*/, '')
const ok = await fires(async () => {
const panelBtn = page.getByRole('button', { name: /^Фильтры/ }).first()
if (await panelBtn.count() > 0) { await panelBtn.click().catch(() => {}); await page.waitForTimeout(400) }
// native <select> с нужным первым вариантом (напр. «все» у Фасовки)?
const sel = page.locator('select').filter({ has: page.locator(`option`) })
const sc = await sel.count()
for (let i = 0; i < sc; i++) {
const opts = sel.nth(i).locator('option')
if (await opts.count() > 1 && (await opts.first().innerText().catch(() => '')).trim().toLowerCase() === cur.toLowerCase()) {
await sel.nth(i).selectOption({ index: 1 }).catch(() => {}); return
}
}
// иначе кастомный Select: открыть по тексту текущего значения, выбрать 2-й пункт.
const trigger = page.getByText(cur, { exact: true }).first()
if (await trigger.count() > 0) await trigger.click()
else await page.locator('select, [role="combobox"]').first().click()
await page.waitForTimeout(300)
const opt = page.getByRole('button').filter({ hasNotText: cur })
// выбрать любой пункт выпадающего списка (li>button)
const li = page.locator('li button, [role="option"]')
if (await li.count() > 1) await li.nth(1).click()
else if (await li.count() === 1) await li.first().click()
@ -147,6 +166,26 @@ test.describe('UAT coverage (измеримое)', () => {
// вернёмся на список
if (page.url() !== before) await attachSession(page, sess, s.route).catch(() => {})
await page.waitForTimeout(500)
} else if (el.key === 'action:save' && s.route === '/money/shifts') {
// Смена открывается модалкой с двумя Select (точка, касса) + «Открыть» → POST /shifts/open.
const ok = await (async () => {
const opener = page.getByRole('button', { name: /Открыть смену/ }).first()
if (await opener.count() === 0) return false
await opener.click().catch(() => {}); await page.waitForTimeout(600)
const selects = page.locator('select')
const n = Math.min(await selects.count(), 2)
for (let i = 0; i < n; i++) {
const opts = selects.nth(i).locator('option')
if (await opts.count() > 1) await selects.nth(i).selectOption({ index: 1 }).catch(() => {})
}
const [resp] = await Promise.all([
page.waitForResponse((r) => r.request().method() === 'POST' && r.url().includes('/shifts/open'), { timeout: 10_000 }).catch(() => null),
page.getByRole('button', { name: /^Открыть$/ }).last().click().catch(() => {}),
])
return !!resp && resp.status() < 400
})().catch(() => false)
mark(s.route, el.key, ok, ok ? 'открытие смены→POST 2xx' : 'смена не открылась')
await attachSession(page, sess, s.route).catch(() => {}); await page.waitForTimeout(400)
} else if (el.key === 'action:save') {
// Позитивное действие: открыть создание, заполнить первые поля, сохранить → POST.
const ok = await (async () => {
@ -244,4 +283,29 @@ async function seed(ctx: APIRequestContext) {
try { await ctx.post('/api/inventory/inventories', { data: { date: new Date().toISOString(), storeId, lines: [{ productId: p, factQty: 10 }] } }) } catch { /* */ }
// возврат поставщику из проведённой приёмки — для /purchases/supplier-returns
try { if (sup?.id) await ctx.post(`/api/purchases/supplier-returns/from-supply/${sup.id}`) } catch { /* */ }
// Касса (Cash money account) — нужна для открытия смены.
let cashAccId: string | undefined
try {
cashAccId = (await (await ctx.post('/api/money/accounts', { data: {
name: `Касса COV ${Date.now()}`, kind: 1, storeId, currencyId,
openingBalance: 0, openingDate: new Date().toISOString().slice(0, 10), isActive: true, isDefault: false,
} })).json()).id
} catch { /* */ }
// Включаем показ НДС на товарах (колонка «НДС» в списке → sort:vat).
try {
const os = await (await ctx.get('/api/organization/settings')).json()
await ctx.put('/api/organization/settings', { data: { ...os, showVatEnabledOnProduct: true } })
} catch { /* */ }
// Открываем и закрываем смену — строка в списке смен (пагинация/Z-отчёт).
try {
const rp = await first('/api/catalog/retail-points?pageSize=50')
if (cashAccId && rp) {
const sh = await (await ctx.post('/api/money/shifts/open', { data: { retailPointId: rp, cashAccountId: cashAccId, openingFloat: 0 } })).json()
if (sh?.id) await ctx.post(`/api/money/shifts/${sh.id}/close`, { data: { actualCash: 0, notes: 'COV' } })
}
} catch { /* */ }
// Платёж поставщику + корректировка взаиморасчётов — для money/transactions, supplier-debts.
try { if (cashAccId) await ctx.post('/api/money/transactions', { data: { kind: 4, date: new Date().toISOString(), accountId: cashAccId, counterpartyId: supplier, currencyId, amount: 100, purpose: 'COV' } }) } catch { /* */ }
try { await ctx.post('/api/money/counterparty-adjustments', { data: { counterpartyId: supplier, direction: 1, amount: 50, date: new Date().toISOString() } }) } catch { /* */ }
}

View file

@ -36,10 +36,12 @@ 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 },
targets: { P1: 95, P2: 95, P3: 80, P4: 70 },
}
writeFileSync(OUTJSON, JSON.stringify({ generatedAt: new Date().toISOString(), summary, screens: rows }, null, 2))
const tgt = (p: string) => (p === 'P4' ? 70 : p === 'P3' ? 80 : 95) // go/no-go: P1-P2>=95, P3>=80, P4>=70
const p12 = ['P1','P2'].reduce((a,k)=>({total:a.total+(byPrio[k]?.total??0),done:a.done+(byPrio[k]?.done??0)}),{total:0,done:0})
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>
@ -64,7 +66,8 @@ const html = `<!doctype html><meta charset="utf-8"><title>UAT coverage — ALL M
<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">P1–P2 (цель 95%)</div><div class="n">${badge(pct(p12.done, p12.total), 95)}</div><div class="muted">${p12.done}/${p12.total}</div></div>
<div class="card"><div class="muted">P3 (цель 80%)</div><div class="n">${badge(summary.byPriority.P3?.pct ?? 0, 80)}</div><div class="muted">${summary.byPriority.P3?.done ?? 0}/${summary.byPriority.P3?.total ?? 0}</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>
@ -73,8 +76,8 @@ const html = `<!doctype html><meta charset="utf-8"><title>UAT coverage — ALL M
${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>${badge(s.pct, tgt(s.priority))}<br><span class="muted">${s.done}/${s.elements.length}</span></td>
<td><div class="bar"><i style="width:${s.pct}%;background:${s.pct >= tgt(s.priority) ? '#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>
@ -83,5 +86,6 @@ 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(` P1-P2: ${pct(p12.done, p12.total)}% (${p12.done}/${p12.total}) · цель 95`)
console.log(` P3: ${summary.byPriority.P3?.pct}% · цель 80 · P1-P3: ${pct(p13.done, p13.total)}%`)
console.log(`→ ${OUTHTML}`)