Context
apps/loopover-miner-ui's Vitest suite runs under environment: "jsdom" (apps/loopover-miner-ui/vitest.config.ts). Under Node 26 (confirmed on 26.5.0), Node itself now defines a global accessor property globalThis.localStorage — Node's own experimental Web Storage API (only functional with --localstorage-file; without it, the getter returns undefined and logs ExperimentalWarning: localStorage is not available because --localstorage-file was not provided.). This property exists on globalThis before jsdom's environment is ever installed. It is configurable: true, but jsdom/Vitest's per-file global setup does not overwrite it with jsdom's own working Storage implementation (window.localStorage).
Any test that reads the bare localStorage global (not window.localStorage) then hits undefined, not a Storage object. apps/loopover-miner-ui/src/theme-toggle.test.tsx does exactly this, mirroring the production component it tests (apps/loopover-miner-ui/src/components/theme-toggle.tsx:39, which also calls the bare global): its beforeEach/afterEach call localStorage.clear() (theme-toggle.test.tsx:10 and :15), which throws TypeError: Cannot read properties of undefined (reading 'clear') under Node 26. All 4 tests in the ThemeToggle (#6508) describe block fail — both standalone (npx vitest run src/theme-toggle.test.tsx) and as part of the full suite.
Confirmed Node-version-gated (checked Object.getOwnPropertyDescriptor(globalThis, "localStorage") directly on each binary):
- Node 26.5.0:
{ get, set, enumerable: false, configurable: true } — property present and broken. Suite fails (4/4 theme-toggle.test.tsx cases).
- Node 24.18.0: no such own property on
globalThis. Suite passes.
- Node 22.23.1 (the
.nvmrc-pinned version CI actually runs): no such own property either. Suite passes.
So this is not CI-blocking (CI pins Node 22 via .nvmrc) and not user-facing (real browsers always provide a working localStorage; this only collides with jsdom inside a Vitest/Node process). It's a papercut for anyone running the miner-ui suite from a Node install newer than the pinned one — e.g. a system-default node rather than one selected via nvm use.
apps/loopover-miner-ui/vitest.setup.ts already has an established pattern for exactly this kind of jsdom-environment gap: it stubs globalThis.ResizeObserver before tests run (vitest.setup.ts:12-17, globalThis.ResizeObserver ??= ResizeObserverStub) because jsdom doesn't implement it. That file is the natural place for a localStorage guard too.
Requirements
apps/loopover-miner-ui/vitest.setup.ts must ensure globalThis.localStorage is jsdom's real, working Storage implementation before any test runs, regardless of whether the current Node version predefines its own (broken, unflagged) localStorage global.
- The fix must not assume
globalThis.localStorage is absent (true on Node 22/24 today) and must not use a ??=-style guard — Node's broken accessor already "exists" as a property, so ??= would keep it. The fix must actively replace whatever is currently installed with jsdom's working Storage object (the property is configurable: true, so this is safe).
- No behavior change on Node versions that don't define the global (22, 24 today) — the guard must be a no-op there beyond assigning the same working
Storage object jsdom already provides.
Deliverables
Test Coverage Requirements
apps/** is outside this repo's Codecov coverage.include scope, so this isn't gated by codecov/patch. The bar is apps/loopover-miner-ui's own Vitest suite (npm test in that workspace) passing under both the pinned Node version and a newer one (e.g. Node 26) — there's no separate regression test needed for vitest.setup.ts itself; a green theme-toggle.test.tsx under Node 26 locally is the practical verification (a repo-level Node-version test matrix is not in scope here).
Expected Outcome
npm test in apps/loopover-miner-ui (and the root npm run ui:test / test:ci) passes identically regardless of which supported Node version runs it, instead of silently depending on the current Node version not yet shipping Node's own localStorage global.
Links & Resources
apps/loopover-miner-ui/vitest.setup.ts (existing ResizeObserverStub pattern to mirror), apps/loopover-miner-ui/src/theme-toggle.test.tsx (the failing suite), apps/loopover-miner-ui/src/components/theme-toggle.tsx:39 (the production localStorage call the test exercises), .nvmrc (pins Node 22, why CI is unaffected).
Context
apps/loopover-miner-ui's Vitest suite runs underenvironment: "jsdom"(apps/loopover-miner-ui/vitest.config.ts). Under Node 26 (confirmed on 26.5.0), Node itself now defines a global accessor propertyglobalThis.localStorage— Node's own experimental Web Storage API (only functional with--localstorage-file; without it, the getter returnsundefinedand logsExperimentalWarning: localStorage is not available because --localstorage-file was not provided.). This property exists onglobalThisbefore jsdom's environment is ever installed. It isconfigurable: true, but jsdom/Vitest's per-file global setup does not overwrite it with jsdom's own workingStorageimplementation (window.localStorage).Any test that reads the bare
localStorageglobal (notwindow.localStorage) then hitsundefined, not aStorageobject.apps/loopover-miner-ui/src/theme-toggle.test.tsxdoes exactly this, mirroring the production component it tests (apps/loopover-miner-ui/src/components/theme-toggle.tsx:39, which also calls the bare global): itsbeforeEach/afterEachcalllocalStorage.clear()(theme-toggle.test.tsx:10and:15), which throwsTypeError: Cannot read properties of undefined (reading 'clear')under Node 26. All 4 tests in theThemeToggle (#6508)describe block fail — both standalone (npx vitest run src/theme-toggle.test.tsx) and as part of the full suite.Confirmed Node-version-gated (checked
Object.getOwnPropertyDescriptor(globalThis, "localStorage")directly on each binary):{ get, set, enumerable: false, configurable: true }— property present and broken. Suite fails (4/4theme-toggle.test.tsxcases).globalThis. Suite passes..nvmrc-pinned version CI actually runs): no such own property either. Suite passes.So this is not CI-blocking (CI pins Node 22 via
.nvmrc) and not user-facing (real browsers always provide a workinglocalStorage; this only collides with jsdom inside a Vitest/Node process). It's a papercut for anyone running the miner-ui suite from a Node install newer than the pinned one — e.g. a system-defaultnoderather than one selected vianvm use.apps/loopover-miner-ui/vitest.setup.tsalready has an established pattern for exactly this kind of jsdom-environment gap: it stubsglobalThis.ResizeObserverbefore tests run (vitest.setup.ts:12-17,globalThis.ResizeObserver ??= ResizeObserverStub) because jsdom doesn't implement it. That file is the natural place for alocalStorageguard too.Requirements
apps/loopover-miner-ui/vitest.setup.tsmust ensureglobalThis.localStorageis jsdom's real, workingStorageimplementation before any test runs, regardless of whether the current Node version predefines its own (broken, unflagged)localStorageglobal.globalThis.localStorageis absent (true on Node 22/24 today) and must not use a??=-style guard — Node's broken accessor already "exists" as a property, so??=would keep it. The fix must actively replace whatever is currently installed with jsdom's workingStorageobject (the property isconfigurable: true, so this is safe).Storageobject jsdom already provides.Deliverables
apps/loopover-miner-ui/vitest.setup.tsguardsglobalThis.localStorageso it's always jsdom's workingStorage, on every supported Node version.npx vitest run src/theme-toggle.test.tsx(and the fullapps/loopover-miner-uisuite) passes under Node 26, without regressing Node 22/24.ResizeObserverStubcomment density) explaining why the guard exists, so it isn't mistaken for dead code once the pinned Node version eventually moves past 22.Test Coverage Requirements
apps/**is outside this repo's Codecovcoverage.includescope, so this isn't gated bycodecov/patch. The bar isapps/loopover-miner-ui's own Vitest suite (npm testin that workspace) passing under both the pinned Node version and a newer one (e.g. Node 26) — there's no separate regression test needed forvitest.setup.tsitself; a greentheme-toggle.test.tsxunder Node 26 locally is the practical verification (a repo-level Node-version test matrix is not in scope here).Expected Outcome
npm testinapps/loopover-miner-ui(and the rootnpm run ui:test/test:ci) passes identically regardless of which supported Node version runs it, instead of silently depending on the current Node version not yet shipping Node's ownlocalStorageglobal.Links & Resources
apps/loopover-miner-ui/vitest.setup.ts(existingResizeObserverStubpattern to mirror),apps/loopover-miner-ui/src/theme-toggle.test.tsx(the failing suite),apps/loopover-miner-ui/src/components/theme-toggle.tsx:39(the productionlocalStoragecall the test exercises),.nvmrc(pins Node 22, why CI is unaffected).