Files
Psychotoxical-psysonic/src/test/setup.ts
T
Frank Stellmacher 377675ae94 test(ui): MiniPlayer + FullscreenPlayer (§4.5 regression) (Phase F5c) (#548)
MiniPlayer.test.tsx (4): mounts without throwing, renders the
always-present titlebar controls (Pin + Open main window — Close is
Linux-only and lives on the manual smoke list per pick 4a), click on
Open main window / Close does not throw, clicking the Pin button
flips the alwaysOnTop label between "Unpin" and "Pin on top". Bridge
contract (mini:ready / mini:sync, geometry persistence) deferred to
B-tier phase B5 -- jsdom does not model two webviews.

FullscreenPlayer.test.tsx (9): renders the labelled Fullscreen Player
dialog + Close Fullscreen control. Control wiring: Close calls
onClose, Stop calls stop, Previous calls previous, Next calls next,
Repeat cycles via toggleRepeat.

§4.5 of the v2 plan -- useCachedUrl(coverUrl, coverKey, false)
regression. Mocks the CachedImage module so the call args are
observable. Pins:
  - the 500 px cover-art call passes opt=false (no fetchUrl fallback;
    prevents double crossfade fetchUrl -> blobUrl);
  - the 300 px art-box call passes opt=true (default behaviour).
A refactor that "tidies up" the useCachedUrl call sites would silently
regress the FS player cover; this test makes it loud.

Harness fix: vi.mock for @tauri-apps/api/event now returns an async
emit that resolves -- components chain .catch() on emit which crashed
on the bare vi.fn() return value during first-render useEffect.
Benefits any future component test that mounts something using emit.

Frontend suite: 399 -> 412 tests (+13). F5 (and Phases F0-F6) complete.
2026-05-11 23:24:40 +02:00

113 lines
4.4 KiB
TypeScript

import '@testing-library/jest-dom/vitest';
import { afterEach, beforeEach, vi } from 'vitest';
import { cleanup } from '@testing-library/react';
import { installBrowserMocks, resetBrowserMocks } from './mocks/browser';
// ─────────────────────────────────────────────────────────────────────────────
// Node 25 ships a native `localStorage` global that is broken on this
// platform — `typeof localStorage === 'object'` but `localStorage.getItem`
// is `undefined`. jsdom 26 simply forwards to it, so both globalThis and
// window expose a non-functional storage. Any code that runs
// `localStorage.getItem(...)` at module load (e.g. `i18n.ts`, `authStore.ts`)
// crashes before tests start.
//
// Fix: install a Map-backed polyfill that conforms to the DOM Storage
// interface, on both globalThis and window. Per-test isolation comes from
// the `afterEach(() => store.clear())` hook below.
// ─────────────────────────────────────────────────────────────────────────────
class MemoryStorage implements Storage {
private map = new Map<string, string>();
get length(): number {
return this.map.size;
}
key(index: number): string | null {
return Array.from(this.map.keys())[index] ?? null;
}
getItem(key: string): string | null {
return this.map.has(key) ? (this.map.get(key) as string) : null;
}
setItem(key: string, value: string): void {
this.map.set(String(key), String(value));
}
removeItem(key: string): void {
this.map.delete(String(key));
}
clear(): void {
this.map.clear();
}
}
const memLocal = new MemoryStorage();
const memSession = new MemoryStorage();
function installStorage(globalKey: 'localStorage' | 'sessionStorage', store: Storage) {
try {
Object.defineProperty(globalThis, globalKey, {
configurable: true,
writable: true,
value: store,
});
} catch {
/* ignore — non-configurable global */
}
if (typeof window !== 'undefined') {
try {
Object.defineProperty(window, globalKey, {
configurable: true,
writable: true,
value: store,
});
} catch {
/* ignore */
}
}
}
installStorage('localStorage', memLocal);
installStorage('sessionStorage', memSession);
// Install jsdom-gap browser API mocks (ResizeObserver / IntersectionObserver /
// matchMedia / clipboard / object URLs) so components don't crash on import.
installBrowserMocks();
// ─────────────────────────────────────────────────────────────────────────────
// Global Tauri mocks.
//
// Every test file that imports `@tauri-apps/api/core` or `@tauri-apps/api/event`
// gets these stubs. They start as bare `vi.fn()`s; the helpers in
// `src/test/mocks/tauri.ts` attach programmable implementations the first time
// they are imported by a test file. Tests that don't need Tauri can ignore
// the helpers entirely.
//
// We mock here (in setupFiles) rather than per-test-file so individual tests
// don't have to repeat `vi.mock('@tauri-apps/api/core', …)` boilerplate.
// ─────────────────────────────────────────────────────────────────────────────
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn(),
convertFileSrc: vi.fn((p: string) => `tauri://localhost/${p}`),
}));
vi.mock('@tauri-apps/api/event', () => ({
listen: vi.fn(),
// Components chain `.catch()` on emit; return a resolved Promise so the
// chain doesn't throw "Cannot read properties of undefined (reading
// 'catch')" inside a useEffect on first render.
emit: vi.fn(async () => undefined),
once: vi.fn(async () => () => {}),
}));
// Linker for Tauri shell / dialog / store plugins — same idea. Extend as needed.
vi.mock('@tauri-apps/plugin-shell', () => ({
open: vi.fn(async () => {}),
}));
beforeEach(() => {
resetBrowserMocks();
});
afterEach(() => {
cleanup();
memLocal.clear();
memSession.clear();
});