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();