import { test, expect, Page } from "@playwright/test"; // Admin credentials from seed data const ADMIN_EMAIL = "admin@example.com"; const ADMIN_PASSWORD = "admin123"; // Regular user from seed data const REGULAR_USER_EMAIL = "user@example.com"; async function loginAsAdmin(page: Page) { await page.goto("/login"); await page.fill('input[type="email"]', ADMIN_EMAIL); await page.fill('input[type="password"]', ADMIN_PASSWORD); await page.click('button[type="submit"]'); await expect(page).toHaveURL("/admin/trades"); } test.describe("Admin Invites Page", () => { test.beforeEach(async ({ page }) => { await page.context().clearCookies(); await loginAsAdmin(page); }); test("admin can access invites page and UI elements are correct", async ({ page }) => { await page.goto("/admin/invites"); // Check page headings await expect(page.getByRole("heading", { name: "Create Invite" })).toBeVisible(); await expect(page.getByRole("heading", { name: "All Invites" })).toBeVisible(); // The godfather selector should be a const selectElement = page.locator("select").first(); await expect(selectElement).toBeVisible(); // Wait for users to load by checking for a known user in the dropdown await expect(selectElement).toContainText(REGULAR_USER_EMAIL); // Verify it has user options (at least the seeded users) const options = selectElement.locator("option"); const optionCount = await options.count(); // Should have at least 2 options: placeholder + at least one user expect(optionCount).toBeGreaterThanOrEqual(2); // There should NOT be a number input for godfather ID const numberInput = page.locator('input[type="number"]'); await expect(numberInput).toHaveCount(0); }); test("can create invite with proper button state management", async ({ page }) => { await page.goto("/admin/invites"); // Wait for page to load await page.waitForSelector("select"); // The create button should be disabled initially (no user selected) const createButton = page.locator('button:has-text("Create Invite")'); await expect(createButton).toBeDisabled(); // Select a user const godfatherSelect = page.locator("select").first(); await godfatherSelect.selectOption({ label: REGULAR_USER_EMAIL }); // Now the button should be enabled await expect(createButton).toBeEnabled(); // Click create invite await page.click('button:has-text("Create Invite")'); // Wait for the invite to appear in the table await expect(page.locator("table")).toContainText(REGULAR_USER_EMAIL); // Verify an invite code appears (format: word-word-NN) const inviteCodeCell = page.locator("td").first(); await expect(inviteCodeCell).toHaveText(/^[a-z]+-[a-z]+-\d{2}$/); }); test("can revoke invite and filter by status", async ({ page }) => { await page.goto("/admin/invites"); await page.waitForSelector("select"); // Create an invite first const godfatherSelect = page.locator("select").first(); await godfatherSelect.selectOption({ label: REGULAR_USER_EMAIL }); // Wait for create invite response const createPromise = page.waitForResponse( (resp) => resp.url().includes("/api/admin/invites") && resp.request().method() === "POST" ); await page.click('button:has-text("Create Invite")'); await createPromise; // Wait for table to update with new invite await expect(page.locator("table")).toContainText("ready"); // Wait for the new invite to appear and capture its code const newInviteRow = page .locator("tr") .filter({ hasText: REGULAR_USER_EMAIL }) .filter({ hasText: "ready" }) .first(); await expect(newInviteRow).toBeVisible(); // Get the invite code from this row (first cell) const inviteCode = await newInviteRow.locator("td").first().textContent(); // Click revoke and wait for the response // The revoke endpoint is POST /api/admin/invites/{invite_id}/revoke const revokePromise = page.waitForResponse( (resp) => resp.url().includes("/api/admin/invites") && resp.url().includes("/revoke") && resp.request().method() === "POST" ); await newInviteRow.locator('button:has-text("Revoke")').click(); await revokePromise; // Wait for table to refresh and verify this specific invite now shows "revoked" const revokedRow = page.locator("tr").filter({ hasText: inviteCode! }); await expect(revokedRow).toContainText("revoked", { timeout: 5000 }); // Test status filter - filter by "revoked" status const statusFilter = page.locator("select").nth(1); // Second select is the status filter await statusFilter.selectOption("revoked"); // Wait for the filter to apply and verify revoked invite is visible await page.waitForResponse((resp) => resp.url().includes("status=revoked")); await expect(revokedRow).toBeVisible({ timeout: 5000 }); // Filter by "ready" status - should not show our revoked invite await statusFilter.selectOption("ready"); await page.waitForResponse((resp) => resp.url().includes("status=ready")); await expect(revokedRow).not.toBeVisible({ timeout: 5000 }); }); }); test.describe("Admin Invites Access Control", () => { test("regular user and unauthenticated user cannot access admin invites page", async ({ page, }) => { // Test unauthenticated access await page.context().clearCookies(); await page.goto("/admin/invites"); await expect(page).toHaveURL("/login"); // Test regular user access await page.goto("/login"); await page.fill('input[type="email"]', REGULAR_USER_EMAIL); await page.fill('input[type="password"]', "user123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL("/"); // Try to access admin invites page await page.goto("/admin/invites"); // Should be redirected away (to home page based on fallbackRedirect) await expect(page).not.toHaveURL("/admin/invites"); }); });