Files
study-bible-project/src/utils/backup.test.js
T
nmemmertandClaude Sonnet 5.5 d13f55faf7 Add Outline stage, parallel reading, offline saves, backup, sync-conflict handling and more
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>
2026-10-01 14:08:08 -04:00

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']);
});