import { expect, test } from "@playwright/test" import AxeBuilder from "@axe-core/playwright" /** * Accessibility Tests — axe-core scans on all pages. * * Uses @axe-core/playwright to run automated a11y checks. * Filters to critical + serious violations only to reduce noise. * First run establishes a baseline — violations are reported but don't fail tests. */ const BASE = process.env.BASE_URL || "http://localhost:3000" /** Helper to run axe and collect violations */ async function runAxeScan(page: import("@playwright/test").Page) { const results = await new AxeBuilder({ page }) .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]) .options({ includedImpacts: ["critical", "serious"] }) .analyze() return results.violations } /** Format violations for readable output */ function formatViolations( violations: Awaited> ): string { if (violations.length === 0) return "No violations found" return violations .map((v) => { const nodes = v.nodes.map((n) => ` - ${n.html.slice(0, 100)}`).join("\n") return ` [${v.impact}] ${v.id}: ${v.description}\n Help: ${v.helpUrl}\n Elements (${v.nodes.length}):\n${nodes}` }) .join("\n\n") } test.describe("Accessibility — Admin Pages", () => { const adminPages = [ { name: "Dashboard", path: "/dashboard" }, { name: "Members", path: "/members" }, { name: "Members New", path: "/members/new" }, { name: "Distributions", path: "/distributions" }, { name: "Distributions New", path: "/distributions/new" }, { name: "Stock", path: "/stock" }, { name: "Stock New", path: "/stock/new" }, { name: "Grow", path: "/grow" }, { name: "Reports", path: "/reports" }, { name: "Audit Log", path: "/audit-log" }, { name: "Staff", path: "/settings/staff" }, { name: "Billing", path: "/settings/billing" }, { name: "Privacy", path: "/settings/privacy" }, { name: "Info Board", path: "/info-board" }, { name: "Calendar", path: "/calendar" }, { name: "Forum", path: "/forum" }, ] for (const { name, path } of adminPages) { test(`${name} (${path}) passes axe scan`, async ({ page }) => { await page.goto(`${BASE}${path}`) await page.waitForLoadState("domcontentloaded") // Wait for content to render await page.waitForTimeout(2000) const violations = await runAxeScan(page) // Log violations for visibility (first run = baseline) if (violations.length > 0) { console.log(`\n⚠️ A11y violations on ${name} (${path}):`) console.log(formatViolations(violations)) } // Soft assertion — report but don't fail on first run // Uncomment below to enforce zero violations: // expect(violations, `A11y violations on ${name}`).toHaveLength(0) // For now, just ensure the page loaded (no crash) const main = page.locator("main, [role='main'], body") await expect(main.first()).toBeVisible() }) } }) test.describe("Accessibility — Public Pages", () => { const publicPages = [ { name: "Login", path: "/login" }, { name: "Home / Marketing", path: "/" }, ] for (const { name, path } of publicPages) { test(`${name} (${path}) passes axe scan`, async ({ page }) => { await page.goto(`${BASE}${path}`) await page.waitForLoadState("domcontentloaded") await page.waitForTimeout(2000) const violations = await runAxeScan(page) if (violations.length > 0) { console.log(`\n⚠️ A11y violations on ${name} (${path}):`) console.log(formatViolations(violations)) } const body = page.locator("body") await expect(body).toBeVisible() }) } }) test.describe("Accessibility — Portal Pages", () => { const portalPages = [ { name: "Portal Login", path: "/portal/login" }, { name: "Portal Dashboard", path: "/portal" }, { name: "Portal History", path: "/portal/history" }, { name: "Portal Profile", path: "/portal/profile" }, ] for (const { name, path } of portalPages) { test(`${name} (${path}) passes axe scan`, async ({ page }) => { await page.goto(`${BASE}${path}`) await page.waitForLoadState("domcontentloaded") await page.waitForTimeout(2000) const violations = await runAxeScan(page) if (violations.length > 0) { console.log(`\n⚠️ A11y violations on ${name} (${path}):`) console.log(formatViolations(violations)) } const body = page.locator("body") await expect(body).toBeVisible() }) } })