Skip to content

Commit da0e8c0

Browse files
simshanithclaude
andcommitted
fix: only send response for handled events (fix #9379)
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 <noreply@anthropic.com>
1 parent 4f8678a commit da0e8c0

File tree

5 files changed

+72
-4
lines changed

5 files changed

+72
-4
lines changed

packages/browser/src/client/tester/tester.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ channel.addEventListener('message', async (e) => {
5555
return
5656
}
5757

58+
let eventHandled = true
59+
5860
switch (data.event) {
5961
case 'execute': {
6062
const { method, files, context } = data
@@ -96,15 +98,18 @@ channel.addEventListener('message', async (e) => {
9698
break
9799
}
98100
default: {
101+
eventHandled = false
99102
const error = new Error(`Unknown event: ${(data as any).event}`)
100103
unhandledError(error, 'Unknown Event')
101104
}
102105
}
103106

104-
channel.postMessage({
105-
event: `response:${data.event}`,
106-
iframeId: getBrowserState().iframeId!,
107-
})
107+
if (eventHandled) {
108+
channel.postMessage({
109+
event: `response:${data.event}`,
110+
iframeId: getBrowserState().iframeId!,
111+
})
112+
}
108113
})
109114

110115
const url = new URL(location.href)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { expect, test } from 'vitest'
2+
import { channel } from '@vitest/browser/client'
3+
4+
test('response:prefixed event is not processed by tester', async () => {
5+
const url = new URL(location.href)
6+
const sessionId = url.searchParams.get('sessionId')!
7+
const iframeId = url.searchParams.get('iframeId')!
8+
9+
const secondChannel = new BroadcastChannel(`vitest:${sessionId}`)
10+
11+
const nestedResponses: string[] = []
12+
const handler = (e: MessageEvent) => {
13+
if (typeof e.data?.event === 'string' && e.data.event.startsWith('response:response:')) {
14+
nestedResponses.push(e.data.event)
15+
}
16+
secondChannel.postMessage({
17+
event: `response:${e.data.event}`,
18+
iframeId: e.data.iframeId!,
19+
})
20+
}
21+
secondChannel.addEventListener('message', handler)
22+
secondChannel.postMessage({
23+
event: 'response:test',
24+
iframeId,
25+
})
26+
27+
await new Promise(resolve => setTimeout(resolve, 5))
28+
29+
secondChannel.removeEventListener('message', handler)
30+
secondChannel.close()
31+
32+
expect(nestedResponses).toHaveLength(0)
33+
})
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { fileURLToPath } from 'node:url'
2+
import { defineConfig } from 'vitest/config'
3+
import { provider, instances } from '../../settings'
4+
5+
export default defineConfig({
6+
cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)),
7+
test: {
8+
browser: {
9+
enabled: true,
10+
provider,
11+
instances,
12+
},
13+
},
14+
})

test/browser/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"test-setup-file": "vitest --root ./fixtures/setup-file",
2323
"test-snapshots": "vitest --root ./fixtures/update-snapshot",
2424
"test-broken-iframe": "vitest --root ./fixtures/broken-iframe",
25+
"test-response-event-loop": "vitest --root ./fixtures/response-event-loop",
2526
"coverage": "vitest --coverage.enabled --coverage.provider=istanbul --browser.headless=yes",
2627
"test:browser:preview": "PROVIDER=preview vitest",
2728
"test:browser:playwright": "PROVIDER=playwright vitest",
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { expect, test } from 'vitest'
2+
import { runBrowserTests } from './utils'
3+
4+
test('unknown events do not cause infinite response event loop', async () => {
5+
const { exitCode, testTree } = await runBrowserTests({
6+
root: './fixtures/response-event-loop',
7+
})
8+
9+
expect(exitCode).toBe(0)
10+
expect(testTree()).toMatchObject({
11+
'response-guard.test.ts': {
12+
'response:prefixed event is not processed by tester': 'passed',
13+
},
14+
})
15+
})

0 commit comments

Comments
 (0)