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.
193 lines
4.5 KiB
JavaScript
193 lines
4.5 KiB
JavaScript
import { afterEach, beforeEach, vi } from "vitest";
|
|
|
|
// 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() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
}
|
|
|
|
class TestIntersectionObserver {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
takeRecords() {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function createMatchMedia(query) {
|
|
return {
|
|
matches: false,
|
|
media: query,
|
|
onchange: null,
|
|
addListener() {},
|
|
removeListener() {},
|
|
addEventListener() {},
|
|
removeEventListener() {},
|
|
dispatchEvent() {
|
|
return false;
|
|
},
|
|
};
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (!globalThis.IntersectionObserver) {
|
|
globalThis.IntersectionObserver = TestIntersectionObserver;
|
|
}
|
|
|
|
if (globalThis.window) {
|
|
if (!window.matchMedia) {
|
|
Object.defineProperty(window, "matchMedia", {
|
|
configurable: true,
|
|
writable: true,
|
|
value: createMatchMedia,
|
|
});
|
|
}
|
|
|
|
if (!window.scrollTo) {
|
|
Object.defineProperty(window, "scrollTo", {
|
|
configurable: true,
|
|
writable: true,
|
|
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) {
|
|
if (!URL.createObjectURL) {
|
|
Object.defineProperty(URL, "createObjectURL", {
|
|
configurable: true,
|
|
writable: true,
|
|
value: vi.fn(() => "blob:vitest-object-url"),
|
|
});
|
|
}
|
|
|
|
if (!URL.revokeObjectURL) {
|
|
Object.defineProperty(URL, "revokeObjectURL", {
|
|
configurable: true,
|
|
writable: true,
|
|
value: vi.fn(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function clearStorage(storage) {
|
|
try {
|
|
storage?.clear();
|
|
} catch {
|
|
// Some jsdom URL modes can make storage unavailable.
|
|
}
|
|
}
|
|
|
|
beforeEach(() => {
|
|
ensureBrowserMocks();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
|
|
vi.clearAllTimers();
|
|
vi.useRealTimers();
|
|
vi.clearAllMocks();
|
|
vi.unstubAllGlobals();
|
|
|
|
if (globalThis.document?.body) {
|
|
document.body.innerHTML = "";
|
|
}
|
|
|
|
if (globalThis.window) {
|
|
clearStorage(window.localStorage);
|
|
clearStorage(window.sessionStorage);
|
|
}
|
|
|
|
ensureBrowserMocks();
|
|
});
|