/** * Comments Moderation E2E Tests * * Tests the admin comment moderation inbox at /comments. * Seeds comments via the public API, then exercises the moderation UI: * page rendering, empty state, comment list, approve, and delete. */ import { test, expect } from "../fixtures"; // Regex patterns (e18e/prefer-static-regex) const PENDING_EMPTY_PATTERN = /no comments awaiting moderation/i; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function apiHeaders(token: string) { return { "Content-Type": "application/json", Authorization: `Bearer ${token}`, "X-EmDash-Request": "1", }; } /** Seed a comment via the public API and return the response body. */ async function seedComment( page: import("@playwright/test").Page, baseUrl: string, token: string, postId: string, overrides: { body?: string; authorName?: string; authorEmail?: string } = {}, ) { const res = await page.request.post(`${baseUrl}/_emdash/api/comments/posts/${postId}`, { headers: apiHeaders(token), data: { body: overrides.body ?? "Test comment from E2E", authorName: overrides.authorName ?? "E2E Tester", authorEmail: overrides.authorEmail ?? "e2e@test.com", }, }); return res; } /** Delete all comments currently in the admin inbox (best-effort cleanup). */ async function cleanupComments( page: import("@playwright/test").Page, baseUrl: string, token: string, ) { const headers = apiHeaders(token); for (const status of ["pending", "approved", "spam", "trash"] as const) { const res = await page.request.fetch( `${baseUrl}/_emdash/api/admin/comments?status=${status}&limit=100`, { headers }, ); if (!res.ok()) continue; const data: { data?: { items?: { id: string }[] } } = await res.json().catch(() => ({})); const ids = data?.data?.items?.map((c) => c.id) ?? []; if (ids.length === 0) continue; await page.request .post(`${baseUrl}/_emdash/api/admin/comments/bulk`, { headers, data: { ids, action: "delete" }, }) .catch(() => {}); } } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- test.describe("Comments Moderation", () => { test.beforeEach(async ({ admin }) => { await admin.devBypassAuth(); }); test("page renders with correct title", async ({ admin }) => { await admin.goto("/comments"); await admin.waitForShell(); await admin.waitForLoading(); await admin.expectPageTitle("Comments"); }); test("shows empty state when no comments exist", async ({ admin, page, serverInfo }) => { // Clean up any existing comments first await cleanupComments(page, serverInfo.baseUrl, serverInfo.token); await admin.goto("/comments"); await admin.waitForShell(); await admin.waitForLoading(); // The "Pending" tab is active by default -- should show empty message await expect(page.locator("td").filter({ hasText: PENDING_EMPTY_PATTERN })).toBeVisible({ timeout: 10000, }); }); test("displays seeded comments with author and body", async ({ admin, page, serverInfo }) => { const postId = serverInfo.contentIds.posts[0]!; // Clean slate await cleanupComments(page, serverInfo.baseUrl, serverInfo.token); // Seed two comments await seedComment(page, serverInfo.baseUrl, serverInfo.token, postId, { body: "First seeded comment", authorName: "Alice Commenter", authorEmail: "alice@test.com", }); await seedComment(page, serverInfo.baseUrl, serverInfo.token, postId, { body: "Second seeded comment", authorName: "Bob Commenter", authorEmail: "bob@test.com", }); await admin.goto("/comments"); await admin.waitForShell(); await admin.waitForLoading(); // Comments land as "pending" by default (moderation: first_time or all). // The Pending tab is the default view, so we should see them. // If the collection auto-approves, check the Approved tab instead. const approvedTab = page.locator('[role="tab"]', { hasText: "Approved" }); // Try pending first -- if empty, check approved let foundAlice = await page .locator("text=Alice Commenter") .isVisible({ timeout: 5000 }) .catch(() => false); if (!foundAlice) { // Comments may have been auto-approved await approvedTab.click(); await admin.waitForLoading(); foundAlice = await page .locator("text=Alice Commenter") .isVisible({ timeout: 5000 }) .catch(() => false); } // At least one comment should be visible with author name and body expect(foundAlice).toBe(true); await expect(page.locator("text=First seeded comment").first()).toBeVisible(); await expect(page.locator("text=Bob Commenter").first()).toBeVisible(); await expect(page.locator("text=Second seeded comment").first()).toBeVisible(); }); test("approve a pending comment", async ({ admin, page, serverInfo }) => { const postId = serverInfo.contentIds.posts[0]!; // Clean slate await cleanupComments(page, serverInfo.baseUrl, serverInfo.token); // Seed a comment await seedComment(page, serverInfo.baseUrl, serverInfo.token, postId, { body: "Comment to approve", authorName: "Approval Tester", authorEmail: "approve@test.com", }); await admin.goto("/comments"); await admin.waitForShell(); await admin.waitForLoading(); // Ensure we're on the Pending tab const pendingTab = page.locator('[role="tab"]', { hasText: "Pending" }); await pendingTab.click(); await admin.waitForLoading(); // If the comment was auto-approved, this test cannot proceed -- skip gracefully const hasPendingComment = await page .locator("text=Approval Tester") .isVisible({ timeout: 5000 }) .catch(() => false); if (!hasPendingComment) { // Comment was auto-approved -- verify it's in the Approved tab instead const approvedTab = page.locator('[role="tab"]', { hasText: "Approved" }); await approvedTab.click(); await admin.waitForLoading(); await expect(page.locator("text=Approval Tester").first()).toBeVisible({ timeout: 5000, }); return; } // Find the comment row and click the Approve button const row = page.locator("tr", { hasText: "Approval Tester" }); const approveBtn = row.locator('button[aria-label="Approve"]'); await expect(approveBtn).toBeVisible({ timeout: 5000 }); await approveBtn.click(); // Wait for the mutation to settle await admin.waitForLoading(); // The comment should disappear from the Pending tab await expect(page.locator("tr", { hasText: "Approval Tester" })).not.toBeVisible({ timeout: 10000, }); // Verify it moved to the Approved tab const approvedTab = page.locator('[role="tab"]', { hasText: "Approved" }); await approvedTab.click(); await admin.waitForLoading(); await expect(page.locator("text=Approval Tester").first()).toBeVisible({ timeout: 10000, }); }); test("delete a comment permanently", async ({ admin, page, serverInfo }) => { const postId = serverInfo.contentIds.posts[0]!; // Clean slate await cleanupComments(page, serverInfo.baseUrl, serverInfo.token); // Seed a comment await seedComment(page, serverInfo.baseUrl, serverInfo.token, postId, { body: "Comment to delete", authorName: "Delete Tester", authorEmail: "delete@test.com", }); await admin.goto("/comments"); await admin.waitForShell(); await admin.waitForLoading(); // Find the comment -- could be in Pending or Approved let commentVisible = await page .locator("text=Delete Tester") .isVisible({ timeout: 5000 }) .catch(() => false); if (!commentVisible) { const approvedTab = page.locator('[role="tab"]', { hasText: "Approved" }); await approvedTab.click(); await admin.waitForLoading(); commentVisible = await page .locator("text=Delete Tester") .isVisible({ timeout: 5000 }) .catch(() => false); } expect(commentVisible).toBe(true); // The admin user should see the "Delete permanently" button const row = page.locator("tr", { hasText: "Delete Tester" }); const deleteBtn = row.locator('button[aria-label="Delete permanently"]'); const hasDeleteBtn = await deleteBtn.isVisible({ timeout: 3000 }).catch(() => false); if (!hasDeleteBtn) { // Fallback: try the "Trash" button instead (non-admin role) const trashBtn = row.locator('button[aria-label="Trash"]'); await expect(trashBtn).toBeVisible({ timeout: 3000 }); await trashBtn.click(); await admin.waitForLoading(); // Comment should disappear from the current tab await expect(row).not.toBeVisible({ timeout: 10000 }); return; } // Click "Delete permanently" -- this opens a ConfirmDialog await deleteBtn.click(); // Confirm deletion in the dialog const dialog = page.locator('[role="dialog"]'); await expect(dialog).toBeVisible({ timeout: 5000 }); await expect(dialog.locator("text=Delete Comment")).toBeVisible(); await dialog.getByRole("button", { name: "Delete" }).click(); // Wait for dialog to close and comment to disappear await expect(dialog).not.toBeVisible({ timeout: 10000 }); await admin.waitForLoading(); // Comment should be gone from all tabs await expect(page.locator("text=Delete Tester")).not.toBeVisible({ timeout: 10000 }); }); });