New specs (4):
- accept-invite: pre-registrant flow happy path + cadence/preset UX
- admin-pre-registrants: list, filter, action gating, redirect
- admin-series: list, create, edit (delete skipped — button no-ops)
- admin-site-content: list whitelist, edit + roundtrip on /
Extended specs (6):
- join-flow: cadence ×12 math, guidance label, paid-tier success
- events: series-pass-required, member-savings gating
- admin-events: full CRUD via /admin/events/create?edit=<id>
- admin-members: add-member submit, status select, detail nav
- a11y: add /accept-invite, /member/account, /board, /admin/pre-registrants
- wave-slack-onboarding: 9 of 16 scaffold tests now passing
Cross-file isolation hardening:
- admin-events CRUD: refresh auth cookie (auth.spec.js logout test
bumps tokenVersion on the shared admin), wait for hydration
before form fill, search by unique title to dodge pagination.
- board: switch memberPage from shared admin to dedicated seeded
member to avoid the same tokenVersion race.
- wave-slack §6.4: create dedicated test member, filter by email
before clicking, removing the "first row" anchor.
Also fixed board heading drift ("Board" → "Bulletin Board").
125 lines
3.7 KiB
JavaScript
125 lines
3.7 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" },
|
|
{ name: "Accept Invite", path: "/accept-invite" },
|
|
];
|
|
|
|
const memberPages = [
|
|
{ name: "Member Dashboard", path: "/member/dashboard" },
|
|
{ name: "Member Profile", path: "/member/profile" },
|
|
{ name: "Member Account", path: "/member/account" },
|
|
{ name: "Board", path: "/board" },
|
|
];
|
|
|
|
const adminPages = [
|
|
{ name: "Admin Members", path: "/admin/members" },
|
|
{ name: "Admin Events Create", path: "/admin/events/create" },
|
|
{ name: "Admin Pre-Registrants", path: "/admin/pre-registrants" },
|
|
];
|
|
|
|
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");
|
|
|
|
// Auth middleware auto-opens the login modal for unauthenticated users
|
|
const modal = page.getByRole("dialog");
|
|
await expect(modal).toBeVisible({ timeout: 10000 });
|
|
|
|
await page.keyboard.press("Escape");
|
|
|
|
// Modal should close
|
|
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
|
});
|
|
});
|