[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

fix(browser): wait for orchestrator readiness before resolving browser sessions (#10397)

authored by

Séamus O'Connor and committed by
GitHub
(Jun 1, 2026, 1:03 PM +0200) fe5ed6bc 2abbadd7

+114 -7
+1
packages/browser/src/types.ts
··· 26 26 readBenchmarkResult: (relativePath: string) => Promise<BaselineData | null> 27 27 writeBenchmarkResult: (relativePath: string, data: BaselineData) => Promise<void> 28 28 onAfterSuiteRun: (meta: AfterSuiteRunMeta) => void 29 + onOrchestratorReady: () => void 29 30 cancelCurrentRun: (reason: CancelReason) => void 30 31 getCountOfFailedTests: () => number 31 32 readSnapshotFile: (id: string) => Promise<string | null>
+73
test/unit/test/browser-sessions.test.ts
··· 1 + import type { TestProject } from 'vitest/node' 2 + import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' 3 + import { BrowserSessions } from '../../../packages/vitest/src/node/browser/sessions' 4 + 5 + function createProject(connectTimeout = 100) { 6 + return { name: 'browser', vitest: { config: { browser: { connectTimeout } } } } as TestProject 7 + } 8 + 9 + describe('BrowserSessions', () => { 10 + beforeEach(() => { 11 + vi.useFakeTimers() 12 + }) 13 + 14 + afterEach(() => { 15 + vi.useRealTimers() 16 + }) 17 + 18 + test('resolves only after the session is connected and ready', async () => { 19 + const sessions = new BrowserSessions() 20 + const promise = sessions.createSession('session-id', createProject(), { reject() {} }) 21 + const session = sessions.getSession('session-id') 22 + expect(session).toBeDefined() 23 + 24 + let resolved = false 25 + promise.then(() => { 26 + resolved = true 27 + }) 28 + 29 + session!.ready() 30 + expect(resolved).toBe(false) 31 + 32 + session!.connected() 33 + await promise 34 + expect(resolved).toBe(true) 35 + }) 36 + 37 + describe('timeouts and failures', () => { 38 + test('times out if the session connects but never becomes ready', async () => { 39 + const sessions = new BrowserSessions() 40 + const promise = sessions.createSession('session-id', createProject(), { reject() {} }) 41 + 42 + const session = sessions.getSession('session-id') 43 + expect(session).toBeDefined() 44 + 45 + const timeoutError = expect(promise).rejects.toThrowError( 46 + 'Failed to connect to the browser session "session-id" [browser] within the timeout.', 47 + ) 48 + 49 + session!.connected() 50 + await vi.advanceTimersByTimeAsync(101) 51 + 52 + await timeoutError 53 + }) 54 + 55 + test('fails the pool without waiting for the connect timeout', async () => { 56 + const sessions = new BrowserSessions() 57 + const poolReject = vi.fn() 58 + const promise = sessions.createSession('session-id', createProject(), { reject: poolReject }) 59 + 60 + const session = sessions.getSession('session-id') 61 + expect(session).toBeDefined() 62 + 63 + const error = new Error('browser failed') 64 + session!.fail(error) 65 + 66 + await expect(promise).resolves.toBeUndefined() 67 + expect(poolReject).toHaveBeenCalledExactlyOnceWith(error) 68 + 69 + await vi.advanceTimersByTimeAsync(101) 70 + expect(poolReject).toHaveBeenCalledTimes(1) 71 + }) 72 + }) 73 + })
+7
packages/browser/src/client/orchestrator.ts
··· 36 36 'message', 37 37 e => this.onGlobalChannelEvent(e), 38 38 ) 39 + 40 + // Notify the server once the websocket is ready without blocking orchestrator creation. 41 + void client.waitForConnection() 42 + .then(() => client.rpc.onOrchestratorReady()) 43 + .catch((error) => { 44 + debug('failed to notify orchestrator readiness', error) 45 + }) 39 46 } 40 47 41 48 public async createTesters(options: BrowserTesterOptions): Promise<void> {
+15 -3
packages/browser/src/node/rpc.ts
··· 69 69 70 70 if (type === 'orchestrator') { 71 71 const session = sessions.getSession(sessionId) 72 - // it's possible the session was already resolved by the preview provider 72 + // it's possible the session was already resolved by the preview provider, 73 + // but we still mark the websocket connection when the page reconnects 73 74 session?.connected() 74 75 } 75 76 ··· 84 85 wss.handleUpgrade(request, socket, head, (ws) => { 85 86 wss.emit('connection', ws, request) 86 87 87 - const { rpc, offCancel } = setupClient(project, rpcId, ws) 88 + const { rpc, offCancel } = setupClient(project, rpcId, ws, { sessionId }) 88 89 const state = project.browser!.state as BrowserServerState 89 90 const clients = type === 'tester' ? state.testers : state.orchestrators 90 91 clients.set(rpcId, rpc) ··· 151 152 } 152 153 } 153 154 154 - function setupClient(project: TestProject, rpcId: string, ws: WebSocket) { 155 + function setupClient( 156 + project: TestProject, 157 + rpcId: string, 158 + ws: WebSocket, 159 + options: { 160 + sessionId: string 161 + }, 162 + ) { 155 163 const mockResolver = new ServerMockResolver(globalServer.vite, { 156 164 moduleDirectories: project.config?.deps?.moduleDirectories, 157 165 }) ··· 159 167 160 168 const rpc = createBirpc<WebSocketBrowserEvents, WebSocketBrowserHandlers>( 161 169 { 170 + onOrchestratorReady() { 171 + const sessions = vitest._browserSessions 172 + sessions.getSession(options.sessionId)?.ready() 173 + }, 162 174 async onUnhandledError(error, type) { 163 175 if (error && typeof error === 'object') { 164 176 const _error = error as TestError
+17 -4
packages/vitest/src/node/browser/sessions.ts
··· 22 22 pool: { reject: (error: Error) => void }, 23 23 options?: { otelCarrier?: OTELCarrier }, 24 24 ): Promise<void> { 25 - // this promise only waits for the WS connection with the orchestrator to be established 25 + // this promise waits until the orchestrator is ready to accept RPC calls 26 26 const defer = createDefer<void>() 27 - 27 + let isConnected = false 28 + let isReady = false 28 29 const timeout = setTimeout(() => { 29 30 defer.reject(new Error(`Failed to connect to the browser session "${sessionId}" [${project.name}] within the timeout.`)) 30 31 }, project.vitest.config.browser.connectTimeout ?? 60_000).unref() 32 + 33 + const resolveIfReady = () => { 34 + if (!isConnected || !isReady) { 35 + return 36 + } 37 + defer.resolve() 38 + clearTimeout(timeout) 39 + } 31 40 32 41 this.sessions.set(sessionId, { 33 42 project, 34 43 otelCarrier: options?.otelCarrier, 35 44 connected: () => { 36 - defer.resolve() 37 - clearTimeout(timeout) 45 + isConnected = true 46 + resolveIfReady() 47 + }, 48 + ready: () => { 49 + isReady = true 50 + resolveIfReady() 38 51 }, 39 52 // this fails the whole test run and cancels the pool 40 53 fail: (error: Error) => {
+1
packages/vitest/src/node/types/browser.ts
··· 376 376 project: TestProject 377 377 otelCarrier?: OTELCarrier 378 378 connected: () => void 379 + ready: () => void 379 380 fail: (v: Error) => void 380 381 } 381 382