HH/e2e/tests/workflows/accessibility-audit.spec.ts
ismail 69cda65bea
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m21s
test: #3-#7 complete — scheduled tasks, email/SMS, concurrency, performance, accessibility
Items #3-#7 all verified:

#3 Scheduled Tasks: all 8 Celery beat tasks verified (overdue detection,
   SLA reminders, dept-response checks). 0 bugs.

#4 Email/SMS: complaint status email verified (correct subject, ref#, from/to,
   HTML body). Notification service verified. SMS works via console backend.
   0 bugs.

#5 Concurrent operations: 3 threads on same complaint — 2 succeeded, 1 correctly
   rejected (invalid transition). No data corruption. State machine enforced.

#6 Performance: 12 pages measured, 0 slow (>3s), heaviest /my/ at 1.7s/287 queries.
   All pages have >50 DB queries (N+1 patterns — optimization opportunity, not bug).

#7 Accessibility: axe-core WCAG audit on 15 pages. Findings (consistent patterns):
   - button-name: icon-only buttons without aria-label (9 nodes/page — lucide icons)
   - link-name: links with no discernible text (16 nodes/page — likely sidebar icons)
   - color-contrast: some text below WCAG AA 4.5:1 ratio (4-8 nodes/page)
   - select-name: select elements without aria-label (1-6 nodes/page)
   - label: form inputs without associated <label> (1-2 nodes on public forms)
   These are UX improvements, not functional bugs. Only public-landing page is clean.
2026-06-18 21:39:08 +03:00

103 lines
4.2 KiB
TypeScript

/* eslint-disable */
/**
* ACCESSIBILITY AUDIT — axe-core automated WCAG check on key pages.
*
* Scans for: color contrast, missing labels, ARIA issues, keyboard nav,
* heading hierarchy, image alt text, form accessibility.
*
* Run headed:
* E2E_MAXIMIZED=1 npx playwright test --headed --project chromium accessibility-audit --workers=1
*/
import { test } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
import { attachObservers, observe, loginAndScope, OBS, BASE_URL } from '../../helpers/audit';
import { RoleName } from '../../helpers/helpers';
const M = 'Accessibility';
const ADMIN: RoleName = 'hospital_admin';
type Page = import('@playwright/test').Page;
async function login(page: Page) { await page.context().clearCookies(); await loginAndScope(page, ADMIN, M); }
// Pages to audit (most user-facing)
const PAGES = [
{ url: '/', label: 'command-center' },
{ url: '/accounts/login/', label: 'login', auth: false },
{ url: '/complaints/', label: 'complaint-list' },
{ url: '/inquiries/', label: 'inquiry-list' },
{ url: '/observations/', label: 'observation-list' },
{ url: '/complaints/public/submit/', label: 'public-complaint-form', auth: false },
{ url: '/observations/new/', label: 'public-observation-form', auth: false },
{ url: '/core/public/submit/', label: 'public-landing', auth: false },
{ url: '/analytics/dashboard/', label: 'analytics-dashboard' },
{ url: '/projects/', label: 'projects-list' },
{ url: '/actions/', label: 'actions-list' },
{ url: '/rca/', label: 'rca-list' },
{ url: '/surveys/templates/', label: 'survey-templates' },
{ url: '/appreciation/', label: 'appreciation-list' },
{ url: '/notifications/inbox/', label: 'notifications-inbox' },
];
test.describe('Accessibility Audit', () => {
for (const { url, label, auth = true } of PAGES) {
test(`${label}: WCAG audit`, async ({ page }) => {
attachObservers(page, M, ADMIN);
if (auth) {
await login(page);
} else {
await page.context().clearCookies();
}
await page.goto(`${BASE_URL}${url}`);
await page.waitForLoadState('domcontentloaded');
await page.waitForTimeout(1000);
try {
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
const violations = results.violations;
const critical = violations.filter(v => v.impact === 'critical');
const serious = violations.filter(v => v.impact === 'serious');
const moderate = violations.filter(v => v.impact === 'moderate');
const minor = violations.filter(v => v.impact === 'minor');
const totalNodes = violations.reduce((sum, v) => sum + v.nodes.length, 0);
if (critical.length > 0 || serious.length > 0) {
const topIssues = [...critical, ...serious].slice(0, 5).map(v =>
`${v.impact}: ${v.id} (${v.nodes.length} nodes) — ${v.description.slice(0, 80)}`
);
observe(M, label, 'WARN',
`${critical.length} critical, ${serious.length} serious, ${moderate.length} moderate, ${minor.length} minor (${totalNodes} nodes total). Top: ${topIssues.join('; ')}`,
{});
} else if (moderate.length > 0) {
observe(M, label, 'PASS',
`0 critical, 0 serious, ${moderate.length} moderate, ${minor.length} minor (${totalNodes} nodes). OK for WCAG AA.`,
{});
} else {
observe(M, label, 'PASS',
`0 critical, 0 serious, 0 moderate, ${minor.length} minor. Clean.`,
{});
}
} catch (e) {
observe(M, label, 'WARN', `axe scan failed: ${(e as Error).message.slice(0, 100)}`, {});
}
});
}
});
test.afterAll(async () => {
const counts = OBS.reduce<Record<string, number>>((a, o) => ((a[o.status] = (a[o.status] || 0) + 1), a), {});
const warns = OBS.filter((o) => o.status === 'WARN');
console.log('\n=========== ACCESSIBILITY SUMMARY ===========');
console.log('Total observations:', OBS.length, JSON.stringify(counts));
console.log('Pages with accessibility issues:', warns.length);
for (const w of warns) {
console.log(` ⚠️ ${w.module}/${w.step}: ${w.detail.slice(0, 120)}`);
}
console.log('=============================================\n');
});