From 4f8678af93d5b84559c4c5004f7a8460b56604e5 Mon Sep 17 00:00:00 2001 From: Shane Daniel Date: Sat, 3 Jan 2026 14:34:53 -0800 Subject: [PATCH 1/2] fix: guard against infinite response event generation (fix #9379) --- packages/browser/src/client/tester/tester.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/browser/src/client/tester/tester.ts b/packages/browser/src/client/tester/tester.ts index db415b6d70b9..ff19c29380b9 100644 --- a/packages/browser/src/client/tester/tester.ts +++ b/packages/browser/src/client/tester/tester.ts @@ -51,6 +51,10 @@ channel.addEventListener('message', async (e) => { return } + if (data.event.startsWith('response:')) { + return + } + switch (data.event) { case 'execute': { const { method, files, context } = data From da0e8c0b0e10a22434ef81aa6ac33ea7c393542e Mon Sep 17 00:00:00 2001 From: Shane Daniel Date: Sun, 4 Jan 2026 14:28:10 -0800 Subject: [PATCH 2/2] fix: only send response for handled events (fix #9379) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avoid infinite response event loop by only sending response messages for events that were successfully handled. Changes: - Add eventHandled flag to track successful event processing - Only call channel.postMessage for handled events - Add reproduction test 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- packages/browser/src/client/tester/tester.ts | 13 +++++--- .../response-guard.test.ts | 33 +++++++++++++++++++ .../response-event-loop/vitest.config.ts | 14 ++++++++ test/browser/package.json | 1 + .../browser/specs/response-event-loop.test.ts | 15 +++++++++ 5 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 test/browser/fixtures/response-event-loop/response-guard.test.ts create mode 100644 test/browser/fixtures/response-event-loop/vitest.config.ts create mode 100644 test/browser/specs/response-event-loop.test.ts diff --git a/packages/browser/src/client/tester/tester.ts b/packages/browser/src/client/tester/tester.ts index ff19c29380b9..ac076f49ff61 100644 --- a/packages/browser/src/client/tester/tester.ts +++ b/packages/browser/src/client/tester/tester.ts @@ -55,6 +55,8 @@ channel.addEventListener('message', async (e) => { return } + let eventHandled = true + switch (data.event) { case 'execute': { const { method, files, context } = data @@ -96,15 +98,18 @@ channel.addEventListener('message', async (e) => { break } default: { + eventHandled = false const error = new Error(`Unknown event: ${(data as any).event}`) unhandledError(error, 'Unknown Event') } } - channel.postMessage({ - event: `response:${data.event}`, - iframeId: getBrowserState().iframeId!, - }) + if (eventHandled) { + channel.postMessage({ + event: `response:${data.event}`, + iframeId: getBrowserState().iframeId!, + }) + } }) const url = new URL(location.href) diff --git a/test/browser/fixtures/response-event-loop/response-guard.test.ts b/test/browser/fixtures/response-event-loop/response-guard.test.ts new file mode 100644 index 000000000000..1660efce260e --- /dev/null +++ b/test/browser/fixtures/response-event-loop/response-guard.test.ts @@ -0,0 +1,33 @@ +import { expect, test } from 'vitest' +import { channel } from '@vitest/browser/client' + +test('response:prefixed event is not processed by tester', async () => { + const url = new URL(location.href) + const sessionId = url.searchParams.get('sessionId')! + const iframeId = url.searchParams.get('iframeId')! + + const secondChannel = new BroadcastChannel(`vitest:${sessionId}`) + + const nestedResponses: string[] = [] + const handler = (e: MessageEvent) => { + if (typeof e.data?.event === 'string' && e.data.event.startsWith('response:response:')) { + nestedResponses.push(e.data.event) + } + secondChannel.postMessage({ + event: `response:${e.data.event}`, + iframeId: e.data.iframeId!, + }) + } + secondChannel.addEventListener('message', handler) + secondChannel.postMessage({ + event: 'response:test', + iframeId, + }) + + await new Promise(resolve => setTimeout(resolve, 5)) + + secondChannel.removeEventListener('message', handler) + secondChannel.close() + + expect(nestedResponses).toHaveLength(0) +}) diff --git a/test/browser/fixtures/response-event-loop/vitest.config.ts b/test/browser/fixtures/response-event-loop/vitest.config.ts new file mode 100644 index 000000000000..52962b06a2cc --- /dev/null +++ b/test/browser/fixtures/response-event-loop/vitest.config.ts @@ -0,0 +1,14 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import { provider, instances } from '../../settings' + +export default defineConfig({ + cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)), + test: { + browser: { + enabled: true, + provider, + instances, + }, + }, +}) diff --git a/test/browser/package.json b/test/browser/package.json index 03007895835e..7df18b22a309 100644 --- a/test/browser/package.json +++ b/test/browser/package.json @@ -22,6 +22,7 @@ "test-setup-file": "vitest --root ./fixtures/setup-file", "test-snapshots": "vitest --root ./fixtures/update-snapshot", "test-broken-iframe": "vitest --root ./fixtures/broken-iframe", + "test-response-event-loop": "vitest --root ./fixtures/response-event-loop", "coverage": "vitest --coverage.enabled --coverage.provider=istanbul --browser.headless=yes", "test:browser:preview": "PROVIDER=preview vitest", "test:browser:playwright": "PROVIDER=playwright vitest", diff --git a/test/browser/specs/response-event-loop.test.ts b/test/browser/specs/response-event-loop.test.ts new file mode 100644 index 000000000000..58c36eb5f95a --- /dev/null +++ b/test/browser/specs/response-event-loop.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from 'vitest' +import { runBrowserTests } from './utils' + +test('unknown events do not cause infinite response event loop', async () => { + const { exitCode, testTree } = await runBrowserTests({ + root: './fixtures/response-event-loop', + }) + + expect(exitCode).toBe(0) + expect(testTree()).toMatchObject({ + 'response-guard.test.ts': { + 'response:prefixed event is not processed by tester': 'passed', + }, + }) +})