From 9fbe07571ead7c9fff1321a551271e04ab84a5ea Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Thu, 23 Apr 2026 02:23:05 +0200 Subject: [PATCH] test(web): polyfill localStorage/sessionStorage in jsdom env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Node 20+ ships a built-in localStorage/sessionStorage stub on globalThis that throws "storage.setItem is not a function" unless Node is launched with --localstorage-file=PATH. Vitest's jsdom env only overwrites a fixed allow-list of globals, and neither storage is in that list — so Node's broken stub shadows jsdom's working implementation and crashes any code using zustand's persist middleware. Override both globals in the test setup with an in-memory Storage implementation. Resolves 10 keybindStore failures plus 1 FriendsPage toast failure (all symptoms of the same root cause). The remaining FriendsPage DM assertion failure is unrelated and is left for follow-up triage as it touches federation DM-creation behavior. --- packages/web/src/test/setup.ts | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/web/src/test/setup.ts b/packages/web/src/test/setup.ts index 509fa785..ffce9c32 100644 --- a/packages/web/src/test/setup.ts +++ b/packages/web/src/test/setup.ts @@ -1,5 +1,40 @@ import '@testing-library/jest-dom/vitest'; +// Node 20+ ships a built-in `localStorage`/`sessionStorage` stub on globalThis +// that has no methods unless `--localstorage-file=PATH` is provided. Vitest's +// jsdom env only overwrites globals it knows about, and neither storage is in +// that list — so Node's broken stub shadows jsdom's working implementation, +// breaking anything that persists via zustand's `persist` middleware. +// Replace both with an in-memory Storage-compatible polyfill. +class InMemoryStorage implements Storage { + private store = new Map(); + get length(): number { + return this.store.size; + } + clear(): void { + this.store.clear(); + } + getItem(key: string): string | null { + return this.store.has(key) ? this.store.get(key)! : null; + } + key(index: number): string | null { + return Array.from(this.store.keys())[index] ?? null; + } + removeItem(key: string): void { + this.store.delete(key); + } + setItem(key: string, value: string): void { + this.store.set(key, String(value)); + } +} +for (const name of ['localStorage', 'sessionStorage'] as const) { + Object.defineProperty(globalThis, name, { + value: new InMemoryStorage(), + configurable: true, + writable: true, + }); +} + // Polyfill ClipboardItem for jsdom (not included in jsdom) if (typeof ClipboardItem === 'undefined') { // eslint-disable-next-line @typescript-eslint/no-explicit-any