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").
133 lines
5.4 KiB
JavaScript
133 lines
5.4 KiB
JavaScript
import { test, expect } from "./helpers/fixtures.js";
|
|
|
|
test.describe("Admin members page", () => {
|
|
test("members list loads for admin", async ({ adminPage }) => {
|
|
await adminPage.goto("/admin/members");
|
|
|
|
await expect(adminPage.locator("h1")).toHaveText("Members");
|
|
await expect(
|
|
adminPage.getByText("Manage members, contributions, and access"),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("search bar works", async ({ adminPage }) => {
|
|
await adminPage.goto("/admin/members");
|
|
|
|
const searchInput = adminPage.getByPlaceholder("Search members...");
|
|
await expect(searchInput).toBeVisible({ timeout: 10000 });
|
|
|
|
// Wait for the initial member list to load before searching
|
|
await expect(
|
|
adminPage
|
|
.locator("table")
|
|
.or(adminPage.getByText("No members found matching your criteria")),
|
|
).toBeVisible({ timeout: 15000 });
|
|
|
|
await searchInput.fill("nonexistent-query-xyz");
|
|
|
|
// Page should not crash -- either shows filtered results or the empty state
|
|
await expect(
|
|
adminPage
|
|
.locator("table")
|
|
.or(adminPage.getByText("No members found matching your criteria")),
|
|
).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
test("non-admin redirect", async ({ browser }) => {
|
|
const context = await browser.newContext();
|
|
const page = await context.newPage();
|
|
|
|
await page.goto("/admin/members");
|
|
|
|
// Admin middleware redirects non-admin users to / or /members
|
|
await page.waitForURL((url) => !url.pathname.startsWith("/admin"));
|
|
expect(page.url()).not.toContain("/admin/members");
|
|
|
|
await context.close();
|
|
});
|
|
|
|
test("add member button opens modal", async ({ adminPage }) => {
|
|
await adminPage.goto("/admin/members");
|
|
await adminPage.waitForLoadState("networkidle"); // ensure Vue hydration is complete
|
|
|
|
// Wait for page to fully load and hydrate
|
|
await expect(adminPage.locator("h1")).toHaveText("Members");
|
|
await adminPage.waitForLoadState("networkidle");
|
|
|
|
const addBtn = adminPage.getByRole("button", { name: "Add Member" });
|
|
await expect(addBtn).toBeVisible({ timeout: 10000 });
|
|
await addBtn.click();
|
|
|
|
// Modal should appear with the form heading and fields
|
|
await expect(adminPage.getByPlaceholder("Full name")).toBeVisible({
|
|
timeout: 10000,
|
|
});
|
|
await expect(
|
|
adminPage.getByPlaceholder("email@example.com"),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("create member, status select reflects STATUS_LABELS, change persists, detail page renders", async ({ adminPage }) => {
|
|
const stamp = Date.now();
|
|
const memberName = `E2E Member ${stamp}`;
|
|
const memberEmail = `e2e-member-${stamp}@example.test`;
|
|
|
|
await adminPage.goto("/admin/members");
|
|
await adminPage.waitForLoadState("networkidle");
|
|
await expect(adminPage.locator("h1")).toHaveText("Members");
|
|
|
|
await adminPage.getByRole("button", { name: "Add Member" }).click();
|
|
await adminPage.getByPlaceholder("Full name").fill(memberName);
|
|
await adminPage.getByPlaceholder("email@example.com").fill(memberEmail);
|
|
await adminPage.getByRole("button", { name: "Create Member" }).click();
|
|
|
|
// Verify the new member shows up via search
|
|
const searchInput = adminPage.getByPlaceholder("Search members...");
|
|
await expect(searchInput).toBeVisible({ timeout: 10000 });
|
|
await searchInput.fill(memberEmail);
|
|
|
|
const memberRow = adminPage.locator("tr", { hasText: memberEmail });
|
|
await expect(memberRow).toBeVisible({ timeout: 10000 });
|
|
await expect(memberRow.getByText(memberName)).toBeVisible();
|
|
|
|
// Open the edit modal for this member, where the STATUS_LABELS-driven <select> lives
|
|
await memberRow.getByRole("button", { name: "Edit" }).click();
|
|
|
|
const statusSelect = adminPage.locator(".modal select").filter({ hasText: "Active" });
|
|
await expect(statusSelect).toBeVisible({ timeout: 10000 });
|
|
|
|
// STATUS_LABELS keys (values) and the rendered labels
|
|
const expectedOptions = [
|
|
{ value: "active", label: "Active" },
|
|
{ value: "pending_payment", label: "Payment setup incomplete" },
|
|
{ value: "suspended", label: "Paused" },
|
|
{ value: "cancelled", label: "Closed" },
|
|
];
|
|
for (const { value, label } of expectedOptions) {
|
|
const opt = statusSelect.locator(`option[value="${value}"]`);
|
|
await expect(opt).toHaveCount(1);
|
|
await expect(opt).toHaveText(label);
|
|
}
|
|
|
|
// Change status to suspended and save
|
|
await statusSelect.selectOption("suspended");
|
|
await adminPage.getByRole("button", { name: "Save Changes" }).click();
|
|
|
|
// Modal closes; verify the row badge reflects the new status
|
|
await expect(adminPage.locator(".modal")).toHaveCount(0, { timeout: 10000 });
|
|
await expect(memberRow.getByText("Paused")).toBeVisible({ timeout: 10000 });
|
|
|
|
// Reload to confirm persistence
|
|
await adminPage.reload();
|
|
await adminPage.waitForLoadState("networkidle");
|
|
await adminPage.getByPlaceholder("Search members...").fill(memberEmail);
|
|
const reloadedRow = adminPage.locator("tr", { hasText: memberEmail });
|
|
await expect(reloadedRow.getByText("Paused")).toBeVisible({ timeout: 10000 });
|
|
|
|
// Click the member name (link to detail page) and verify URL + heading
|
|
await reloadedRow.getByRole("link", { name: memberName }).click();
|
|
await adminPage.waitForURL(/\/admin\/members\/[a-f0-9]{24}$/, { timeout: 10000 });
|
|
await expect(adminPage.locator("h1")).toHaveText(memberName);
|
|
await expect(adminPage.locator(".member-email")).toHaveText(memberEmail);
|
|
});
|
|
});
|