Skip to content

fix(test): restore jsdom Web Storage in UI vitest setups on Node 25+#7603

Closed
davion-knight wants to merge 1 commit into
JSONbored:mainfrom
davion-knight:fix-test-jsdom-localstorage
Closed

fix(test): restore jsdom Web Storage in UI vitest setups on Node 25+#7603
davion-knight wants to merge 1 commit into
JSONbored:mainfrom
davion-knight:fix-test-jsdom-localstorage

Conversation

@davion-knight

Copy link
Copy Markdown
Contributor

Problem

Node 25 shipped a native globalThis.localStorage that evaluates to undefined unless --localstorage-file is passed (nodejs/node#60303, still open). Vitest's jsdom environment copies a window key onto the test global only when that key is in its static allowlist or not already presentlocalStorage/sessionStorage are in neither. So on Node ≤24 jsdom's real Storage is copied over, but on Node 25+ the key already exists (Node's broken accessor) and the copy is skipped, leaving undefined.

Result: all 6 storage-touching suites in loopover-ui and theme-toggle.test.tsx in loopover-miner-ui throw Cannot read properties of undefined (reading 'clear'). Reported upstream as vitest-dev/vitest#8757 and closed as "non-LTS is not supported", so there is no upstream fix to wait for. .nvmrc pins Node 22, so CI is unaffected today — this is a make-local-dev-work fix that becomes CI-breaking the moment .nvmrc moves past Node 24.

Fix

Restore jsdom's Storage from the raw JSDOM instance vitest exposes as globalThis.jsdom — no Node CLI flags (the flag workarounds are unrecognized by the pinned Node 22, so they would break CI). Uses Object.defineProperty rather than direct assignment, since the DOM lib types localStorage as read-only. A strict no-op on Node ≤24, where the key is absent and jsdom's Storage was already copied.

Verification

Node 22 is what .nvmrc pins and what I have locally, so the broken path can't reproduce here. Instead:

  • All 7 affected suites pass unchanged on Node 22 (34 + 4 tests) — confirming the shim is a strict no-op on the pinned version.
  • Simulated the exact Node 25 state (key present, value undefined, jsdom Storage on globalThis.jsdom) and ran the shipped loop against it: it restores working localStorage/sessionStorage (setItem/getItem/clear all functional) on the 25+ condition and leaves an already-copied Storage untouched on the ≤24 condition.
  • ui:typecheck exits 0; ui:lint reports 0 errors; the two loopover-miner-ui typecheck errors are pre-existing (verified identical with these changes stashed).

Canary test

apps/loopover-ui/src/lib/jsdom-web-storage.test.ts asserts the jsdom environment exposes working localStorage/sessionStorage and that the bare global resolves to the same object as the window property. It passes trivially on Node 22; its job is to fail on one explicit assertion the moment .nvmrc moves past Node 24 with this shim removed, instead of every storage-touching suite failing opaquely.

apps/** is outside Codecov's scope, so no codecov/patch obligation. Test infrastructure only — no visual change.

Closes #7576

Node 25 shipped a native globalThis.localStorage that evaluates to undefined
unless --localstorage-file is passed (nodejs/node#60303, still open). Vitest's
jsdom environment copies a window key onto the test global only when that key
is in its static allowlist or not already present; localStorage and
sessionStorage are in neither, so on Node <=24 jsdom's real Storage is copied
over, but on Node 25+ the key already exists as Node's broken accessor and the
copy is skipped. Every localStorage-touching suite in loopover-ui and
loopover-miner-ui then throws on its first .clear()/.setItem().

Restore jsdom's Storage from the raw JSDOM instance vitest exposes as
globalThis.jsdom, which needs no Node CLI flags -- the flag workarounds are
unrecognized by the Node 22 that .nvmrc pins, so they would break CI. Uses
Object.defineProperty rather than assignment because the DOM lib types
localStorage as read-only. A strict no-op on Node <=24, where the key is
absent and jsdom's Storage was already copied.

Adds a canary test so a future .nvmrc bump past Node 24 fails on one explicit
assertion instead of every storage-touching suite throwing opaquely.

Closes JSONbored#7576
@davion-knight
davion-knight requested a review from JSONbored as a code owner July 21, 2026 00:36
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@JSONbored JSONbored left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge conflicts, closing:

This branch has conflicts that must be resolved
Use the [web editor](https://github.com/JSONbored/loopover/pull/7603/conflicts) or the command line to resolve conflicts before continuing.

apps/loopover-miner-ui/vitest.setup.ts

@JSONbored JSONbored closed this Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(test): vitest jsdom environment silently loses window.localStorage on Node 25+

3 participants