import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance } from 'vitest'; import { preferences } from './preferences.svelte'; function ok(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } }); } function envelope(status: number, code: string, message: string): Response { return new Response(JSON.stringify({ error: { code, message } }), { status, headers: { 'content-type': 'application/json' } }); } const flush = () => new Promise((r) => setTimeout(r, 0)); describe('preferences store', () => { let fetchSpy: MockInstance; beforeEach(async () => { localStorage.clear(); // Default fetch implementation returns 401 so the reset calls below // don't escape as real network requests (jsdom can't resolve // /api/...). Each test overrides with mockResolvedValueOnce. fetchSpy = vi .spyOn(globalThis, 'fetch') .mockResolvedValue(envelope(401, 'unauthenticated', 'reset')); // Reset the singleton's state for the next test. preferences.setMode('single'); preferences.setGap('none'); await flush(); fetchSpy.mockReset(); }); afterEach(() => { vi.restoreAllMocks(); localStorage.clear(); }); it('init hydrates from localStorage immediately and pulls server values when authenticated', async () => { localStorage.setItem('mangalord-reader-mode', 'continuous'); localStorage.setItem('mangalord-reader-gap', 'small'); fetchSpy.mockResolvedValueOnce( ok({ reader_mode: 'single', reader_page_gap: 'large', updated_at: '2026-05-17T12:00:00Z' }) ); const p = preferences.init(); // Synchronous hydration from localStorage happens before the await. expect(preferences.readerMode).toBe('continuous'); expect(preferences.readerPageGap).toBe('small'); await p; // Server response overrides the localStorage values. expect(preferences.readerMode).toBe('single'); expect(preferences.readerPageGap).toBe('large'); expect(localStorage.getItem('mangalord-reader-mode')).toBe('single'); expect(localStorage.getItem('mangalord-reader-gap')).toBe('large'); }); it('init keeps localStorage values when the user is anonymous (401)', async () => { localStorage.setItem('mangalord-reader-mode', 'continuous'); localStorage.setItem('mangalord-reader-gap', 'medium'); fetchSpy.mockResolvedValueOnce(envelope(401, 'unauthenticated', 'no session')); await preferences.init(); expect(preferences.readerMode).toBe('continuous'); expect(preferences.readerPageGap).toBe('medium'); }); it('setMode updates state, writes localStorage, and PATCHes the server', async () => { fetchSpy.mockResolvedValueOnce( ok({ reader_mode: 'continuous', reader_page_gap: 'none', updated_at: '2026-05-17T12:00:00Z' }) ); preferences.setMode('continuous'); expect(preferences.readerMode).toBe('continuous'); expect(localStorage.getItem('mangalord-reader-mode')).toBe('continuous'); await flush(); expect(fetchSpy).toHaveBeenCalledOnce(); const init = fetchSpy.mock.calls[0][1] as RequestInit; expect(init.method).toBe('PATCH'); expect(JSON.parse(init.body as string)).toEqual({ reader_mode: 'continuous' }); }); it('setGap survives a 401 (guest) without throwing', async () => { fetchSpy.mockResolvedValueOnce(envelope(401, 'unauthenticated', 'no session')); preferences.setGap('large'); expect(preferences.readerPageGap).toBe('large'); await flush(); // localStorage still holds the choice — the guest preference persists. expect(localStorage.getItem('mangalord-reader-gap')).toBe('large'); }); it('setMode reverts the optimistic update when the server returns 5xx', async () => { // Suppress the expected console.error so the test output stays clean. const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); fetchSpy.mockResolvedValueOnce(envelope(500, 'internal_error', 'boom')); preferences.setMode('continuous'); expect(preferences.readerMode).toBe('continuous'); await flush(); expect(preferences.readerMode).toBe('single'); expect(localStorage.getItem('mangalord-reader-mode')).toBe('single'); errSpy.mockRestore(); }); it('clearForLogout resets state and removes localStorage entries', () => { localStorage.setItem('mangalord-reader-mode', 'continuous'); localStorage.setItem('mangalord-reader-gap', 'large'); preferences.clearForLogout(); expect(preferences.readerMode).toBe('single'); expect(preferences.readerPageGap).toBe('none'); expect(localStorage.getItem('mangalord-reader-mode')).toBeNull(); expect(localStorage.getItem('mangalord-reader-gap')).toBeNull(); }); it('init ignores an unknown reader_page_gap from the server (forward compat)', async () => { localStorage.setItem('mangalord-reader-mode', 'continuous'); localStorage.setItem('mangalord-reader-gap', 'medium'); fetchSpy.mockResolvedValueOnce( ok({ reader_mode: 'continuous', reader_page_gap: 'huge', updated_at: '2026-05-17T12:00:00Z' }) ); await preferences.init(); // Mode comes through (known value); unknown gap is rejected, the // pre-existing 'medium' value is retained. expect(preferences.readerMode).toBe('continuous'); expect(preferences.readerPageGap).toBe('medium'); }); });