import { renderHook, act } from '@testing-library/react'; import { useState } from 'react'; import { vi, test, expect, beforeEach } from 'vitest'; const remote = { prefs: {}, puts: [] }; vi.mock('../syncService.js', () => ({ getRemotePrefs: vi.fn(async () => ({ ok: true, data: { prefs: remote.prefs } })), saveRemotePrefs: vi.fn(async (prefs) => { remote.puts.push(prefs); return { ok: true, data: { prefs } }; }), })); import { usePrefsSync } from './usePrefsSync.js'; beforeEach(() => { localStorage.clear(); remote.prefs = {}; remote.puts = []; }); // A stand-in for the Reader's state, wired the same way useBibleReader wires it. function setup({ canApplyPosition = () => true, initial = {} } = {}) { return renderHook(() => { const [theme, setTheme] = useState(initial.theme ?? 'paper'); const [layout, setLayout] = useState(initial.layout ?? 'flow'); const [position, setPosition] = useState(initial.position ?? { book: 'GEN', chapter: 1 }); const sync = usePrefsSync({ values: { theme, translation: 'BSB', layout, commentarySource: 'matthew-henry', planTranslation: 'same', startPage: 'studies', position }, apply: (key, v) => { if (key === 'theme') setTheme(v); else if (key === 'layout') setLayout(v); else if (key === 'position') setPosition(v); }, canApplyPosition, }); return { theme, layout, position, setTheme, setLayout, setPosition, ...sync }; }); } test('the server\'s newer value wins, an older one does not', async () => { localStorage.setItem('prefs-meta', JSON.stringify({ theme: 500, layout: 900 })); remote.prefs = { theme: { v: 'dark', at: 800 }, layout: { v: 'verses', at: 100 } }; const h = setup(); await act(async () => { await h.result.current.pullServerPrefs(); }); expect(h.result.current.theme).toBe('dark'); // server is newer expect(h.result.current.layout).toBe('flow'); // this device changed it later expect(remote.puts).toHaveLength(1); expect(Object.keys(remote.puts[0])).toContain('layout'); // ...so the server hears about it expect(Object.keys(remote.puts[0])).not.toContain('theme'); }); test('a device with no history takes the account\'s settings; a first device uploads its own', async () => { remote.prefs = { theme: { v: 'sepia', at: 700 } }; const h = setup(); await act(async () => { await h.result.current.pullServerPrefs(); }); expect(h.result.current.theme).toBe('sepia'); // keys the account doesn't have yet are filled in from this device expect(Object.keys(remote.puts[0]).sort()).toEqual(['commentarySource', 'layout', 'planTranslation', 'position', 'startPage', 'translation']); }); test('reading position is not moved under you while the Reader is open', async () => { remote.prefs = { position: { v: { book: 'JHN', chapter: 3 }, at: 900 }, theme: { v: 'dark', at: 900 } }; const h = setup({ canApplyPosition: () => false }); await act(async () => { await h.result.current.pullServerPrefs(); }); expect(h.result.current.position).toEqual({ book: 'GEN', chapter: 1 }); // stays put expect(h.result.current.theme).toBe('dark'); // everything else still syncs }); test('and it is applied when the Reader is not open', async () => { remote.prefs = { position: { v: { book: 'JHN', chapter: 3 }, at: 900 } }; const h = setup({ canApplyPosition: () => true }); await act(async () => { await h.result.current.pullServerPrefs(); }); expect(h.result.current.position).toEqual({ book: 'JHN', chapter: 3 }); expect(remote.puts.every((p) => !('position' in p))).toBe(true); // applying it isn't mistaken for a local edit }); test('a local change is stamped and pushed shortly after', async () => { vi.useFakeTimers(); try { const h = setup(); await act(async () => { await h.result.current.pullServerPrefs(); }); remote.puts = []; act(() => h.result.current.setTheme('dark')); await act(async () => { await vi.advanceTimersByTimeAsync(1200); }); expect(remote.puts).toHaveLength(1); expect(remote.puts[0].theme.v).toBe('dark'); expect(JSON.parse(localStorage.getItem('prefs-meta')).theme).toBe(remote.puts[0].theme.at); } finally { vi.useRealTimers(); } }); test('stopping sync forgets the timestamps so the next account starts from its own settings', async () => { const h = setup(); await act(async () => { await h.result.current.pullServerPrefs(); }); expect(localStorage.getItem('prefs-meta')).not.toBeNull(); act(() => h.result.current.stopPrefsSync()); expect(localStorage.getItem('prefs-meta')).toBeNull(); });