From 70b1c4429d2d4f299c2495288852f56e91d3a59b Mon Sep 17 00:00:00 2001 From: Till JS Date: Fri, 20 Mar 2026 17:16:47 +0100 Subject: [PATCH] test(mukke): add vitest setup and 34 frontend tests for player & library stores - Set up vitest with jsdom, testing-library/svelte, and SvelteKit mocks - Player store: 16 tests covering playSong, queue, shuffle, repeat, volume, error handling, clearQueue, removeFromQueue - Library store: 18 tests covering loadSongs, loadCoverUrls (including non-image path filtering), albums, artists, genres, stats, favorites, tabs, upload, and delete Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/mukke/apps/web/package.json | 8 +- .../web/src/lib/stores/library.svelte.test.ts | 264 ++++++++++++++++++ .../web/src/lib/stores/player.svelte.test.ts | 254 +++++++++++++++++ .../web/src/test/mocks/app/environment.ts | 4 + .../apps/web/src/test/mocks/app/navigation.ts | 9 + .../apps/web/src/test/mocks/app/stores.ts | 17 ++ .../web/src/test/mocks/env/static/public.ts | 2 + apps/mukke/apps/web/src/test/setup.ts | 82 ++++++ apps/mukke/apps/web/vitest.config.ts | 20 ++ 9 files changed, 658 insertions(+), 2 deletions(-) create mode 100644 apps/mukke/apps/web/src/lib/stores/library.svelte.test.ts create mode 100644 apps/mukke/apps/web/src/lib/stores/player.svelte.test.ts create mode 100644 apps/mukke/apps/web/src/test/mocks/app/environment.ts create mode 100644 apps/mukke/apps/web/src/test/mocks/app/navigation.ts create mode 100644 apps/mukke/apps/web/src/test/mocks/app/stores.ts create mode 100644 apps/mukke/apps/web/src/test/mocks/env/static/public.ts create mode 100644 apps/mukke/apps/web/src/test/setup.ts create mode 100644 apps/mukke/apps/web/vitest.config.ts diff --git a/apps/mukke/apps/web/package.json b/apps/mukke/apps/web/package.json index 26c8d2da1..743449fd0 100644 --- a/apps/mukke/apps/web/package.json +++ b/apps/mukke/apps/web/package.json @@ -11,7 +11,9 @@ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "eslint .", "format": "prettier --write .", - "type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" + "type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "test": "vitest run", + "test:watch": "vitest" }, "devDependencies": { "@manacore/shared-pwa": "workspace:*", @@ -20,6 +22,7 @@ "@sveltejs/kit": "^2.47.1", "@sveltejs/vite-plugin-svelte": "^5.0.0", "@tailwindcss/vite": "^4.1.7", + "@testing-library/svelte": "^5.2.6", "@types/node": "^20.0.0", "@vite-pwa/sveltekit": "^1.1.0", "prettier": "^3.1.1", @@ -29,7 +32,8 @@ "tailwindcss": "^4.1.7", "tslib": "^2.4.1", "typescript": "^5.9.3", - "vite": "^6.0.0" + "vite": "^6.0.0", + "vitest": "^4.1.0" }, "dependencies": { "@manacore/shared-api-client": "workspace:*", diff --git a/apps/mukke/apps/web/src/lib/stores/library.svelte.test.ts b/apps/mukke/apps/web/src/lib/stores/library.svelte.test.ts new file mode 100644 index 000000000..269f51bfa --- /dev/null +++ b/apps/mukke/apps/web/src/lib/stores/library.svelte.test.ts @@ -0,0 +1,264 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// Mock auth store +vi.mock('./auth.svelte', () => ({ + authStore: { + getAuthHeaders: vi.fn().mockResolvedValue({ Authorization: 'Bearer test-token' }), + }, +})); + +let libraryStore: typeof import('./library.svelte').libraryStore; + +beforeEach(async () => { + vi.clearAllMocks(); + vi.resetModules(); + const mod = await import('./library.svelte'); + libraryStore = mod.libraryStore; +}); + +function mockFetchResponse(data: unknown) { + (global.fetch as ReturnType).mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(data), + }); +} + +function mockFetchError(message = 'Request failed') { + (global.fetch as ReturnType).mockResolvedValueOnce({ + ok: false, + json: () => Promise.resolve({ message }), + }); +} + +describe('libraryStore', () => { + describe('initial state', () => { + it('starts empty', () => { + expect(libraryStore.songs).toEqual([]); + expect(libraryStore.albums).toEqual([]); + expect(libraryStore.artists).toEqual([]); + expect(libraryStore.genres).toEqual([]); + expect(libraryStore.stats).toBeNull(); + expect(libraryStore.activeTab).toBe('songs'); + expect(libraryStore.isLoading).toBe(false); + expect(libraryStore.error).toBeNull(); + }); + }); + + describe('loadSongs', () => { + it('fetches songs and sets state', async () => { + const songs = [ + { id: '1', title: 'Song 1', coverArtPath: null }, + { id: '2', title: 'Song 2', coverArtPath: null }, + ]; + mockFetchResponse({ songs }); + + await libraryStore.loadSongs(); + + expect(libraryStore.songs).toEqual(songs); + expect(libraryStore.isLoading).toBe(false); + expect(libraryStore.error).toBeNull(); + }); + + it('sets error on failure', async () => { + mockFetchError('Server error'); + + await libraryStore.loadSongs(); + + expect(libraryStore.songs).toEqual([]); + expect(libraryStore.error).toBe('Server error'); + expect(libraryStore.isLoading).toBe(false); + }); + + it('loads cover URLs for songs with cover art', async () => { + const songs = [ + { id: '1', title: 'Song 1', coverArtPath: 'users/test/covers/1.jpg' }, + { id: '2', title: 'Song 2', coverArtPath: null }, + ]; + mockFetchResponse({ songs }); + // Cover URLs fetch + mockFetchResponse({ urls: { 'users/test/covers/1.jpg': 'https://minio.test/cover.jpg' } }); + + await libraryStore.loadSongs(); + + // Wait for cover URLs to load (async, non-blocking) + await vi.waitFor(() => { + expect(libraryStore.coverUrls['users/test/covers/1.jpg']).toBe( + 'https://minio.test/cover.jpg' + ); + }); + }); + }); + + describe('loadCoverUrls', () => { + it('filters out non-image paths', async () => { + // Should not make any fetch for .mp3 paths + await libraryStore.loadCoverUrls(['users/test/song.mp3', 'users/test/audio.wav']); + + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it('loads valid image paths', async () => { + mockFetchResponse({ + urls: { 'users/test/covers/1.jpg': 'https://minio.test/cover.jpg' }, + }); + + await libraryStore.loadCoverUrls(['users/test/covers/1.jpg']); + + expect(libraryStore.coverUrls['users/test/covers/1.jpg']).toBe( + 'https://minio.test/cover.jpg' + ); + }); + + it('does not refetch cached URLs', async () => { + mockFetchResponse({ + urls: { 'users/test/covers/1.jpg': 'https://minio.test/cover.jpg' }, + }); + await libraryStore.loadCoverUrls(['users/test/covers/1.jpg']); + + // Second call with same path should not fetch + await libraryStore.loadCoverUrls(['users/test/covers/1.jpg']); + + expect(global.fetch).toHaveBeenCalledTimes(1); + }); + + it('silently handles errors', async () => { + (global.fetch as ReturnType).mockRejectedValueOnce(new Error('Network error')); + + await libraryStore.loadCoverUrls(['users/test/covers/1.png']); + + // Should not throw or set error + expect(libraryStore.error).toBeNull(); + }); + + it('accepts various image extensions', async () => { + const paths = [ + 'covers/1.jpg', + 'covers/2.jpeg', + 'covers/3.png', + 'covers/4.webp', + 'covers/5.gif', + 'covers/6.avif', + 'covers/7.svg', + ]; + const urls: Record = {}; + paths.forEach((p) => (urls[p] = `https://minio.test/${p}`)); + mockFetchResponse({ urls }); + + await libraryStore.loadCoverUrls(paths); + + expect(global.fetch).toHaveBeenCalledTimes(1); + paths.forEach((p) => { + expect(libraryStore.coverUrls[p]).toBe(`https://minio.test/${p}`); + }); + }); + }); + + describe('loadAlbums', () => { + it('fetches albums and sets state', async () => { + const albums = [{ album: 'Album 1', songCount: 5, coverArtPath: null }]; + mockFetchResponse({ albums }); + + await libraryStore.loadAlbums(); + + expect(libraryStore.albums).toEqual(albums); + }); + }); + + describe('loadArtists', () => { + it('fetches artists and sets state', async () => { + const artists = [{ artist: 'Artist 1', songCount: 3, albumCount: 1 }]; + mockFetchResponse({ artists }); + + await libraryStore.loadArtists(); + + expect(libraryStore.artists).toEqual(artists); + }); + }); + + describe('loadGenres', () => { + it('fetches genres and sets state', async () => { + const genres = [{ genre: 'Rock', songCount: 10 }]; + mockFetchResponse({ genres }); + + await libraryStore.loadGenres(); + + expect(libraryStore.genres).toEqual(genres); + }); + }); + + describe('loadStats', () => { + it('fetches stats and sets state', async () => { + const stats = { totalSongs: 50, totalArtists: 10, totalAlbums: 5, totalGenres: 3 }; + mockFetchResponse({ stats }); + + await libraryStore.loadStats(); + + expect(libraryStore.stats).toEqual(stats); + }); + }); + + describe('toggleFavorite', () => { + it('updates song in list', async () => { + const songs = [{ id: '1', title: 'Song 1', favorite: false, coverArtPath: null }]; + mockFetchResponse({ songs }); + await libraryStore.loadSongs(); + + mockFetchResponse({ song: { ...songs[0], favorite: true } }); + await libraryStore.toggleFavorite('1'); + + expect(libraryStore.songs[0].favorite).toBe(true); + }); + }); + + describe('setActiveTab', () => { + it('changes active tab', () => { + expect(libraryStore.activeTab).toBe('songs'); + libraryStore.setActiveTab('albums'); + expect(libraryStore.activeTab).toBe('albums'); + }); + + it('triggers load for empty tabs', async () => { + mockFetchResponse({ albums: [] }); + libraryStore.setActiveTab('albums'); + + // setActiveTab triggers an async load internally + await vi.waitFor(() => { + expect(global.fetch).toHaveBeenCalled(); + }); + expect(libraryStore.activeTab).toBe('albums'); + }); + }); + + describe('uploadSong', () => { + it('creates song and uploads file', async () => { + const song = { id: '1', title: 'New Song' }; + mockFetchResponse({ song, uploadUrl: 'https://minio.test/upload' }); + // Upload PUT + (global.fetch as ReturnType).mockResolvedValueOnce({ ok: true }); + + const file = new File(['audio data'], 'song.mp3', { type: 'audio/mpeg' }); + const result = await libraryStore.uploadSong(file); + + expect(result).toEqual(song); + expect(libraryStore.songs).toContainEqual(song); + expect(global.fetch).toHaveBeenCalledTimes(2); + }); + }); + + describe('deleteSong', () => { + it('removes song from list', async () => { + const songs = [ + { id: '1', title: 'Song 1', coverArtPath: null }, + { id: '2', title: 'Song 2', coverArtPath: null }, + ]; + mockFetchResponse({ songs }); + await libraryStore.loadSongs(); + + mockFetchResponse({}); + await libraryStore.deleteSong('1'); + + expect(libraryStore.songs).toHaveLength(1); + expect(libraryStore.songs[0].id).toBe('2'); + }); + }); +}); diff --git a/apps/mukke/apps/web/src/lib/stores/player.svelte.test.ts b/apps/mukke/apps/web/src/lib/stores/player.svelte.test.ts new file mode 100644 index 000000000..d40727f3f --- /dev/null +++ b/apps/mukke/apps/web/src/lib/stores/player.svelte.test.ts @@ -0,0 +1,254 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// Mock auth store before importing player store +vi.mock('./auth.svelte', () => ({ + authStore: { + getAuthHeaders: vi.fn().mockResolvedValue({ Authorization: 'Bearer test-token' }), + }, +})); + +// Dynamic import to allow mock setup first +let playerStore: typeof import('./player.svelte').playerStore; + +beforeEach(async () => { + vi.clearAllMocks(); + + // Reset module registry so each test gets a fresh store + vi.resetModules(); + const mod = await import('./player.svelte'); + playerStore = mod.playerStore; +}); + +function makeSong(overrides: Partial<{ id: string; title: string; artist: string }> = {}) { + return { + id: overrides.id ?? '1', + title: overrides.title ?? 'Test Song', + artist: overrides.artist ?? 'Test Artist', + album: null, + albumArtist: null, + genre: null, + trackNumber: null, + year: null, + duration: 180, + storagePath: 'users/test/1.mp3', + coverArtPath: null, + fileSize: null, + bpm: null, + favorite: false, + playCount: 0, + lastPlayedAt: null, + addedAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + userId: 'user-1', + } as any; +} + +describe('playerStore', () => { + describe('initial state', () => { + it('starts with no song playing', () => { + expect(playerStore.currentSong).toBeNull(); + expect(playerStore.isPlaying).toBe(false); + expect(playerStore.currentTime).toBe(0); + expect(playerStore.duration).toBe(0); + expect(playerStore.error).toBeNull(); + }); + + it('starts with default settings', () => { + expect(playerStore.volume).toBe(1); + expect(playerStore.repeatMode).toBe('off'); + expect(playerStore.shuffleOn).toBe(false); + expect(playerStore.queue).toEqual([]); + expect(playerStore.showFullPlayer).toBe(false); + expect(playerStore.showQueue).toBe(false); + }); + }); + + describe('playSong', () => { + it('sets current song and fetches download URL', async () => { + const song = makeSong(); + (global.fetch as ReturnType).mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ url: 'https://minio.test/song.mp3' }), + }); + + await playerStore.playSong(song); + + expect(playerStore.currentSong).toEqual(song); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/songs/1/download-url'), + expect.any(Object) + ); + }); + + it('sets up queue when provided', async () => { + const songs = [makeSong({ id: '1' }), makeSong({ id: '2' }), makeSong({ id: '3' })]; + (global.fetch as ReturnType).mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ url: 'https://minio.test/song.mp3' }), + }); + + await playerStore.playSong(songs[1], songs, 1); + + expect(playerStore.currentSong?.id).toBe('2'); + expect(playerStore.queue).toHaveLength(3); + expect(playerStore.currentIndex).toBe(1); + }); + + it('sets error when download URL fetch fails', async () => { + const song = makeSong(); + (global.fetch as ReturnType).mockResolvedValueOnce({ + ok: false, + json: () => Promise.resolve({ message: 'Not found' }), + }); + + await playerStore.playSong(song); + + expect(playerStore.isPlaying).toBe(false); + expect(playerStore.error).toBeTruthy(); + }); + }); + + describe('togglePlay', () => { + it('does nothing when no song is loaded', () => { + playerStore.togglePlay(); + expect(playerStore.isPlaying).toBe(false); + }); + }); + + describe('toggleRepeat', () => { + it('cycles through off -> all -> one -> off', () => { + expect(playerStore.repeatMode).toBe('off'); + + playerStore.toggleRepeat(); + expect(playerStore.repeatMode).toBe('all'); + + playerStore.toggleRepeat(); + expect(playerStore.repeatMode).toBe('one'); + + playerStore.toggleRepeat(); + expect(playerStore.repeatMode).toBe('off'); + }); + }); + + describe('toggleShuffle', () => { + it('toggles shuffle state', () => { + expect(playerStore.shuffleOn).toBe(false); + playerStore.toggleShuffle(); + expect(playerStore.shuffleOn).toBe(true); + playerStore.toggleShuffle(); + expect(playerStore.shuffleOn).toBe(false); + }); + }); + + describe('toggleFullPlayer', () => { + it('toggles full player visibility', () => { + expect(playerStore.showFullPlayer).toBe(false); + playerStore.toggleFullPlayer(); + expect(playerStore.showFullPlayer).toBe(true); + playerStore.toggleFullPlayer(); + expect(playerStore.showFullPlayer).toBe(false); + }); + }); + + describe('toggleQueue', () => { + it('toggles queue panel visibility', () => { + expect(playerStore.showQueue).toBe(false); + playerStore.toggleQueue(); + expect(playerStore.showQueue).toBe(true); + playerStore.toggleQueue(); + expect(playerStore.showQueue).toBe(false); + }); + }); + + describe('error handling', () => { + it('clearError resets error state', async () => { + const song = makeSong(); + (global.fetch as ReturnType).mockResolvedValueOnce({ + ok: false, + json: () => Promise.resolve({ message: 'Not found' }), + }); + + await playerStore.playSong(song); + expect(playerStore.error).toBeTruthy(); + + playerStore.clearError(); + expect(playerStore.error).toBeNull(); + }); + }); + + describe('clearQueue', () => { + it('resets all state', async () => { + const song = makeSong(); + (global.fetch as ReturnType).mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ url: 'https://minio.test/song.mp3' }), + }); + + await playerStore.playSong(song, [song], 0); + playerStore.clearQueue(); + + expect(playerStore.currentSong).toBeNull(); + expect(playerStore.isPlaying).toBe(false); + expect(playerStore.queue).toEqual([]); + expect(playerStore.error).toBeNull(); + expect(playerStore.showFullPlayer).toBe(false); + expect(playerStore.showQueue).toBe(false); + }); + }); + + describe('playQueue', () => { + it('starts playing from given index', async () => { + const songs = [makeSong({ id: '1' }), makeSong({ id: '2' }), makeSong({ id: '3' })]; + (global.fetch as ReturnType).mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ url: 'https://minio.test/song2.mp3' }), + }); + + await playerStore.playQueue(songs, 1); + + expect(playerStore.currentSong?.id).toBe('2'); + expect(playerStore.queue).toHaveLength(3); + }); + }); + + describe('setVolume', () => { + it('clamps volume between 0 and 1', () => { + playerStore.setVolume(0.5); + expect(playerStore.volume).toBe(0.5); + + playerStore.setVolume(-1); + expect(playerStore.volume).toBe(0); + + playerStore.setVolume(2); + expect(playerStore.volume).toBe(1); + }); + }); + + describe('removeFromQueue', () => { + it('does not remove current song', async () => { + const songs = [makeSong({ id: '1' }), makeSong({ id: '2' })]; + (global.fetch as ReturnType).mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ url: 'https://minio.test/song.mp3' }), + }); + + await playerStore.playSong(songs[0], songs, 0); + playerStore.removeFromQueue(0); + + expect(playerStore.queue).toHaveLength(2); + }); + + it('removes non-current song from queue', async () => { + const songs = [makeSong({ id: '1' }), makeSong({ id: '2' }), makeSong({ id: '3' })]; + (global.fetch as ReturnType).mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ url: 'https://minio.test/song.mp3' }), + }); + + await playerStore.playSong(songs[0], songs, 0); + playerStore.removeFromQueue(2); + + expect(playerStore.queue).toHaveLength(2); + }); + }); +}); diff --git a/apps/mukke/apps/web/src/test/mocks/app/environment.ts b/apps/mukke/apps/web/src/test/mocks/app/environment.ts new file mode 100644 index 000000000..a75920f41 --- /dev/null +++ b/apps/mukke/apps/web/src/test/mocks/app/environment.ts @@ -0,0 +1,4 @@ +export const browser = true; +export const building = false; +export const dev = true; +export const version = 'test'; diff --git a/apps/mukke/apps/web/src/test/mocks/app/navigation.ts b/apps/mukke/apps/web/src/test/mocks/app/navigation.ts new file mode 100644 index 000000000..1d0b3de7f --- /dev/null +++ b/apps/mukke/apps/web/src/test/mocks/app/navigation.ts @@ -0,0 +1,9 @@ +import { vi } from 'vitest'; + +export const goto = vi.fn(); +export const invalidate = vi.fn(); +export const invalidateAll = vi.fn(); +export const prefetch = vi.fn(); +export const prefetchRoutes = vi.fn(); +export const beforeNavigate = vi.fn(); +export const afterNavigate = vi.fn(); diff --git a/apps/mukke/apps/web/src/test/mocks/app/stores.ts b/apps/mukke/apps/web/src/test/mocks/app/stores.ts new file mode 100644 index 000000000..c9285027e --- /dev/null +++ b/apps/mukke/apps/web/src/test/mocks/app/stores.ts @@ -0,0 +1,17 @@ +import { writable, readable } from 'svelte/store'; + +export const page = readable({ + url: new URL('http://localhost'), + params: {}, + route: { id: '/' }, + status: 200, + error: null, + data: {}, + form: null, +}); + +export const navigating = readable(null); +export const updated = { + subscribe: writable(false).subscribe, + check: async () => false, +}; diff --git a/apps/mukke/apps/web/src/test/mocks/env/static/public.ts b/apps/mukke/apps/web/src/test/mocks/env/static/public.ts new file mode 100644 index 000000000..4703e547f --- /dev/null +++ b/apps/mukke/apps/web/src/test/mocks/env/static/public.ts @@ -0,0 +1,2 @@ +export const PUBLIC_BACKEND_URL = 'http://localhost:3010'; +export const PUBLIC_MANA_CORE_AUTH_URL = 'http://localhost:3001'; diff --git a/apps/mukke/apps/web/src/test/setup.ts b/apps/mukke/apps/web/src/test/setup.ts new file mode 100644 index 000000000..51b95bb19 --- /dev/null +++ b/apps/mukke/apps/web/src/test/setup.ts @@ -0,0 +1,82 @@ +import { vi, beforeEach } from 'vitest'; + +// Mock localStorage +const localStorageMock = { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + clear: vi.fn(), +}; +Object.defineProperty(global, 'localStorage', { value: localStorageMock }); + +// Mock fetch +global.fetch = vi.fn(); + +// Mock Audio +class MockAudio { + src = ''; + currentTime = 0; + duration = 0; + volume = 1; + paused = true; + error: MediaError | null = null; + private listeners: Record = {}; + + addEventListener(event: string, handler: Function) { + if (!this.listeners[event]) this.listeners[event] = []; + this.listeners[event].push(handler); + } + + removeEventListener(event: string, handler: Function) { + if (this.listeners[event]) { + this.listeners[event] = this.listeners[event].filter((h) => h !== handler); + } + } + + dispatchEvent(event: Event) { + const handlers = this.listeners[event.type] || []; + handlers.forEach((h) => h(event)); + return true; + } + + emit(eventName: string) { + const handlers = this.listeners[eventName] || []; + handlers.forEach((h) => h()); + } + + async play() { + this.paused = false; + return Promise.resolve(); + } + + pause() { + this.paused = true; + } +} +global.Audio = MockAudio as unknown as typeof Audio; + +// Mock MediaError +if (typeof global.MediaError === 'undefined') { + (global as Record).MediaError = { + MEDIA_ERR_ABORTED: 1, + MEDIA_ERR_NETWORK: 2, + MEDIA_ERR_DECODE: 3, + MEDIA_ERR_SRC_NOT_SUPPORTED: 4, + }; +} + +// Mock MediaSession +if (typeof navigator !== 'undefined' && !('mediaSession' in navigator)) { + Object.defineProperty(navigator, 'mediaSession', { + value: { + metadata: null, + setActionHandler: vi.fn(), + }, + }); +} + +// Reset mocks before each test +beforeEach(() => { + vi.clearAllMocks(); + localStorageMock.getItem.mockReturnValue(null); +}); diff --git a/apps/mukke/apps/web/vitest.config.ts b/apps/mukke/apps/web/vitest.config.ts new file mode 100644 index 000000000..53fbf6211 --- /dev/null +++ b/apps/mukke/apps/web/vitest.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vitest/config'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; +import path from 'path'; + +export default defineConfig({ + plugins: [svelte({ hot: !process.env.VITEST })], + test: { + include: ['src/**/*.{test,spec}.{js,ts}'], + environment: 'jsdom', + globals: true, + setupFiles: ['./src/test/setup.ts'], + }, + resolve: { + alias: { + $lib: path.resolve(__dirname, './src/lib'), + $app: path.resolve(__dirname, './src/test/mocks/app'), + '$env/static/public': path.resolve(__dirname, './src/test/mocks/env/static/public.ts'), + }, + }, +});