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>
21 lines
1.1 KiB
TypeScript
21 lines
1.1 KiB
TypeScript
import { expect, gotoHydrated, type Page } from "../fixtures/test";
|
|
|
|
/**
|
|
* Set the logged-in user's profile visibility via /settings/profile.
|
|
* Single source of truth — this was copy-pasted across the explore,
|
|
* social, and notifications specs, two of which omitted the hydration
|
|
* wait and so raced the fetcher-backed Save on a cold server.
|
|
*/
|
|
export async function setProfileVisibility(page: Page, value: "public" | "private") {
|
|
// gotoHydrated so the visibility form submits via the fetcher (and
|
|
// shows the "Profile saved." toast) instead of native-navigating.
|
|
await gotoHydrated(page, "/settings/profile");
|
|
// Target the radio by name+value; getByLabel collides because one
|
|
// radio's help text 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, so don't wait on networkidle — wait for
|
|
// the explicit save confirmation instead.
|
|
await expect(page.getByText("Profile saved.")).toBeVisible({ timeout: 10000 });
|
|
}
|