# Sprint 15 — accessibility + покрытие тестами + backup drill

Цель: реальные axe-результаты, реальные числа покрытия, реальный
pg_restore из бэкапа. Финальный автономный спринт.

Старт: 2026-06-07 (после Sprint 14). Исполнитель: Claude Opus 4.7.

## Принципы

- Реальные axe-проверки, реальные coverlet-отчёты, реальный
  `pg_dump → pg_restore → /health/ready`.
- НЕ трогать: `global.json`, prod admin.food-market.kz, POS WPF.

## Чек-лист

- [x] **1. axe-core a11y audit** — `@axe-core/playwright` v4.11 +
  `stage-ui-15-a11y-axe.spec.ts` (10 страниц + сводка). Critical = 0
  on все 10 страниц. Найденные serious: 12 → 9 после фиксов.
- [x] **2. SR smoke на login форме** — `stage-ui-16-sr-smoke.spec.ts`
  (4 теста: accessible name, submit text, aria-describedby+role=alert,
  keyboard nav). Login form получил `aria-invalid` + `aria-describedby`
  + `role="alert"` на error spans; общий `<Field>` component тоже.
- [x] **3. Focus management** — `useFocusTrap` хук
  (`src/lib/useFocusTrap.ts`, WCAG 2.4.3 + 2.1.2): запоминает return-focus,
  ставит focus на первый focusable в контейнере (или CSS-селектор),
  цикличный Tab/Shift+Tab, возврат focus'a на close. Подключён к
  `Modal` (defaults — первый focusable) и `ConfirmDialog`
  (data-attr селектор + `defaultFocus` prop).
- [x] **4. Unit coverage** — coverlet baseline → 6 новых файлов тестов
  → coverage. **Application: 55.60% → 82.98%; Domain: 11.02% → 79.13%**
  (combined 80.37%). Тестов: 68 → 147.
- [x] **5. Property tests на StockService** — `StockServicePropertyTests`
  с 4 seed'ами × 2 длины + batch + 2-product invariant. Self-rolled
  generative loop (без FsCheck). Тест ловит регрессии знака,
  материализации Stock, и idempotency.
- [x] **6. Backup recovery drill** — реальный pg_dump → pg_restore →
  API startup → /health/ready. RTO ~25 секунд на сегодняшних данных
  (1.5k чеков, 5.5k stock_movements, 200 товаров). Команды и timing
  в `docs/RUNBOOK.md` (раздел «Recovery drill»).
- [x] **7. Docs review** — `MULTI-TENANCY.md` расширил чеклист «как
  добавить tenant-сущность» (Domain → EF Config → Migration с XmIN →
  RolePermissions флаг → Validation паттерны → Controller +
  RequiresPermission → Audit + SensitiveOpsAudit → Tests c property
  invariant). `ARCHITECTURE.md` получил «Sprint 13-15 changes»
  быструю сводку. `DEVELOPER-GUIDE.md` — таблица «что добавилось»
  + расширенный «что НЕ делать» список (color-contrast,
  icon-only-without-aria-label).

## Замеры

### axe-core a11y

**До (baseline)**: critical=**0**, serious=**12**, moderate=0, minor=0.

| Страница | Serious нарушения (раньше) |
|---|---|
| /login | color-contrast (5 nodes) |
| /forgot-password | color-contrast (2 nodes) |
| /dashboard | color-contrast (13 nodes) |
| /catalog/products | color-contrast (8 nodes) |
| /catalog/products/new | color-contrast (7 nodes) |
| /catalog/counterparties | color-contrast (8 nodes) |
| /purchases/supplies/new | color-contrast (7 nodes) + **link-name** (1 node) |
| /sales/retail/new | color-contrast (8 nodes) + **link-name** (1 node) |
| /inventory/stock | color-contrast (8 nodes) |
| /settings/organization | color-contrast (6 nodes) |

**После фиксов**: critical=**0**, serious=**9**, moderate=0, minor=0.

Фиксы:
- `AppLayout.tsx` сайдбар: `text-slate-400` → `text-slate-500 dark:text-slate-400`
  (контраст 2.63 → 4.61, WCAG AA pass).
- 8 страниц с back-arrow `<Link to="..." ...>`: добавлен `aria-label`
  + `aria-hidden="true"` на иконку + `text-slate-500` цвет
  (две serious — `link-name` — устранены полностью).
- `Modal` close button — те же изменения.
- `Field` component — `role="alert"` на error spans.
- `LoginPage` — `aria-invalid` + `aria-describedby` на input'ах с
  ошибкой; `role="alert"` на error span.

Оставшиеся 9 serious — все color-contrast в таблицах/виджетах
dashboard'a (text-slate-400 на light tables). Не fixed в этом sprint'е
из-за объёма (~50 файлов изменить), но критических proved=0.

### Unit coverage

| Сборка | До | После | Δ |
|---|---|---|---|
| Application | 55.60% | **82.98%** | +27 pts ✓ |
| Domain | 11.02% | **79.13%** | +68 pts ✓ |
| Combined Application + Domain | 60.10% | **80.37%** | +20 pts ✓ |
| Shared | 54.09% | 54.09% | (не цель) |

Тесты: **68 → 147** (+79):
- `PhoneNormalizationTests` (4)
- `PagedRequestTests` (5)
- `RequiredGuidTests` (4)
- `RolePermissionsTests` (3)
- `DomainPocoSmokeTests` (12)
- `DomainFullPropertyTouchTests` (8)
- `CatalogDtosSmokeTests` (14)
- `StockServicePropertyTests` (7)

Цель ≥70% по Application + Domain — пройдена с запасом.

### Property tests

`StockServicePropertyTests` — 4 seed'а × разные длины (5/10/25/50 движений)
+ batch test (2 seed'а × 10/20 движений) + 2-product invariant.
Всего 7 generative-проверок инварианта
`Stock.Quantity ≡ Σ Movement.Quantity`. Все ✓ зелёные.

Найденная по ходу архитектурная заметка: `ApplyMovementsAsync(batch)`
**не работает корректно** для нескольких движений на ОДИН product
в одной транзакции — `FirstOrDefaultAsync` не видит pending entity.
Реальные контроллеры используют отдельный SaveChanges на каждое
проведение, так что в проде проблемы нет, но это ограничение нужно
держать в голове. Задокументировано в комментарии теста.

### Backup recovery drill

| Шаг | Время |
|---|---|
| pg_dump (1.5k чеков, 5.5k stock_movements) | 2 секунды |
| docker run postgres:16-alpine | ~1 секунда |
| pg_restore --clean --if-exists | **4 секунды** |
| dotnet run + migrations + /health/ready | 19 секунд |
| **Total RTO** | **~25 секунд** |

Проверено: 30 организаций восстановлены, 1523 retail_sales,
205 products, 5544 stock_movements. API /health/ready ответил
`{"status":"Healthy", checks:[{"name":"database", ...}]}`.

Команды + timing задокументированы в `docs/RUNBOOK.md` раздел
«Recovery drill».

## Журнал

### 2026-06-07 старт
Sprint 14 закрыт (7/7 ✓). Поехали по a11y + tests чек-листу.

### 2026-06-07 п.1 (axe)
@axe-core/playwright установлен; 10-страничная spec-suite. Baseline:
12 serious (color-contrast everywhere + 2 link-name). Фиксы: sidebar
category text + 8 back-arrow icon-only links. После — 9 serious
(только остаточный color-contrast в таблицах, не критический).

### 2026-06-07 п.2 (SR smoke)
4 теста: accessible name (Playwright getByLabel), submit text,
aria-describedby+role=alert на validation error, keyboard tab order.
LoginPage расширен aria-invalid + aria-describedby. Field component
получил role="alert" на error span.

### 2026-06-07 п.3 (focus management)
`useFocusTrap<T>(active, initialFocusSelector?)` хук — return-focus,
Tab-cycle, mount-focus. Подключён к Modal (defaults) и
ConfirmDialog (data-attr selector + defaultFocus prop:
'cancel' для destructive, 'confirm' для info).

### 2026-06-07 п.4 (coverage)
Coverlet baseline → 6 файлов тестов (PhoneNormalization, PagedRequest,
RequiredGuid, RolePermissions, DomainPocoSmoke,
DomainFullPropertyTouch, CatalogDtosSmoke). Application 56→83%,
Domain 11→79%, combined 60→80%.

### 2026-06-07 п.5 (property tests)
`StockServicePropertyTests` self-rolled (без FsCheck) — 4 seeds × 4 sizes
+ batch + isolation. Ловит знак-регрессии, идемпотентность,
независимость пар (product, store).

### 2026-06-07 п.6 (backup drill)
pg_dump со stage'а → docker run postgres:16-alpine → pg_restore →
ASPNETCORE_ENVIRONMENT=Production dotnet run против восстановленной
БД → /health/ready Healthy. RTO 25s end-to-end. Команды + замеры
в RUNBOOK.md.

### 2026-06-07 п.7 (docs)
MULTI-TENANCY.md чеклист «добавить tenant-сущность» расширен до
19 шагов (Domain → EF → Migration → RolePermissions → Validation →
Controller с RequiresPermission → Audit + SensitiveOpsAudit → Tests
с property invariant). ARCHITECTURE.md получил «Sprint 13-15 changes»
таблицу. DEVELOPER-GUIDE.md — «что добавилось после первого релиза
guide'а» + «что НЕ делать» расширен a11y-pitfall'ами.

## Итог

Все 7 пунктов ✓ с реальными числами. Локальные тесты:
**147/147 unit ✓** (было 68). axe-core e2e: **0 critical** на 10
страницах stage'а. SR smoke: **4/4 ✓** (a11y attributes присутствуют).
Backup drill: **RTO 25 секунд** verified end-to-end.

Это **последний автономно-безопасный спринт**. Дальше реально нужен
вход от user'а:
1. **Реальные ОФД-ApiKey** (Webkassa приоритетно) — Sprint 11/fiscal
   ждёт это для активации.
2. **MoySklad webhook-tokens** для inline-импорта.
3. **Windows-машина** (или CI runner) для POS WPF сборки.
4. **Прод-деплой план** (домен + cert + DNS).
5. **Казахский переводчик** для UI (i18n уже подготовлен).
6. **Реальный SMTP-провайдер** (Mailgun / Postmark / Yandex) для платформы.

Плюс non-blocking improvements которые имеют смысл делать как
выяснятся приоритеты:
- Domain Shared coverage остаётся на 54% — можно добавить sanity-тестов.
- Серая зона color-contrast в таблицах — ~50 файлов поменять `text-slate-400`
  на `text-slate-500` (mostly automatable).
- Lighthouse на authenticated-страницы (`/dashboard`, `/products`) —
  требует scripted-auth setup.
- Hangfire-jobs реальные замеры длительности — ждать первого
  ночного запуска.
- pg_stat_statements продолжать собирать на stage'е при росте данных.
