[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!: don't emit localStorage warnings on Node 26, fail gracefully when worker fails to start (#10293)

authored by

Vladimir and committed by
GitHub
(Jul 1, 2026, 10:37 AM +0200) 334edef9 bd9cc9d8

+77 -15
+1 -1
.github/workflows/ci.yml
··· 99 99 strategy: 100 100 matrix: 101 101 os: [ubuntu-latest] 102 - node_version: [22, 24] 102 + node_version: [22, 24, 26] 103 103 include: 104 104 - os: macos-latest 105 105 node_version: 24
+3 -3
docs/guide/environment.md
··· 96 96 interface PopulateResult { 97 97 // a list of all keys that were copied, even if value doesn't exist on original object 98 98 keys: Set<string> 99 - // a map of original object that might have been overridden with keys 100 - // you can return these values inside `teardown` function 101 - originals: Map<string | symbol, any> 99 + // a map of property descriptors for keys that might have been overridden 100 + // you can restore them with `Object.defineProperty` inside `teardown` 101 + originals: Map<string | symbol, PropertyDescriptor> 102 102 } 103 103 104 104 export function populateGlobal(global: any, original: any, options: PopulateOptions): PopulateResult
+11
docs/guide/migration.md
··· 328 328 329 329 Assignments to properties on `globalThis` or `window` in `jsdom` and `happy-dom` environments are now propagated to the underlying DOM implementation. Mutable properties such as `innerWidth` can affect APIs implemented by the DOM environment, for example `happy-dom`'s `matchMedia`. 330 330 331 + ### `populateGlobal` Returns Descriptors in `originals` 332 + 333 + The `originals` map returned by [`populateGlobal`](/guide/environment#custom-environment) now holds [property descriptors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor) instead of plain values. This avoids invoking native lazy getters (such as Node's `localStorage`) while capturing the original, and restores them faithfully on teardown. 334 + 335 + If you restore them manually in a custom environment, use `Object.defineProperty` instead of an assignment: 336 + 337 + ```ts 338 + originals.forEach((value, key) => (global[key] = value)) // [!code --] 339 + originals.forEach((descriptor, key) => Object.defineProperty(global, key, descriptor)) // [!code ++] 340 + ``` 341 + 331 342 ### Browser Orchestrator URL Requires a Session 332 343 333 344 Vitest no longer serves the browser orchestrator UI from a bare `/__vitest_test__/` URL. Browser runner URLs are now session-bound and must include the `sessionId` generated by Vitest, for example `/__vitest_test__/?sessionId=...`.
+1 -1
test/e2e/test/no-module-runner.test.ts
··· 712 712 `) 713 713 }) 714 714 715 - test('with --experimental-transform-types is supported', async () => { 715 + test.runIf(process.allowedNodeEnvironmentFlags.has('--experimental-transform-types'))('with --experimental-transform-types is supported', async () => { 716 716 const { errorTree } = await runNoViteModuleRunnerTests( 717 717 { 718 718 'add.cts': /* ts */`
+31 -1
test/e2e/test/pool-worker-exit.test.ts
··· 1 1 import { sep } from 'node:path' 2 - import { runVitest, StableTestFileOrderSorter } from '#test-utils' 2 + import { runInlineTests, runVitest, StableTestFileOrderSorter } from '#test-utils' 3 3 import { resolve } from 'pathe' 4 4 import { expect, test } from 'vitest' 5 5 import { readCoverageMap } from '../../coverage-test/utils' ··· 142 142 }, 143 143 }, 144 144 } 145 + `) 146 + }) 147 + 148 + test('worker that fails to start surfaces the error instead of hanging', async () => { 149 + // An invalid `execArgv` makes the worker process exit immediately, before it 150 + // ever reports back. The run must fail fast with the worker error rather than 151 + // wait out the worker-start timeout (which previously made the run hang). 152 + const { stderr, thrown } = await runInlineTests({ 153 + 'basic.test.ts': `import { test } from 'vitest'\ntest('ok', () => {})`, 154 + }, { 155 + pool: 'forks', 156 + execArgv: ['--vitest-invalid-flag-regression-test'], 157 + }) 158 + 159 + expect(thrown).toBe(false) 160 + 161 + const errors = stderr 162 + .split('\n') 163 + .filter(line => !line.startsWith(' ❯') && !line.includes('bad option') && line.trim().length > 0) 164 + .join('\n') 165 + 166 + expect(stderr).toContain('bad option: --vitest-invalid-flag-regression-test') 167 + expect(errors).toMatchInlineSnapshot(` 168 + "⎯⎯⎯⎯⎯⎯ Unhandled Errors ⎯⎯⎯⎯⎯⎯ 169 + Vitest caught 1 unhandled error during the test run. 170 + This might cause false positive tests. Resolve unhandled errors to make sure your tests are not affected. 171 + ⎯⎯⎯⎯⎯⎯ Unhandled Error ⎯⎯⎯⎯⎯⎯⎯ 172 + Error: [vitest-pool]: Worker forks emitted error. 173 + Caused by: Error: Worker exited unexpectedly with exit code 9 during starting state 174 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" 145 175 `) 146 176 })
+1 -1
test/unit/test/env-runtime.test.ts
··· 9 9 } 10 10 const win = { Event: winEvent } 11 11 const { originals } = populateGlobal(global, win) 12 - expect(originals.get('Event')).toBe(globalEvent) 12 + expect(originals.get('Event')?.value).toBe(globalEvent) 13 13 expect(win.Event).toBe(winEvent) 14 14 expect(global.Event).toBe(winEvent) 15 15 })
+1 -1
packages/vitest/src/integrations/env/edge-runtime.ts
··· 43 43 return { 44 44 teardown(global) { 45 45 keys.forEach(key => delete global[key]) 46 - originals.forEach((v, k) => (global[k] = v)) 46 + originals.forEach((d, k) => Object.defineProperty(global, k, d)) 47 47 }, 48 48 } 49 49 },
+1 -1
packages/vitest/src/integrations/env/happy-dom.ts
··· 82 82 async teardown(global) { 83 83 await teardownWindow(win) 84 84 keys.forEach(key => delete global[key]) 85 - originals.forEach((v, k) => (global[k] = v)) 85 + originals.forEach((d, k) => Object.defineProperty(global, k, d)) 86 86 }, 87 87 } 88 88 },
+2
packages/vitest/src/integrations/env/jsdom-keys.ts
··· 209 209 'innerHeight', 210 210 'innerWidth', 211 211 'length', 212 + 'localStorage', 212 213 'location', 213 214 'matchMedia', 214 215 'moveBy', ··· 241 242 'scrollX', 242 243 'scrollY', 243 244 'self', 245 + 'sessionStorage', 244 246 /* 'setInterval', */ 245 247 /* 'setTimeout', */ 246 248 'stop',
+1 -1
packages/vitest/src/integrations/env/jsdom.ts
··· 225 225 dom.window.close() 226 226 delete global.jsdom 227 227 keys.forEach(key => delete global[key]) 228 - originals.forEach((v, k) => (global[k] = v)) 228 + originals.forEach((d, k) => Object.defineProperty(global, k, d)) 229 229 }, 230 230 } 231 231 },
+8 -3
packages/vitest/src/integrations/env/utils.ts
··· 45 45 ): { 46 46 keys: Set<string> 47 47 skipKeys: string[] 48 - originals: Map<string | symbol, any> 48 + originals: Map<string | symbol, PropertyDescriptor> 49 49 } { 50 50 const { bindFunctions = false } = options 51 51 const keys = getWindowKeys(global, win, options.additionalKeys) 52 52 53 - const originals = new Map<string | symbol, any>() 53 + const originals = new Map<string | symbol, PropertyDescriptor>() 54 54 55 55 const overriddenKeys = new Set([...KEYS, ...options.additionalKeys || []]) 56 56 ··· 63 63 && win[key].bind(win) 64 64 65 65 if (overriddenKeys.has(key) && key in global) { 66 - originals.set(key, global[key]) 66 + // capture the descriptor instead of the value to avoid invoking native 67 + // lazy getters such as Node's `localStorage`, which warns when accessed 68 + // without `--localstorage-file` 69 + const descriptor = Object.getOwnPropertyDescriptor(global, key) 70 + ?? { value: global[key], configurable: true, writable: true, enumerable: true } 71 + originals.set(key, descriptor) 67 72 } 68 73 69 74 Object.defineProperty(global, key, {
+16 -2
packages/vitest/src/node/pools/poolRunner.ts
··· 384 384 385 385 private waitForStart() { 386 386 return new Promise<void>((resolve, reject) => { 387 - const onStart = (message: WorkerResponse) => { 387 + const cleanup = () => { 388 + this.off('message', onStart) 389 + this.off('error', onError) 390 + } 391 + 392 + function onStart(message: WorkerResponse) { 388 393 if (message.type === 'started') { 389 - this.off('message', onStart) 394 + cleanup() 390 395 if (message.error) { 391 396 reject(message.error) 392 397 } ··· 396 401 } 397 402 } 398 403 404 + // The worker can die before it ever reports back (e.g. an invalid 405 + // `execArgv` makes Node exit immediately). Reject as soon as that 406 + // happens instead of waiting for the start timeout to elapse. 407 + function onError(error: Error) { 408 + cleanup() 409 + reject(error) 410 + } 411 + 399 412 this.on('message', onStart) 413 + this.on('error', onError) 400 414 }) 401 415 } 402 416