···247247 const remainingTime = Math.floor(endTime - currentTime)
248248 // keep some buffer to process the timeout, but always hand the provider a
249249 // positive value so it surfaces a descriptive, source-mapped locator error
250250- // instead of letting the task timer win the race with a generic timeout
251251- options_.timeout = Math.max(remainingTime - 100, 1)
250250+ // instead of letting the task timer win the race with a generic timeout;
251251+ // the buffer covers the provider->server->client round-trip of the rejection,
252252+ // which can exceed 100ms on loaded CI machines running several browsers
253253+ options_.timeout = Math.max(remainingTime - 250, 1)
252254 return options_
253255}
254256
+20-10
packages/vitest/src/node/core.ts
···168168 /** @internal */ _tmpDir = join(tmpdir(), nanoid())
169169 /** @internal */ _traces!: Traces
170170 /** @internal */ _harness: PluginHarness
171171+ /** @internal */ _exitTimeout: ReturnType<typeof setTimeout> | undefined
171172172173 private isFirstRun = true
173174 private restartsCount = 0
···15151516 })
15161517 }
1517151815191519+ // close the pool (and the browser pages with it) BEFORE the Vite
15201520+ // servers: closing a server releases its port while automated pages may
15211521+ // still be alive — a page's websocket client would auto-reconnect onto
15221522+ // the next server that binds the same port and fail with "Unknown session id"
15231523+ if (this.pool) {
15241524+ try {
15251525+ await this.pool.close?.()
15261526+ }
15271527+ catch (error) {
15281528+ teardownErrors.push(error)
15291529+ }
15301530+15311531+ this.pool = undefined
15321532+ }
15331533+15181534 const closePromises: unknown[] = this.projects.map(w => w.close())
15191535 // close the core workspace server only once
15201536 // it's possible that it's not initialized at all because it's not running any tests
···15221538 closePromises.push(this.coreWorkspaceProject.close().then(() => this.vite = undefined as any))
15231539 }
1524154015251525- if (this.pool) {
15261526- closePromises.push((async () => {
15271527- await this.pool?.close?.()
15281528-15291529- this.pool = undefined
15301530- })())
15311531- }
15321532-15331541 closePromises.push(...this._onClose.map(fn => fn()))
1534154215351543 await Promise.allSettled(closePromises).then((results) => {
···15501558 * @param force If true, the process will exit immediately after closing the projects.
15511559 */
15521560 public async exit(force = false): Promise<void> {
15531553- setTimeout(() => {
15611561+ clearTimeout(this._exitTimeout)
15621562+ this._exitTimeout = setTimeout(() => {
15541563 this.report('onProcessTimeout').then(() => {
15551564 console.warn(`close timed out after ${this.config.teardownTimeout}ms`)
15561565···1574158315751584 process.exit()
15761585 })
15771577- }, this.config.teardownTimeout).unref()
15861586+ }, this.config.teardownTimeout)
15871587+ this._exitTimeout.unref()
1578158815791589 await this.close()
15801590 if (force) {
+1-2
test/browser/fixtures/user-event/wheel.test.ts
···55555656 await (testType === 'userEvent' ? userEvent.wheel(selector, options) : selector.wheel(options))
57575858- // the wheel event is dispatched asynchronously in the browser, so poll for
5959- // it instead of asserting synchronously (matches the default-delta case above)
5858+ // the browser dispatches the event asynchronously, poll like the tests above
6059 await expect.poll(() => wheel).toHaveBeenCalledOnce()
6160 expect(wheel.mock.calls[0][0].deltaX).toBe(deltaX)
6261 expect(wheel.mock.calls[0][0].deltaY).toBe(deltaY)
+7-5
test/browser/test/commands.test.ts
···44const { readFile, writeFile, removeFile, myCustomCommand } = server.commands
5566it('can manipulate files', async () => {
77- const file = './test.txt'
77+ // all browser instances run this file against the same cwd in parallel,
88+ // so the file name must be unique per instance to avoid races
99+ const file = `./test-${server.browser}.txt`
810911 try {
1012 await readFile(file)
···1315 catch (err) {
1416 expect(err.message).toMatch(`ENOENT: no such file or directory, open`)
1517 if (server.platform === 'win32') {
1616- expect(err.message).toMatch('test\\browser\\test.txt')
1818+ expect(err.message).toMatch(`test\\browser\\test-${server.browser}.txt`)
1719 }
1820 else {
1919- expect(err.message).toMatch('test/browser/test.txt')
2121+ expect(err.message).toMatch(`test/browser/test-${server.browser}.txt`)
2022 }
2123 }
2224···3436 catch (err) {
3537 expect(err.message).toMatch(`ENOENT: no such file or directory, open`)
3638 if (server.platform === 'win32') {
3737- expect(err.message).toMatch('test\\browser\\test.txt')
3939+ expect(err.message).toMatch(`test\\browser\\test-${server.browser}.txt`)
3840 }
3941 else {
4040- expect(err.message).toMatch('test/browser/test.txt')
4242+ expect(err.message).toMatch(`test/browser/test-${server.browser}.txt`)
4143 }
4244 }
4345})
+10-2
test/e2e/test/cancel-run.test.ts
···2121 include: ['blocked-thread.test.ts'],
2222 reporters: [{ onTestModuleStart: () => onTestModuleStart.resolve() }],
2323 })
2424- onTestFinished(() => vitest.close())
2424+ onTestFinished(async () => {
2525+ await vitest.close()
2626+ // this test stubs `process.exit` to survive `vitest.exit()`, so it also has
2727+ // to disarm the force-exit watchdog that `exit()` armed — otherwise the
2828+ // timer would `process.exit()` this worker `teardownTimeout` later
2929+ clearTimeout(vitest._exitTimeout)
3030+ })
25312632 const stdin = new Readable({ read: () => '' }) as NodeJS.ReadStream
2733 stdin.isTTY = true
···4652 stdin.emit('data', CTRL_C)
4753 await promise
48544949- expect(onExit).toHaveBeenCalled()
5555+ // `exit()` calls `process.exit` only after `close()` finishes — poll instead
5656+ // of racing the teardown
5757+ await expect.poll(() => onExit).toHaveBeenCalled()
5058})
51595260test('cancelling test run stops test execution immediately', async () => {
+6
test/test-utils/index.ts
···265265 exitCode = process.exitCode
266266 process.exitCode = 0
267267268268+ // tests emulating CLI shortcuts (`q`, double CTRL+C) trigger `vitest.exit()`,
269269+ // which arms an unref'd force-exit watchdog; it must be disarmed before the
270270+ // real `process.exit` is restored, or it would kill this worker
271271+ // `teardownTimeout` later, in the middle of a subsequent test file
268272 if (TestRunner.getCurrentTest()) {
269273 onTestFinished(async () => {
274274+ clearTimeout(ctx?._exitTimeout)
270275 await ctx?.close()
271276 process.exit = exit
272277 })
273278 }
274279 else {
275280 afterEach(async () => {
281281+ clearTimeout(ctx?._exitTimeout)
276282 await ctx?.close()
277283 process.exit = exit
278284 })