All checks were successful
Build and Push Docker Image / build (push) Successful in 2m21s
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.
103 lines
4.2 KiB
TypeScript
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');
|
|
});
|