Diagnosis (reproduced by looping cold-server runs): the recurring "cold start" failures in the auth/follow specs were a hydration race, not a Vite dep-reload (the server.warmup config already handles that — no reload events in the logs). FollowButton is an onClick button, and Playwright considers it actionable (visible + enabled) before React attaches the handler on a freshly-navigated page. A click in that window is dropped (or triggers a native form submit), so the expected state change never happens — e.g. notifications.test.ts:59 clicked "Request to follow" with no hydration wait and the button never flipped to "Requested". The existing waitForHydration guard was applied per-interaction and so was easy to forget (test 1 had it, test 3 didn't). Fix it at the navigation instead: - add gotoHydrated(page, url) = goto + waitForHydration, documented as the default for "navigate then interact with a React control" - use it before every FollowButton interaction in notifications + social - consolidate the setProfileVisibility helper (copy-pasted in three specs, two missing the hydration wait) into e2e/helpers/profile.ts, built on gotoHydrated Validation against a fresh Postgres, cold servers: notifications 0/10 failures (was the proven flaker); notifications+social+explore 0/5 (55 tests); planner unaffected (25/25). Note: two other "cold-start"-attributed flakes are NOT this race and are out of scope here — planner map-load latency (mitigated by warmup + CI retries) and the /explore directory assertion (a local artifact of the scratch DB accumulating >20 public users; CI's fresh DB stays under the page size, so it doesn't bite there). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
87 lines
3.7 KiB
TypeScript
87 lines
3.7 KiB
TypeScript
import { test, expect, gotoHydrated, type CDPSession } from "./fixtures/test";
|
|
import { setupVirtualAuthenticator, registerUser } from "./helpers/auth";
|
|
import { setProfileVisibility } from "./helpers/profile";
|
|
|
|
test.describe.configure({ mode: "serial" });
|
|
|
|
test.describe("Notifications", () => {
|
|
test("anonymous /notifications redirects to login", async ({ page }) => {
|
|
await page.goto("/notifications");
|
|
await expect(page).toHaveURL(/\/auth\/login/, { timeout: 10000 });
|
|
});
|
|
|
|
test("public auto-accept follow → recipient sees follow_received notification", async ({ page, browser }) => {
|
|
const cdp = await page.context().newCDPSession(page);
|
|
await setupVirtualAuthenticator(cdp);
|
|
|
|
const stamp = Date.now();
|
|
const aEmail = `nf-a-${stamp}@example.com`;
|
|
const aUsername = `nfa${stamp}`;
|
|
const bEmail = `nf-b-${stamp}@example.com`;
|
|
const bUsername = `nfb${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);
|
|
await setProfileVisibility(bPage, "public");
|
|
|
|
// A follows B (auto-accept).
|
|
await gotoHydrated(page, `/users/${bUsername}`);
|
|
await page.getByRole("button", { name: "Follow" }).click();
|
|
await expect(page.getByRole("button", { name: "Unfollow" })).toBeVisible({ timeout: 5000 });
|
|
|
|
// B opens /notifications — should see one row referencing A.
|
|
await bPage.goto("/notifications");
|
|
await expect(bPage.getByText(new RegExp(`${aUsername}.+started following`))).toBeVisible({ timeout: 10000 });
|
|
|
|
// Mark all read clears the unread badge. The button disappears when
|
|
// hasUnread flips to false; the toBeHidden assertion polls.
|
|
await bPage.getByRole("button", { name: /Mark all read/i }).click();
|
|
await expect(bPage.getByRole("button", { name: /Mark all read/i })).toBeHidden({ timeout: 10000 });
|
|
|
|
await bCtx.close();
|
|
});
|
|
|
|
test("private profile: request → approve → follower sees follow_request_approved notification", async ({ page, browser }) => {
|
|
const cdp = await page.context().newCDPSession(page);
|
|
await setupVirtualAuthenticator(cdp);
|
|
|
|
const stamp = Date.now();
|
|
const aEmail = `nfap-a-${stamp}@example.com`;
|
|
const aUsername = `nfapa${stamp}`;
|
|
const bEmail = `nfap-b-${stamp}@example.com`;
|
|
const bUsername = `nfapb${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 private (default).
|
|
|
|
// A requests to follow B.
|
|
await gotoHydrated(page, `/users/${bUsername}`);
|
|
await page.getByRole("button", { name: /Request to follow/i }).click();
|
|
await expect(page.getByRole("button", { name: /Requested/i })).toBeVisible({ timeout: 5000 });
|
|
|
|
// B approves the request from the Requests tab on /notifications.
|
|
// After the fetcher.Form revalidates, the empty-state copy
|
|
// replaces the request row.
|
|
await bPage.goto("/notifications?tab=requests");
|
|
await bPage.getByRole("button", { name: "Approve" }).click();
|
|
await expect(bPage.getByText(/No pending follow requests/i)).toBeVisible({ timeout: 10000 });
|
|
|
|
// A reloads /notifications — should see follow_request_approved
|
|
// referencing B (the target's username).
|
|
await page.goto("/notifications");
|
|
await expect(page.getByText(new RegExp(`${bUsername}.+accepted your follow request`))).toBeVisible({ timeout: 10000 });
|
|
|
|
await bCtx.close();
|
|
});
|
|
});
|