From ab2c323abf236d81a9715be0a2ee08a0fabf933f Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sat, 22 Aug 2026 15:43:16 +0300 Subject: [PATCH] Fix E2E test --- app/root.tsx | 6 ++++++ e2e/helpers/playwright.ts | 31 ++++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/app/root.tsx b/app/root.tsx index 4bef1f6cd..395b99de6 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -22,6 +22,7 @@ import { useFetchers, useHref, useLoaderData, + useLocation, useMatches, useNavigate, useNavigation, @@ -445,6 +446,7 @@ function HydrationTestIndicator() { const navigation = useNavigation(); const revalidator = useRevalidator(); const fetchers = useFetchers(); + const location = useLocation(); if (!isHydrated) return null; @@ -469,6 +471,10 @@ function HydrationTestIndicator() { data-testid="hydrated" data-router-idle={routerIdle ? "true" : undefined} data-router-busy={routerIdle ? undefined : busy.join(" | ")} + // the rendered search, trailing the browser's own by a commit: only + // once the toast params are gone from here have the forms keyed on + // the location (see SendouForm) finished remounting + data-location-search={location.search} /> ); } diff --git a/e2e/helpers/playwright.ts b/e2e/helpers/playwright.ts index e3ca002e3..af52fac07 100644 --- a/e2e/helpers/playwright.ts +++ b/e2e/helpers/playwright.ts @@ -331,11 +331,15 @@ export async function submit(page: Page, target?: string | Locator) { await button.click(); }); - // Toast flash params are stripped right after via a replace navigation - // (without revalidation); wait for it so it can't abort a later click. - await page.waitForURL((url) => !/__(?:success|error)=/.test(url.href), { - timeout: 5_000, - }); + // An action's toast redirect adds flash params that a replace navigation + // strips right after (without revalidation), remounting every form on the + // page twice. Waiting on the rendered search rather than the browser's URL + // covers the commit those remounts land in, which trails the history entry + // — otherwise the second remount tears down whatever the test opens next. + await page.waitForSelector( + '[data-testid="hydrated"]:not([data-location-search*="__success"]):not([data-location-search*="__error"])', + { state: "attached", timeout: 5_000 }, + ); } export async function waitForPOSTResponse(page: Page, cb: () => Promise) { @@ -344,6 +348,22 @@ export async function waitForPOSTResponse(page: Page, cb: () => Promise) { const MAX_ATTEMPTS = 3; const PER_ATTEMPT_TIMEOUT = 10_000; + // The busy marker only appears once React commits the submission, which on a + // loaded machine lands well after the POST left the browser. Watching for it + // from before the click keeps the idle of the *previous* render from being + // read as the action having settled below. + const routerWentBusy = page + .waitForFunction( + () => + document + .querySelector('[data-testid="hydrated"]') + ?.getAttribute("data-router-idle") !== "true", + undefined, + { timeout: PER_ATTEMPT_TIMEOUT }, + ) + // a POST that no fetcher or navigation drives never turns the router busy + .catch(() => {}); + // React Aria buttons fire their handler on press end. Occasionally a click // registers the press start (the button goes `:active`) but the press never // completes into a submit, so no POST fires — e.g. when a re-render lands @@ -367,6 +387,7 @@ export async function waitForPOSTResponse(page: Page, cb: () => Promise) { // The POST's revalidation (and any redirect it drives) is still in flight; // an interaction landing mid-flight aborts it, and routes that opt out of // revalidation on navigation (e.g. to.$id) then keep the stale data. + await routerWentBusy; await expectRouterIdle(page); return response!;