diff --git a/tests/browser/setup.ts b/tests/browser/setup.ts index 2a6647028..837dbdc05 100644 --- a/tests/browser/setup.ts +++ b/tests/browser/setup.ts @@ -16,6 +16,7 @@ import "@/app/globals.css"; import { afterEach, beforeAll, beforeEach, vi } from "vitest"; +import { commands } from "vitest/browser"; import { loadMessages } from "@/lib/i18n/load-locale"; @@ -24,7 +25,16 @@ import { rememberAddress, restoreAddress } from "./url"; document.documentElement.classList.add("dark"); +declare module "vitest/browser" { + interface BrowserCommands { + /** Waits until the provider's frame lookup finds this file's iframe. */ + awaitOwnFrame: () => Promise; + } +} + beforeAll(async () => { + // See `awaitOwnFrame` in vitest.config.mts. + await commands.awaitOwnFrame(); await loadMessages("en"); }); diff --git a/vitest.config.mts b/vitest.config.mts index 995c34e16..cb9f7a9c8 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -165,6 +165,31 @@ export default defineConfig({ }), instances: [{ browser: "chromium" }], screenshotFailures: false, + commands: { + // Every test iframe is named "vitest-iframe", and the provider + // finds the one to act on by that name. When a page runs a + // second file, the first file's iframe can still be found + // until its detach event arrives, so the second file's first + // click lands in a frame that is going away ("Frame was + // detached"). The setup waits here until the iframe found by + // name is this file's own. + async awaitOwnFrame({ page, testPath }) { + const isOwn = () => { + const url = page.frame("vitest-iframe")?.url(); + if (!url || !testPath) return false; + return new URL(url).searchParams.get("iframeId") === testPath; + }; + const deadline = Date.now() + 5_000; + while (!isOwn()) { + if (Date.now() > deadline) { + throw new Error( + `the vitest-iframe found by name never became ${testPath}`, + ); + } + await new Promise((done) => setTimeout(done, 10)); + } + }, + }, }, }, },