test(uat): измеримое покрытие — прогон + HTML с цифрами (P1–P3 80%, P4 37%)
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
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions

Доработаны хендлеры браузерного прогона (пагинация со сбросом поиска, обобщённое
создание/сохранение/удаление, период-фильтры, доп. сид: чек/приёмка/оприходование/
списание/инвентаризация/возврат). Экстрактор ловит строковый фолбэк заголовка колонки.
docs/uat-coverage.html — цифры по экрану/приоритету/общим. Цели (P1–P3 ≥95%, P4 ≥70%)
пока НЕ достигнуты (переход на ALL MARKET в приоритете) — остаток задокументирован в
docs/uat-coverage-progress.md, продолжим после перехода.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-09 13:09:46 +05:00
parent 848e747379
commit 2b751dc129
6 changed files with 3126 additions and 14 deletions

View file

@ -0,0 +1,77 @@
# UAT — измеримое покрытие каждого элемента каждого экрана
Цель: матрица покрытия, построенная ИЗ КОДА, + браузерный прогон → `docs/uat-coverage.html`
с % по экрану/приоритету/общим. Цели: P1–P3 ≥95%, P4 ≥70%. НЕ перепроверять уже покрытое.
Легенда: [x] готово · [~] частично · [ ] не начато.
## Инструменты
- `tests/e2e/tools/extract-controls.ts` — статически из `web/src/pages` достаёт все
интерактивные элементы экрана (поиск/фильтры/сортировки/пагинация/экспорт/тумблеры/
действия) → `docs/uat-coverage.expected.json`. Фильтр списка отличается от поля формы
по `setPage` в обработчике. 36 экранов, 195 элементов.
- `tests/e2e/scenarios/stage-ui-uat-coverage.spec.ts` — браузерный прогон: задействует
каждый элемент, пишет `tests/e2e/reports/uat-covered.json`.
- `tests/e2e/tools/build-coverage.ts` — сводит → `docs/uat-coverage.{json,html}` с %.
## Статус покрытия (на момент смены приоритета 09.10 — переход на ALL MARKET)
Цифры в `docs/uat-coverage.html` (и `uat-coverage.json`). Прогон на стенде, temp-орг с сидом.
| Приоритет | Покрыто | Цель |
|---|---|---|
| **Общее** | **61% (119/195)** | — |
| P1 | 84% (27/32) | 95% |
| P2 | 86% (12/14) | 95% |
| P3 | 76% (48/63) | 95% |
| **P1–P3** | **80% (87/109)** | **95%** |
| P4 | 37% (32/86) | 70% |
Прогресс по итерациям: базовый 50% → после доработки хендлеров (пагинация со сбросом
поиска, обобщённое создание/сохранение/удаление, период-фильтры, доп. сид: чек/приёмка/
оприходование/списание/инвентаризация/возврат) → 61% (P1–P3 71%→80%, P4 24%→37%).
### Что уже покрыто хорошо (из кода × браузер)
- По типам: **export 5/5, sort 60/77, toggle 2/2, search 18/25, filter (часть), pagination
(где есть данные)**. Действия create/save — частично (обобщённый CRUD-хендлер).
- Инструменты рабочие: экстрактор (195 элементов из 36 экранов), браузерный прогон, HTML с %.
### Что НЕ успел (продолжить после перехода на ALL MARKET)
Цели (P1–P3 ≥95%, P4 ≥70%) НЕ достигнуты — осталось добрать ~22 элемента P1–P3 и ~28 P4:
- **Сортировки с динамическим/условным заголовком** (ProductsPage `systemPrice`→имя типа цены,
`vat`→колонка только при вкл. НДС): экстрактор доработан ловить строковый фолбэк
(`header: expr ?? 'Текст'`) — нужно перегенерировать expected и прогнать ещё раз.
- **Фильтры со СМЫСЛОМ** (правильные строки по известным данным): сейчас проверяется, что
контрол шлёт новый запрос; семантику части списков держит filters-sweep. Добить smысл для
продаж (период/оплата), приёмок (поставщик/склад/статус/оплата), движений (тип/товар/период),
денег (счёт/период/тип), отчётов (период/группировки/склад/группа на дату).
- **Действия документов** post/unpost на списках money/transactions, invoices (сейчас — наличие
элемента); довести до реального POST с проверкой 2xx.
- **P4 save/delete**: обобщённый хендлер покрывает простые CRUD-модалки; формы со множеством
обязательных полей (счета/точки/сотрудники/настройки) требуют адресного заполнения.
- **Пагинация** на экранах без сид-данных: отрисовывается только при total>0 — досидить или
считать покрытой по общему компоненту.
- nightly: добавить `stage-ui-uat-coverage` в ночной прогон (пока не добавлено).
Запуск: `cd tests/e2e && pnpm exec tsx tools/extract-controls.ts` (матрица) →
`pnpm exec playwright test stage-ui-uat-coverage` (прогон) → `pnpm exec tsx tools/build-coverage.ts` (HTML).
## Найдено Mac-Claude (быстрые фиксы — сделаны)
1. **«Проверка данных» на Демо — все 0, lastCheckedAt=null.** Hangfire-джоба `DataQualityJob`
зарегистрирована (ежедневный cron 02:20 UTC, идёт по всем активным орг), и `RecomputeAsync`
корректна — просто ночной прогон ещё не отрабатывал после пересева Демо. Фикс: разовый
`BackgroundJob.Enqueue(DataQualityJob)` при старте → считается сразу после деплоя/пересева.
**Реальные числа Демо (стенд, после фикса):** total 1276 ·
без группы 0 · **без цены продажи 718** · без штрихкода 0 · дубль ШК 0 ·
**возможный дубль (имя+ед.) 558** (включая 4× «"BIOBLAS" шампунь д/волос 360мл») ·
товары в минусе 0. `lastCheckedAt` проставлен.
2. **Внутренний ШК с ведущим 0 («0201259508594»).** `InternalBarcode.PickPrefix` брал «02»
(ведущий 0 → зона UPC-A, риск совпасть с реальным товаром). Фикс: берём 2x из
restricted-circulation 20–29 (без ведущего нуля), не пересекаясь с весовыми префиксами
орг; «02» — крайний резерв. Для весовых — короткий «27+5» по настройке (отдельно).
**Проверка на стенде:** новые коды — `2021712729230`, `2043662520927`, `2084330808596`
(префикс 20, без ведущего нуля). Существующие коды НЕ мигрировали: смена ШК ломает
ссылки, а отличить ранее сгенерированный «02…» от реального невозможно; генератор
исправлен для всех НОВЫХ товаров.
## Что НЕ трогаем
Прод, shared-postgres, global.json, gateway nginx, sklad.

282
docs/uat-coverage.html Normal file
View file

@ -0,0 +1,282 @@
<!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">Сгенерировано 2026-10-09T08:08:43.726Z · элементы извлечены из кода (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>
<table>
<thead><tr><th>Пр.</th><th>Экран</th><th>%</th><th></th><th>Элементы (зелёные покрыты)</th></tr></thead>
<tbody>
<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>
</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>
</tr>
<tr>
<td>P1</td>
<td><b>Розничные продажи</b><br><span class="muted">/sales/retail</span></td>
<td><b style="color:#16a34a">100%</b><br><span class="muted">9/9</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=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:#dcfce7;color:#166534" title="переход на создание">✓ action:create</span></td>
</tr>
<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>
</tr>
<tr>
<td>P2</td>
<td><b>Приёмки от поставщиков</b><br><span class="muted">/purchases/supplies</span></td>
<td><b style="color:#16a34a">100%</b><br><span class="muted">11/11</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=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=supplier">✓ sort:supplier</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:#dcfce7;color:#166534" title="смена фильтра→новый запрос списка">✓ filter:оплата-все</span> <span class="chip" style="background:#dcfce7;color:#166534" title="открылась форма">✓ action:create</span></td>
</tr>
<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><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>
<td>P3</td>
<td><b>Проверка данных</b><br><span class="muted">/catalog/data-quality</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="не удалось удалить">✗ action:delete</span></td>
</tr>
<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><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>
<td>P3</td>
<td><b>Инвентаризации</b><br><span class="muted">/inventory/inventories</span></td>
<td><b style="color:#16a34a">100%</b><br><span class="muted">6/6</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="клик «№»→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="открылась форма">✓ action:create</span></td>
</tr>
<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><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>
</tr>
<tr>
<td>P3</td>
<td><b>Отчёт «Деньги»</b><br><span class="muted">/money/report</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>
</tr>
<tr>
<td>P3</td>
<td><b>Долги поставщикам</b><br><span class="muted">/money/supplier-debts</span></td>
<td><b style="color:#dc2626">0%</b><br><span class="muted">0/2</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="не найден здесь">✗ action:post</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="сохранение не удалось">✗ action:save</span></td>
</tr>
<tr>
<td>P3</td>
<td><b>Платежи и ордера</b><br><span class="muted">/money/transactions</span></td>
<td><b style="color:#dc2626">38%</b><br><span class="muted">3/8</span></td>
<td><div class="bar"><i style="width:38%;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:#dcfce7;color:#166534" title="смена фильтра→новый запрос списка">✓ filter:все-статусы</span> <span class="chip" style="background:#dcfce7;color:#166534" title="смена фильтра→новый запрос списка">✓ filter:все-типы</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="не найден здесь">✗ action:post</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="не найден здесь">✗ action:unpost</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>P3</td>
<td><b>Накладные</b><br><span class="muted">/purchases/invoices</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="элемент присутствует">✓ action:unpost</span></td>
</tr>
<tr>
<td>P3</td>
<td><b>Возвраты поставщикам</b><br><span class="muted">/purchases/supplier-returns</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="поиск→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=supplier">✓ sort:supplier</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Сумма»→sort=total">✓ sort:total</span> <span class="chip" style="background:#dcfce7;color:#166534" title="переход на создание">✓ action:create</span></td>
</tr>
<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>
</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>
</tr>
<tr>
<td>P4</td>
<td><b>Импорт из МойСклад</b><br><span class="muted">/admin/import/moysklad</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="не удалось удалить">✗ action:delete</span></td>
</tr>
<tr>
<td>P4</td>
<td><b>Журнал изменений</b><br><span class="muted">/audit-log</span></td>
<td><b style="color:#dc2626">0%</b><br><span class="muted">0/6</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="поиск→search=">✗ search</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="список пуст — пагинации нет">✗ pagination</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="смена фильтра→новый запрос списка">✗ filter:sel1</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="смена фильтра→новый запрос списка">✗ filter:sel2</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="смена фильтра→новый запрос списка">✗ filter:все</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="период не изменил выдачу">✗ filter:period</span></td>
</tr>
<tr>
<td>P4</td>
<td><b>Типы цен</b><br><span class="muted">/catalog/price-types</span></td>
<td><b style="color:#dc2626">0%</b><br><span class="muted">0/6</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="поиск→search=">✗ search</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="список пуст — пагинации нет">✗ pagination</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="нет заголовка «Название»">✗ sort:name</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="нет заголовка «Обязательно»">✗ sort:isRequired</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>
<td><b>Группы товаров</b><br><span class="muted">/catalog/product-groups</span></td>
<td><b style="color:#16a34a">83%</b><br><span class="muted">5/6</span></td>
<td><div class="bar"><i style="width:83%;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=name">✓ sort:name</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Путь»→sort=path">✓ sort:path</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>
<td>P4</td>
<td><b>Кассы</b><br><span class="muted">/catalog/retail-points</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:#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=name">✓ sort:name</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Код»→sort=code">✓ sort:code</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=address">✓ sort:address</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Активна»→sort=isActive">✓ sort:isActive</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>
<td>P4</td>
<td><b>Склады</b><br><span class="muted">/catalog/stores</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:#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=name">✓ sort:name</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Код»→sort=code">✓ sort:code</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Адрес»→sort=address">✓ sort:address</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Основной»→sort=isMain">✓ sort:isMain</span> <span class="chip" style="background:#dcfce7;color:#166534" title="клик «Активен»→sort=isActive">✓ sort:isActive</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>
<td>P4</td>
<td><b>Единицы измерения</b><br><span class="muted">/catalog/units</span></td>
<td><b style="color:#16a34a">75%</b><br><span class="muted">3/4</span></td>
<td><div class="bar"><i style="width:75%;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=name">✓ sort:name</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="не удалось удалить">✗ action:delete</span></td>
</tr>
<tr>
<td>P4</td>
<td><b>Инвентаризация с телефона</b><br><span class="muted">/inventory/mobile</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="не найден здесь">✗ action:post</span></td>
</tr>
<tr>
<td>P4</td>
<td><b>Перемещения</b><br><span class="muted">/inventory/transfers</span></td>
<td><b style="color:#dc2626">0%</b><br><span class="muted">0/7</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="поиск→search=">✗ search</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="список пуст — пагинации нет">✗ pagination</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="нет заголовка «№»">✗ sort:number</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="нет заголовка «Дата»">✗ sort:date</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="нет заголовка «Статус»">✗ sort:status</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="нет заголовка «Сумма»">✗ sort:total</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="не открылось">✗ action:create</span></td>
</tr>
<tr>
<td>P4</td>
<td><b>Карты лояльности</b><br><span class="muted">/loyalty/cards</span></td>
<td><b style="color:#dc2626">0%</b><br><span class="muted">0/3</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="поиск→search=">✗ search</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="список пуст — пагинации нет">✗ pagination</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="нет заголовка «Номер»">✗ sort:cardNumber</span></td>
</tr>
<tr>
<td>P4</td>
<td><b>Программы лояльности</b><br><span class="muted">/loyalty/programs</span></td>
<td><b style="color:#dc2626">0%</b><br><span class="muted">0/5</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="поиск→search=">✗ search</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="список пуст — пагинации нет">✗ pagination</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="нет заголовка «Название»">✗ sort:name</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>
<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>
</tr>
<tr>
<td>P4</td>
<td><b>Статьи расходов</b><br><span class="muted">/money/expense-items</span></td>
<td><b style="color:#16a34a">75%</b><br><span class="muted">3/4</span></td>
<td><div class="bar"><i style="width:75%;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:#fee2e2;color:#991b1b" title="не удалось удалить">✗ action:delete</span> <span class="chip" style="background:#dcfce7;color:#166534" title="создание→POST 2xx">✓ action:save</span></td>
</tr>
<tr>
<td>P4</td>
<td><b>Акции и промокоды</b><br><span class="muted">/promotions</span></td>
<td><b style="color:#dc2626">0%</b><br><span class="muted">0/4</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="поиск→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>
</tr>
<tr>
<td>P4</td>
<td><b>Оптовые отгрузки</b><br><span class="muted">/sales/demands</span></td>
<td><b style="color:#dc2626">0%</b><br><span class="muted">0/8</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="поиск→search=">✗ search</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="список пуст — пагинации нет">✗ pagination</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="нет заголовка «№»">✗ sort:number</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="нет заголовка «Дата»">✗ sort:date</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="нет заголовка «Статус»">✗ sort:status</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="нет заголовка «Контрагент»">✗ sort:customer</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="нет заголовка «Сумма»">✗ sort:total</span> <span class="chip" style="background:#fee2e2;color:#991b1b" title="не открылось">✗ action:create</span></td>
</tr>
<tr>
<td>P4</td>
<td><b>Роли сотрудников</b><br><span class="muted">/settings/employee-roles</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>
<td><b>Сотрудники</b><br><span class="muted">/settings/employees</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>
<td><b>Настройки организации</b><br><span class="muted">/settings/organization</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="не удалось удалить">✗ action:delete</span></td>
</tr>
</tbody>
</table>

1835
docs/uat-coverage.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,857 @@
{
"generatedAt": "2026-10-09T08:05:20.852Z",
"covered": {
"/catalog/products": {
"search": {
"covered": true,
"how": "поиск→search="
},
"pagination": {
"covered": true,
"how": "пагинация отрисована"
},
"export": {
"covered": true,
"how": "экспорт→/export&format="
},
"sort:name": {
"covered": true,
"how": "клик «Название»→sort=name"
},
"sort:packaging": {
"covered": true,
"how": "клик «Фасовка»→sort=packaging"
},
"sort:cost": {
"covered": true,
"how": "клик «Себестоимость»→sort=cost"
},
"sort:systemPrice": {
"covered": false,
"how": "нет заголовка «systemPrice»"
},
"sort:vat": {
"covered": false,
"how": "нет заголовка «НДС»"
},
"filter:все": {
"covered": false,
"how": "смена фильтра→новый запрос списка"
},
"action:delete": {
"covered": false,
"how": "не удалось удалить"
},
"action:create": {
"covered": true,
"how": "переход на создание"
}
},
"/inventory/stock": {
"search": {
"covered": true,
"how": "поиск→search="
},
"pagination": {
"covered": true,
"how": "пагинация отрисована"
},
"export": {
"covered": true,
"how": "экспорт→/export&format="
},
"sort:name": {
"covered": true,
"how": "клик «Товар»→sort=name"
},
"sort:store": {
"covered": false,
"how": "клик «Склад»→sort=store"
},
"sort:unit": {
"covered": true,
"how": "клик «Ед.»→sort=unit"
},
"sort:quantity": {
"covered": true,
"how": "клик «Остаток»→sort=quantity"
},
"sort:reserved": {
"covered": true,
"how": "клик «Резерв»→sort=reserved"
},
"sort:available": {
"covered": true,
"how": "клик «Доступно»→sort=available"
},
"filter:все-склады": {
"covered": true,
"how": "смена фильтра→новый запрос списка"
},
"toggle:показать-нулевые": {
"covered": true,
"how": "тумблер→новый запрос списка"
},
"toggle:только-минусы": {
"covered": true,
"how": "тумблер→новый запрос списка"
}
},
"/sales/retail": {
"search": {
"covered": true,
"how": "поиск→search="
},
"pagination": {
"covered": true,
"how": "пагинация отрисована"
},
"export": {
"covered": true,
"how": "экспорт→/export&format="
},
"sort:number": {
"covered": true,
"how": "клик «№»→sort=number"
},
"sort:date": {
"covered": true,
"how": "клик «Дата/время»→sort=date"
},
"sort:status": {
"covered": true,
"how": "клик «Статус»→sort=status"
},
"sort:store": {
"covered": true,
"how": "клик «Магазин»→sort=store"
},
"sort:total": {
"covered": true,
"how": "клик «Сумма»→sort=total"
},
"action:create": {
"covered": true,
"how": "переход на создание"
}
},
"/money/shifts": {
"pagination": {
"covered": false,
"how": "список пуст — пагинации нет"
},
"action:print": {
"covered": true,
"how": "элемент присутствует"
},
"action:save": {
"covered": false,
"how": "сохранение не удалось"
}
},
"/purchases/supplies": {
"search": {
"covered": true,
"how": "поиск→search="
},
"pagination": {
"covered": true,
"how": "пагинация отрисована"
},
"export": {
"covered": true,
"how": "экспорт→/export&format="
},
"sort:number": {
"covered": true,
"how": "клик «№»→sort=number"
},
"sort:date": {
"covered": true,
"how": "клик «Дата»→sort=date"
},
"sort:status": {
"covered": true,
"how": "клик «Статус»→sort=status"
},
"sort:supplier": {
"covered": true,
"how": "клик «Поставщик»→sort=supplier"
},
"sort:store": {
"covered": true,
"how": "клик «Склад»→sort=store"
},
"sort:total": {
"covered": true,
"how": "клик «Сумма»→sort=total"
},
"filter:оплата-все": {
"covered": true,
"how": "смена фильтра→новый запрос списка"
},
"action:create": {
"covered": true,
"how": "открылась форма"
}
},
"/catalog/counterparties": {
"search": {
"covered": true,
"how": "поиск→search="
},
"pagination": {
"covered": true,
"how": "пагинация отрисована"
},
"export": {
"covered": true,
"how": "экспорт→/export&format="
},
"sort:name": {
"covered": true,
"how": "клик «Название»→sort=name"
},
"sort:type": {
"covered": true,
"how": "клик «Тип»→sort=type"
},
"sort:phone": {
"covered": true,
"how": "клик «Телефон»→sort=phone"
},
"sort:country": {
"covered": true,
"how": "клик «Страна»→sort=country"
},
"action:delete": {
"covered": false,
"how": "не удалось удалить"
},
"action:save": {
"covered": true,
"how": "создание→POST 2xx"
}
},
"/catalog/data-quality": {
"action:delete": {
"covered": false,
"how": "не удалось удалить"
}
},
"/inventory/enters": {
"search": {
"covered": true,
"how": "поиск→search="
},
"pagination": {
"covered": true,
"how": "пагинация отрисована"
},
"sort:number": {
"covered": true,
"how": "клик «№»→sort=number"
},
"sort:date": {
"covered": true,
"how": "клик «Дата»→sort=date"
},
"sort:status": {
"covered": true,
"how": "клик «Статус»→sort=status"
},
"sort:store": {
"covered": true,
"how": "клик «Склад»→sort=store"
},
"sort:total": {
"covered": true,
"how": "клик «Сумма»→sort=total"
},
"action:create": {
"covered": false,
"how": "не открылось"
}
},
"/inventory/inventories": {
"search": {
"covered": true,
"how": "поиск→search="
},
"pagination": {
"covered": true,
"how": "пагинация отрисована"
},
"sort:number": {
"covered": true,
"how": "клик «№»→sort=number"
},
"sort:date": {
"covered": true,
"how": "клик «Дата»→sort=date"
},
"sort:status": {
"covered": true,
"how": "клик «Статус»→sort=status"
},
"action:create": {
"covered": true,
"how": "открылась форма"
}
},
"/inventory/losses": {
"search": {
"covered": true,
"how": "поиск→search="
},
"pagination": {
"covered": true,
"how": "пагинация отрисована"
},
"sort:number": {
"covered": true,
"how": "клик «№»→sort=number"
},
"sort:date": {
"covered": true,
"how": "клик «Дата»→sort=date"
},
"sort:status": {
"covered": true,
"how": "клик «Статус»→sort=status"
},
"sort:reason": {
"covered": true,
"how": "клик «Причина»→sort=reason"
},
"sort:store": {
"covered": true,
"how": "клик «Склад»→sort=store"
},
"sort:total": {
"covered": true,
"how": "клик «Сумма»→sort=total"
},
"action:create": {
"covered": false,
"how": "не открылось"
}
},
"/inventory/movements": {
"pagination": {
"covered": true,
"how": "пагинация отрисована"
},
"sort:occurredAt": {
"covered": true,
"how": "клик «Дата»→sort=occurredAt"
},
"sort:type": {
"covered": true,
"how": "клик «Операция»→sort=type"
},
"sort:product": {
"covered": true,
"how": "клик «Товар»→sort=product"
},
"sort:store": {
"covered": false,
"how": "клик «Склад»→sort=store"
},
"sort:quantity": {
"covered": true,
"how": "клик «Количество»→sort=quantity"
},
"sort:documentType": {
"covered": true,
"how": "клик «Документ»→sort=documentType"
},
"filter:все-склады": {
"covered": true,
"how": "смена фильтра→новый запрос списка"
}
},
"/money/report": {
"filter:period": {
"covered": false,
"how": "период не изменил выдачу"
}
},
"/money/supplier-debts": {
"action:post": {
"covered": false,
"how": "не найден здесь"
},
"action:save": {
"covered": false,
"how": "сохранение не удалось"
}
},
"/money/transactions": {
"search": {
"covered": true,
"how": "поиск→search="
},
"pagination": {
"covered": false,
"how": "список пуст — пагинации нет"
},
"filter:все-статусы": {
"covered": true,
"how": "смена фильтра→новый запрос списка"
},
"filter:все-типы": {
"covered": true,
"how": "смена фильтра→новый запрос списка"
},
"action:post": {
"covered": false,
"how": "не найден здесь"
},
"action:unpost": {
"covered": false,
"how": "не найден здесь"
},
"action:delete": {
"covered": false,
"how": "не удалось удалить"
},
"action:save": {
"covered": false,
"how": "сохранение не удалось"
}
},
"/purchases/invoices": {
"action:unpost": {
"covered": true,
"how": "элемент присутствует"
}
},
"/purchases/supplier-returns": {
"search": {
"covered": true,
"how": "поиск→search="
},
"pagination": {
"covered": true,
"how": "пагинация отрисована"
},
"sort:number": {
"covered": true,
"how": "клик «№»→sort=number"
},
"sort:date": {
"covered": true,
"how": "клик «Дата»→sort=date"
},
"sort:status": {
"covered": true,
"how": "клик «Статус»→sort=status"
},
"sort:supplier": {
"covered": true,
"how": "клик «Поставщик»→sort=supplier"
},
"sort:total": {
"covered": true,
"how": "клик «Сумма»→sort=total"
},
"action:create": {
"covered": true,
"how": "переход на создание"
}
},
"/reports/sales": {
"filter:period": {
"covered": false,
"how": "период не изменил выдачу"
}
},
"/reports/stock": {
"filter:period": {
"covered": false,
"how": "период не изменил выдачу"
}
},
"/admin/import/moysklad": {
"action:delete": {
"covered": false,
"how": "не удалось удалить"
}
},
"/audit-log": {
"search": {
"covered": false,
"how": "поиск→search="
},
"pagination": {
"covered": false,
"how": "список пуст — пагинации нет"
},
"filter:sel1": {
"covered": false,
"how": "смена фильтра→новый запрос списка"
},
"filter:sel2": {
"covered": false,
"how": "смена фильтра→новый запрос списка"
},
"filter:все": {
"covered": false,
"how": "смена фильтра→новый запрос списка"
},
"filter:period": {
"covered": false,
"how": "период не изменил выдачу"
}
},
"/catalog/price-types": {
"search": {
"covered": false,
"how": "поиск→search="
},
"pagination": {
"covered": false,
"how": "список пуст — пагинации нет"
},
"sort:name": {
"covered": false,
"how": "нет заголовка «Название»"
},
"sort:isRequired": {
"covered": false,
"how": "нет заголовка «Обязательно»"
},
"action:delete": {
"covered": false,
"how": "не удалось удалить"
},
"action:save": {
"covered": false,
"how": "сохранение не удалось"
}
},
"/catalog/product-groups": {
"search": {
"covered": true,
"how": "поиск→search="
},
"pagination": {
"covered": true,
"how": "пагинация отрисована"
},
"sort:name": {
"covered": true,
"how": "клик «Название»→sort=name"
},
"sort:path": {
"covered": true,
"how": "клик «Путь»→sort=path"
},
"action:delete": {
"covered": false,
"how": "не удалось удалить"
},
"action:save": {
"covered": true,
"how": "создание→POST 2xx"
}
},
"/catalog/retail-points": {
"search": {
"covered": true,
"how": "поиск→search="
},
"pagination": {
"covered": true,
"how": "пагинация отрисована"
},
"sort:name": {
"covered": true,
"how": "клик «Название»→sort=name"
},
"sort:code": {
"covered": true,
"how": "клик «Код»→sort=code"
},
"sort:store": {
"covered": true,
"how": "клик «Склад»→sort=store"
},
"sort:address": {
"covered": true,
"how": "клик «Адрес»→sort=address"
},
"sort:isActive": {
"covered": true,
"how": "клик «Активна»→sort=isActive"
},
"action:delete": {
"covered": false,
"how": "не удалось удалить"
},
"action:save": {
"covered": true,
"how": "создание→POST 2xx"
}
},
"/catalog/stores": {
"search": {
"covered": true,
"how": "поиск→search="
},
"pagination": {
"covered": true,
"how": "пагинация отрисована"
},
"sort:name": {
"covered": true,
"how": "клик «Название»→sort=name"
},
"sort:code": {
"covered": true,
"how": "клик «Код»→sort=code"
},
"sort:address": {
"covered": true,
"how": "клик «Адрес»→sort=address"
},
"sort:isMain": {
"covered": true,
"how": "клик «Основной»→sort=isMain"
},
"sort:isActive": {
"covered": true,
"how": "клик «Активен»→sort=isActive"
},
"action:delete": {
"covered": false,
"how": "не удалось удалить"
},
"action:save": {
"covered": true,
"how": "создание→POST 2xx"
}
},
"/catalog/units": {
"search": {
"covered": true,
"how": "поиск→search="
},
"pagination": {
"covered": true,
"how": "пагинация отрисована"
},
"sort:name": {
"covered": true,
"how": "клик «Название»→sort=name"
},
"action:delete": {
"covered": false,
"how": "не удалось удалить"
}
},
"/inventory/mobile": {
"action:post": {
"covered": false,
"how": "не найден здесь"
}
},
"/inventory/transfers": {
"search": {
"covered": false,
"how": "поиск→search="
},
"pagination": {
"covered": false,
"how": "список пуст — пагинации нет"
},
"sort:number": {
"covered": false,
"how": "нет заголовка «№»"
},
"sort:date": {
"covered": false,
"how": "нет заголовка «Дата»"
},
"sort:status": {
"covered": false,
"how": "нет заголовка «Статус»"
},
"sort:total": {
"covered": false,
"how": "нет заголовка «Сумма»"
},
"action:create": {
"covered": false,
"how": "не открылось"
}
},
"/loyalty/cards": {
"search": {
"covered": false,
"how": "поиск→search="
},
"pagination": {
"covered": false,
"how": "список пуст — пагинации нет"
},
"sort:cardNumber": {
"covered": false,
"how": "нет заголовка «Номер»"
}
},
"/loyalty/programs": {
"search": {
"covered": false,
"how": "поиск→search="
},
"pagination": {
"covered": false,
"how": "список пуст — пагинации нет"
},
"sort:name": {
"covered": false,
"how": "нет заголовка «Название»"
},
"action:delete": {
"covered": false,
"how": "не удалось удалить"
},
"action:save": {
"covered": false,
"how": "сохранение не удалось"
}
},
"/money/accounts": {
"search": {
"covered": true,
"how": "поиск→search="
},
"pagination": {
"covered": false,
"how": "список пуст — пагинации нет"
},
"action:delete": {
"covered": false,
"how": "не удалось удалить"
},
"action:save": {
"covered": false,
"how": "сохранение не удалось"
}
},
"/money/expense-items": {
"search": {
"covered": true,
"how": "поиск→search="
},
"pagination": {
"covered": true,
"how": "пагинация отрисована"
},
"action:delete": {
"covered": false,
"how": "не удалось удалить"
},
"action:save": {
"covered": true,
"how": "создание→POST 2xx"
}
},
"/promotions": {
"search": {
"covered": false,
"how": "поиск→search="
},
"pagination": {
"covered": false,
"how": "список пуст — пагинации нет"
},
"action:delete": {
"covered": false,
"how": "не удалось удалить"
},
"action:save": {
"covered": false,
"how": "сохранение не удалось"
}
},
"/sales/demands": {
"search": {
"covered": false,
"how": "поиск→search="
},
"pagination": {
"covered": false,
"how": "список пуст — пагинации нет"
},
"sort:number": {
"covered": false,
"how": "нет заголовка «№»"
},
"sort:date": {
"covered": false,
"how": "нет заголовка «Дата»"
},
"sort:status": {
"covered": false,
"how": "нет заголовка «Статус»"
},
"sort:customer": {
"covered": false,
"how": "нет заголовка «Контрагент»"
},
"sort:total": {
"covered": false,
"how": "нет заголовка «Сумма»"
},
"action:create": {
"covered": false,
"how": "не открылось"
}
},
"/settings/employee-roles": {
"search": {
"covered": true,
"how": "поиск→search="
},
"pagination": {
"covered": true,
"how": "пагинация отрисована"
},
"action:delete": {
"covered": false,
"how": "не удалось удалить"
},
"action:save": {
"covered": false,
"how": "сохранение не удалось"
}
},
"/settings/employees": {
"search": {
"covered": true,
"how": "поиск→search="
},
"pagination": {
"covered": true,
"how": "пагинация отрисована"
},
"action:delete": {
"covered": false,
"how": "не удалось удалить"
},
"action:save": {
"covered": false,
"how": "сохранение не удалось"
}
},
"/settings/organization": {
"action:delete": {
"covered": false,
"how": "не удалось удалить"
}
}
}
}

View file

@ -88,6 +88,8 @@ test.describe('UAT coverage (измеримое)', () => {
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') {
// Чистим поиск, чтобы показать полный список (пагинация есть при total>0).
if (hasSearch) { try { await searchBox().fill(''); await page.waitForTimeout(900) } catch { /* */ } }
const pager = page.getByText(/из \d+/).first() const pager = page.getByText(/из \d+/).first()
const present = await pager.count() > 0 const present = await pager.count() > 0
const next = page.getByRole('button', { name: '→' }).first() const next = page.getByRole('button', { name: '→' }).first()
@ -95,9 +97,21 @@ test.describe('UAT coverage (измеримое)', () => {
if (present && await next.count() > 0 && await next.isEnabled().catch(() => false)) { if (present && await next.count() > 0 && await next.isEnabled().catch(() => false)) {
ok = await fires(() => next.click(), list, 'page=2') || present ok = await fires(() => next.click(), list, 'page=2') || present
} }
mark(s.route, el.key, ok, present ? 'пагинация отрисована' : 'нет пагинации') mark(s.route, el.key, ok, present ? 'пагинация отрисована' : 'список пуст — пагинации нет')
} else if (el.key === 'export') { } else if (el.key === 'export') {
mark(s.route, el.key, await fires(clickExport, '/export', 'format='), 'экспорт→/export&format=') mark(s.route, el.key, await fires(clickExport, '/export', 'format='), 'экспорт→/export&format=')
} else if (el.type === 'filter' && el.key === 'filter:period') {
// Период/дата: меняем дату-инпут или кликаем пресет периода → новый запрос.
const ok = await fires(async () => {
const date = page.locator('input[type="date"]').first()
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()
else { const mi = page.locator('input[type="month"]').first(); if (await mi.count() > 0) await mi.fill('2026-01') }
}
}, list)
mark(s.route, el.key, ok, ok ? 'смена периода→новый запрос' : 'период не изменил выдачу')
} else if (el.type === 'filter') { } else if (el.type === 'filter') {
// Кастомный Select: открыть по тексту первого варианта из метки, выбрать 2-й пункт. // Кастомный Select: открыть по тексту первого варианта из метки, выбрать 2-й пункт.
const cur = el.label.replace(/^фильтр:\s*/, '') const cur = el.label.replace(/^фильтр:\s*/, '')
@ -133,18 +147,49 @@ 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') {
// Позитивное действие: открыть создание, заполнить первые поля, сохранить → POST.
const ok = await (async () => {
const opener = page.getByRole('button', { name: /Создать|Новый|Новая|Добавить|Пригласить/ }).first()
if (await opener.count() > 0) await opener.click().catch(() => {})
await page.waitForTimeout(700)
// заполнить первые 2 текстовых поля формы/модалки осмысленными значениями
const inputs = page.locator('input[type="text"], input:not([type]):not([type=checkbox])')
const n = Math.min(await inputs.count(), 2)
for (let i = 0; i < n; i++) await inputs.nth(i).fill(`UAT${Date.now() % 100000}${i}`).catch(() => {})
const save = page.getByRole('button', { name: /^Сохранить|^Создать|^Добавить|^Пригласить/ }).last()
const [resp] = await Promise.all([
page.waitForResponse((r) => r.request().method() === 'POST' && r.url().includes(list.replace(/\/export$/, '')), { timeout: 10_000 }).catch(() => null),
save.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(500)
} else if (el.key === 'action:delete') {
// Удаление: ищем иконку/кнопку удаления в строке таблицы, подтверждаем → DELETE.
const ok = await (async () => {
const del = page.getByRole('button', { name: /Удалить/ }).first()
.or(page.locator('button:has(svg.lucide-trash-2), button[title*="далить"]').first())
if (await del.count() === 0) return false
const [resp] = await Promise.all([
page.waitForResponse((r) => r.request().method() === 'DELETE', { timeout: 10_000 }).catch(() => null),
(async () => { await del.click().catch(() => {}); await page.getByRole('button', { name: /^Удалить/ }).last().click().catch(() => {}) })(),
])
return !!resp && resp.status() < 400
})().catch(() => false)
mark(s.route, el.key, ok, ok ? 'удаление→DELETE 2xx' : 'не удалось удалить')
await attachSession(page, sess, s.route).catch(() => {}); await page.waitForTimeout(500)
} else { } else {
// action:delete/save/post/unpost/print — покрываются спец-спеком controls; // action:post/unpost/print — наличие управляющего элемента на экране.
// здесь отметим наличие кнопки (presence), полноценное действие — отдельно.
const names: Record<string, RegExp> = { const names: Record<string, RegExp> = {
'action:delete': /Удалить/, 'action:save': /Сохранить|Создать/,
'action:post': /Провести|Проведено/, 'action:unpost': /Снять|Отменить проведение/, 'action:post': /Провести|Проведено/, 'action:unpost': /Снять|Отменить проведение/,
'action:print': /Печать|Z-отчёт|Печатать/, 'action:print': /Печать|Z-отчёт|Печатать/,
} }
const re = names[el.key] const re = names[el.key]
const present = re ? (await page.getByRole('button', { name: re }).count() > 0 const present = re ? (await page.getByRole('button', { name: re }).count() > 0
|| await page.getByText(re).count() > 0) : false || await page.getByText(re).count() > 0) : false
mark(s.route, el.key, present, present ? 'кнопка присутствует' : 'кнопка не найдена здесь') mark(s.route, el.key, present, present ? 'элемент присутствует' : 'не найден здесь')
} }
} catch (e: any) { } catch (e: any) {
mark(s.route, el.key, false, (e?.message ?? '').split('\n')[0].slice(0, 80)) mark(s.route, el.key, false, (e?.message ?? '').split('\n')[0].slice(0, 80))
@ -181,13 +226,22 @@ async function seed(ctx: APIRequestContext) {
})).json()).id })).json()).id
const p = await mk('COVTOVAR поиск', 'COV999', 1234) const p = await mk('COVTOVAR поиск', 'COV999', 1234)
for (let i = 0; i < 54; i++) await mk(`COV товар ${String(i).padStart(2, '0')}`, `C${1000 + i}`, 100 + i) for (let i = 0; i < 54; i++) await mk(`COV товар ${String(i).padStart(2, '0')}`, `C${1000 + i}`, 100 + i)
// приёмка (проведённая) + черновик // приёмка (проведённая) + черновик — даёт остаток и движения
const sup = await (await ctx.post('/api/purchases/supplies', { data: { date: new Date().toISOString(), supplierId: supplier, storeId, currencyId, lines: [{ productId: p, quantity: 20, unitPrice: 800 }] } })).json() const sup = await (await ctx.post('/api/purchases/supplies', { data: { date: new Date().toISOString(), supplierId: supplier, storeId, currencyId, lines: [{ productId: p, quantity: 40, unitPrice: 800 }] } })).json()
await ctx.post(`/api/purchases/supplies/${sup.id}/post`) if (sup?.id) await ctx.post(`/api/purchases/supplies/${sup.id}/post`)
await ctx.post('/api/purchases/supplies', { data: { date: new Date().toISOString(), supplierId: supplier, storeId, currencyId, lines: [{ productId: p, quantity: 3, unitPrice: 810 }] } }) await ctx.post('/api/purchases/supplies', { data: { date: new Date().toISOString(), supplierId: supplier, storeId, currencyId, lines: [{ productId: p, quantity: 3, unitPrice: 810 }] } })
// оприходование и списание (проведённые) — чтобы таблицы были не пустые // оприходование и списание (проведённые)
const ent = await (await ctx.post('/api/inventory/enters', { data: { date: new Date().toISOString(), storeId, currencyId, lines: [{ productId: p, quantity: 5, unitCost: 50 }] } })).json() const ent = await (await ctx.post('/api/inventory/enters', { data: { date: new Date().toISOString(), storeId, currencyId, lines: [{ productId: p, quantity: 5, unitCost: 50 }] } })).json()
if (ent?.id) await ctx.post(`/api/inventory/enters/${ent.id}/post`) if (ent?.id) await ctx.post(`/api/inventory/enters/${ent.id}/post`)
const loss = await (await ctx.post('/api/inventory/losses', { data: { date: new Date().toISOString(), storeId, currencyId, reason: 0, lines: [{ productId: p, quantity: 1, unitCost: 50 }] } })).json() const loss = await (await ctx.post('/api/inventory/losses', { data: { date: new Date().toISOString(), storeId, currencyId, reason: 0, lines: [{ productId: p, quantity: 1, unitCost: 50 }] } })).json()
if (loss?.id) await ctx.post(`/api/inventory/losses/${loss.id}/post`) if (loss?.id) await ctx.post(`/api/inventory/losses/${loss.id}/post`)
// розничный чек (проведённый) — для /sales/retail
try {
const sale = await (await ctx.post('/api/sales/retail', { data: { date: new Date().toISOString(), storeId, retailPointId: null, customerId: null, currencyId, payment: 0, paidCash: 5000, paidCard: 0, lines: [{ productId: p, quantity: 1, unitPrice: 1234, discount: 0, vatPercent: 0 }] } })).json()
if (sale?.id) await ctx.post(`/api/sales/retail/${sale.id}/post`)
} catch { /* продажа опциональна */ }
// инвентаризация (черновик) — для /inventory/inventories
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 { /* */ }
} }

View file

@ -87,13 +87,20 @@ function extract(route: string, comp: string): Screen | null {
add('export', `экспорт (${m[1]})`, 'export') add('export', `экспорт (${m[1]})`, 'export')
if (!els.some((e) => e.key === 'export') && /<ExportButton\b/.test(src)) add('export', 'экспорт', 'export') if (!els.some((e) => e.key === 'export') && /<ExportButton\b/.test(src)) add('export', 'экспорт', 'export')
// Сортируемые колонки DataTable: sortKey + ближайший header-строка. // Сортируемые колонки DataTable: sortKey + ближайший header-строка (в т.ч. строковый
// фолбэк в выражении header: expr ?? 'Текст').
for (const m of src.matchAll(/sortKey:\s*['"]([^'"]+)['"]/g)) { for (const m of src.matchAll(/sortKey:\s*['"]([^'"]+)['"]/g)) {
const key = m[1] const key = m[1]
// ищем header: '...' в пределах 220 символов вокруг const around = src.slice(Math.max(0, m.index! - 260), m.index! + 60)
const around = src.slice(Math.max(0, m.index! - 220), m.index! + 60) // header: '...' ИЛИ любая строка-литерал в значении header (фолбэк ?? '...')
const h = [...around.matchAll(/header:\s*['"]([^'"]+)['"]/g)].pop() const direct = [...around.matchAll(/header:\s*['"]([^'"]+)['"]/g)].pop()
add(`sort:${key}`, `сортировка: ${h ? h[1] : key}`, 'sort') let label = direct?.[1]
if (!label) {
const hExpr = around.match(/header:\s*([^,]*?),/)
const quoted = hExpr?.[1]?.match(/['"]([^'"]{2,})['"]/)
label = quoted?.[1]
}
add(`sort:${key}`, `сортировка: ${label ?? key}`, 'sort')
} }
// Фильтры списка: <Select>/<select>, у которого обработчик сбрасывает страницу // Фильтры списка: <Select>/<select>, у которого обработчик сбрасывает страницу