Files
study-bible-project/src/hooks/usePrefsSync.test.js
T
nmemmertandClaude Sonnet 5.5 8c4f54e033
Build and Push Docker Image / build-and-push (push) Failing after 20s
Preferences in Settings and a home page the user chooses
Settings gets a Preferences section: theme, default Bible, the Bible new reading
plans start in, verse layout, commentary, text size, reading width, audio
narrator (the last three stay per device; text size and narrator are now
remembered). A Home page setting (My Studies, Bible Reader or Reading Plans)
decides where the app opens and where every Home button goes; already being
there means My Studies. Back arrows are labelled Home. Tests cover every page
leading back.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-29 10:55:07 -04:00

88 lines
4.4 KiB
JavaScript

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();
});