Add aria-labels to form controls (selects, checkboxes, switches), set html lang attribute and page title, fix color contrast for --candle-dim and --text-faint tokens, underline inline links, remove opacity hack. Harden dev login endpoints with atomic findOneAndUpdate and tokenVersion in JWT. Update Playwright timeouts and E2E test helpers.
115 lines
3.6 KiB
JavaScript
115 lines
3.6 KiB
JavaScript
import { test, expect } from '@playwright/test'
|
|
import AxeBuilder from '@axe-core/playwright'
|
|
import { loginAsAdmin } from './helpers/auth.js'
|
|
|
|
const publicPages = [
|
|
{ name: 'Home', path: '/' },
|
|
{ name: 'Join', path: '/join' },
|
|
{ name: 'Events', path: '/events' },
|
|
{ name: 'Coming Soon', path: '/coming-soon' },
|
|
]
|
|
|
|
const memberPages = [
|
|
{ name: 'Member Dashboard', path: '/member/dashboard' },
|
|
{ name: 'Member Profile', path: '/member/profile' },
|
|
]
|
|
|
|
const adminPages = [
|
|
{ name: 'Admin Members', path: '/admin/members' },
|
|
{ name: 'Admin Events Create', path: '/admin/events/create' },
|
|
]
|
|
|
|
test.describe('accessibility — public pages', () => {
|
|
for (const { name, path } of publicPages) {
|
|
test(`${name} has no critical a11y violations`, async ({ page }) => {
|
|
await page.goto(path)
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
const results = await new AxeBuilder({ page })
|
|
.withTags(['wcag2a', 'wcag2aa'])
|
|
.analyze()
|
|
|
|
const critical = results.violations.filter(
|
|
(v) => v.impact === 'critical' || v.impact === 'serious'
|
|
)
|
|
|
|
expect(critical, `${name} has critical/serious a11y issues`).toEqual([])
|
|
})
|
|
}
|
|
})
|
|
|
|
test.describe('accessibility — member pages', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginAsAdmin(page)
|
|
})
|
|
|
|
for (const { name, path } of memberPages) {
|
|
test(`${name} has no critical a11y violations`, async ({ page }) => {
|
|
await page.goto(path)
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
const results = await new AxeBuilder({ page })
|
|
.withTags(['wcag2a', 'wcag2aa'])
|
|
.analyze()
|
|
|
|
const critical = results.violations.filter(
|
|
(v) => v.impact === 'critical' || v.impact === 'serious'
|
|
)
|
|
|
|
expect(critical, `${name} has critical/serious a11y issues`).toEqual([])
|
|
})
|
|
}
|
|
})
|
|
|
|
test.describe('accessibility — admin pages', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginAsAdmin(page)
|
|
})
|
|
|
|
for (const { name, path } of adminPages) {
|
|
test(`${name} has no critical a11y violations`, async ({ page }) => {
|
|
await page.goto(path)
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
const results = await new AxeBuilder({ page })
|
|
.withTags(['wcag2a', 'wcag2aa'])
|
|
.analyze()
|
|
|
|
const critical = results.violations.filter(
|
|
(v) => v.impact === 'critical' || v.impact === 'serious'
|
|
)
|
|
|
|
expect(critical, `${name} has critical/serious a11y issues`).toEqual([])
|
|
})
|
|
}
|
|
})
|
|
|
|
test.describe('keyboard navigation', () => {
|
|
test('tab through join form fields in order', async ({ page }) => {
|
|
await page.goto('/join')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
// Focus the name field and tab through
|
|
await page.locator('#join-name').focus()
|
|
expect(await page.locator('#join-name').evaluate((el) => el === document.activeElement)).toBe(true)
|
|
|
|
await page.keyboard.press('Tab')
|
|
// Email field should receive focus next
|
|
expect(await page.locator('#join-email').evaluate((el) => el === document.activeElement)).toBe(true)
|
|
})
|
|
|
|
test('escape closes login modal', async ({ page }) => {
|
|
await page.goto('/member/dashboard')
|
|
// Wait for the sign-in prompt to appear, then click to open the modal
|
|
await expect(page.getByRole('button', { name: 'Sign In' })).toBeVisible({ timeout: 10000 })
|
|
await page.getByRole('button', { name: 'Sign In' }).click()
|
|
|
|
const modalTitle = page.locator('.modal-title')
|
|
await expect(modalTitle).toBeVisible({ timeout: 5000 })
|
|
|
|
await page.keyboard.press('Escape')
|
|
|
|
// Modal should close
|
|
await expect(modalTitle).not.toBeVisible({ timeout: 5000 })
|
|
})
|
|
})
|