Skip to content

theme-toggle tests fail under Node 26 — jsdom's localStorage is shadowed by Node's experimental global #7592

Description

@JSONbored

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

  • apps/loopover-miner-ui/vitest.setup.ts guards globalThis.localStorage so it's always jsdom's working Storage, on every supported Node version.
  • npx vitest run src/theme-toggle.test.tsx (and the full apps/loopover-miner-ui suite) passes under Node 26, without regressing Node 22/24.
  • A short comment (matching the file's existing ResizeObserverStub comment 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 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions