fix(test): polyfill localStorage and align jsdom env for spec files (#267)

Switches `vitest.config.js` to `environmentMatchGlobs` so source-reading specs keep Node URL resolution while Vue specs run under jsdom. Adds an in-memory `localStorage`/`sessionStorage` polyfill (and ResizeObserver/IntersectionObserver fallbacks) to `tests/unit/setup.js` so jsdom 29 + vitest 4 environments that ship no localStorage stop crashing the 109 unit tests that touched SessionUser / InvoicingBillingPeriod caches at module-load time.

Test result: 1343/1343 fast + 1688/1688 serial pass (was 1195/1304 on master). All 197 invoicing-period / invoice-distribution / superuser-invoices / xlvask-usage-amount-cache tests green.
This commit is contained in:
Jeppe B
2026-08-09 15:40:29 +00:00
committed by GitHub
parent 82c95d32c0
commit 4810e113f3
2 changed files with 89 additions and 2 deletions
+81 -2
View File
@@ -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) {