import { expect, test } from "@playwright/test"; import { captureBrowserFailures, loginWithMagicLink, projectEmail, projectText, registerAndConfirm, selectOptionContaining, waitForMapReady, } from "./helpers"; test("activity approval, privacy controls, reporting, and moderation work end to end", async ({ browser, request, }, testInfo) => { const projectName = testInfo.project.name; const organizerEmail = projectEmail("activity-organizer", projectName); const participantEmail = projectEmail("activity-participant", projectName); const activityTitle = projectText("E2E public coffee meetup", projectName); const organizerMessageText = projectText( "Welcome to the E2E activity group.", projectName, ); const reportDescription = projectText( "E2E moderation evidence for the selected activity message.", projectName, ); const proposalTitle = projectText("E2E quiet board games", projectName); const organizer = await registerAndConfirm( browser, request, organizerEmail, "E2E Activity Organizer", ); const participant = await registerAndConfirm( browser, request, participantEmail, "E2E Activity Participant", ); const assertOrganizerClean = captureBrowserFailures(organizer.page); const assertParticipantClean = captureBrowserFailures(participant.page); await participant.page.goto("/moderation"); await expect(participant.page).toHaveURL(/\/requests$/); await expect(participant.page.getByText("Moderator access is required.")).toBeVisible(); await organizer.page.goto("/activities/new"); await selectOptionContaining(organizer.page, "Activity type", "Coffee or tea"); await organizer.page.getByLabel("Preferred setting").selectOption("cafe"); await organizer.page.getByLabel("Short title").fill(activityTitle); await organizer.page .getByLabel("Plan and expectations") .fill("Meet at a public café for an hour of safe conversation."); const startsAt = new Date(Date.now() + 4 * 60 * 60 * 1000).toISOString().slice(0, 16); const joinDeadline = new Date(Date.now() + 2 * 60 * 60 * 1000) .toISOString() .slice(0, 16); await organizer.page.getByLabel("Starts at (UTC)").fill(startsAt); await organizer.page.getByLabel("Join requests close (UTC)").fill(joinDeadline); await organizer.page.getByLabel("Total group capacity").fill("3"); await organizer.page .getByLabel("Approximate public meeting area") .fill("E2E central public café"); await organizer.page.getByLabel("Public map visibility").selectOption("approximate_public"); await organizer.page.getByLabel("Latitude").fill("50.4501"); await organizer.page.getByLabel("Longitude").fill("30.5234"); await organizer.page.locator("#activity-form input[type=checkbox]").check(); await organizer.page.getByRole("button", { name: "Publish activity" }).click(); await expect(organizer.page).toHaveURL(/\/activities\/[0-9a-f-]+$/); const activityURL = organizer.page.url(); await participant.page.goto(activityURL); await expect(participant.page.getByRole("heading", { name: "Approved group chat" })).toHaveCount( 0, ); await participant.page.getByRole("button", { name: "Request to join" }).click(); await expect(participant.page.getByText("Approval pending")).toBeVisible(); await expect(organizer.page.getByText("E2E Activity Participant")).toBeVisible(); const organizerControls = organizer.page .getByRole("heading", { name: "Organizer controls" }) .locator(".."); await organizerControls.getByRole("button", { name: "Approve" }).click(); await expect( participant.page.getByRole("heading", { name: "Approved group chat" }), ).toBeVisible(); await participant.page .getByPlaceholder("Message the approved group") .fill("I will meet you by the public entrance."); await participant.page.getByRole("button", { name: "Send", exact: true }).click(); await expect(organizer.page.getByText("I will meet you by the public entrance.")).toBeVisible(); await organizer.page .getByPlaceholder("Message the approved group") .fill(organizerMessageText); await organizer.page.getByRole("button", { name: "Send", exact: true }).click(); await expect(participant.page.getByText(organizerMessageText)).toBeVisible(); const organizerMessage = participant.page .locator("#activity-messages article") .filter({ hasText: organizerMessageText }); await organizerMessage.getByRole("button", { name: "Report" }).click(); const safetyControls = participant.page .getByRole("heading", { name: "Safety controls" }) .locator(".."); await expect( safetyControls.getByText("The report is scoped to the selected group message."), ).toBeVisible(); await safetyControls.getByLabel("Reason").selectOption("harassment"); await safetyControls .getByLabel("What happened?") .fill(reportDescription); await safetyControls.getByRole("button", { name: "Send report" }).click(); await expect(participant.page.getByText("Report sent to moderators.")).toBeVisible(); await waitForMapReady(participant.page); await participant.page.goto("/categories/proposals"); await participant.page.getByLabel("Proposed category").fill(proposalTitle); await participant.page.getByLabel("Mode").selectOption("activity"); await participant.page .getByLabel("Why is this useful?") .fill("A moderated public activity category for quiet board-game meetups."); await participant.page.getByRole("button", { name: "Publish proposal" }).click(); await expect( participant.page.getByText("Proposal published for community voting."), ).toBeVisible(); await participant.page.goto(activityURL); await waitForMapReady(participant.page); await participant.page.getByRole("button", { name: "Block organizer" }).click(); await expect(participant.page).toHaveURL(/\/activities$/); await waitForMapReady(participant.page); await participant.page.goto("/profile"); const blockedUsers = participant.page.getByRole("heading", { name: "Blocked users" }).locator(".."); await expect(blockedUsers.getByText("E2E Activity Organizer")).toBeVisible(); await blockedUsers.getByRole("button", { name: "Unblock" }).click(); await expect(participant.page.getByText("User unblocked.")).toBeVisible(); await participant.page.getByLabel("Default location visibility").selectOption( "exact_for_active_match", ); await participant.page .getByLabel("Optional external thank-you link") .fill("https://example.invalid/e2e-thanks"); await participant.page.getByRole("button", { name: "Save profile" }).click(); await expect(participant.page.getByText("Profile updated.")).toBeVisible(); await participant.page.getByLabel("Network").selectOption("telegram"); await participant.page.getByLabel("Public profile URL").fill("https://t.me/e2e_participant"); await participant.page.getByLabel("Handle (optional)").fill("@e2e_participant"); await participant.page.getByRole("button", { name: "Add unverified link" }).click(); await expect(participant.page.getByText("Social link added as unverified.")).toBeVisible(); const socialSection = participant.page .getByRole("heading", { name: "Social links" }) .locator("xpath=ancestor::section"); await expect(socialSection.getByText("telegram", { exact: true })).toBeVisible(); await socialSection.getByRole("button", { name: "Remove" }).click(); await expect(socialSection.getByText("No social links added.")).toBeVisible(); await participant.page.goto("/requests/new"); await expect(participant.page.getByLabel("Location visibility")).toHaveValue( "exact_for_active_match", ); const admin = await loginWithMagicLink(browser, request, "e2e-admin@example.invalid"); const assertAdminClean = captureBrowserFailures(admin.page); await admin.page.goto("/moderation"); await expect(admin.page.getByRole("heading", { name: "Moderation" })).toBeVisible(); const reportCard = admin.page .locator("article") .filter({ hasText: reportDescription }); await reportCard.getByRole("button", { name: "View scoped evidence" }).click(); const evidence = admin.page .getByRole("heading", { name: "Reported evidence" }) .locator("xpath=ancestor::section"); await expect(evidence.getByText(organizerMessageText)).toBeVisible(); await evidence.getByRole("button", { name: "Close" }).click(); await reportCard.getByRole("combobox").selectOption("resolved"); await reportCard.getByPlaceholder("Resolution note").fill("Reviewed in isolated browser E2E."); await reportCard.getByRole("button", { name: "Save" }).click(); await expect(admin.page.getByText("Report updated.")).toBeVisible(); const proposalCard = admin.page .locator("article") .filter({ hasText: proposalTitle }); await proposalCard.getByPlaceholder("Rejection note").fill("Covered by an existing category."); await proposalCard.getByRole("button", { name: "Reject" }).click(); await expect(admin.page.getByText("Proposal rejected.")).toBeVisible(); const participantRow = admin.page .locator("tbody tr") .filter({ hasText: participantEmail }); await participantRow.getByRole("combobox").first().selectOption("restricted"); await participantRow.getByPlaceholder("Internal note").fill("E2E restriction boundary."); await participantRow.getByRole("button", { name: "Save" }).click(); await expect(admin.page.getByText("User status updated.")).toBeVisible(); await participant.page.goto("/categories/proposals"); await participant.page.getByLabel("Proposed category").fill("E2E restricted proposal"); await participant.page.getByLabel("Mode").selectOption("help"); await participant.page .getByLabel("Why is this useful?") .fill("This must be rejected because the account is restricted."); await participant.page.getByRole("button", { name: "Publish proposal" }).click(); await expect( participant.page.getByText("Confirm your account and ensure it is active first."), ).toBeVisible(); await waitForMapReady(organizer.page); assertOrganizerClean(); assertParticipantClean(); assertAdminClean(); await organizer.context.close(); await participant.context.close(); await admin.context.close(); });