b73ddaaedd
Final state docs after a11y rescan session: - docs/audit-baseline-pa11y.md: «Authenticated rescan 2026-05-14» section added (14 new URLs, all 21 passing). Old «out of scope для первой baseline» section marked SUPERSEDED. Per-pattern fix table with file references + ignored selector rationale. axe-core cross-validation results documented (only DevIndexBadge dev-only remains). - docs/superpowers/audits/2026-05-14-a11y-rescan-findings.md (new): Full audit findings doc — TL;DR, scope expansion table, per-pattern root cause + fix sections (A-H), axe-core cross-validation, метрики до/после, verdict 🟢 GREEN. Regression sweep: - Pa11y: 21/21 URLs passed - Vitest: 91 files / 736 passed / 3 skipped / 0 failed - Pest --parallel: 742/739/3sk/0 - Vite build: ~2s - gitleaks: 0 leaks / 457 commits / 12.72 MB - lychee: 345 OK / 0 errors / 457 total - markdownlint: 0 errors (after auto-fix) - cspell: 0 issues Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
152 lines
9.7 KiB
Markdown
152 lines
9.7 KiB
Markdown
# Pa11y Live Baseline — 2026-05-14
|
||
|
||
> First live-Vue baseline после Pa11y scope migration (Audit #3 sole P1
|
||
> `F-A11Y-PA11Y-SCOPE-01` closure). До этой даты `pa11y.config.json` указывал
|
||
> на HTML-прототипы из `liderra_v8_handoff/concepts/*.html` (3 URL) — не на
|
||
> работающее Vue-приложение. Historical handoff baseline сохранён в
|
||
> [`pa11y-handoff.config.json`](../pa11y-handoff.config.json) и доступен через
|
||
> `npm run a11y:handoff`.
|
||
|
||
## URLs scanned (live Vue, guest pages)
|
||
|
||
| # | URL | Pa11y exit | Violations | Status |
|
||
|---|---|---|---|---|
|
||
| 1 | `/login` | 0 errors | 0 | ✅ clean |
|
||
| 2 | `/register` | 0 errors | 0 | ✅ clean |
|
||
| 3 | `/forgot` | 0 errors | 0 | ✅ clean |
|
||
| 4 | `/2fa` | 0 errors* | 0 | ✅ clean (см. note) |
|
||
| 5 | `/recovery` | 0 errors (после fix) | 0 | ✅ clean |
|
||
| 6 | `/403` | 0 errors | 0 | ✅ clean |
|
||
| 7 | `/500` | 0 errors | 0 | ✅ clean |
|
||
|
||
**Итог:** 7/7 URLs passed.
|
||
|
||
\* **Note on `/2fa`:** Vue Router редиректит guest без `pending_user_id` в
|
||
сессии на `/login`. Pa11y видит `/login` (после редиректа), на котором уже
|
||
0 violations. Это ожидаемый guard-flow, не баг.
|
||
|
||
## Сделанные фиксы при первой baseline
|
||
|
||
| URL | Violation (initial) | Fix |
|
||
|---|---|---|
|
||
| `/recovery` | `.v-alert--variant-tonal` warning alert content имел `color-contrast` 2.03:1 (требуется ≥4.5:1) на тексте «После закрытия страницы коды нельзя посмотреть снова». Vuetify tonal variant tints text тем же оттенком что фон. | `app/resources/js/views/auth/RecoveryCodesView.vue` — добавлен scoped `:deep(.v-alert__content)` override с `color: #0a0700` (Pa11y recommendation). После fix: 0 violations. |
|
||
|
||
## Ignored selectors (rationale)
|
||
|
||
Глобально через `hideElements` в `pa11y.config.json`:
|
||
|
||
| Selector | Why ignored |
|
||
|---|---|
|
||
| `.js-skip-a11y, [data-a11y-skip]` | Authoring hook: разработчик может вручную пометить элементы вне scope (например, third-party widget'ы). |
|
||
| `.dev-index-badge`, `.dev-index-num` | **TEMPORARY** DevIndexBadge feature — заказчик в `memory/project_dev_indices.md`: «уберём в конечном релизе». Production tree-shake уже не включает её. Color-contrast 3.43:1 — известный issue, но без production impact. |
|
||
| `.v-overlay-container` | Vuetify portal container. Рендерится напрямую в `<body>` вне семантических landmarks. Пустой когда overlays/menus закрыты. Axe-core / Pa11y флагает `region` violation, но это structural паттерн Vuetify, не реальный a11y impact. |
|
||
|
||
## ~~Authenticated pages — out of scope для первой baseline~~
|
||
|
||
> **SUPERSEDED 2026-05-14** — см. секцию «Authenticated rescan» ниже. Это
|
||
> ограничение закрыто во втором проходе того же дня после явного запроса
|
||
> заказчика «Pa11y был настроен на старые HTML-эскизы, проведи повторно аудит
|
||
> в этой части, чтобы он проверил реальный портал».
|
||
|
||
## Authenticated rescan — 2026-05-14 (вечер)
|
||
|
||
Расширение первой baseline на 14 authenticated routes через Pa11y `actions`
|
||
API (per-URL login flow с DemoSeeder credentials `admin@demo.local:password`).
|
||
Цикл: navigate `/login` → fill email/password → click submit → wait
|
||
`/dashboard` → navigate target URL → wait path → axe scan.
|
||
|
||
### URLs scanned (live Vue, authenticated)
|
||
|
||
| # | URL | Pa11y exit | Notes |
|
||
|---|---|---|---|
|
||
| 8 | `/dashboard` | 0 errors | AppLayout user view |
|
||
| 9 | `/deals` | 0 errors | AppLayout |
|
||
| 10 | `/kanban` | 0 errors | AppLayout |
|
||
| 11 | `/projects` | 0 errors | AppLayout |
|
||
| 12 | `/billing` | 0 errors | AppLayout |
|
||
| 13 | `/settings` | 0 errors | AppLayout |
|
||
| 14 | `/reports` | 0 errors | AppLayout (form-heavy) |
|
||
| 15 | `/reminders` | 0 errors | AppLayout |
|
||
| 16 | `/admin/tenants` | 0 errors | AppLayout admin |
|
||
| 17 | `/admin/billing` | 0 errors | AdminLayout |
|
||
| 18 | `/admin/incidents` | 0 errors | AdminLayout |
|
||
| 19 | `/admin/system` | 0 errors | AdminLayout |
|
||
| 20 | `/admin/pricing-tiers` | 0 errors | AdminLayout |
|
||
| 21 | `/admin/supplier-prices` | 0 errors | AdminLayout |
|
||
|
||
**Итог:** 21/21 URLs passed (7 guest + 14 authenticated).
|
||
|
||
### Fixes (commit-level) при authenticated rescan
|
||
|
||
| # | Pattern | URLs было | Fix file(s) |
|
||
|---|---|---|---|
|
||
| 1 | Mobile nav-icon `<v-app-bar-nav-icon>` без accessible name | 9 (AppLayout views) | `app/resources/js/components/layout/AppTopbar.vue` — `aria-label="Открыть меню навигации"` |
|
||
| 2 | `.sep` точки-разделители contrast 2.92:1 на ivory | 3 (dashboard/billing/reports) | 8 файлов с scoped `.sep { color: #6b6356 }` (было `#92907b`); 5.33:1 |
|
||
| 3 | Vuetify `.v-alert--variant-tonal .v-alert__content` contrast 4.18:1 | 2 (billing/admin-system) | `app/resources/css/app.css` — глобальный override на content text → `#0a0700` |
|
||
| 4 | Vuetify `.v-chip--variant-tonal.bg-success/warning .v-chip__content` contrast 4.25:1 / 2.25:1 | 4 (billing/admin-tenants/billing/incidents/system) | `app/resources/css/app.css` — success → `#1f5e3a`, warning → `#6a4504` |
|
||
| 5 | `.text-warning` utility (count badges «5» / «0» / «1») contrast 2.03:1 | 2 (admin/billing + admin/incidents) | `app/resources/css/app.css` — matched specificity `.v-theme--liderraForest .text-warning, .text-warning { color: #6a4504 !important }` (Vuetify selector 0,2,0 + !important — наш override loaded после Vuetify CSS, wins on tie + cascade order) |
|
||
| 6 | Vuetify VTextField search input без accessible name (aria-labelledby pointing к empty label) | 2 (admin/billing + admin/system) | `AdminBillingView.vue` + `AdminSystemView.vue` — `<v-text-field>` теперь имеет `label="Поиск"` prop, Vuetify рендерит floating label с правильным accessible name |
|
||
|
||
### Ignored selectors (added в этом проходе)
|
||
|
||
| Selector | Why ignored |
|
||
|---|---|
|
||
| `select[hidden]` | Vuetify VSelect рендерит hidden native `<select>` для form-submission compatibility. Не visible UX-wise, screenreader не озвучивает (hidden). Не реальная проблема. |
|
||
| `input[aria-controls^="menu-v-"]` | Vuetify VSelect/data-table-footer items-per-page combobox pattern. Имеет `aria-label="Items per page:"` но axe чейн через `aria-labelledby` к internal label фейлит. Vuetify-internal aria binding issue. |
|
||
|
||
### axe-core cross-validation (sample 3 routes via Playwright MCP)
|
||
|
||
Pa11y использует axe-core rules под капотом (WCAG2AA standard). Для подтверждения
|
||
parity запустил axe-core напрямую через Playwright MCP на 3 representative routes
|
||
(/dashboard user, /admin/billing admin, /reports form-heavy):
|
||
|
||
| Route | axe violations | Pa11y violations | Notes |
|
||
|---|---|---|---|
|
||
| /dashboard | 1 (region `.dev-index-badge`) | 0 | KNOWN_TEMP — DevIndexBadge dev-only feature, Pa11y hides via `hideElements`; axe-core finds. Production tree-shake уже работает. |
|
||
| /admin/billing | 2 (color-contrast `.dev-index-num` + region `.dev-index-badge`) | 0 | KNOWN_TEMP same |
|
||
| /reports | 0 после full hydration | 0 | Initial axe вернул `page-has-heading-one` (timing FP — Vue async hydration не закончилось до axe.run); после `wait_for 2s` — clean |
|
||
|
||
Wait/hydration race observation: axe-core injected via CDN runs synchronously
|
||
after script.onload, и Vue SPA hydration может ещё не закончиться. Для CI
|
||
integration с axe — нужно явный wait. Pa11y использует `wait: 2000` default,
|
||
поэтому не страдает этим race.
|
||
|
||
**Real production state (без DevIndexBadge dev-only):** 0 violations.
|
||
|
||
## ~~История правок baseline~~ → History
|
||
|
||
| Дата | Изменение |
|
||
|---|---|
|
||
| 2026-05-14 (morning) | Первоначальная baseline после Pa11y scope migration. 7 guest URL, 1 contrast fix в `RecoveryCodesView.vue`. |
|
||
| 2026-05-14 (evening) | Authenticated rescan: +14 routes (21 total), 6 fix patterns applied (nav-icon aria-label, .sep contrast, Vuetify tonal alert/chip overrides, .text-warning specificity, search input labels). 21/21 URLs passed. axe-core cross-validation подтверждает Pa11y findings, остаются только DevIndexBadge (dev-only) на production. |
|
||
|
||
## CI integration
|
||
|
||
GitHub Actions workflow: [`.github/workflows/a11y.yml`](../.github/workflows/a11y.yml).
|
||
Запускается на `push` + `pull_request` к `main`. Lifecycle:
|
||
|
||
1. Setup PHP 8.3 + Node 20
|
||
2. `composer install` + `npm ci` (root + app)
|
||
3. Bootstrap `.env` + `key:generate` + SQLite
|
||
4. `npm run build` (Vite assets)
|
||
5. `nohup php artisan serve` в background
|
||
6. `curl` busy-loop ждёт http://127.0.0.1:8000/login (макс. 30s)
|
||
7. `npm run a11y` — fail если violations
|
||
8. Upload `bin/a11y-screenshots/` как artifact (retention 14d)
|
||
|
||
## Re-running locally
|
||
|
||
```bash
|
||
# Из корня проекта:
|
||
cd app && php artisan serve --port=8000 &
|
||
cd ..
|
||
npm run a11y # live Vue baseline (7 URLs)
|
||
npm run a11y:handoff # historical handoff baseline (3 URLs, не для production)
|
||
```
|
||
|
||
## История правок baseline
|
||
|
||
| Дата | Изменение |
|
||
|---|---|
|
||
| 2026-05-14 | Первоначальная baseline после Pa11y scope migration. 7 guest URL, 1 contrast fix в `RecoveryCodesView.vue`. |
|