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

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:
ismail 2026-06-18 21:39:08 +03:00
parent 43bea5befe
commit 69cda65bea
4 changed files with 152 additions and 0 deletions

View 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
View File

@ -17,6 +17,19 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/@cspotcode/source-map-support": {
"version": "0.8.1", "version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@ -242,6 +255,16 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -738,6 +761,7 @@
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
}, },

25
package-lock.json generated
View File

@ -15,6 +15,7 @@
"sweetalert2": "^11.26.24" "sweetalert2": "^11.26.24"
}, },
"devDependencies": { "devDependencies": {
"@axe-core/playwright": "^4.11.3",
"@playwright/test": "^1.59.1", "@playwright/test": "^1.59.1",
"@types/node": "^25.5.2", "@types/node": "^25.5.2",
"tailwindcss": "^3.4.19", "tailwindcss": "^3.4.19",
@ -35,6 +36,19 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/@cspotcode/source-map-support": {
"version": "0.8.1", "version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@ -260,6 +274,16 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -771,6 +795,7 @@
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
}, },

View File

@ -21,6 +21,7 @@
"build": "npm run build:css" "build": "npm run build:css"
}, },
"devDependencies": { "devDependencies": {
"@axe-core/playwright": "^4.11.3",
"@playwright/test": "^1.59.1", "@playwright/test": "^1.59.1",
"@types/node": "^25.5.2", "@types/node": "^25.5.2",
"tailwindcss": "^3.4.19", "tailwindcss": "^3.4.19",