Build and Push Docker Image / build-and-push (push) Failing after 20s
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>
88 lines
4.4 KiB
JavaScript
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();
|
|
});
|