diff --git a/web/src/test-utils/appModuleMocks.test.ts b/web/src/test-utils/appModuleMocks.test.ts new file mode 100644 index 00000000..1359f45c --- /dev/null +++ b/web/src/test-utils/appModuleMocks.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test, vi } from 'vitest'; +import { goto } from '$app/navigation'; +import { page } from '$app/state'; + +// Guards the suite-wide $app mocks in vitest.setup.ts (#3943). If they are +// removed, tests that never mock $app themselves go back to loading +// SvelteKit's client runtime, which fails at module load on some CI runs. +describe('suite-wide SvelteKit app-module mocks', () => { + test('$app/navigation is the mock, not the client runtime', () => { + expect(vi.isMockFunction(goto)).toBe(true); + }); + + test('$app/state gives a page at the root URL', () => { + expect(page.url.pathname).toBe('/'); + }); +}); diff --git a/web/vitest.setup.ts b/web/vitest.setup.ts index 8f28a4de..450f1133 100644 --- a/web/vitest.setup.ts +++ b/web/vitest.setup.ts @@ -15,6 +15,51 @@ vi.mock('@tanstack/svelte-query', async (orig) => { 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