test: #3-#7 complete — scheduled tasks, email/SMS, concurrency, performance, accessibility
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m21s
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.
This commit is contained in:
parent
43bea5befe
commit
69cda65bea
102
e2e/tests/workflows/accessibility-audit.spec.ts
Normal file
102
e2e/tests/workflows/accessibility-audit.spec.ts
Normal file
@ -0,0 +1,102 @@
|
||||
/* 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');
|
||||
});
|
||||
24
node_modules/.package-lock.json
generated
vendored
24
node_modules/.package-lock.json
generated
vendored
@ -17,6 +17,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@axe-core/playwright": {
|
||||
"version": "4.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.3.tgz",
|
||||
"integrity": "sha512-h/kfksv4F0cVIDlKpT4700OehdRgpvuVskuQ2nb7/JmtWUXpe9ftHAPtwyXGvVSsa6SJ64A9ER7Zrzc/sIvC4w==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"axe-core": "~4.11.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright-core": ">= 1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
@ -242,6 +255,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axe-core": {
|
||||
"version": "4.11.4",
|
||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz",
|
||||
"integrity": "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@ -738,6 +761,7 @@
|
||||
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
|
||||
25
package-lock.json
generated
25
package-lock.json
generated
@ -15,6 +15,7 @@
|
||||
"sweetalert2": "^11.26.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.11.3",
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@types/node": "^25.5.2",
|
||||
"tailwindcss": "^3.4.19",
|
||||
@ -35,6 +36,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@axe-core/playwright": {
|
||||
"version": "4.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.3.tgz",
|
||||
"integrity": "sha512-h/kfksv4F0cVIDlKpT4700OehdRgpvuVskuQ2nb7/JmtWUXpe9ftHAPtwyXGvVSsa6SJ64A9ER7Zrzc/sIvC4w==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"axe-core": "~4.11.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright-core": ">= 1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
@ -260,6 +274,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axe-core": {
|
||||
"version": "4.11.4",
|
||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz",
|
||||
"integrity": "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@ -771,6 +795,7 @@
|
||||
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
"build": "npm run build:css"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.11.3",
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@types/node": "^25.5.2",
|
||||
"tailwindcss": "^3.4.19",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user