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.'); }); import { movePinned, parsePrayerImport, pinned, prayersToJson, setPinned, setStatus } from './prayers.js'; test('pinning orders prayers, moving swaps them, and answering unpins', () => { let list = ['a', 'b', 'c'].map((t, i) => newPrayer({ text: t }, T0 + i)); const [a, b, c] = list.map((p) => p.id); list = setPinned(list, b, true, T0 + 10); list = setPinned(list, a, true, T0 + 11); expect(pinned(list).map((p) => p.id)).toEqual([b, a]); list = movePinned(list, a, -1, T0 + 12); expect(pinned(list).map((p) => p.id)).toEqual([a, b]); expect(movePinned(list, a, -1)).toBe(list); // already first expect(filterPrayers(list, { tab: 'open' }).map((p) => p.id)[0]).toBe(a); expect(filterPrayers(list, { tab: 'open' }).map((p) => p.id)).toEqual([a, b, c]); list = list.map((p) => (p.id === a ? markAnswered(p, '', T0 + 20) : p)); expect(pinned(list).map((p) => p.id)).toEqual([b]); }); test('waiting and partly answered stay open; partial keeps a note; answering clears them', () => { let p = setStatus(newPrayer({ text: 'x' }, T0), 'partial', ' half way ', T0 + 1); expect(p).toMatchObject({ status: 'partial', answerNote: 'half way', answeredAt: null }); p = setStatus(p, 'waiting', 'ignored', T0 + 2); expect(p).toMatchObject({ status: 'waiting', answerNote: '' }); expect(setStatus(p, '', '', T0 + 3).status).toBe(''); expect(markAnswered(p, 'done', T0 + 4)).toMatchObject({ status: '', answeredAt: T0 + 4 }); }); test('import reads our markdown and JSON, and skips prayers already in the journal', () => { const src = [ { ...newPrayer({ text: 'Wisdom', forWhom: 'Sam', verses: [{ book: 'JAS', chapter: 1, verse: 5, label: 'James 1:5' }] }, T0), pinned: true, rank: 1, status: 'waiting' }, markAnswered(newPrayer({ text: 'A job' }, T0), 'Hired', T0 + DAY), ]; const md = prayersToMarkdown(src, { date: new Date(T0) }); const fromMd = parsePrayerImport(md, [], T0); expect(fromMd.prayers).toHaveLength(2); expect(fromMd.prayers[0]).toMatchObject({ text: 'Wisdom', forWhom: 'Sam', status: 'waiting', pinned: true, answeredAt: null }); expect(fromMd.prayers[0].verses[0]).toMatchObject({ book: 'JAS', chapter: 1, verse: 5 }); expect(fromMd.prayers[1]).toMatchObject({ text: 'A job', answerNote: 'Hired' }); expect(fromMd.prayers[1].answeredAt).toBeTruthy(); const fromJson = parsePrayerImport(prayersToJson(src), [], T0); expect(fromJson.prayers.map((p) => p.id)).toEqual(src.map((p) => p.id)); expect(fromJson.prayers[0]).toMatchObject({ pinned: true, status: 'waiting' }); expect(parsePrayerImport(prayersToJson(src), [src[0]], T0)).toMatchObject({ skipped: 1, prayers: [expect.objectContaining({ text: 'A job' })] }); expect(parsePrayerImport(md, [src[0]], T0).prayers.map((p) => p.text)).toEqual(['A job']); // same words, same person expect(parsePrayerImport('{"nope":1}', []).error).toBeTruthy(); expect(parsePrayerImport('', []).error).toBeTruthy(); });