From e77ad2ba294b15f993cd10a7d34974200347b455 Mon Sep 17 00:00:00 2001 From: Krasimir Kostadinov <12051991+krazymir@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:29:00 +0300 Subject: [PATCH] fix(e2e): ride out dev-server stalls in sign-in; await the profile save before reloading Jenkins build 191 failed on one spec and flagged five as flaky. Every failure lined up with two windows where the next dev server stalled for up to 30 s per request (Turbopack cache compaction), plus one genuine race. - discovery.spec: the e-mail switch flips optimistically before its PATCH lands, and the test reloaded straight away, aborting the save ("client disconnected" for /api/profile) so the reload read the old value. It now waits for the PATCH response before reloading. - helpers/auth signIn: the whole sign-in is retried (up to 100 s), not just the panel load. Each attempt waits for the account menu or the dev panel; if the menu is already there, an earlier slow callback landed and the persona is not posted again (which would re-seed it, e.g. reset fresh onboarding). - public-submissions.spec: the two end-to-end flows (submit/confirm/accept and Specific people) get test.slow(); the Specific-people flow cannot fit 120 s on a stalled server and failed all three attempts. Test-only: no version bump, no changelog entry. --- packages/web/e2e/discovery.spec.ts | 5 +++ packages/web/e2e/helpers/auth.ts | 43 +++++++++++++-------- packages/web/e2e/public-submissions.spec.ts | 7 ++++ 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/packages/web/e2e/discovery.spec.ts b/packages/web/e2e/discovery.spec.ts index 4581f6c..87a78d9 100644 --- a/packages/web/e2e/discovery.spec.ts +++ b/packages/web/e2e/discovery.spec.ts @@ -125,7 +125,12 @@ test("profile: the e-mail-notifications toggle round-trips, and the public profi const emailSwitch = page.getByRole("switch", { name: "E-mail notifications" }); const before = await emailSwitch.getAttribute("aria-checked"); const after = before === "true" ? "false" : "true"; + // The switch flips optimistically, before its PATCH lands. Reloading before the server has + // answered aborts the request (logged as "client disconnected") and the reload then reads the + // old value — so hold the PATCH's response and reload only once it has been committed. + const saved = page.waitForResponse((r) => r.url().endsWith("/api/profile") && r.request().method() === "PATCH"); await emailSwitch.click(); + expect((await saved).status()).toBe(200); await expect(emailSwitch).toHaveAttribute("aria-checked", after); // The page carries one switch per notification preference (§12.1) — read the word that sits // beside THIS switch, inside its own .toggle-field. diff --git a/packages/web/e2e/helpers/auth.ts b/packages/web/e2e/helpers/auth.ts index 78281d2..7f8adb0 100644 --- a/packages/web/e2e/helpers/auth.ts +++ b/packages/web/e2e/helpers/auth.ts @@ -41,25 +41,34 @@ export async function signIn( // if that chain breaks — e.g. a page loaded while Next compiles /_error mid-run (it does so on // the first aborted request) never hydrates — so a load showing no panel within 20 s is // retried with a fresh navigation instead of being waited on until the test times out. + // + // The whole sign-in is retried, not just the panel load: a dev server that stalls (Turbopack + // cache compaction, a heap-pressure pause) can hold the callback or the post-sign-in render + // past any single wait, and the attempt that follows must not leave the test stuck. + const menu = page.locator(".user-trigger"); + const panel = page.locator("#dev-name"); await expect(async () => { await page.goto(`${base}/`); - await page.locator("#dev-name").waitFor({ state: "visible", timeout: 20_000 }); - }).toPass({ timeout: 75_000 }); - await page.locator("#dev-name").fill(opts.name); - await page.locator("#dev-email").fill(`${devSlug(opts.name)}@dev.test`); - await page.locator("#dev-admin").fill(opts.admin ? "1" : "0"); - await page.locator("#dev-fresh-onboarding").fill(opts.freshOnboarding ? "1" : "0"); - await page.locator("#dev-whats-new-seen").fill(opts.whatsNewSeen ?? ""); - await page.locator("#dev-whats-new-since").fill(opts.whatsNewSinceDate ?? ""); - // §13.10: "1" = survey-eligible with every roll forced to win; otherwise surveys are off. - await page.locator("#dev-survey").fill(opts.survey ? "1" : "0"); - await page.getByRole("button", { name: "Dev sign-in" }).click(); - // On success the app returns to "/" authenticated and the account menu (.user-trigger, rendered - // only when signed in) appears; on failure it stays on the signed-out landing with the dev - // panel — so waiting for the account menu both confirms success and fails fast otherwise. - // 30 s like the suite's expect timeout: a fresh-onboarding persona is redirected to - // /quick-start server-side, and that route's first hit compiles cold in dev mode. - await page.locator(".user-trigger").waitFor({ state: "visible", timeout: 30_000 }); + // Whichever renders first: the account menu means an earlier attempt's callback landed after + // its wait gave up — the session cookie is already set, so it must not be posted again (that + // would re-seed the persona, e.g. reset a fresh-onboarding flag the test relies on). + await menu.or(panel).first().waitFor({ state: "visible", timeout: 20_000 }); + if (await menu.isVisible()) return; + await panel.fill(opts.name); + await page.locator("#dev-email").fill(`${devSlug(opts.name)}@dev.test`); + await page.locator("#dev-admin").fill(opts.admin ? "1" : "0"); + await page.locator("#dev-fresh-onboarding").fill(opts.freshOnboarding ? "1" : "0"); + await page.locator("#dev-whats-new-seen").fill(opts.whatsNewSeen ?? ""); + await page.locator("#dev-whats-new-since").fill(opts.whatsNewSinceDate ?? ""); + // §13.10: "1" = survey-eligible with every roll forced to win; otherwise surveys are off. + await page.locator("#dev-survey").fill(opts.survey ? "1" : "0"); + await page.getByRole("button", { name: "Dev sign-in" }).click(); + // On success the app returns to "/" authenticated and the account menu (.user-trigger, + // rendered only when signed in) appears; on failure it stays on the signed-out landing with + // the dev panel. 30 s like the suite's expect timeout: a fresh-onboarding persona is + // redirected to /quick-start server-side, and that route's first hit compiles cold in dev. + await menu.waitFor({ state: "visible", timeout: 30_000 }); + }).toPass({ timeout: 100_000 }); const meRes = await context.request.get(`${base}/api/me`, { headers: { accept: "application/json" } }); const me = (await meRes.json()) as { user?: { id?: string }; roles?: { platformAdmin?: boolean } }; diff --git a/packages/web/e2e/public-submissions.spec.ts b/packages/web/e2e/public-submissions.spec.ts index 0e2df6e..2daf83d 100644 --- a/packages/web/e2e/public-submissions.spec.ts +++ b/packages/web/e2e/public-submissions.spec.ts @@ -83,6 +83,9 @@ test("while the switch is off, /submit and every public route answer 404", async }); test("submit on /submit, confirm through the link, accept in the queue → External submission in triage", async ({ browser }) => { + // Sign-ins, a proof-of-work wait, confirm and accept run end to end: on a dev server that stalls + // for a few seconds per request this outgrows the default budget, so it gets three times it. + test.slow(); const stamp = uniqueStamp(); const admin = await browser.newContext(); const outsider = await browser.newContext(); // never signed in @@ -183,6 +186,10 @@ test("submit on /submit, confirm through the link, accept in the queue → Exter }); test("Specific people: the tag field, the pre-filled Accept, and sharing once the challenge leaves triage", async ({ browser }) => { + // Three sign-ins, a proof-of-work wait, confirm, accept, a triage move and the inbox check: + // the longest flow in the suite, and the first to outgrow the default budget when the dev + // server stalls — so it gets three times it. + test.slow(); const stamp = uniqueStamp(); const admin = await browser.newContext(); const listed = await browser.newContext();