From 2b751dc1292a9fafa4b4fb46c8d4abf794e86b2a Mon Sep 17 00:00:00 2001 From: nns Date: Fri, 9 Oct 2026 13:09:46 +0500 Subject: [PATCH] =?UTF-8?q?test(uat):=20=D0=B8=D0=B7=D0=BC=D0=B5=D1=80?= =?UTF-8?q?=D0=B8=D0=BC=D0=BE=D0=B5=20=D0=BF=D0=BE=D0=BA=D1=80=D1=8B=D1=82?= =?UTF-8?q?=D0=B8=D0=B5=20=E2=80=94=20=D0=BF=D1=80=D0=BE=D0=B3=D0=BE=D0=BD?= =?UTF-8?q?=20+=20HTML=20=D1=81=20=D1=86=D0=B8=D1=84=D1=80=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8=20(P1=E2=80=93P3=2080%,=20P4=2037%)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Доработаны хендлеры браузерного прогона (пагинация со сбросом поиска, обобщённое создание/сохранение/удаление, период-фильтры, доп. сид: чек/приёмка/оприходование/ списание/инвентаризация/возврат). Экстрактор ловит строковый фолбэк заголовка колонки. docs/uat-coverage.html — цифры по экрану/приоритету/общим. Цели (P1–P3 ≥95%, P4 ≥70%) пока НЕ достигнуты (переход на ALL MARKET в приоритете) — остаток задокументирован в docs/uat-coverage-progress.md, продолжим после перехода. Co-Authored-By: Claude Opus 5.5 --- docs/uat-coverage-progress.md | 77 + docs/uat-coverage.html | 282 +++ docs/uat-coverage.json | 1835 +++++++++++++++++ tests/e2e/reports/uat-covered.json | 857 ++++++++ .../scenarios/stage-ui-uat-coverage.spec.ts | 72 +- tests/e2e/tools/extract-controls.ts | 17 +- 6 files changed, 3126 insertions(+), 14 deletions(-) create mode 100644 docs/uat-coverage-progress.md create mode 100644 docs/uat-coverage.html create mode 100644 docs/uat-coverage.json create mode 100644 tests/e2e/reports/uat-covered.json diff --git a/docs/uat-coverage-progress.md b/docs/uat-coverage-progress.md new file mode 100644 index 0000000..445e709 --- /dev/null +++ b/docs/uat-coverage-progress.md @@ -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. diff --git a/docs/uat-coverage.html b/docs/uat-coverage.html new file mode 100644 index 0000000..85c8e80 --- /dev/null +++ b/docs/uat-coverage.html @@ -0,0 +1,282 @@ +UAT coverage — ALL MARKET + +

UAT coverage — покрытие каждого элемента каждого экрана

+
Сгенерировано 2026-10-09T08:08:43.726Z · элементы извлечены из кода (web/src/pages), покрытие — браузерный прогон на стенде.
+
+
Всего элементов
195
+
Покрыто
119
+
Общее
61%
+
P1–P3 (цель 95%)
80%
87/109
+
P4 (цель 70%)
37%
32/86
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Пр.Экран%Элементы (зелёные покрыты)
P1Здесь пока пусто
/catalog/products
64%
7/11
✓ search ✓ pagination ✓ export ✓ sort:name ✓ sort:packaging ✓ sort:cost ✗ sort:systemPrice ✗ sort:vat ✗ filter:все ✗ action:delete ✓ action:create
P1Остатки
/inventory/stock
92%
11/12
✓ search ✓ pagination ✓ export ✓ sort:name ✗ sort:store ✓ sort:unit ✓ sort:quantity ✓ sort:reserved ✓ sort:available ✓ filter:все-склады ✓ toggle:показать-нулевые ✓ toggle:только-минусы
P1Розничные продажи
/sales/retail
100%
9/9
✓ search ✓ pagination ✓ export ✓ sort:number ✓ sort:date ✓ sort:status ✓ sort:store ✓ sort:total ✓ action:create
P2Смены кассы
/money/shifts
33%
1/3
✗ pagination ✓ action:print ✗ action:save
P2Приёмки от поставщиков
/purchases/supplies
100%
11/11
✓ search ✓ pagination ✓ export ✓ sort:number ✓ sort:date ✓ sort:status ✓ sort:supplier ✓ sort:store ✓ sort:total ✓ filter:оплата-все ✓ action:create
P3Контрагенты
/catalog/counterparties
89%
8/9
✓ search ✓ pagination ✓ export ✓ sort:name ✓ sort:type ✓ sort:phone ✓ sort:country ✗ action:delete ✓ action:save
P3Проверка данных
/catalog/data-quality
0%
0/1
✗ action:delete
P3Оприходования
/inventory/enters
88%
7/8
✓ search ✓ pagination ✓ sort:number ✓ sort:date ✓ sort:status ✓ sort:store ✓ sort:total ✗ action:create
P3Инвентаризации
/inventory/inventories
100%
6/6
✓ search ✓ pagination ✓ sort:number ✓ sort:date ✓ sort:status ✓ action:create
P3Списания
/inventory/losses
89%
8/9
✓ search ✓ pagination ✓ sort:number ✓ sort:date ✓ sort:status ✓ sort:reason ✓ sort:store ✓ sort:total ✗ action:create
P3Движения
/inventory/movements
88%
7/8
✓ pagination ✓ sort:occurredAt ✓ sort:type ✓ sort:product ✗ sort:store ✓ sort:quantity ✓ sort:documentType ✓ filter:все-склады
P3Отчёт «Деньги»
/money/report
0%
0/1
✗ filter:period
P3Долги поставщикам
/money/supplier-debts
0%
0/2
✗ action:post ✗ action:save
P3Платежи и ордера
/money/transactions
38%
3/8
✓ search ✗ pagination ✓ filter:все-статусы ✓ filter:все-типы ✗ action:post ✗ action:unpost ✗ action:delete ✗ action:save
P3Накладные
/purchases/invoices
100%
1/1
✓ action:unpost
P3Возвраты поставщикам
/purchases/supplier-returns
100%
8/8
✓ search ✓ pagination ✓ sort:number ✓ sort:date ✓ sort:status ✓ sort:supplier ✓ sort:total ✓ action:create
P3Данных для отчёта нет
/reports/sales
0%
0/1
✗ filter:period
P3Остатки пусты
/reports/stock
0%
0/1
✗ filter:period
P4Импорт из МойСклад
/admin/import/moysklad
0%
0/1
✗ action:delete
P4Журнал изменений
/audit-log
0%
0/6
✗ search ✗ pagination ✗ filter:sel1 ✗ filter:sel2 ✗ filter:все ✗ filter:period
P4Типы цен
/catalog/price-types
0%
0/6
✗ search ✗ pagination ✗ sort:name ✗ sort:isRequired ✗ action:delete ✗ action:save
P4Группы товаров
/catalog/product-groups
83%
5/6
✓ search ✓ pagination ✓ sort:name ✓ sort:path ✗ action:delete ✓ action:save
P4Кассы
/catalog/retail-points
89%
8/9
✓ search ✓ pagination ✓ sort:name ✓ sort:code ✓ sort:store ✓ sort:address ✓ sort:isActive ✗ action:delete ✓ action:save
P4Склады
/catalog/stores
89%
8/9
✓ search ✓ pagination ✓ sort:name ✓ sort:code ✓ sort:address ✓ sort:isMain ✓ sort:isActive ✗ action:delete ✓ action:save
P4Единицы измерения
/catalog/units
75%
3/4
✓ search ✓ pagination ✓ sort:name ✗ action:delete
P4Инвентаризация с телефона
/inventory/mobile
0%
0/1
✗ action:post
P4Перемещения
/inventory/transfers
0%
0/7
✗ search ✗ pagination ✗ sort:number ✗ sort:date ✗ sort:status ✗ sort:total ✗ action:create
P4Карты лояльности
/loyalty/cards
0%
0/3
✗ search ✗ pagination ✗ sort:cardNumber
P4Программы лояльности
/loyalty/programs
0%
0/5
✗ search ✗ pagination ✗ sort:name ✗ action:delete ✗ action:save
P4Счета и кассы
/money/accounts
25%
1/4
✓ search ✗ pagination ✗ action:delete ✗ action:save
P4Статьи расходов
/money/expense-items
75%
3/4
✓ search ✓ pagination ✗ action:delete ✓ action:save
P4Акции и промокоды
/promotions
0%
0/4
✗ search ✗ pagination ✗ action:delete ✗ action:save
P4Оптовые отгрузки
/sales/demands
0%
0/8
✗ search ✗ pagination ✗ sort:number ✗ sort:date ✗ sort:status ✗ sort:customer ✗ sort:total ✗ action:create
P4Роли сотрудников
/settings/employee-roles
50%
2/4
✓ search ✓ pagination ✗ action:delete ✗ action:save
P4Сотрудники
/settings/employees
50%
2/4
✓ search ✓ pagination ✗ action:delete ✗ action:save
P4Настройки организации
/settings/organization
0%
0/1
✗ action:delete
\ No newline at end of file diff --git a/docs/uat-coverage.json b/docs/uat-coverage.json new file mode 100644 index 0000000..c6ec6e3 --- /dev/null +++ b/docs/uat-coverage.json @@ -0,0 +1,1835 @@ +{ + "generatedAt": "2026-10-09T08:08:43.723Z", + "summary": { + "overall": 61, + "total": 195, + "done": 119, + "byPriority": { + "P1": { + "pct": 84, + "total": 32, + "done": 27 + }, + "P2": { + "pct": 86, + "total": 14, + "done": 12 + }, + "P3": { + "pct": 76, + "total": 63, + "done": 48 + }, + "P4": { + "pct": 37, + "total": 86, + "done": 32 + } + }, + "targets": { + "P1": 95, + "P2": 95, + "P3": 95, + "P4": 70 + } + }, + "screens": [ + { + "route": "/catalog/products", + "page": "ProductsPage", + "file": "pages/ProductsPage.tsx", + "priority": "P1", + "title": "Здесь пока пусто", + "listUrl": "/api/catalog/products", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": true, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": true, + "how": "пагинация отрисована" + }, + { + "key": "export", + "label": "экспорт (/api/catalog/products/export)", + "type": "export", + "covered": true, + "how": "экспорт→/export&format=" + }, + { + "key": "sort:name", + "label": "сортировка: Название", + "type": "sort", + "covered": true, + "how": "клик «Название»→sort=name" + }, + { + "key": "sort:packaging", + "label": "сортировка: Фасовка", + "type": "sort", + "covered": true, + "how": "клик «Фасовка»→sort=packaging" + }, + { + "key": "sort:cost", + "label": "сортировка: Себестоимость", + "type": "sort", + "covered": true, + "how": "клик «Себестоимость»→sort=cost" + }, + { + "key": "sort:systemPrice", + "label": "сортировка: systemPrice", + "type": "sort", + "covered": false, + "how": "нет заголовка «systemPrice»" + }, + { + "key": "sort:vat", + "label": "сортировка: НДС", + "type": "sort", + "covered": false, + "how": "нет заголовка «НДС»" + }, + { + "key": "filter:все", + "label": "фильтр: все", + "type": "filter", + "covered": false, + "how": "смена фильтра→новый запрос списка" + }, + { + "key": "action:delete", + "label": "действие: удалить", + "type": "action", + "covered": false, + "how": "не удалось удалить" + }, + { + "key": "action:create", + "label": "действие: создать", + "type": "action", + "covered": true, + "how": "переход на создание" + } + ], + "pct": 64, + "done": 7 + }, + { + "route": "/inventory/stock", + "page": "StockPage", + "file": "pages/StockPage.tsx", + "priority": "P1", + "title": "Остатки", + "listUrl": "/api/inventory/stock", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": true, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": true, + "how": "пагинация отрисована" + }, + { + "key": "export", + "label": "экспорт (/api/inventory/stock/export)", + "type": "export", + "covered": true, + "how": "экспорт→/export&format=" + }, + { + "key": "sort:name", + "label": "сортировка: Товар", + "type": "sort", + "covered": true, + "how": "клик «Товар»→sort=name" + }, + { + "key": "sort:store", + "label": "сортировка: Склад", + "type": "sort", + "covered": false, + "how": "клик «Склад»→sort=store" + }, + { + "key": "sort:unit", + "label": "сортировка: Ед.", + "type": "sort", + "covered": true, + "how": "клик «Ед.»→sort=unit" + }, + { + "key": "sort:quantity", + "label": "сортировка: Остаток", + "type": "sort", + "covered": true, + "how": "клик «Остаток»→sort=quantity" + }, + { + "key": "sort:reserved", + "label": "сортировка: Резерв", + "type": "sort", + "covered": true, + "how": "клик «Резерв»→sort=reserved" + }, + { + "key": "sort:available", + "label": "сортировка: Доступно", + "type": "sort", + "covered": true, + "how": "клик «Доступно»→sort=available" + }, + { + "key": "filter:все-склады", + "label": "фильтр: Все склады", + "type": "filter", + "covered": true, + "how": "смена фильтра→новый запрос списка" + }, + { + "key": "toggle:показать-нулевые", + "label": "переключатель: Показать нулевые", + "type": "toggle", + "covered": true, + "how": "тумблер→новый запрос списка" + }, + { + "key": "toggle:только-минусы", + "label": "переключатель: Только минусы", + "type": "toggle", + "covered": true, + "how": "тумблер→новый запрос списка" + } + ], + "pct": 92, + "done": 11 + }, + { + "route": "/sales/retail", + "page": "RetailSalesPage", + "file": "pages/RetailSalesPage.tsx", + "priority": "P1", + "title": "Розничные продажи", + "listUrl": "/api/sales/retail", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": true, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": true, + "how": "пагинация отрисована" + }, + { + "key": "export", + "label": "экспорт (/api/sales/retail/export)", + "type": "export", + "covered": true, + "how": "экспорт→/export&format=" + }, + { + "key": "sort:number", + "label": "сортировка: №", + "type": "sort", + "covered": true, + "how": "клик «№»→sort=number" + }, + { + "key": "sort:date", + "label": "сортировка: Дата/время", + "type": "sort", + "covered": true, + "how": "клик «Дата/время»→sort=date" + }, + { + "key": "sort:status", + "label": "сортировка: Статус", + "type": "sort", + "covered": true, + "how": "клик «Статус»→sort=status" + }, + { + "key": "sort:store", + "label": "сортировка: Магазин", + "type": "sort", + "covered": true, + "how": "клик «Магазин»→sort=store" + }, + { + "key": "sort:total", + "label": "сортировка: Сумма", + "type": "sort", + "covered": true, + "how": "клик «Сумма»→sort=total" + }, + { + "key": "action:create", + "label": "действие: создать", + "type": "action", + "covered": true, + "how": "переход на создание" + } + ], + "pct": 100, + "done": 9 + }, + { + "route": "/money/shifts", + "page": "CashShiftsPage", + "file": "pages/money/CashShiftsPage.tsx", + "priority": "P2", + "title": "Смены кассы", + "listUrl": "/api/money/shifts", + "elements": [ + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": false, + "how": "список пуст — пагинации нет" + }, + { + "key": "action:print", + "label": "действие: печать", + "type": "action", + "covered": true, + "how": "элемент присутствует" + }, + { + "key": "action:save", + "label": "действие: сохранить", + "type": "action", + "covered": false, + "how": "сохранение не удалось" + } + ], + "pct": 33, + "done": 1 + }, + { + "route": "/purchases/supplies", + "page": "SuppliesPage", + "file": "pages/SuppliesPage.tsx", + "priority": "P2", + "title": "Приёмки от поставщиков", + "listUrl": "/api/purchases/supplies", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": true, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": true, + "how": "пагинация отрисована" + }, + { + "key": "export", + "label": "экспорт (/api/purchases/supplies/export)", + "type": "export", + "covered": true, + "how": "экспорт→/export&format=" + }, + { + "key": "sort:number", + "label": "сортировка: №", + "type": "sort", + "covered": true, + "how": "клик «№»→sort=number" + }, + { + "key": "sort:date", + "label": "сортировка: Дата", + "type": "sort", + "covered": true, + "how": "клик «Дата»→sort=date" + }, + { + "key": "sort:status", + "label": "сортировка: Статус", + "type": "sort", + "covered": true, + "how": "клик «Статус»→sort=status" + }, + { + "key": "sort:supplier", + "label": "сортировка: Поставщик", + "type": "sort", + "covered": true, + "how": "клик «Поставщик»→sort=supplier" + }, + { + "key": "sort:store", + "label": "сортировка: Склад", + "type": "sort", + "covered": true, + "how": "клик «Склад»→sort=store" + }, + { + "key": "sort:total", + "label": "сортировка: Сумма", + "type": "sort", + "covered": true, + "how": "клик «Сумма»→sort=total" + }, + { + "key": "filter:оплата-все", + "label": "фильтр: Оплата: все", + "type": "filter", + "covered": true, + "how": "смена фильтра→новый запрос списка" + }, + { + "key": "action:create", + "label": "действие: создать", + "type": "action", + "covered": true, + "how": "открылась форма" + } + ], + "pct": 100, + "done": 11 + }, + { + "route": "/catalog/counterparties", + "page": "CounterpartiesPage", + "file": "pages/CounterpartiesPage.tsx", + "priority": "P3", + "title": "Контрагенты", + "listUrl": "/api/catalog/counterparties", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": true, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": true, + "how": "пагинация отрисована" + }, + { + "key": "export", + "label": "экспорт (/api/catalog/counterparties/export)", + "type": "export", + "covered": true, + "how": "экспорт→/export&format=" + }, + { + "key": "sort:name", + "label": "сортировка: Название", + "type": "sort", + "covered": true, + "how": "клик «Название»→sort=name" + }, + { + "key": "sort:type", + "label": "сортировка: Тип", + "type": "sort", + "covered": true, + "how": "клик «Тип»→sort=type" + }, + { + "key": "sort:phone", + "label": "сортировка: Телефон", + "type": "sort", + "covered": true, + "how": "клик «Телефон»→sort=phone" + }, + { + "key": "sort:country", + "label": "сортировка: Страна", + "type": "sort", + "covered": true, + "how": "клик «Страна»→sort=country" + }, + { + "key": "action:delete", + "label": "действие: удалить", + "type": "action", + "covered": false, + "how": "не удалось удалить" + }, + { + "key": "action:save", + "label": "действие: сохранить", + "type": "action", + "covered": true, + "how": "создание→POST 2xx" + } + ], + "pct": 89, + "done": 8 + }, + { + "route": "/catalog/data-quality", + "page": "DataQualityPage", + "file": "pages/DataQualityPage.tsx", + "priority": "P3", + "title": "Проверка данных", + "listUrl": "", + "elements": [ + { + "key": "action:delete", + "label": "действие: удалить", + "type": "action", + "covered": false, + "how": "не удалось удалить" + } + ], + "pct": 0, + "done": 0 + }, + { + "route": "/inventory/enters", + "page": "EntersPage", + "file": "pages/EntersPage.tsx", + "priority": "P3", + "title": "Оприходования", + "listUrl": "/api/inventory/enters", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": true, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": true, + "how": "пагинация отрисована" + }, + { + "key": "sort:number", + "label": "сортировка: №", + "type": "sort", + "covered": true, + "how": "клик «№»→sort=number" + }, + { + "key": "sort:date", + "label": "сортировка: Дата", + "type": "sort", + "covered": true, + "how": "клик «Дата»→sort=date" + }, + { + "key": "sort:status", + "label": "сортировка: Статус", + "type": "sort", + "covered": true, + "how": "клик «Статус»→sort=status" + }, + { + "key": "sort:store", + "label": "сортировка: Склад", + "type": "sort", + "covered": true, + "how": "клик «Склад»→sort=store" + }, + { + "key": "sort:total", + "label": "сортировка: Сумма", + "type": "sort", + "covered": true, + "how": "клик «Сумма»→sort=total" + }, + { + "key": "action:create", + "label": "действие: создать", + "type": "action", + "covered": false, + "how": "не открылось" + } + ], + "pct": 88, + "done": 7 + }, + { + "route": "/inventory/inventories", + "page": "InventoriesPage", + "file": "pages/InventoriesPage.tsx", + "priority": "P3", + "title": "Инвентаризации", + "listUrl": "/api/inventory/inventories", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": true, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": true, + "how": "пагинация отрисована" + }, + { + "key": "sort:number", + "label": "сортировка: №", + "type": "sort", + "covered": true, + "how": "клик «№»→sort=number" + }, + { + "key": "sort:date", + "label": "сортировка: Дата", + "type": "sort", + "covered": true, + "how": "клик «Дата»→sort=date" + }, + { + "key": "sort:status", + "label": "сортировка: Статус", + "type": "sort", + "covered": true, + "how": "клик «Статус»→sort=status" + }, + { + "key": "action:create", + "label": "действие: создать", + "type": "action", + "covered": true, + "how": "открылась форма" + } + ], + "pct": 100, + "done": 6 + }, + { + "route": "/inventory/losses", + "page": "LossesPage", + "file": "pages/LossesPage.tsx", + "priority": "P3", + "title": "Списания", + "listUrl": "/api/inventory/losses", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": true, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": true, + "how": "пагинация отрисована" + }, + { + "key": "sort:number", + "label": "сортировка: №", + "type": "sort", + "covered": true, + "how": "клик «№»→sort=number" + }, + { + "key": "sort:date", + "label": "сортировка: Дата", + "type": "sort", + "covered": true, + "how": "клик «Дата»→sort=date" + }, + { + "key": "sort:status", + "label": "сортировка: Статус", + "type": "sort", + "covered": true, + "how": "клик «Статус»→sort=status" + }, + { + "key": "sort:reason", + "label": "сортировка: Причина", + "type": "sort", + "covered": true, + "how": "клик «Причина»→sort=reason" + }, + { + "key": "sort:store", + "label": "сортировка: Склад", + "type": "sort", + "covered": true, + "how": "клик «Склад»→sort=store" + }, + { + "key": "sort:total", + "label": "сортировка: Сумма", + "type": "sort", + "covered": true, + "how": "клик «Сумма»→sort=total" + }, + { + "key": "action:create", + "label": "действие: создать", + "type": "action", + "covered": false, + "how": "не открылось" + } + ], + "pct": 89, + "done": 8 + }, + { + "route": "/inventory/movements", + "page": "StockMovementsPage", + "file": "pages/StockMovementsPage.tsx", + "priority": "P3", + "title": "Движения", + "listUrl": "/api/inventory/movements", + "elements": [ + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": true, + "how": "пагинация отрисована" + }, + { + "key": "sort:occurredAt", + "label": "сортировка: Дата", + "type": "sort", + "covered": true, + "how": "клик «Дата»→sort=occurredAt" + }, + { + "key": "sort:type", + "label": "сортировка: Операция", + "type": "sort", + "covered": true, + "how": "клик «Операция»→sort=type" + }, + { + "key": "sort:product", + "label": "сортировка: Товар", + "type": "sort", + "covered": true, + "how": "клик «Товар»→sort=product" + }, + { + "key": "sort:store", + "label": "сортировка: Склад", + "type": "sort", + "covered": false, + "how": "клик «Склад»→sort=store" + }, + { + "key": "sort:quantity", + "label": "сортировка: Количество", + "type": "sort", + "covered": true, + "how": "клик «Количество»→sort=quantity" + }, + { + "key": "sort:documentType", + "label": "сортировка: Документ", + "type": "sort", + "covered": true, + "how": "клик «Документ»→sort=documentType" + }, + { + "key": "filter:все-склады", + "label": "фильтр: Все склады", + "type": "filter", + "covered": true, + "how": "смена фильтра→новый запрос списка" + } + ], + "pct": 88, + "done": 7 + }, + { + "route": "/money/report", + "page": "MoneyReportPage", + "file": "pages/money/MoneyReportPage.tsx", + "priority": "P3", + "title": "Отчёт «Деньги»", + "listUrl": "/api/money/reports/balances", + "elements": [ + { + "key": "filter:period", + "label": "фильтр: период/дата", + "type": "filter", + "covered": false, + "how": "период не изменил выдачу" + } + ], + "pct": 0, + "done": 0 + }, + { + "route": "/money/supplier-debts", + "page": "SupplierDebtsPage", + "file": "pages/money/SupplierDebtsPage.tsx", + "priority": "P3", + "title": "Долги поставщикам", + "listUrl": "/api/money/reports/supplier-debts", + "elements": [ + { + "key": "action:post", + "label": "действие: провести", + "type": "action", + "covered": false, + "how": "не найден здесь" + }, + { + "key": "action:save", + "label": "действие: сохранить", + "type": "action", + "covered": false, + "how": "сохранение не удалось" + } + ], + "pct": 0, + "done": 0 + }, + { + "route": "/money/transactions", + "page": "MoneyTransactionsPage", + "file": "pages/money/MoneyTransactionsPage.tsx", + "priority": "P3", + "title": "Платежи и ордера", + "listUrl": "/api/money/transactions", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": true, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": false, + "how": "список пуст — пагинации нет" + }, + { + "key": "filter:все-статусы", + "label": "фильтр: Все статусы", + "type": "filter", + "covered": true, + "how": "смена фильтра→новый запрос списка" + }, + { + "key": "filter:все-типы", + "label": "фильтр: Все типы", + "type": "filter", + "covered": true, + "how": "смена фильтра→новый запрос списка" + }, + { + "key": "action:post", + "label": "действие: провести", + "type": "action", + "covered": false, + "how": "не найден здесь" + }, + { + "key": "action:unpost", + "label": "действие: снять проведение", + "type": "action", + "covered": false, + "how": "не найден здесь" + }, + { + "key": "action:delete", + "label": "действие: удалить", + "type": "action", + "covered": false, + "how": "не удалось удалить" + }, + { + "key": "action:save", + "label": "действие: сохранить", + "type": "action", + "covered": false, + "how": "сохранение не удалось" + } + ], + "pct": 38, + "done": 3 + }, + { + "route": "/purchases/invoices", + "page": "InvoicesPage", + "file": "pages/InvoicesPage.tsx", + "priority": "P3", + "title": "Накладные", + "listUrl": "/api/purchases/invoices", + "elements": [ + { + "key": "action:unpost", + "label": "действие: снять проведение", + "type": "action", + "covered": true, + "how": "элемент присутствует" + } + ], + "pct": 100, + "done": 1 + }, + { + "route": "/purchases/supplier-returns", + "page": "SupplierReturnsPage", + "file": "pages/SupplierReturnsPage.tsx", + "priority": "P3", + "title": "Возвраты поставщикам", + "listUrl": "/api/purchases/supplier-returns", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": true, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": true, + "how": "пагинация отрисована" + }, + { + "key": "sort:number", + "label": "сортировка: №", + "type": "sort", + "covered": true, + "how": "клик «№»→sort=number" + }, + { + "key": "sort:date", + "label": "сортировка: Дата", + "type": "sort", + "covered": true, + "how": "клик «Дата»→sort=date" + }, + { + "key": "sort:status", + "label": "сортировка: Статус", + "type": "sort", + "covered": true, + "how": "клик «Статус»→sort=status" + }, + { + "key": "sort:supplier", + "label": "сортировка: Поставщик", + "type": "sort", + "covered": true, + "how": "клик «Поставщик»→sort=supplier" + }, + { + "key": "sort:total", + "label": "сортировка: Сумма", + "type": "sort", + "covered": true, + "how": "клик «Сумма»→sort=total" + }, + { + "key": "action:create", + "label": "действие: создать", + "type": "action", + "covered": true, + "how": "переход на создание" + } + ], + "pct": 100, + "done": 8 + }, + { + "route": "/reports/sales", + "page": "SalesReportPage", + "file": "pages/SalesReportPage.tsx", + "priority": "P3", + "title": "Данных для отчёта нет", + "listUrl": "/api/reports/sales", + "elements": [ + { + "key": "filter:period", + "label": "фильтр: период/дата", + "type": "filter", + "covered": false, + "how": "период не изменил выдачу" + } + ], + "pct": 0, + "done": 0 + }, + { + "route": "/reports/stock", + "page": "StockReportPage", + "file": "pages/StockReportPage.tsx", + "priority": "P3", + "title": "Остатки пусты", + "listUrl": "/api/reports/stock", + "elements": [ + { + "key": "filter:period", + "label": "фильтр: период/дата", + "type": "filter", + "covered": false, + "how": "период не изменил выдачу" + } + ], + "pct": 0, + "done": 0 + }, + { + "route": "/admin/import/moysklad", + "page": "MoySkladImportPage", + "file": "pages/MoySkladImportPage.tsx", + "priority": "P4", + "title": "Импорт из МойСклад", + "listUrl": "/api/admin/jobs/", + "elements": [ + { + "key": "action:delete", + "label": "действие: удалить", + "type": "action", + "covered": false, + "how": "не удалось удалить" + } + ], + "pct": 0, + "done": 0 + }, + { + "route": "/audit-log", + "page": "OrgAuditLogPage", + "file": "pages/OrgAuditLogPage.tsx", + "priority": "P4", + "title": "Журнал изменений", + "listUrl": "/api/organization/employees", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": false, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": false, + "how": "список пуст — пагинации нет" + }, + { + "key": "filter:sel1", + "label": "фильтр: фильтр 1", + "type": "filter", + "covered": false, + "how": "смена фильтра→новый запрос списка" + }, + { + "key": "filter:sel2", + "label": "фильтр: фильтр 2", + "type": "filter", + "covered": false, + "how": "смена фильтра→новый запрос списка" + }, + { + "key": "filter:все", + "label": "фильтр: Все", + "type": "filter", + "covered": false, + "how": "смена фильтра→новый запрос списка" + }, + { + "key": "filter:period", + "label": "фильтр: период/дата", + "type": "filter", + "covered": false, + "how": "период не изменил выдачу" + } + ], + "pct": 0, + "done": 0 + }, + { + "route": "/catalog/price-types", + "page": "PriceTypesPage", + "file": "pages/PriceTypesPage.tsx", + "priority": "P4", + "title": "Типы цен", + "listUrl": "/api/catalog/price-types", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": false, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": false, + "how": "список пуст — пагинации нет" + }, + { + "key": "sort:name", + "label": "сортировка: Название", + "type": "sort", + "covered": false, + "how": "нет заголовка «Название»" + }, + { + "key": "sort:isRequired", + "label": "сортировка: Обязательно", + "type": "sort", + "covered": false, + "how": "нет заголовка «Обязательно»" + }, + { + "key": "action:delete", + "label": "действие: удалить", + "type": "action", + "covered": false, + "how": "не удалось удалить" + }, + { + "key": "action:save", + "label": "действие: сохранить", + "type": "action", + "covered": false, + "how": "сохранение не удалось" + } + ], + "pct": 0, + "done": 0 + }, + { + "route": "/catalog/product-groups", + "page": "ProductGroupsPage", + "file": "pages/ProductGroupsPage.tsx", + "priority": "P4", + "title": "Группы товаров", + "listUrl": "/api/catalog/product-groups", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": true, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": true, + "how": "пагинация отрисована" + }, + { + "key": "sort:name", + "label": "сортировка: Название", + "type": "sort", + "covered": true, + "how": "клик «Название»→sort=name" + }, + { + "key": "sort:path", + "label": "сортировка: Путь", + "type": "sort", + "covered": true, + "how": "клик «Путь»→sort=path" + }, + { + "key": "action:delete", + "label": "действие: удалить", + "type": "action", + "covered": false, + "how": "не удалось удалить" + }, + { + "key": "action:save", + "label": "действие: сохранить", + "type": "action", + "covered": true, + "how": "создание→POST 2xx" + } + ], + "pct": 83, + "done": 5 + }, + { + "route": "/catalog/retail-points", + "page": "RetailPointsPage", + "file": "pages/RetailPointsPage.tsx", + "priority": "P4", + "title": "Кассы", + "listUrl": "/api/catalog/retail-points", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": true, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": true, + "how": "пагинация отрисована" + }, + { + "key": "sort:name", + "label": "сортировка: Название", + "type": "sort", + "covered": true, + "how": "клик «Название»→sort=name" + }, + { + "key": "sort:code", + "label": "сортировка: Код", + "type": "sort", + "covered": true, + "how": "клик «Код»→sort=code" + }, + { + "key": "sort:store", + "label": "сортировка: Склад", + "type": "sort", + "covered": true, + "how": "клик «Склад»→sort=store" + }, + { + "key": "sort:address", + "label": "сортировка: Адрес", + "type": "sort", + "covered": true, + "how": "клик «Адрес»→sort=address" + }, + { + "key": "sort:isActive", + "label": "сортировка: Активна", + "type": "sort", + "covered": true, + "how": "клик «Активна»→sort=isActive" + }, + { + "key": "action:delete", + "label": "действие: удалить", + "type": "action", + "covered": false, + "how": "не удалось удалить" + }, + { + "key": "action:save", + "label": "действие: сохранить", + "type": "action", + "covered": true, + "how": "создание→POST 2xx" + } + ], + "pct": 89, + "done": 8 + }, + { + "route": "/catalog/stores", + "page": "StoresPage", + "file": "pages/StoresPage.tsx", + "priority": "P4", + "title": "Склады", + "listUrl": "/api/catalog/stores", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": true, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": true, + "how": "пагинация отрисована" + }, + { + "key": "sort:name", + "label": "сортировка: Название", + "type": "sort", + "covered": true, + "how": "клик «Название»→sort=name" + }, + { + "key": "sort:code", + "label": "сортировка: Код", + "type": "sort", + "covered": true, + "how": "клик «Код»→sort=code" + }, + { + "key": "sort:address", + "label": "сортировка: Адрес", + "type": "sort", + "covered": true, + "how": "клик «Адрес»→sort=address" + }, + { + "key": "sort:isMain", + "label": "сортировка: Основной", + "type": "sort", + "covered": true, + "how": "клик «Основной»→sort=isMain" + }, + { + "key": "sort:isActive", + "label": "сортировка: Активен", + "type": "sort", + "covered": true, + "how": "клик «Активен»→sort=isActive" + }, + { + "key": "action:delete", + "label": "действие: удалить", + "type": "action", + "covered": false, + "how": "не удалось удалить" + }, + { + "key": "action:save", + "label": "действие: сохранить", + "type": "action", + "covered": true, + "how": "создание→POST 2xx" + } + ], + "pct": 89, + "done": 8 + }, + { + "route": "/catalog/units", + "page": "UnitsOfMeasurePage", + "file": "pages/UnitsOfMeasurePage.tsx", + "priority": "P4", + "title": "Единицы измерения", + "listUrl": "/api/catalog/units-of-measure", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": true, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": true, + "how": "пагинация отрисована" + }, + { + "key": "sort:name", + "label": "сортировка: Название", + "type": "sort", + "covered": true, + "how": "клик «Название»→sort=name" + }, + { + "key": "action:delete", + "label": "действие: удалить", + "type": "action", + "covered": false, + "how": "не удалось удалить" + } + ], + "pct": 75, + "done": 3 + }, + { + "route": "/inventory/mobile", + "page": "InventoryMobilePage", + "file": "pages/InventoryMobilePage.tsx", + "priority": "P4", + "title": "Инвентаризация с телефона", + "listUrl": "/api/catalog/stores", + "elements": [ + { + "key": "action:post", + "label": "действие: провести", + "type": "action", + "covered": false, + "how": "не найден здесь" + } + ], + "pct": 0, + "done": 0 + }, + { + "route": "/inventory/transfers", + "page": "TransfersPage", + "file": "pages/TransfersPage.tsx", + "priority": "P4", + "title": "Перемещения", + "listUrl": "/api/inventory/transfers", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": false, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": false, + "how": "список пуст — пагинации нет" + }, + { + "key": "sort:number", + "label": "сортировка: №", + "type": "sort", + "covered": false, + "how": "нет заголовка «№»" + }, + { + "key": "sort:date", + "label": "сортировка: Дата", + "type": "sort", + "covered": false, + "how": "нет заголовка «Дата»" + }, + { + "key": "sort:status", + "label": "сортировка: Статус", + "type": "sort", + "covered": false, + "how": "нет заголовка «Статус»" + }, + { + "key": "sort:total", + "label": "сортировка: Сумма", + "type": "sort", + "covered": false, + "how": "нет заголовка «Сумма»" + }, + { + "key": "action:create", + "label": "действие: создать", + "type": "action", + "covered": false, + "how": "не открылось" + } + ], + "pct": 0, + "done": 0 + }, + { + "route": "/loyalty/cards", + "page": "LoyaltyCardsPage", + "file": "pages/LoyaltyCardsPage.tsx", + "priority": "P4", + "title": "Карты лояльности", + "listUrl": "/api/loyalty/cards", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": false, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": false, + "how": "список пуст — пагинации нет" + }, + { + "key": "sort:cardNumber", + "label": "сортировка: Номер", + "type": "sort", + "covered": false, + "how": "нет заголовка «Номер»" + } + ], + "pct": 0, + "done": 0 + }, + { + "route": "/loyalty/programs", + "page": "LoyaltyProgramsPage", + "file": "pages/LoyaltyProgramsPage.tsx", + "priority": "P4", + "title": "Программы лояльности", + "listUrl": "/api/loyalty/programs", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": false, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": false, + "how": "список пуст — пагинации нет" + }, + { + "key": "sort:name", + "label": "сортировка: Название", + "type": "sort", + "covered": false, + "how": "нет заголовка «Название»" + }, + { + "key": "action:delete", + "label": "действие: удалить", + "type": "action", + "covered": false, + "how": "не удалось удалить" + }, + { + "key": "action:save", + "label": "действие: сохранить", + "type": "action", + "covered": false, + "how": "сохранение не удалось" + } + ], + "pct": 0, + "done": 0 + }, + { + "route": "/money/accounts", + "page": "MoneyAccountsPage", + "file": "pages/money/MoneyAccountsPage.tsx", + "priority": "P4", + "title": "Счета и кассы", + "listUrl": "/api/money/accounts", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": true, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": false, + "how": "список пуст — пагинации нет" + }, + { + "key": "action:delete", + "label": "действие: удалить", + "type": "action", + "covered": false, + "how": "не удалось удалить" + }, + { + "key": "action:save", + "label": "действие: сохранить", + "type": "action", + "covered": false, + "how": "сохранение не удалось" + } + ], + "pct": 25, + "done": 1 + }, + { + "route": "/money/expense-items", + "page": "ExpenseItemsPage", + "file": "pages/money/ExpenseItemsPage.tsx", + "priority": "P4", + "title": "Статьи расходов", + "listUrl": "/api/money/expense-items", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": true, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": true, + "how": "пагинация отрисована" + }, + { + "key": "action:delete", + "label": "действие: удалить", + "type": "action", + "covered": false, + "how": "не удалось удалить" + }, + { + "key": "action:save", + "label": "действие: сохранить", + "type": "action", + "covered": true, + "how": "создание→POST 2xx" + } + ], + "pct": 75, + "done": 3 + }, + { + "route": "/promotions", + "page": "PromotionsPage", + "file": "pages/PromotionsPage.tsx", + "priority": "P4", + "title": "Акции и промокоды", + "listUrl": "/api/promotions", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": false, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": false, + "how": "список пуст — пагинации нет" + }, + { + "key": "action:delete", + "label": "действие: удалить", + "type": "action", + "covered": false, + "how": "не удалось удалить" + }, + { + "key": "action:save", + "label": "действие: сохранить", + "type": "action", + "covered": false, + "how": "сохранение не удалось" + } + ], + "pct": 0, + "done": 0 + }, + { + "route": "/sales/demands", + "page": "DemandsPage", + "file": "pages/DemandsPage.tsx", + "priority": "P4", + "title": "Оптовые отгрузки", + "listUrl": "/api/sales/demands", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": false, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": false, + "how": "список пуст — пагинации нет" + }, + { + "key": "sort:number", + "label": "сортировка: №", + "type": "sort", + "covered": false, + "how": "нет заголовка «№»" + }, + { + "key": "sort:date", + "label": "сортировка: Дата", + "type": "sort", + "covered": false, + "how": "нет заголовка «Дата»" + }, + { + "key": "sort:status", + "label": "сортировка: Статус", + "type": "sort", + "covered": false, + "how": "нет заголовка «Статус»" + }, + { + "key": "sort:customer", + "label": "сортировка: Контрагент", + "type": "sort", + "covered": false, + "how": "нет заголовка «Контрагент»" + }, + { + "key": "sort:total", + "label": "сортировка: Сумма", + "type": "sort", + "covered": false, + "how": "нет заголовка «Сумма»" + }, + { + "key": "action:create", + "label": "действие: создать", + "type": "action", + "covered": false, + "how": "не открылось" + } + ], + "pct": 0, + "done": 0 + }, + { + "route": "/settings/employee-roles", + "page": "EmployeeRolesPage", + "file": "pages/EmployeeRolesPage.tsx", + "priority": "P4", + "title": "Роли сотрудников", + "listUrl": "/api/organization/employee-roles", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": true, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": true, + "how": "пагинация отрисована" + }, + { + "key": "action:delete", + "label": "действие: удалить", + "type": "action", + "covered": false, + "how": "не удалось удалить" + }, + { + "key": "action:save", + "label": "действие: сохранить", + "type": "action", + "covered": false, + "how": "сохранение не удалось" + } + ], + "pct": 50, + "done": 2 + }, + { + "route": "/settings/employees", + "page": "EmployeesPage", + "file": "pages/EmployeesPage.tsx", + "priority": "P4", + "title": "Сотрудники", + "listUrl": "/api/organization/employees", + "elements": [ + { + "key": "search", + "label": "поиск", + "type": "search", + "covered": true, + "how": "поиск→search=" + }, + { + "key": "pagination", + "label": "пагинация", + "type": "pagination", + "covered": true, + "how": "пагинация отрисована" + }, + { + "key": "action:delete", + "label": "действие: удалить", + "type": "action", + "covered": false, + "how": "не удалось удалить" + }, + { + "key": "action:save", + "label": "действие: сохранить", + "type": "action", + "covered": false, + "how": "сохранение не удалось" + } + ], + "pct": 50, + "done": 2 + }, + { + "route": "/settings/organization", + "page": "OrganizationSettingsPage", + "file": "pages/OrganizationSettingsPage.tsx", + "priority": "P4", + "title": "Настройки организации", + "listUrl": "/api/organization/telegram/status", + "elements": [ + { + "key": "action:delete", + "label": "действие: удалить", + "type": "action", + "covered": false, + "how": "не удалось удалить" + } + ], + "pct": 0, + "done": 0 + } + ] +} \ No newline at end of file diff --git a/tests/e2e/reports/uat-covered.json b/tests/e2e/reports/uat-covered.json new file mode 100644 index 0000000..ed29f34 --- /dev/null +++ b/tests/e2e/reports/uat-covered.json @@ -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": "не удалось удалить" + } + } + } +} \ No newline at end of file diff --git a/tests/e2e/scenarios/stage-ui-uat-coverage.spec.ts b/tests/e2e/scenarios/stage-ui-uat-coverage.spec.ts index 2bb061a..432f2b9 100644 --- a/tests/e2e/scenarios/stage-ui-uat-coverage.spec.ts +++ b/tests/e2e/scenarios/stage-ui-uat-coverage.spec.ts @@ -88,6 +88,8 @@ test.describe('UAT coverage (измеримое)', () => { if (await btn.count() === 0) { mark(s.route, el.key, false, `нет заголовка «${header}»`); continue } mark(s.route, el.key, await fires(() => btn.click(), list, `sort=${sortKey}`), `клик «${header}»→sort=${sortKey}`) } else if (el.key === 'pagination') { + // Чистим поиск, чтобы показать полный список (пагинация есть при total>0). + if (hasSearch) { try { await searchBox().fill(''); await page.waitForTimeout(900) } catch { /* */ } } const pager = page.getByText(/из \d+/).first() const present = await pager.count() > 0 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)) { 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') { 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') { // Кастомный Select: открыть по тексту первого варианта из метки, выбрать 2-й пункт. 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(() => {}) 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 { - // action:delete/save/post/unpost/print — покрываются спец-спеком controls; - // здесь отметим наличие кнопки (presence), полноценное действие — отдельно. + // action:post/unpost/print — наличие управляющего элемента на экране. const names: Record = { - 'action:delete': /Удалить/, 'action:save': /Сохранить|Создать/, 'action:post': /Провести|Проведено/, 'action:unpost': /Снять|Отменить проведение/, 'action:print': /Печать|Z-отчёт|Печатать/, } const re = names[el.key] const present = re ? (await page.getByRole('button', { name: re }).count() > 0 || await page.getByText(re).count() > 0) : false - mark(s.route, el.key, present, present ? 'кнопка присутствует' : 'кнопка не найдена здесь') + mark(s.route, el.key, present, present ? 'элемент присутствует' : 'не найден здесь') } } catch (e: any) { 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 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) - // приёмка (проведённая) + черновик - 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() - await ctx.post(`/api/purchases/supplies/${sup.id}/post`) + // приёмка (проведённая) + черновик — даёт остаток и движения + 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() + 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 }] } }) - // оприходование и списание (проведённые) — чтобы таблицы были не пустые + // оприходование и списание (проведённые) 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`) 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`) + // розничный чек (проведённый) — для /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 { /* */ } } diff --git a/tests/e2e/tools/extract-controls.ts b/tests/e2e/tools/extract-controls.ts index 550930f..ee0b7cf 100644 --- a/tests/e2e/tools/extract-controls.ts +++ b/tests/e2e/tools/extract-controls.ts @@ -87,13 +87,20 @@ function extract(route: string, comp: string): Screen | null { add('export', `экспорт (${m[1]})`, 'export') if (!els.some((e) => e.key === 'export') && //