release / govulncheck (push) Successful in 32s
release / web (push) Successful in 1m25s
release / go (push) Successful in 1m41s
release / integration (push) Successful in 4m55s
release / android (push) Successful in 6m15s
release / Build signed APK (releases and dev) (push) Successful in 6m42s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 2m4s
release / Verify release artifacts (tag releases only) (push) Skipped
The home page reaches the real $app/navigation through AlbumCard and AlbumMenu. That loads SvelteKit's client runtime, whose $app/paths reads __SVELTEKIT_PAYLOAD__ at module load. The global is only there when the kit plugin's define reaches the module, and under vitest that is not reliable: page.test.ts failed to load on CI run 6576 and passed on its re-run. #374 was the same class of failure. vitest.setup.ts now mocks $app/navigation, $app/state and $app/paths for every test. Per-file mocks still win. A small guard test fails if the suite-wide mocks are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
4.8 KiB
TypeScript
109 lines
4.8 KiB
TypeScript
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<string, unknown>;
|
|
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<string, string>();
|
|
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
|
|
// <ToastHost /> 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();
|
|
});
|