import { test, expect, type CDPSession, type Page } from "./fixtures/test"; // Inline virtual-authenticator + register helpers, mirroring the pattern // in auth.test.ts / public-content.test.ts so this file runs standalone. async function setupVirtualAuthenticator(cdp: CDPSession) { await cdp.send("WebAuthn.enable"); const { authenticatorId } = await cdp.send("WebAuthn.addVirtualAuthenticator", { options: { protocol: "ctap2", transport: "internal", hasResidentKey: true, hasUserVerification: true, isUserVerified: true, }, }); return authenticatorId; } async function registerUser(page: Page, email: string, username: string) { await page.goto("/auth/register"); await expect(page.getByRole("heading", { name: "Register" })).toBeVisible(); await page.getByLabel("Email").fill(email); await page.getByLabel("Username").fill(username); await page.getByRole("checkbox").check(); await page.getByRole("button", { name: /Register with Passkey/ }).click(); await expect(page).toHaveURL("/", { timeout: 10000 }); } async function setProfileVisibility(page: Page, value: "public" | "private") { await page.goto("/settings"); // Target the radio by name+value; getByLabel collides because the // help text of one radio mentions the other's word ("public" appears // in the Private radio's helper sentence). await page.locator(`input[type=radio][name=profileVisibility][value=${value}]`).check(); await page.getByRole("button", { name: /^Save$/ }).first().click(); // SSE keeps the network busy; wait for the save confirmation instead. await expect(page.getByText("Profile saved.")).toBeVisible({ timeout: 10000 }); } // WebAuthn + parallel workers + shared local Postgres race; serialize. test.describe.configure({ mode: "serial" }); test.describe("Social follows + /feed", () => { test("/feed redirects anonymous visitors to login", async ({ page }) => { await page.goto("/feed"); await expect(page).toHaveURL(/\/auth\/login/, { timeout: 10000 }); }); test("/follows/requests redirects to /notifications?tab=requests", async ({ page, browser }) => { // The route was folded into the Notifications page as a tab. The // 301 still resolves; for anonymous visitors the notifications // loader then redirects to /auth/login. await page.goto("/follows/requests"); await expect(page).toHaveURL(/\/auth\/login/, { timeout: 10000 }); // Signed-in visitors land on /notifications?tab=requests after the // 301. const cdp = await page.context().newCDPSession(page); await setupVirtualAuthenticator(cdp); const stamp = Date.now(); const ctx = await browser.newContext(); const p2 = await ctx.newPage(); const p2cdp = await p2.context().newCDPSession(p2); await setupVirtualAuthenticator(p2cdp); await registerUser(p2, `redir-${stamp}@example.com`, `redir${stamp}`); await p2.goto("/follows/requests"); await expect(p2).toHaveURL(/\/notifications\?tab=requests/, { timeout: 10000 }); await ctx.close(); }); test("Follow button toggles state on a public profile", async ({ page, browser }) => { const cdp = await page.context().newCDPSession(page); await setupVirtualAuthenticator(cdp); const stamp = Date.now(); const aEmail = `social-a-${stamp}@example.com`; const aUsername = `sa${stamp}`; const bEmail = `social-b-${stamp}@example.com`; const bUsername = `sb${stamp}`; await registerUser(page, aEmail, aUsername); // Register B in a separate browser context so we have two independent // sessions. New users default to `private` — B flips to public so this // test exercises the auto-accept path. const bCtx = await browser.newContext(); const bPage = await bCtx.newPage(); const bCdp = await bPage.context().newCDPSession(bPage); await setupVirtualAuthenticator(bCdp); await registerUser(bPage, bEmail, bUsername); await setProfileVisibility(bPage, "public"); // A visits B's profile and follows. await page.goto(`/users/${bUsername}`); await expect(page.getByRole("button", { name: "Follow" })).toBeVisible(); await page.getByRole("button", { name: "Follow" }).click(); await expect(page.getByRole("button", { name: "Unfollow" })).toBeVisible({ timeout: 5000 }); // Follower count on B's profile should now read 1. await page.reload(); await expect(page.getByRole("link", { name: /1\s+Followers/i })).toBeVisible(); // Unfollow. await page.getByRole("button", { name: "Unfollow" }).click(); await expect(page.getByRole("button", { name: "Follow" })).toBeVisible({ timeout: 5000 }); await bCtx.close(); }); test("Private profile: stub for visitors, Request → Pending → Approve → full view", async ({ page, browser }) => { const cdp = await page.context().newCDPSession(page); await setupVirtualAuthenticator(cdp); const stamp = Date.now(); const aEmail = `req-a-${stamp}@example.com`; const aUsername = `ra${stamp}`; const bEmail = `req-b-${stamp}@example.com`; const bUsername = `rb${stamp}`; await registerUser(page, aEmail, aUsername); const bCtx = await browser.newContext(); const bPage = await bCtx.newPage(); const bCdp = await bPage.context().newCDPSession(bPage); await setupVirtualAuthenticator(bCdp); await registerUser(bPage, bEmail, bUsername); // B stays at the default `private`. Verify by loading the profile // anonymously and seeing the stub. const anonCtx = await browser.newContext(); const anon = await anonCtx.newPage(); const anonResp = await anon.goto(`/users/${bUsername}`); expect(anonResp?.status()).toBe(200); await expect(anon.getByText(/This profile is private/i)).toBeVisible(); // A (signed in) visits B's private profile — sees stub + Request button. await page.goto(`/users/${bUsername}`); await expect(page.getByText(/This profile is private/i)).toBeVisible(); await expect(page.getByRole("button", { name: /Request to follow/i })).toBeVisible(); await page.getByRole("button", { name: /Request to follow/i }).click(); await expect(page.getByRole("button", { name: /Requested/i })).toBeVisible({ timeout: 5000 }); // B sees the request in the Requests tab on /notifications. await bPage.goto("/notifications?tab=requests"); await expect(bPage.getByText(`@${aUsername}`)).toBeVisible(); await bPage.getByRole("button", { name: "Approve" }).click(); // Empty state after approval. The text-visibility assertion below // already polls; explicit networkidle would hang on SSE. await expect(bPage.getByText(/No pending follow requests/i)).toBeVisible(); // A reloads B's profile — now sees full content (no stub) + Unfollow. await page.goto(`/users/${bUsername}`); await expect(page.getByText(/This profile is private/i)).not.toBeVisible(); await expect(page.getByRole("button", { name: "Unfollow" })).toBeVisible(); await bCtx.close(); await anonCtx.close(); }); test("Private profile: visitor sees stub layout (200, not 404)", async ({ page, browser }) => { const cdp = await page.context().newCDPSession(page); await setupVirtualAuthenticator(cdp); const stamp = Date.now(); const username = `priv${stamp}`; await registerUser(page, `priv-${stamp}@example.com`, username); // User stays at default `private`. const anonCtx = await browser.newContext(); const anon = await anonCtx.newPage(); const resp = await anon.goto(`/users/${username}`); expect(resp?.status()).toBe(200); await expect(anon.getByText(/This profile is private/i)).toBeVisible(); await anonCtx.close(); }); });