Study - New Outline stage (hierarchical I./A./1. points, linked to chunks, in all exports); the chunk stage is now "Chunks" - Home cards: passages shown as ranges, tags capped with "+N more" Reader - Side-by-side translations and a Compare tab per verse - Typeface, line spacing, verse-number settings; text and column scale with the screen - Highlights/bookmarks sheet with named colours (synced); BSB footnote spacing fix - Cmd-K palette (passages, studies, notes, marks), keyboard shortcuts and "?" help Flow and safety - Toasts with Undo replace most confirm()/alert() - Back/Forward move between pages - Server refuses saves over a newer copy (409); keep-mine / use-other banner; opening a study no longer re-stamps it - Latest chapter request wins when flipping quickly Offline and data - Save books, plan days or a whole BSB/NET for offline - Service worker fixes: helloao /api/ URLs were skipped and responses cloned too late - Settings: download/restore a full backup Accessibility - Sheets trap focus and close on Escape, skip link, focus ring, reduced motion, theme contrast raised to 4.5:1 (with a test) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
59 lines
3.3 KiB
JavaScript
59 lines
3.3 KiB
JavaScript
import { beforeEach, expect, test } from 'vitest';
|
|
import { backupFileName, buildBackup, mergeReadingPlans, restoreProjects, validateBackup } from './backup.js';
|
|
import { loadProjectById, saveProjectToStorage } from './projectStorage.js';
|
|
import { switchStorageUser } from './bibleUtils.jsx';
|
|
|
|
const project = (id, lastEdited, title = 'Romans') => ({
|
|
id, title, translation: 'BSB', lastEdited, selectedChunkId: null,
|
|
chapters: [{ book: 'Romans', bookAbbrev: 'ROM', chapter: '8', verses: [{ number: 1, text: 'x' }], chunks: [] }],
|
|
});
|
|
|
|
beforeEach(() => { localStorage.clear(); switchStorageUser(null); });
|
|
|
|
test('a backup holds the studies, marks and plans, and round-trips through validation', () => {
|
|
saveProjectToStorage(project('p1', 100));
|
|
const backup = buildBackup({
|
|
reader: { bookmarks: { 'JHN-3-16': '#fef08a' }, highlights: [{ id: 'h1' }], colorLabels: { '#fef08a': 'Key' }, ink: { JHN_3: [] } },
|
|
readingPlans: { progress: [{ id: 'r1' }], customPlans: [], activity: ['2026-09-01'] },
|
|
now: new Date('2026-10-01T12:00:00Z'),
|
|
});
|
|
expect(backup.projects.map((p) => p.id)).toEqual(['p1']);
|
|
const checked = validateBackup(JSON.parse(JSON.stringify(backup)));
|
|
expect(checked.ok).toBe(true);
|
|
expect(checked.summary).toEqual({ projects: 1, bookmarks: 1, highlights: 1, inkPages: 1, plans: 1 });
|
|
expect(backupFileName(new Date('2026-10-01T12:00:00Z'))).toBe('bible-study-backup-2026-10-01.json');
|
|
});
|
|
|
|
test('rejects files that are not backups, or are from a newer version', () => {
|
|
expect(validateBackup(null).ok).toBe(false);
|
|
expect(validateBackup({ app: 'something-else', version: 1 }).ok).toBe(false);
|
|
expect(validateBackup({ app: 'bible-study-project', version: 99 }).ok).toBe(false);
|
|
});
|
|
|
|
test('ignores malformed entries instead of failing', () => {
|
|
const checked = validateBackup({ app: 'bible-study-project', version: 1, projects: [{ id: 'ok', title: 'T', chapters: [] }, { nope: true }, null], reader: { highlights: [{ id: 'h' }, 5] } });
|
|
expect(checked.ok).toBe(true);
|
|
expect(checked.summary).toMatchObject({ projects: 1, highlights: 1 });
|
|
});
|
|
|
|
test('restoring adds missing studies and only replaces older copies', () => {
|
|
saveProjectToStorage(project('newer', 500, 'Local newer'));
|
|
saveProjectToStorage(project('older', 100, 'Local older'));
|
|
const written = restoreProjects([project('newer', 200, 'Backup'), project('older', 300, 'Backup newer'), project('missing', 50, 'Brought back')]);
|
|
expect(written.map((p) => p.id).sort()).toEqual(['missing', 'older']);
|
|
expect(loadProjectById('newer').title).toBe('Local newer');
|
|
expect(loadProjectById('older').title).toBe('Backup newer');
|
|
expect(loadProjectById('missing').title).toBe('Brought back');
|
|
});
|
|
|
|
test('merging plans keeps every run once, preferring the later activity', () => {
|
|
const merged = mergeReadingPlans(
|
|
{ progress: [{ id: 'a', lastActivityAt: 10, completedDays: [1] }], customPlans: [{ id: 'c1' }], activity: ['2026-09-02'] },
|
|
{ progress: [{ id: 'a', lastActivityAt: 20, completedDays: [1, 2] }, { id: 'b', lastActivityAt: 5 }], customPlans: [{ id: 'c1' }, { id: 'c2' }], activity: ['2026-09-01', '2026-09-02'] },
|
|
);
|
|
expect(merged.progress.find((p) => p.id === 'a').completedDays).toEqual([1, 2]);
|
|
expect(merged.progress).toHaveLength(2);
|
|
expect(merged.customPlans.map((p) => p.id)).toEqual(['c1', 'c2']);
|
|
expect(merged.activity).toEqual(['2026-09-01', '2026-09-02']);
|
|
});
|