Files
study-bible-project/src/utils/prayers.test.js
T
nmemmertandClaude Sonnet 5.5 1cf157be64
Build and Push Docker Image / build-and-push (push) Successful in 5m14s
Prayer journal: pinning, waiting/partial status, import, dashboard card
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 16:27:04 -04:00

120 lines
6.8 KiB
JavaScript

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();
});