import AxeBuilder from "@axe-core/playwright"; import { expect, test } from "@playwright/test"; import { loginWithMagicLink, newIsolatedContext, projectEmail, registerAndConfirm, } from "./helpers"; const publicAccessibilityPaths = [ "/", "/safety", "/users/log-in", "/users/register", "/support", "/account/delete", "/legal/content-removal", "/legal/take-it-down", ]; async function expectAccessible(page: import("@playwright/test").Page): Promise { const results = await new AxeBuilder({ page }).analyze(); expect( results.violations, results.violations .map( (violation) => `${violation.id}: ${violation.help}\n${violation.nodes .map((node) => ` ${node.target.join(" ")}: ${node.failureSummary}`) .join("\n")}`, ) .join("\n\n"), ).toEqual([]); } test("public pages are accessible by keyboard in light and dark themes", async ({ browser }) => { const context = await newIsolatedContext(browser); const page = await context.newPage(); await page.goto("/"); await page.keyboard.press("Tab"); await expect(page.getByRole("link", { name: "Skip to main content" })).toBeFocused(); await page.keyboard.press("Enter"); await expect(page.locator("#main-content")).toBeFocused(); for (const theme of ["light", "dark"] as const) { await page.getByRole("button", { name: `Use ${theme} theme` }).click(); await expect(page.locator("html")).toHaveAttribute("data-theme", theme); for (const path of publicAccessibilityPaths) { await page.goto(path); await expectAccessible(page); } } await context.close(); }); test("public navigation does not overflow narrow or wide viewports", async ({ browser }) => { for (const viewport of [ { width: 360, height: 800 }, { width: 768, height: 1024 }, { width: 1030, height: 900 }, { width: 1440, height: 900 }, ]) { const context = await newIsolatedContext(browser); const page = await context.newPage(); await page.setViewportSize(viewport); for (const path of ["/", "/support", "/legal/content-removal", "/legal/take-it-down"]) { await page.goto(path); const dimensions = await page.evaluate(() => ({ clientWidth: document.documentElement.clientWidth, scrollWidth: document.documentElement.scrollWidth, })); expect(dimensions.scrollWidth, `${path} overflowed at ${viewport.width}px`).toBeLessThanOrEqual( dimensions.clientWidth, ); } await page.goto("/"); await expect(page.getByRole("navigation", { name: "Primary navigation" })).toBeVisible(); await expect(page.locator("body > header")).toHaveCount(1); await expect(page.locator("body > nav[aria-label='Account and language']")).toHaveCount(0); if (viewport.width < 1280) { const menuButton = page.locator("summary[aria-label='Primary navigation']"); await expect(menuButton).toBeVisible(); await menuButton.click(); await expect(page.getByRole("link", { name: "Requests", exact: true })).toBeVisible(); await expect(page.getByRole("link", { name: "Activities", exact: true })).toBeVisible(); await expect(page.getByRole("link", { name: "Categories", exact: true })).toBeVisible(); await expect(page.getByRole("link", { name: "Helpers", exact: true })).toBeVisible(); await expect( page.locator("summary[aria-label='Language: English']:visible"), ).toBeVisible(); await expect(page.getByRole("link", { name: "Log in", exact: true })).toBeVisible(); const openDimensions = await page.evaluate(() => ({ clientWidth: document.documentElement.clientWidth, scrollWidth: document.documentElement.scrollWidth, })); expect(openDimensions.scrollWidth).toBeLessThanOrEqual(openDimensions.clientWidth); } else { await expect(page.locator("summary[aria-label='Primary navigation']")).toBeHidden(); await expect(page.getByRole("link", { name: "Requests", exact: true })).toBeVisible(); await expect( page.locator("summary[aria-label='Language: English']:visible"), ).toBeVisible(); await expect(page.getByRole("link", { name: "Register", exact: true })).toBeVisible(); await expect(page.getByRole("link", { name: "Log in", exact: true })).toBeVisible(); } await context.close(); } }); test("authenticated core workflows remain accessible on a phone viewport", async ({ browser, request, }, testInfo) => { const user = await registerAndConfirm( browser, request, projectEmail("mobile-accessibility", testInfo.project.name), "Mobile accessibility", ); await user.page.setViewportSize({ width: 390, height: 844 }); for (const path of [ "/requests", "/requests/new", "/activities", "/notifications", "/profile", "/users/settings", "/support/requests", "/reports", ]) { await user.page.goto(path); await expectAccessible(user.page); const dimensions = await user.page.evaluate(() => ({ clientWidth: document.documentElement.clientWidth, scrollWidth: document.documentElement.scrollWidth, })); expect(dimensions.scrollWidth, `${path} overflowed at 390px`).toBeLessThanOrEqual( dimensions.clientWidth, ); } await user.context.close(); }); test("staff workspaces remain accessible without horizontal overflow on a phone viewport", async ({ browser, request, }) => { const admin = await loginWithMagicLink(browser, request, "e2e-admin@example.invalid"); await admin.page.setViewportSize({ width: 390, height: 844 }); for (const path of [ "/admin", "/admin/users", "/support/operations", "/moderation", "/analytics", "/admin/audit", ]) { await admin.page.goto(path); await expectAccessible(admin.page); const dimensions = await admin.page.evaluate(() => ({ clientWidth: document.documentElement.clientWidth, scrollWidth: document.documentElement.scrollWidth, })); expect(dimensions.scrollWidth, `${path} overflowed at 390px`).toBeLessThanOrEqual( dimensions.clientWidth, ); } await admin.context.close(); });