ghostguild-org/e2e/admin-members.spec.js
Jennie Robinson Faber 8dd55ccc09 test(e2e): expand coverage and harden cross-file isolation
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").
2026-04-30 22:26:11 +01:00

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);
});
});