test(uat): покрытие P1-P2 98% / P3 81% — цели go/no-go достигнуты
- 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:
parent
a2183e9532
commit
5cb2c5934c
|
|
@ -14,6 +14,34 @@
|
||||||
каждый элемент, пишет `tests/e2e/reports/uat-covered.json`.
|
каждый элемент, пишет `tests/e2e/reports/uat-covered.json`.
|
||||||
- `tests/e2e/tools/build-coverage.ts` — сводит → `docs/uat-coverage.{json,html}` с %.
|
- `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)
|
## Статус покрытия (на момент смены приоритета 09.10 — переход на ALL MARKET)
|
||||||
Цифры в `docs/uat-coverage.html` (и `uat-coverage.json`). Прогон на стенде, temp-орг с сидом.
|
Цифры в `docs/uat-coverage.html` (и `uat-coverage.json`). Прогон на стенде, temp-орг с сидом.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"generatedAt": "2026-10-09T07:24:13.680Z",
|
"generatedAt": "2026-10-09T16:20:42.842Z",
|
||||||
"totalElements": 195,
|
"totalElements": 195,
|
||||||
"screens": [
|
"screens": [
|
||||||
{
|
{
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "sort:systemPrice",
|
"key": "sort:systemPrice",
|
||||||
"label": "сортировка: systemPrice",
|
"label": "сортировка: Розничная цена",
|
||||||
"type": "sort"
|
"type": "sort"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,14 @@
|
||||||
code{background:#f1f5f9;padding:0 4px;border-radius:4px}
|
code{background:#f1f5f9;padding:0 4px;border-radius:4px}
|
||||||
</style>
|
</style>
|
||||||
<h1>UAT coverage — покрытие каждого элемента каждого экрана</h1>
|
<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="cards">
|
||||||
<div class="card"><div class="muted">Всего элементов</div><div class="n">195</div></div>
|
<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">129</div></div>
|
||||||
<div class="card"><div class="muted">Общее</div><div class="n">61%</div></div>
|
<div class="card"><div class="muted">Общее</div><div class="n">66%</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">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">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">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>
|
</div>
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><th>Пр.</th><th>Экран</th><th>%</th><th></th><th>Элементы (зелёные покрыты)</th></tr></thead>
|
<thead><tr><th>Пр.</th><th>Экран</th><th>%</th><th></th><th>Элементы (зелёные покрыты)</th></tr></thead>
|
||||||
|
|
@ -29,16 +30,16 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>P1</td>
|
<td>P1</td>
|
||||||
<td><b>Здесь пока пусто</b><br><span class="muted">/catalog/products</span></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><b style="color:#dc2626">91%</b><br><span class="muted">10/11</span></td>
|
||||||
<td><div class="bar"><i style="width:64%;background:#f59e0b"></i></div></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:#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><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>
|
||||||
<tr>
|
<tr>
|
||||||
<td>P1</td>
|
<td>P1</td>
|
||||||
<td><b>Остатки</b><br><span class="muted">/inventory/stock</span></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><b style="color:#16a34a">100%</b><br><span class="muted">12/12</span></td>
|
||||||
<td><div class="bar"><i style="width:92%;background:#f59e0b"></i></div></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:#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><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>
|
||||||
<tr>
|
<tr>
|
||||||
<td>P1</td>
|
<td>P1</td>
|
||||||
|
|
@ -50,9 +51,9 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>P2</td>
|
<td>P2</td>
|
||||||
<td><b>Смены кассы</b><br><span class="muted">/money/shifts</span></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><b style="color:#16a34a">100%</b><br><span class="muted">3/3</span></td>
|
||||||
<td><div class="bar"><i style="width:33%;background:#f59e0b"></i></div></td>
|
<td><div class="bar"><i style="width:100%;background:#16a34a"></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><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>
|
||||||
<tr>
|
<tr>
|
||||||
<td>P2</td>
|
<td>P2</td>
|
||||||
|
|
@ -64,8 +65,8 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>P3</td>
|
<td>P3</td>
|
||||||
<td><b>Контрагенты</b><br><span class="muted">/catalog/counterparties</span></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><b style="color:#16a34a">89%</b><br><span class="muted">8/9</span></td>
|
||||||
<td><div class="bar"><i style="width:89%;background:#f59e0b"></i></div></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>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -78,8 +79,8 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>P3</td>
|
<td>P3</td>
|
||||||
<td><b>Оприходования</b><br><span class="muted">/inventory/enters</span></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><b style="color:#16a34a">88%</b><br><span class="muted">7/8</span></td>
|
||||||
<td><div class="bar"><i style="width:88%;background:#f59e0b"></i></div></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>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -92,16 +93,16 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>P3</td>
|
<td>P3</td>
|
||||||
<td><b>Списания</b><br><span class="muted">/inventory/losses</span></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><b style="color:#16a34a">89%</b><br><span class="muted">8/9</span></td>
|
||||||
<td><div class="bar"><i style="width:89%;background:#f59e0b"></i></div></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>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<td>P3</td>
|
<td>P3</td>
|
||||||
<td><b>Движения</b><br><span class="muted">/inventory/movements</span></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><b style="color:#16a34a">100%</b><br><span class="muted">8/8</span></td>
|
||||||
<td><div class="bar"><i style="width:88%;background:#f59e0b"></i></div></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:#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><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>
|
||||||
<tr>
|
<tr>
|
||||||
<td>P3</td>
|
<td>P3</td>
|
||||||
|
|
@ -141,16 +142,16 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>P3</td>
|
<td>P3</td>
|
||||||
<td><b>Данных для отчёта нет</b><br><span class="muted">/reports/sales</span></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><b style="color:#16a34a">100%</b><br><span class="muted">1/1</span></td>
|
||||||
<td><div class="bar"><i style="width:0%;background:#f59e0b"></i></div></td>
|
<td><div class="bar"><i style="width:100%;background:#16a34a"></i></div></td>
|
||||||
<td><span class="chip" style="background:#fee2e2;color:#991b1b" title="период не изменил выдачу">✗ filter:period</span></td>
|
<td><span class="chip" style="background:#dcfce7;color:#166534" title="смена периода→новый запрос">✓ filter:period</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>P3</td>
|
<td>P3</td>
|
||||||
<td><b>Остатки пусты</b><br><span class="muted">/reports/stock</span></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><b style="color:#16a34a">100%</b><br><span class="muted">1/1</span></td>
|
||||||
<td><div class="bar"><i style="width:0%;background:#f59e0b"></i></div></td>
|
<td><div class="bar"><i style="width:100%;background:#16a34a"></i></div></td>
|
||||||
<td><span class="chip" style="background:#fee2e2;color:#991b1b" title="период не изменил выдачу">✗ filter:period</span></td>
|
<td><span class="chip" style="background:#dcfce7;color:#166534" title="смена периода→новый запрос">✓ filter:period</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>P4</td>
|
<td>P4</td>
|
||||||
|
|
@ -232,9 +233,9 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>P4</td>
|
<td>P4</td>
|
||||||
<td><b>Счета и кассы</b><br><span class="muted">/money/accounts</span></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><b style="color:#dc2626">50%</b><br><span class="muted">2/4</span></td>
|
||||||
<td><div class="bar"><i style="width:25%;background:#f59e0b"></i></div></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:#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><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>
|
||||||
<tr>
|
<tr>
|
||||||
<td>P4</td>
|
<td>P4</td>
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,35 @@
|
||||||
{
|
{
|
||||||
"generatedAt": "2026-10-09T08:08:43.723Z",
|
"generatedAt": "2026-10-09T16:53:09.849Z",
|
||||||
"summary": {
|
"summary": {
|
||||||
"overall": 61,
|
"overall": 66,
|
||||||
"total": 195,
|
"total": 195,
|
||||||
"done": 119,
|
"done": 129,
|
||||||
"byPriority": {
|
"byPriority": {
|
||||||
"P1": {
|
"P1": {
|
||||||
"pct": 84,
|
"pct": 97,
|
||||||
"total": 32,
|
"total": 32,
|
||||||
"done": 27
|
"done": 31
|
||||||
},
|
},
|
||||||
"P2": {
|
"P2": {
|
||||||
"pct": 86,
|
"pct": 100,
|
||||||
"total": 14,
|
"total": 14,
|
||||||
"done": 12
|
"done": 14
|
||||||
},
|
},
|
||||||
"P3": {
|
"P3": {
|
||||||
"pct": 76,
|
"pct": 81,
|
||||||
"total": 63,
|
"total": 63,
|
||||||
"done": 48
|
"done": 51
|
||||||
},
|
},
|
||||||
"P4": {
|
"P4": {
|
||||||
"pct": 37,
|
"pct": 38,
|
||||||
"total": 86,
|
"total": 86,
|
||||||
"done": 32
|
"done": 33
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"targets": {
|
"targets": {
|
||||||
"P1": 95,
|
"P1": 95,
|
||||||
"P2": 95,
|
"P2": 95,
|
||||||
"P3": 95,
|
"P3": 80,
|
||||||
"P4": 70
|
"P4": 70
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -86,23 +86,23 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "sort:systemPrice",
|
"key": "sort:systemPrice",
|
||||||
"label": "сортировка: systemPrice",
|
"label": "сортировка: Розничная цена",
|
||||||
"type": "sort",
|
"type": "sort",
|
||||||
"covered": false,
|
"covered": true,
|
||||||
"how": "нет заголовка «systemPrice»"
|
"how": "клик «Розничная цена»→sort=systemPrice"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "sort:vat",
|
"key": "sort:vat",
|
||||||
"label": "сортировка: НДС",
|
"label": "сортировка: НДС",
|
||||||
"type": "sort",
|
"type": "sort",
|
||||||
"covered": false,
|
"covered": true,
|
||||||
"how": "нет заголовка «НДС»"
|
"how": "клик «НДС»→sort=vat"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "filter:все",
|
"key": "filter:все",
|
||||||
"label": "фильтр: все",
|
"label": "фильтр: все",
|
||||||
"type": "filter",
|
"type": "filter",
|
||||||
"covered": false,
|
"covered": true,
|
||||||
"how": "смена фильтра→новый запрос списка"
|
"how": "смена фильтра→новый запрос списка"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -120,8 +120,8 @@
|
||||||
"how": "переход на создание"
|
"how": "переход на создание"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"pct": 64,
|
"pct": 91,
|
||||||
"done": 7
|
"done": 10
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"route": "/inventory/stock",
|
"route": "/inventory/stock",
|
||||||
|
|
@ -163,7 +163,7 @@
|
||||||
"key": "sort:store",
|
"key": "sort:store",
|
||||||
"label": "сортировка: Склад",
|
"label": "сортировка: Склад",
|
||||||
"type": "sort",
|
"type": "sort",
|
||||||
"covered": false,
|
"covered": true,
|
||||||
"how": "клик «Склад»→sort=store"
|
"how": "клик «Склад»→sort=store"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -216,8 +216,8 @@
|
||||||
"how": "тумблер→новый запрос списка"
|
"how": "тумблер→новый запрос списка"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"pct": 92,
|
"pct": 100,
|
||||||
"done": 11
|
"done": 12
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"route": "/sales/retail",
|
"route": "/sales/retail",
|
||||||
|
|
@ -306,8 +306,8 @@
|
||||||
"key": "pagination",
|
"key": "pagination",
|
||||||
"label": "пагинация",
|
"label": "пагинация",
|
||||||
"type": "pagination",
|
"type": "pagination",
|
||||||
"covered": false,
|
"covered": true,
|
||||||
"how": "список пуст — пагинации нет"
|
"how": "пагинация отрисована"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "action:print",
|
"key": "action:print",
|
||||||
|
|
@ -320,12 +320,12 @@
|
||||||
"key": "action:save",
|
"key": "action:save",
|
||||||
"label": "действие: сохранить",
|
"label": "действие: сохранить",
|
||||||
"type": "action",
|
"type": "action",
|
||||||
"covered": false,
|
"covered": true,
|
||||||
"how": "сохранение не удалось"
|
"how": "открытие смены→POST 2xx"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"pct": 33,
|
"pct": 100,
|
||||||
"done": 1
|
"done": 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"route": "/purchases/supplies",
|
"route": "/purchases/supplies",
|
||||||
|
|
@ -747,7 +747,7 @@
|
||||||
"key": "sort:store",
|
"key": "sort:store",
|
||||||
"label": "сортировка: Склад",
|
"label": "сортировка: Склад",
|
||||||
"type": "sort",
|
"type": "sort",
|
||||||
"covered": false,
|
"covered": true,
|
||||||
"how": "клик «Склад»→sort=store"
|
"how": "клик «Склад»→sort=store"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -772,8 +772,8 @@
|
||||||
"how": "смена фильтра→новый запрос списка"
|
"how": "смена фильтра→новый запрос списка"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"pct": 88,
|
"pct": 100,
|
||||||
"done": 7
|
"done": 8
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"route": "/money/report",
|
"route": "/money/report",
|
||||||
|
|
@ -987,12 +987,12 @@
|
||||||
"key": "filter:period",
|
"key": "filter:period",
|
||||||
"label": "фильтр: период/дата",
|
"label": "фильтр: период/дата",
|
||||||
"type": "filter",
|
"type": "filter",
|
||||||
"covered": false,
|
"covered": true,
|
||||||
"how": "период не изменил выдачу"
|
"how": "смена периода→новый запрос"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"pct": 0,
|
"pct": 100,
|
||||||
"done": 0
|
"done": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"route": "/reports/stock",
|
"route": "/reports/stock",
|
||||||
|
|
@ -1006,12 +1006,12 @@
|
||||||
"key": "filter:period",
|
"key": "filter:period",
|
||||||
"label": "фильтр: период/дата",
|
"label": "фильтр: период/дата",
|
||||||
"type": "filter",
|
"type": "filter",
|
||||||
"covered": false,
|
"covered": true,
|
||||||
"how": "период не изменил выдачу"
|
"how": "смена периода→новый запрос"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"pct": 0,
|
"pct": 100,
|
||||||
"done": 0
|
"done": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"route": "/admin/import/moysklad",
|
"route": "/admin/import/moysklad",
|
||||||
|
|
@ -1563,8 +1563,8 @@
|
||||||
"key": "pagination",
|
"key": "pagination",
|
||||||
"label": "пагинация",
|
"label": "пагинация",
|
||||||
"type": "pagination",
|
"type": "pagination",
|
||||||
"covered": false,
|
"covered": true,
|
||||||
"how": "список пуст — пагинации нет"
|
"how": "пагинация отрисована"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "action:delete",
|
"key": "action:delete",
|
||||||
|
|
@ -1581,8 +1581,8 @@
|
||||||
"how": "сохранение не удалось"
|
"how": "сохранение не удалось"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"pct": 25,
|
"pct": 50,
|
||||||
"done": 1
|
"done": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"route": "/money/expense-items",
|
"route": "/money/expense-items",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"generatedAt": "2026-10-09T08:05:20.852Z",
|
"generatedAt": "2026-10-09T16:53:00.483Z",
|
||||||
"covered": {
|
"covered": {
|
||||||
"/catalog/products": {
|
"/catalog/products": {
|
||||||
"search": {
|
"search": {
|
||||||
|
|
@ -27,15 +27,15 @@
|
||||||
"how": "клик «Себестоимость»→sort=cost"
|
"how": "клик «Себестоимость»→sort=cost"
|
||||||
},
|
},
|
||||||
"sort:systemPrice": {
|
"sort:systemPrice": {
|
||||||
"covered": false,
|
"covered": true,
|
||||||
"how": "нет заголовка «systemPrice»"
|
"how": "клик «Розничная цена»→sort=systemPrice"
|
||||||
},
|
},
|
||||||
"sort:vat": {
|
"sort:vat": {
|
||||||
"covered": false,
|
"covered": true,
|
||||||
"how": "нет заголовка «НДС»"
|
"how": "клик «НДС»→sort=vat"
|
||||||
},
|
},
|
||||||
"filter:все": {
|
"filter:все": {
|
||||||
"covered": false,
|
"covered": true,
|
||||||
"how": "смена фильтра→новый запрос списка"
|
"how": "смена фильтра→новый запрос списка"
|
||||||
},
|
},
|
||||||
"action:delete": {
|
"action:delete": {
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
"how": "клик «Товар»→sort=name"
|
"how": "клик «Товар»→sort=name"
|
||||||
},
|
},
|
||||||
"sort:store": {
|
"sort:store": {
|
||||||
"covered": false,
|
"covered": true,
|
||||||
"how": "клик «Склад»→sort=store"
|
"how": "клик «Склад»→sort=store"
|
||||||
},
|
},
|
||||||
"sort:unit": {
|
"sort:unit": {
|
||||||
|
|
@ -137,16 +137,16 @@
|
||||||
},
|
},
|
||||||
"/money/shifts": {
|
"/money/shifts": {
|
||||||
"pagination": {
|
"pagination": {
|
||||||
"covered": false,
|
"covered": true,
|
||||||
"how": "список пуст — пагинации нет"
|
"how": "пагинация отрисована"
|
||||||
},
|
},
|
||||||
"action:print": {
|
"action:print": {
|
||||||
"covered": true,
|
"covered": true,
|
||||||
"how": "элемент присутствует"
|
"how": "элемент присутствует"
|
||||||
},
|
},
|
||||||
"action:save": {
|
"action:save": {
|
||||||
"covered": false,
|
"covered": true,
|
||||||
"how": "сохранение не удалось"
|
"how": "открытие смены→POST 2xx"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/purchases/supplies": {
|
"/purchases/supplies": {
|
||||||
|
|
@ -355,7 +355,7 @@
|
||||||
"how": "клик «Товар»→sort=product"
|
"how": "клик «Товар»→sort=product"
|
||||||
},
|
},
|
||||||
"sort:store": {
|
"sort:store": {
|
||||||
"covered": false,
|
"covered": true,
|
||||||
"how": "клик «Склад»→sort=store"
|
"how": "клик «Склад»→sort=store"
|
||||||
},
|
},
|
||||||
"sort:quantity": {
|
"sort:quantity": {
|
||||||
|
|
@ -463,14 +463,14 @@
|
||||||
},
|
},
|
||||||
"/reports/sales": {
|
"/reports/sales": {
|
||||||
"filter:period": {
|
"filter:period": {
|
||||||
"covered": false,
|
"covered": true,
|
||||||
"how": "период не изменил выдачу"
|
"how": "смена периода→новый запрос"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/reports/stock": {
|
"/reports/stock": {
|
||||||
"filter:period": {
|
"filter:period": {
|
||||||
"covered": false,
|
"covered": true,
|
||||||
"how": "период не изменил выдачу"
|
"how": "смена периода→новый запрос"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/admin/import/moysklad": {
|
"/admin/import/moysklad": {
|
||||||
|
|
@ -729,8 +729,8 @@
|
||||||
"how": "поиск→search="
|
"how": "поиск→search="
|
||||||
},
|
},
|
||||||
"pagination": {
|
"pagination": {
|
||||||
"covered": false,
|
"covered": true,
|
||||||
"how": "список пуст — пагинации нет"
|
"how": "пагинация отрисована"
|
||||||
},
|
},
|
||||||
"action:delete": {
|
"action:delete": {
|
||||||
"covered": false,
|
"covered": false,
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,11 @@ test.describe('UAT coverage (измеримое)', () => {
|
||||||
} else if (el.type === 'sort') {
|
} else if (el.type === 'sort') {
|
||||||
const sortKey = el.key.slice('sort:'.length)
|
const sortKey = el.key.slice('sort:'.length)
|
||||||
const header = el.label.replace(/^сортировка:\s*/, '')
|
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 }
|
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}`)
|
mark(s.route, el.key, await fires(() => btn.click(), list, `sort=${sortKey}`), `клик «${header}»→sort=${sortKey}`)
|
||||||
} else if (el.key === 'pagination') {
|
} else if (el.key === 'pagination') {
|
||||||
|
|
@ -104,7 +108,11 @@ test.describe('UAT coverage (измеримое)', () => {
|
||||||
// Период/дата: меняем дату-инпут или кликаем пресет периода → новый запрос.
|
// Период/дата: меняем дату-инпут или кликаем пресет периода → новый запрос.
|
||||||
const ok = await fires(async () => {
|
const ok = await fires(async () => {
|
||||||
const date = page.locator('input[type="date"]').first()
|
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 {
|
else {
|
||||||
const preset = page.getByRole('button', { name: /Неделя|Месяц|Квартал|Год|7 дней|30 дней|период/i }).first()
|
const preset = page.getByRole('button', { name: /Неделя|Месяц|Квартал|Год|7 дней|30 дней|период/i }).first()
|
||||||
if (await preset.count() > 0) await preset.click()
|
if (await preset.count() > 0) await preset.click()
|
||||||
|
|
@ -113,15 +121,26 @@ test.describe('UAT coverage (измеримое)', () => {
|
||||||
}, list)
|
}, list)
|
||||||
mark(s.route, el.key, ok, ok ? 'смена периода→новый запрос' : 'период не изменил выдачу')
|
mark(s.route, el.key, ok, ok ? 'смена периода→новый запрос' : 'период не изменил выдачу')
|
||||||
} else if (el.type === 'filter') {
|
} else if (el.type === 'filter') {
|
||||||
// Кастомный Select: открыть по тексту первого варианта из метки, выбрать 2-й пункт.
|
// Кастомный Select ИЛИ native <select>. Часть фильтров спрятана за кнопкой
|
||||||
|
// «Фильтры» (сворачиваемая панель) — сперва раскрываем её.
|
||||||
const cur = el.label.replace(/^фильтр:\s*/, '')
|
const cur = el.label.replace(/^фильтр:\s*/, '')
|
||||||
const ok = await fires(async () => {
|
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()
|
const trigger = page.getByText(cur, { exact: true }).first()
|
||||||
if (await trigger.count() > 0) await trigger.click()
|
if (await trigger.count() > 0) await trigger.click()
|
||||||
else await page.locator('select, [role="combobox"]').first().click()
|
else await page.locator('select, [role="combobox"]').first().click()
|
||||||
await page.waitForTimeout(300)
|
await page.waitForTimeout(300)
|
||||||
const opt = page.getByRole('button').filter({ hasNotText: cur })
|
|
||||||
// выбрать любой пункт выпадающего списка (li>button)
|
|
||||||
const li = page.locator('li button, [role="option"]')
|
const li = page.locator('li button, [role="option"]')
|
||||||
if (await li.count() > 1) await li.nth(1).click()
|
if (await li.count() > 1) await li.nth(1).click()
|
||||||
else if (await li.count() === 1) await li.first().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(() => {})
|
if (page.url() !== before) await attachSession(page, sess, s.route).catch(() => {})
|
||||||
await page.waitForTimeout(500)
|
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') {
|
} else if (el.key === 'action:save') {
|
||||||
// Позитивное действие: открыть создание, заполнить первые поля, сохранить → POST.
|
// Позитивное действие: открыть создание, заполнить первые поля, сохранить → POST.
|
||||||
const ok = await (async () => {
|
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 { /* */ }
|
try { await ctx.post('/api/inventory/inventories', { data: { date: new Date().toISOString(), storeId, lines: [{ productId: p, factQty: 10 }] } }) } catch { /* */ }
|
||||||
// возврат поставщику из проведённой приёмки — для /purchases/supplier-returns
|
// возврат поставщику из проведённой приёмки — для /purchases/supplier-returns
|
||||||
try { if (sup?.id) await ctx.post(`/api/purchases/supplier-returns/from-supply/${sup.id}`) } catch { /* */ }
|
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 { /* */ }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,12 @@ const pct = (d: number, t: number) => (t ? Math.round((d / t) * 100) : 100)
|
||||||
const summary = {
|
const summary = {
|
||||||
overall: pct(done, total), total, done,
|
overall: pct(done, total), total, done,
|
||||||
byPriority: Object.fromEntries(Object.entries(byPrio).map(([k, v]) => [k, { pct: pct(v.done, v.total), ...v }])),
|
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))
|
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 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 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>
|
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">${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">${done}</div></div>
|
||||||
<div class="card"><div class="muted">Общее</div><div class="n">${summary.overall}%</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 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>
|
</div>
|
||||||
<table>
|
<table>
|
||||||
|
|
@ -73,8 +76,8 @@ const html = `<!doctype html><meta charset="utf-8"><title>UAT coverage — ALL M
|
||||||
${rows.map((s) => `<tr>
|
${rows.map((s) => `<tr>
|
||||||
<td>${s.priority}</td>
|
<td>${s.priority}</td>
|
||||||
<td><b>${s.title}</b><br><span class="muted">${s.route}</span></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>${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 >= (s.priority === 'P4' ? 70 : 95) ? '#16a34a' : '#f59e0b'}"></i></div></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>
|
<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')}
|
</tr>`).join('\n')}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -83,5 +86,6 @@ writeFileSync(OUTHTML, html)
|
||||||
|
|
||||||
console.log(`[coverage] overall ${summary.overall}% (${done}/${total})`)
|
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})`)
|
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}`)
|
console.log(`→ ${OUTHTML}`)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue