288 lines
9.1 KiB
TypeScript
288 lines
9.1 KiB
TypeScript
/**
|
|
* 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 });
|
|
});
|
|
});
|