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>
9.7 KiB
Pa11y Live Baseline — 2026-05-14
First live-Vue baseline после Pa11y scope migration (Audit #3 sole P1
F-A11Y-PA11Y-SCOPE-01closure). До этой датыpa11y.config.jsonуказывал на HTML-прототипы изliderra_v8_handoff/concepts/*.html(3 URL) — не на работающее Vue-приложение. Historical handoff baseline сохранён в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.
Запускается на push + pull_request к main. Lifecycle:
- Setup PHP 8.3 + Node 20
composer install+npm ci(root + app)- Bootstrap
.env+key:generate+ SQLite npm run build(Vite assets)nohup php artisan serveв backgroundcurlbusy-loop ждёт http://127.0.0.1:8000/login (макс. 30s)npm run a11y— fail если violations- Upload
bin/a11y-screenshots/как artifact (retention 14d)
Re-running locally
# Из корня проекта:
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. |