- Commentary and study aids wait for 15 words of your own Observe notes. - Insights page: this week's own work, themes across studies, words you keep studying, what slipped in recalls, open follow-ups. - Recall can record what slipped; session notes (teach it back, who came, group notes, follow-ups); "Pray about this" from Apply. - Compare any two passages side by side. - Handout layouts: standard, questions only, large print, note-taking. - Search understands "in <book>", "last week/month/year" and a year. - Home shows the last backup age and nudges after two weeks. - Phone: the stage bar scrolls instead of clipping Script and Export. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
81 lines
4.6 KiB
JavaScript
81 lines
4.6 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, prayers: 0, 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']);
|
|
});
|
|
|
|
|
|
test('prayers are in the backup, deleted ones are not, and they survive validation', () => {
|
|
const prayers = [{ id: 'a', text: 'Peace', forWhom: '', verses: [], prayedOn: [], createdAt: 1, updatedAt: 2, answeredAt: null, answerNote: '' }, { id: 'b', deleted: true, updatedAt: 3 }];
|
|
const backup = buildBackup({ reader: {}, readingPlans: null, prayers });
|
|
expect(backup.prayers.map((p) => p.id)).toEqual(['a']);
|
|
const checked = validateBackup(JSON.parse(JSON.stringify(backup)));
|
|
expect(checked.ok).toBe(true);
|
|
expect(checked.summary.prayers).toBe(1);
|
|
expect(checked.backup.prayers[0].text).toBe('Peace');
|
|
expect(validateBackup({ app: 'bible-study-project', version: 1, prayers: [{ id: 'x' }, 5, { id: 'y', text: ' ' }] }).backup.prayers).toEqual([]);
|
|
});
|
|
|
|
import { backupAge } from './backup.js';
|
|
|
|
test('the backup age says how long ago, and flags a long gap', () => {
|
|
const now = 1_000_000_000_000;
|
|
expect(backupAge(null, now)).toMatchObject({ stale: true });
|
|
expect(backupAge(now - 3600_000, now)).toEqual({ text: 'Backed up today', stale: false });
|
|
expect(backupAge(now - 5 * 86400000, now)).toEqual({ text: 'Last backup 5 days ago', stale: false });
|
|
expect(backupAge(now - 20 * 86400000, now).stale).toBe(true);
|
|
});
|