From a8d8a9ad748bb32fa2a6e8dd31e44465ae8f0e92 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 8 Oct 2026 18:03:58 +0200 Subject: [PATCH] fix(browser): report an iframe that never loads - the tester path without a session id answers 400 instead of falling through to a bare 404, so Firefox fires the iframe load event after a form submit - the orchestrator fails the file when the tester document unloads during a test and no document loads within 2 s; Firefox and WebKit fire no load event for a document that failed to load, so the run waited forever --- packages/browser/src/client/orchestrator.ts | 49 +++++++++++++++---- .../src/node/middlewares/testerMiddleware.ts | 2 +- test/browser/specs/bail-out.test.ts | 24 +++++++++ 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/packages/browser/src/client/orchestrator.ts b/packages/browser/src/client/orchestrator.ts index 26ea4a271..e1215da70 100644 --- a/packages/browser/src/client/orchestrator.ts +++ b/packages/browser/src/client/orchestrator.ts @@ -18,6 +18,8 @@ import { getUiAPI } from './ui' import { getBrowserState, getConfig } from './utils' const ID_ALL = '__vitest_all__' +// the next document gets this long to fire "load" before the unload counts as a broken iframe +const IFRAME_UNLOAD_TIMEOUT = 5_000 export class IframeOrchestrator { private cancelled = false @@ -243,17 +245,9 @@ export class IframeOrchestrator { iframe.onload = () => { const href = this.getIframeHref(iframe) debug('iframe loaded with href', href) + this.iframeLoads.set(iframe, (this.iframeLoads.get(iframe) ?? 0) + 1) if (href !== iframe.src) { - reject( - this.dispatchIframeError( - new Error( - `Cannot connect to the iframe. ` + - `Did you change the location or submitted a form? ` + - "If so, don't forget to call `event.preventDefault()` to avoid reloading the page.\n\n" + - `Received URL: ${href || 'unknown due to CORS'}\nExpected: ${iframe.src}`, - ), - ), - ) + reject(this.dispatchIframeError(this.createConnectError(iframe, href))) } else if (this.iframes.has(iframeId)) { const events = this.iframeEvents.get(iframe) if (events?.size) { @@ -262,9 +256,11 @@ export class IframeOrchestrator { ) } else { this.warnReload(iframe, iframeId) + this.watchUnload(iframe, iframeId) } } else { this.iframes.set(iframeId, iframe) + this.watchUnload(iframe, iframeId) this.waitForReady(iframeId) .then(() => this.sendEventToIframe({ @@ -346,6 +342,39 @@ export class IframeOrchestrator { } private loggedIframe = new WeakSet() + private iframeLoads = new WeakMap() + + private createConnectError(iframe: HTMLIFrameElement, href: string | undefined) { + return new Error( + `Cannot connect to the iframe. ` + + `Did you change the location or submitted a form? ` + + "If so, don't forget to call `event.preventDefault()` to avoid reloading the page.\n\n" + + `Received URL: ${href || 'unknown due to CORS'}\nExpected: ${iframe.src}`, + ) + } + + // Firefox and WebKit fire no "load" for a document that failed to load + // (an empty response, a refused connection), so the tester that navigated + // away during a test would keep the run waiting forever + private watchUnload(iframe: HTMLIFrameElement, iframeId: string) { + const loads = this.iframeLoads.get(iframe) ?? 0 + iframe.contentWindow?.addEventListener( + 'pagehide', + () => { + setTimeout(() => { + if ( + this.iframes.get(iframeId) !== iframe || + !this.iframeEvents.get(iframe)?.size || + this.iframeLoads.get(iframe) !== loads + ) { + return + } + this.dispatchIframeError(this.createConnectError(iframe, this.getIframeHref(iframe))) + }, IFRAME_UNLOAD_TIMEOUT) + }, + { once: true }, + ) + } private createWarningMessage(iframeId: string, location: string) { return ( diff --git a/packages/browser/src/node/middlewares/testerMiddleware.ts b/packages/browser/src/node/middlewares/testerMiddleware.ts index 3d35a66f1..8762ec68a 100644 --- a/packages/browser/src/node/middlewares/testerMiddleware.ts +++ b/packages/browser/src/node/middlewares/testerMiddleware.ts @@ -11,7 +11,7 @@ export function createTesterMiddleware( return next() } const url = new URL(req.url, 'http://localhost') - if (url.pathname !== browserServer.prefixTesterUrl || !url.searchParams.has('sessionId')) { + if (url.pathname !== browserServer.prefixTesterUrl) { return next() } diff --git a/test/browser/specs/bail-out.test.ts b/test/browser/specs/bail-out.test.ts index 72a166d6c..74ae2f34c 100644 --- a/test/browser/specs/bail-out.test.ts +++ b/test/browser/specs/bail-out.test.ts @@ -66,3 +66,27 @@ test('vitest bails out when the iframe is no longer accessible', async () => { expect(stderr).toContain('Received URL: http://') expect(stderr).toContain('Expected: http://') }) + +// Firefox fires no "load" event for a document that failed to load, so the +// orchestrator has to notice the unload itself instead of waiting forever +test.runIf( + provider.name === 'playwright' && instances.some((instance) => instance.browser !== 'webkit'), +)('vitest bails out when the iframe navigates to an unreachable address', async () => { + const { stderr } = await runInlineBrowserTests( + { + 'navigate.test.ts': ` + import { test } from 'vitest' + + test('navigates away', async () => { + location.href = 'http://127.0.0.1:1/' + await new Promise(() => {}) + }) + `, + }, + { + browser: { instances: instances.filter((instance) => instance.browser !== 'webkit') }, + }, + { fails: true }, + ) + expect(stderr).toMatch(/Cannot connect to the iframe/) +}) -- 2.51.2