Version history, notes on highlights and bookmarks, "you've studied this", prayer journal
Build and Push Docker Image / build-and-push (push) Successful in 5m13s
Build and Push Docker Image / build-and-push (push) Successful in 5m13s
- Earlier versions of a study: the server keeps compressed copies (about one per ten minutes of editing, the latest 30); Export shows what differs and puts one back (the replaced copy is kept) - Notes on highlights and bookmarks: synced, merged without loss, in exports and backups - Reader verse panel lists your studies and chunks that cover the verse, and opens them - Prayer journal: requests, who for, linked verses, days prayed, answers; synced, in backups and the account export; daily prayer reminder through ntfy - TODO.md lists the ideas not built yet Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
54cb1c217d
commit
207bfcb919
45 files changed
+1669
-75
No files matched your search
@@ -0,0 +1,70 @@
|
||||
import { test, expect } from 'vitest';
|
||||
import { daysToAnswer, filterPrayers, markAnswered, markDeleted, mergePrayers, newPrayer, parseVerses, prayersToMarkdown, reopen, togglePrayedToday } from './prayers.js';
|
||||
|
||||
const DAY = 86_400_000;
|
||||
const T0 = Date.UTC(2026, 9, 1);
|
||||
|
||||
test('a new prayer is trimmed and starts open', () => {
|
||||
const p = newPrayer({ text: ' Wisdom for the job ', forWhom: ' Sam ' }, T0);
|
||||
expect(p).toMatchObject({ text: 'Wisdom for the job', forWhom: 'Sam', answeredAt: null, prayedOn: [], createdAt: T0, updatedAt: T0 });
|
||||
expect(typeof p.id).toBe('string');
|
||||
});
|
||||
|
||||
test('praying today can be switched on and off, and answering or reopening is tracked', () => {
|
||||
let p = newPrayer({ text: 'x' }, T0);
|
||||
p = togglePrayedToday(p, '2026-10-02', T0 + 1);
|
||||
expect(p.prayedOn).toEqual(['2026-10-02']);
|
||||
p = togglePrayedToday(p, '2026-10-02', T0 + 2);
|
||||
expect(p.prayedOn).toEqual([]);
|
||||
p = markAnswered(p, ' He did it ', T0 + 5 * DAY);
|
||||
expect(p).toMatchObject({ answerNote: 'He did it', answeredAt: T0 + 5 * DAY });
|
||||
expect(daysToAnswer(p)).toBe(5);
|
||||
expect(reopen(p, T0 + 6 * DAY)).toMatchObject({ answeredAt: null, answerNote: '' });
|
||||
expect(daysToAnswer(newPrayer({ text: 'y' }))).toBeNull();
|
||||
});
|
||||
|
||||
test('verses are read from what you type; unreadable bits are reported', () => {
|
||||
const { verses, unknown } = parseVerses('John 3:16, Philippians 4:6-7; Psalm 23; blah blah');
|
||||
expect(verses.map((v) => [v.book, v.chapter, v.verse])).toEqual([['JHN', 3, 16], ['PHP', 4, 6], ['PSA', 23, null]]);
|
||||
expect(verses[1].label).toBe('Philippians 4:6-7');
|
||||
expect(unknown).toEqual(['blah blah']);
|
||||
expect(parseVerses('')).toEqual({ verses: [], unknown: [] });
|
||||
});
|
||||
|
||||
test('devices merge: the newer prayer wins, days combine, a deletion sticks', () => {
|
||||
const a = newPrayer({ text: 'A' }, T0);
|
||||
const here = [{ ...a, prayedOn: ['2026-10-01'], updatedAt: T0 + 10 }];
|
||||
const there = [{ ...a, text: 'A (edited)', prayedOn: ['2026-10-02'], updatedAt: T0 + 20 }];
|
||||
const merged = mergePrayers(here, there, T0 + 30);
|
||||
expect(merged).toHaveLength(1);
|
||||
expect(merged[0]).toMatchObject({ text: 'A (edited)', prayedOn: ['2026-10-01', '2026-10-02'] });
|
||||
const afterDelete = mergePrayers(merged, [markDeleted(a, T0 + 40)], T0 + 50);
|
||||
expect(afterDelete).toEqual([{ id: a.id, deleted: true, updatedAt: T0 + 40 }]);
|
||||
expect(mergePrayers(afterDelete, here, T0 + 60)[0].deleted).toBe(true); // an older copy can't bring it back
|
||||
});
|
||||
|
||||
test('the list can be filtered by tab and searched', () => {
|
||||
const open = newPrayer({ text: 'Peace at home', forWhom: 'Family' }, T0);
|
||||
const answered = markAnswered(newPrayer({ text: 'A new job', verses: [{ book: 'PHP', chapter: 4, verse: 6, label: 'Philippians 4:6' }] }, T0), 'Hired!', T0 + DAY);
|
||||
const gone = markDeleted(newPrayer({ text: 'Removed' }, T0), T0 + 5);
|
||||
const all = [open, answered, gone];
|
||||
expect(filterPrayers(all, { tab: 'open' })).toEqual([open]);
|
||||
expect(filterPrayers(all, { tab: 'answered' })).toEqual([answered]);
|
||||
expect(filterPrayers(all, { tab: 'all' })).toHaveLength(2);
|
||||
expect(filterPrayers(all, { tab: 'all', query: 'philippians' })).toEqual([answered]); // searches verses
|
||||
expect(filterPrayers(all, { tab: 'all', query: 'hired' })).toEqual([answered]); // and the answer
|
||||
expect(filterPrayers(all, { tab: 'all', query: 'family' })).toEqual([open]);
|
||||
});
|
||||
|
||||
test('the Markdown separates what is being prayed for from what was answered', () => {
|
||||
const open = { ...newPrayer({ text: 'Peace at home', forWhom: 'Family' }, T0), prayedOn: ['2026-10-01', '2026-10-02'] };
|
||||
const answered = markAnswered(newPrayer({ text: 'A new job' }, T0), 'Hired!', T0 + DAY);
|
||||
const md = prayersToMarkdown([open, answered], { date: new Date(2026, 9, 6) });
|
||||
expect(md).toContain('## Praying');
|
||||
expect(md).toContain('- **Family:** Peace at home');
|
||||
expect(md).toContain('prayed on 2 days');
|
||||
expect(md).toContain('## Answered');
|
||||
expect(md).toContain('Answered');
|
||||
expect(md).toContain(': Hired!');
|
||||
expect(prayersToMarkdown([])).toContain('No prayers yet.');
|
||||
});
|
||||
Reference in new issue
Block a user