trails/e2e/explore.test.ts
Ullrich Schäfer b7ed54ba72
Stabilize e2e against Vite dev hydration race
Two issues caused the same class of flake locally:

1. Default workers were CPU-count, but the journal/planner are served
   by Vite dev (not the production build CI uses). Cold-compiling
   `/api/auth/register` under N parallel hits produced 30s timeouts
   on a quarter of runs. Set workers to 1 in both environments for
   parity with CI.
2. Even sequentially, a button is clickable per Playwright's
   actionability check before React has hydrated its `onClick`. So
   the first click after a navigation could fire native form submit
   (or do nothing), which manifested as "URL never changed to /" or
   "menuitem Log Out never appeared". Add a `waitForHydration` helper
   that polls for React fibers (`__reactProps$<id>`) attached to a
   DOM node and call it after each cross-page navigation that ends
   in an interactive form or dropdown.

CI is unaffected (production builds hydrate fast and didn't expose
either bug), but the helper is harmless there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 01:06:27 +02:00

72 lines
2.8 KiB
TypeScript

import { test, expect, waitForHydration, type CDPSession, type Page } from "./fixtures/test";
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 waitForHydration(page);
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");
await page.locator(`input[type=radio][name=profileVisibility][value=${value}]`).check();
await page.getByRole("button", { name: /^Save$/ }).first().click();
await expect(page.getByText("Profile saved.")).toBeVisible({ timeout: 10000 });
}
test.describe.configure({ mode: "serial" });
test.describe("/explore", () => {
test("anonymous visitor can load /explore", async ({ page }) => {
const resp = await page.goto("/explore");
expect(resp?.status()).toBe(200);
await expect(page.getByRole("heading", { name: "Explore" })).toBeVisible();
});
test("private profile is excluded from the directory", async ({ page, browser }) => {
const cdp = await page.context().newCDPSession(page);
await setupVirtualAuthenticator(cdp);
const stamp = Date.now();
// A: signed-in viewer (public, default in test below)
const aEmail = `ex-a-${stamp}@example.com`;
const aUsername = `exa${stamp}`;
await registerUser(page, aEmail, aUsername);
await setProfileVisibility(page, "public");
// B: a separate user who stays at the default `private`
const bCtx = await browser.newContext();
const bPage = await bCtx.newPage();
const bCdp = await bPage.context().newCDPSession(bPage);
await setupVirtualAuthenticator(bCdp);
const bEmail = `ex-b-${stamp}@example.com`;
const bUsername = `exb${stamp}`;
await registerUser(bPage, bEmail, bUsername);
// B stays private — should NOT appear on /explore.
// A loads /explore — the directory should include A but not B.
await page.goto("/explore");
await expect(page.getByText(`@${aUsername}`)).toBeVisible({ timeout: 5000 });
await expect(page.getByText(`@${bUsername}`)).toHaveCount(0);
await bCtx.close();
});
});