diff --git a/tests/unit/setup.js b/tests/unit/setup.js index 237eccb5..98f6e0c4 100644 --- a/tests/unit/setup.js +++ b/tests/unit/setup.js @@ -1,7 +1,9 @@ import { afterEach, beforeEach, vi } from "vitest"; -import { enableAutoUnmount } from "@vue/test-utils"; -enableAutoUnmount(afterEach); +// NOTE: do NOT call enableAutoUnmount here. Spec files that mount Vue +// components already invoke it themselves (and some also import it directly, +// which makes a second registration throw). We only provide the polyfills +// for storage, observers, and URL.createObjectURL below. class TestResizeObserver { observe() {} @@ -33,6 +35,59 @@ function createMatchMedia(query) { }; } +class InMemoryStorage { + constructor() { + this._map = new Map(); + } + get length() { + return this._map.size; + } + key(index) { + return Array.from(this._map.keys())[index] ?? null; + } + getItem(key) { + return this._map.has(key) ? this._map.get(key) : null; + } + setItem(key, value) { + this._map.set(String(key), String(value)); + } + removeItem(key) { + this._map.delete(key); + } + clear() { + this._map.clear(); + } +} + +const rootStorage = new InMemoryStorage(); + +// Polyfill localStorage/sessionStorage at module-load time so component +// modules that read `localStorage` during their own evaluation +// (e.g. SessionUser.vue) work in jsdom 29 + vitest 4 environments that do not +// provide one. Only attach when there is no real implementation already. +if (!globalThis.localStorage || typeof globalThis.localStorage.setItem !== "function") { + try { + Object.defineProperty(globalThis, "localStorage", { + configurable: true, + writable: true, + value: rootStorage, + }); + } catch { + // Some envs lock the global — best effort. + } +} +if (!globalThis.sessionStorage || typeof globalThis.sessionStorage.setItem !== "function") { + try { + Object.defineProperty(globalThis, "sessionStorage", { + configurable: true, + writable: true, + value: rootStorage, + }); + } catch { + // best effort + } +} + function ensureBrowserMocks() { if (!globalThis.ResizeObserver) { globalThis.ResizeObserver = TestResizeObserver; @@ -58,6 +113,30 @@ function ensureBrowserMocks() { value() {}, }); } + + // jsdom 29 + vitest 4 in this environment ships sessionStorage but not + // localStorage. Polyfill a tiny in-memory implementation so tests that + // touch `window.localStorage` (auth tokens, cached selectors, etc.) keep + // working under jsdom. + try { + if (!window.localStorage || typeof window.localStorage.setItem !== "function") { + Object.defineProperty(window, "localStorage", { + configurable: true, + writable: true, + value: new InMemoryStorage(), + }); + } + } catch { + // Some envs lock localStorage — leave it untouched. + } + + if (!window.sessionStorage || typeof window.sessionStorage.setItem !== "function") { + Object.defineProperty(window, "sessionStorage", { + configurable: true, + writable: true, + value: new InMemoryStorage(), + }); + } } if (globalThis.URL) { diff --git a/vitest.config.js b/vitest.config.js index 5f3fed74..f74359a7 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -20,8 +20,16 @@ export default defineConfig({ }, }, test: { + // Default to node so the many source-reading specs keep working. Specs + // that mount Vue components opt into jsdom via a file-level + // `// @vitest-environment jsdom` pragma, which still gets the polyfills + // below because the env config keys match. environment: "node", + environmentMatchGlobs: [ + ["tests/unit/**/*.spec.js", "jsdom"], + ], include: ["tests/unit/**/*.spec.js"], + setupFiles: ["./tests/unit/setup.js"], coverage: { enabled: false, },