import '@testing-library/jest-dom/vitest'; import { beforeEach, afterEach, vi } from 'vitest'; import { render } from '@testing-library/svelte'; import ToastHost from './src/lib/components/ToastHost.svelte'; import { clearToast } from './src/lib/stores/toast.svelte'; // Default svelte-query mock for the whole test suite. Most component // tests just need useQueryClient to return SOMETHING — they never assert // on the result. Tests that need to assert on invalidateQueries (named // mock pattern) override this with their own per-file vi.mock. The // override wins because per-file mocks take precedence over setup-file // mocks in vitest. vi.mock('@tanstack/svelte-query', async (orig) => { const actual = (await orig()) as Record; return { ...actual, useQueryClient: () => ({ invalidateQueries: vi.fn() }) }; }); // Default SvelteKit app-module mocks for the whole suite (#3943). Without // them, any page or component that reaches $app/navigation or $app/state — // the home page does, through AlbumCard → AlbumMenu — loads SvelteKit's // client runtime, whose $app/paths reads __SVELTEKIT_PAYLOAD__ at module // load. That global only exists when the kit plugin's define reaches the // module, which it does not reliably under vitest: page.test.ts failed to // load on one CI run and passed on the re-run of the same commit (#374 // was the same class). Stubbing the modules keeps every test off the // client bootstrap. Tests that assert on goto or need a particular URL // still mock per file (pageUrlModule in test-utils/mocks/appState.ts), // and per-file mocks win over these. vi.mock('$app/navigation', () => ({ goto: vi.fn(() => Promise.resolve()), invalidate: vi.fn(() => Promise.resolve()), invalidateAll: vi.fn(() => Promise.resolve()), preloadData: vi.fn(() => Promise.resolve()), preloadCode: vi.fn(() => Promise.resolve()), beforeNavigate: vi.fn(), afterNavigate: vi.fn(), onNavigate: vi.fn(), pushState: vi.fn(), replaceState: vi.fn(), disableScrollHandling: vi.fn() })); vi.mock('$app/state', () => ({ page: { url: new URL('http://localhost/'), params: {}, route: { id: null }, status: 200, error: null, data: {}, form: null, state: {} }, navigating: null, updated: { current: false, check: () => Promise.resolve(false) } })); vi.mock('$app/paths', () => ({ base: '', assets: '', resolve: (path: string) => path, asset: (path: string) => path })); // Node 25 ships a partial localStorage global that lacks getItem/setItem/clear // unless launched with --localstorage-file. jsdom sees Node's global and skips // installing its own. Replace with an in-memory Storage-compatible polyfill so // tests (and browser-only code under test) see a working localStorage. class MemoryStorage implements Storage { private store = new Map(); get length() { return this.store.size; } clear(): void { this.store.clear(); } getItem(key: string): string | null { return this.store.get(key) ?? null; } key(i: number): string | null { return Array.from(this.store.keys())[i] ?? null; } removeItem(key: string): void { this.store.delete(key); } setItem(key: string, value: string): void { this.store.set(key, String(value)); } } const memLocal = new MemoryStorage(); const memSession = new MemoryStorage(); Object.defineProperty(globalThis, 'localStorage', { configurable: true, value: memLocal }); Object.defineProperty(globalThis, 'sessionStorage', { configurable: true, value: memSession }); if (typeof window !== 'undefined') { Object.defineProperty(window, 'localStorage', { configurable: true, value: memLocal }); Object.defineProperty(window, 'sessionStorage', { configurable: true, value: memSession }); } // jsdom doesn't implement Element.prototype.scrollIntoView. Components that // call it (queue auto-scroll to the now-playing row, the alphabetical rail) // would throw an unhandled TypeError in tests — which fails the run even when // every assertion passes. No-op it; tests never assert on scroll position. if (typeof Element !== 'undefined' && !Element.prototype.scrollIntoView) { Element.prototype.scrollIntoView = () => {}; } // W-T3 moved toast rendering out of per-page markup into a single // mounted in +layout.svelte. Tests render individual pages // without the layout, so we mount ToastHost here so `pushToast()` calls // from page code surface in the DOM the same way they do in production. // `render()` from @testing-library/svelte registers auto-cleanup via the // svelteTesting() vite plugin, so the host gets unmounted between tests // alongside whatever component the test itself rendered. clearToast() // resets the module-scope store state so a stale toast from one test // can't leak into another. beforeEach(() => { render(ToastHost); }); afterEach(() => { clearToast(); });