import { afterEach, beforeEach, expect, test, vi } from 'vitest'; import { bibleRefs, bookRefs, canSaveOffline, downloadChapters, loadSavedBooks, markBookSaved } from './offline.js'; beforeEach(() => localStorage.clear()); afterEach(() => vi.unstubAllGlobals()); const chapterResponse = { ok: true, json: () => Promise.resolve({ chapter: { content: [{ type: 'verse', number: 1, content: ['In the beginning.'] }] } }) }; test('lists every chapter of a book and of the whole Bible', () => { expect(bookRefs('JHN')).toHaveLength(21); expect(bookRefs('JHN')[0]).toEqual({ book: 'JHN', chapter: 1 }); expect(bibleRefs()).toHaveLength(1189); }); test('only the free translations can be saved', () => { expect(canSaveOffline('BSB')).toBe(true); expect(canSaveOffline('NET')).toBe(true); expect(canSaveOffline('ESV')).toBe(false); }); test('remembers which books are saved', () => { markBookSaved('BSB', 'JHN'); markBookSaved('BSB', 'JHN'); markBookSaved('BSB', 'PSA'); expect(loadSavedBooks()).toEqual({ BSB: ['JHN', 'PSA'] }); }); test('downloads every chapter, reports progress, and keeps going past a failure', async () => { const fetchMock = vi.fn((url) => Promise.resolve(url.includes('/JHN/2.json') ? { ok: false } : chapterResponse)); vi.stubGlobal('fetch', fetchMock); const progress = []; const result = await downloadChapters('BSB', bookRefs('JHN').slice(0, 4), { onProgress: (d, t) => progress.push([d, t]) }); expect(result).toEqual({ done: 3, failed: 1 }); expect(fetchMock).toHaveBeenCalledTimes(4); expect(progress.at(-1)).toEqual([4, 4]); }); test('stops when told to', async () => { const controller = new AbortController(); const fetchMock = vi.fn(() => { controller.abort(); return Promise.resolve(chapterResponse); }); vi.stubGlobal('fetch', fetchMock); await downloadChapters('BSB', bookRefs('PSA'), { signal: controller.signal, concurrency: 1 }); expect(fetchMock.mock.calls.length).toBeLessThan(5); }); import { offlineStatus } from './offline.js'; test('a study works offline once every book in it is saved in its translation', () => { const project = { translation: 'BSB', chapters: [{ book: 'John', bookAbbrev: 'JHN' }, { book: 'John', bookAbbrev: 'JHN' }, { book: 'Titus', bookAbbrev: 'TIT' }] }; expect(offlineStatus(project, { BSB: ['JHN', 'TIT'] })).toMatchObject({ state: 'ready', missing: [] }); const partial = offlineStatus(project, { BSB: ['JHN'] }); expect(partial.state).toBe('partial'); expect(partial.missing).toEqual([{ abbrev: 'TIT', name: 'Titus' }]); expect(offlineStatus(project, { NET: ['JHN', 'TIT'] }).state).toBe('partial'); // saved in a different translation }); test('licensed translations cannot be saved offline', () => { expect(offlineStatus({ translation: 'ESV', chapters: [{ book: 'John', bookAbbrev: 'JHN' }] }, {}).state).toBe('licensed'); });