mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
377675ae94
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.
113 lines
4.4 KiB
TypeScript
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();
|
|
});
|