From abb754e6a5db5562e911d20b6af281e970904c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 26 Apr 2026 01:37:37 +0200 Subject: [PATCH] Replace waitForLoadState("networkidle") in e2e helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SSE connection to /api/events (added with notifications) keeps the network in-flight forever, so `networkidle` never resolves. Each save in the affected helpers timed out at 30s × 3 retries, which both broke the run and made it suspiciously long. Switched to explicit waits: - "Profile saved." text after settings save (notifications + public-content + social) - "No pending follow requests." after Approve (social.test.ts) - toBeHidden poll for the Mark all read button (notifications) - toBeVisible poll for the empty-state copy after Approve (notifications) These are all the affected `networkidle` call sites in e2e. The fetcher.Form pattern stays — once the action commits, the page revalidates and the post-state element appears. Co-Authored-By: Claude Opus 4.7 (1M context) --- e2e/notifications.test.ts | 16 ++++++++++------ e2e/public-content.test.ts | 5 ++++- e2e/social.test.ts | 7 ++++--- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/e2e/notifications.test.ts b/e2e/notifications.test.ts index 8e967a5..4d9e704 100644 --- a/e2e/notifications.test.ts +++ b/e2e/notifications.test.ts @@ -28,7 +28,10 @@ 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 page.waitForLoadState("networkidle"); + // Don't use waitForLoadState("networkidle") — the SSE connection to + // /api/events keeps the network busy indefinitely. Wait for the + // explicit save confirmation instead. + await expect(page.getByText("Profile saved.")).toBeVisible({ timeout: 10000 }); } test.describe.configure({ mode: "serial" }); @@ -67,10 +70,10 @@ test.describe("Notifications", () => { await bPage.goto("/notifications"); await expect(bPage.getByText(new RegExp(`${aUsername}.+started following`))).toBeVisible({ timeout: 10000 }); - // Mark all read clears the unread badge. + // 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 bPage.waitForLoadState("networkidle"); - await expect(bPage.getByRole("button", { name: /Mark all read/i })).toBeHidden(); + await expect(bPage.getByRole("button", { name: /Mark all read/i })).toBeHidden({ timeout: 10000 }); await bCtx.close(); }); @@ -99,10 +102,11 @@ test.describe("Notifications", () => { 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. + // B approves the request. After the fetcher.Form revalidates, the + // empty-state copy replaces the request row. await bPage.goto("/follows/requests"); await bPage.getByRole("button", { name: "Approve" }).click(); - await bPage.waitForLoadState("networkidle"); + 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). diff --git a/e2e/public-content.test.ts b/e2e/public-content.test.ts index 5fd2243..f0941f7 100644 --- a/e2e/public-content.test.ts +++ b/e2e/public-content.test.ts @@ -58,7 +58,10 @@ async function setProfileVisibilityPublic(page: Page) { await page.goto("/settings"); await page.locator('input[type=radio][name=profileVisibility][value=public]').check(); await page.getByRole("button", { name: /^Save$/ }).first().click(); - await page.waitForLoadState("networkidle"); + // Don't wait for networkidle — the SSE connection to /api/events + // (added with notifications) keeps the network in-flight forever. + // Wait for the explicit save confirmation instead. + await expect(page.getByText("Profile saved.")).toBeVisible({ timeout: 10000 }); } // Registration + WebAuthn virtual authenticator can race under parallel diff --git a/e2e/social.test.ts b/e2e/social.test.ts index dd0ca1b..b5651f0 100644 --- a/e2e/social.test.ts +++ b/e2e/social.test.ts @@ -33,7 +33,8 @@ async function setProfileVisibility(page: Page, value: "public" | "private") { // 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(); - await page.waitForLoadState("networkidle"); + // 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. @@ -126,8 +127,8 @@ test.describe("Social follows + /feed", () => { await bPage.goto("/follows/requests"); await expect(bPage.getByText(`@${aUsername}`)).toBeVisible(); await bPage.getByRole("button", { name: "Approve" }).click(); - await bPage.waitForLoadState("networkidle"); - // Empty state after approval. + // 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.