Jest "SecurityError: localStorage is not available"
jsdom refuses localStorage/sessionStorage access when the document origin is "opaque" - the default about:blank. The spec forbids storage on opaque origins, so jsdom throws a SecurityError.
What this error means
Any code touching localStorage under jsdom throws SecurityError: localStorage is not available for opaque origins. It is deterministic and tied to the jsdom URL, not to the test logic.
SecurityError: localStorage is not available for opaque origins
at Window.get localStorage (node_modules/jsdom/lib/jsdom/browser/Window.js)Diagnose it: flake, environment, or genuine failure?
Before debugging the assertion, establish whether the test is deterministic. A test that fails only in CI is usually order-dependent, time-dependent, or racing something, and fixing the assertion will not help.
# does it fail in isolation?
npx vitest run path/to/file.test.ts
# is it order dependent? run the suite in a random order twice
npx vitest run --sequence.shuffle
# is it a race? run the same file repeatedly
for i in $(seq 1 20); do npx vitest run path/to/file.test.ts || break; doneCommon causes
Default opaque jsdom origin
With no configured URL, jsdom runs at about:blank, an opaque origin. Per the HTML storage spec, localStorage on an opaque origin must throw.
No storage mock provided
If you do not give jsdom a real http(s) origin, you must instead stub localStorage yourself; otherwise every access errors.
How to fix it
Give jsdom a concrete origin
Set a real URL so the origin is no longer opaque and storage works.
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
testEnvironmentOptions: { url: 'http://localhost/' },
};Mock storage in setup
// jest.setup.js
const store = {};
global.localStorage = {
getItem: (k) => store[k] ?? null,
setItem: (k, v) => { store[k] = String(v); },
removeItem: (k) => { delete store[k]; },
clear: () => { for (const k in store) delete store[k]; },
};CI-only causes worth ruling out
- Runners have fewer cores than a laptop, so timing-sensitive tests that pass locally fail under contention.
- No TTY and a different locale or timezone. Snapshot tests containing formatted dates or numbers are the usual casualty; pin
TZandLANGin the job. - Parallel workers sharing a database, a port, or a temp directory. Give each worker its own namespace.
- Default timeouts calibrated on a fast machine. A cold runner is slower on first execution, especially before any cache warms.
How to prevent it
- Set a non-opaque
urlintestEnvironmentOptions. - Centralize a storage mock in a shared setup file.
- Reset storage between tests to avoid cross-test leakage.