import { render, screen, fireEvent } from '@testing-library/react'; import { vi, test, expect } from 'vitest'; const noop = () => {}; const ctx = { goHome: noop, readerBookAbbrev: 'JHN', setReaderBookAbbrev: noop, readerTranslation: 'ESV', setReaderTranslation: noop, readerTheme: 'paper', setReaderTheme: noop, readerCommentary: 'matthew-henry', setReaderCommentary: noop, readerLayout: 'flow', setReaderLayout: noop, readerChapter: 3, setReaderChapter: noop, readerVerses: [{ number: 1, text: 'Now there was a man of the Pharisees.' }, { number: 2, text: 'This man came to Jesus by night.' }], readerTotalChapters: 21, readerLoading: false, readerError: '', readerInterlinear: null, readerSelectedVerse: null, setReaderSelectedVerse: vi.fn(), readerFontSize: 1, setReaderFontSize: noop, readerWide: false, setReaderWide: noop, readerBookmarks: {}, readerCrossRefs: null, setReaderCrossRefs: noop, readerCrossRefsLoading: false, readerShowCrossRefs: false, setReaderShowCrossRefs: noop, readerSearch: '', setReaderSearch: noop, setReaderSearchActive: noop, readerSearchScope: 'chapter', setReaderSearchScope: noop, readerAudioState: { status: 'idle' }, audioProgress: { currentTime: 0, duration: 0 }, bibleIndexStatusFor: () => 'idle', audioNarrator: 'david', setAudioNarrator: noop, _bibleIndexCacheRef: { current: {} }, readerGoToPreviousChapter: noop, readerGoToNextChapter: noop, jumpToReaderVerse: noop, toggleReaderBookmark: noop, cycleBookmarkColor: noop, loadReaderCrossRefs: noop, loadBibleIndex: noop, handlePlayReaderAudio: noop, handleToggleReaderAudioPause: noop, handleStopBookAudio: noop, handleSeekAudio: noop, handleSeekAudioTo: noop, readerInkByPage: {}, updateReaderPageInk: noop, readerTextHighlights: [], addTextHighlight: noop, removeTextHighlight: noop, planProgress: [], toggleDay: vi.fn(), toggleChapterRead: vi.fn(), readingActivity: [], readerPlan: null, setReaderPlan: vi.fn(), openPlanReading: vi.fn(), studyReadingPlanDay: vi.fn(), studyBuildBusy: false, setReaderBookAbbrev: vi.fn(), setReaderChapter: vi.fn(), }; vi.mock('../context/AppContext.js', () => ({ useApp: () => ctx })); import ReaderPage from './ReaderPage.jsx'; test('renders the chapter, chrome and opens the tool sheets', () => { render(); expect(screen.getByText('John 3 · ESV')).toBeTruthy(); expect(screen.getByText(/man of the Pharisees/)).toBeTruthy(); fireEvent.click(screen.getByText('John 3 · ESV')); expect(screen.getByText('Go to')).toBeTruthy(); fireEvent.click(screen.getByLabelText('Close')); fireEvent.click(screen.getByLabelText('Reading settings')); expect(screen.getByText('Text size')).toBeTruthy(); fireEvent.click(screen.getByLabelText('Close')); fireEvent.click(screen.getByLabelText('Search')); expect(screen.getByPlaceholderText(/Search John 3/)).toBeTruthy(); }); test('the compare button next to Aa opens the comparison sheet, and choosing a Bible turns it on', () => { ctx.setReaderParallel = vi.fn(); render(); const compare = screen.getByLabelText('Compare translations'); expect(compare.nextElementSibling).toBe(screen.getByLabelText('Reading settings')); // sits right beside Aa fireEvent.click(compare); expect(screen.getByText(/how different its wording is/)).toBeTruthy(); fireEvent.click(screen.getByRole('button', { name: 'NET' })); expect(ctx.setReaderParallel).toHaveBeenCalledWith('NET'); }); test('tapping a verse selects it', () => { render(); fireEvent.click(screen.getByText(/This man came to Jesus/)); expect(ctx.setReaderSelectedVerse).toHaveBeenCalledWith(2); }); // --- reading plan integration ------------------------------------------------- import { getPlanSchedule, findDayIndexForRef } from '../utils/readingPlans.js'; const schedule = getPlanSchedule('gospels-30'); const dayIndex = findDayIndexForRef(schedule, 'JHN', 3); const refs = schedule[dayIndex]; const keys = refs.map((r) => `${r.book}.${r.chapter}`); const position = keys.indexOf('JHN.3') + 1; const isLast = position === refs.length; const withPlan = (over = {}) => { ctx.planProgress = [{ id: 'p1', planId: 'gospels-30', startedAt: Date.now(), completedDays: [], chaptersRead: {}, completedAt: null, translation: 'ESV', ...over }]; ctx.readerPlan = { progressId: 'p1', dayIndex }; [ctx.setReaderPlan, ctx.toggleChapterRead, ctx.toggleDay, ctx.setReaderChapter, ctx.setReaderBookAbbrev].forEach((f) => f.mockClear()); ctx.setReaderTranslation = vi.fn(); }; const stripButton = () => screen.getAllByText(new RegExp(`Day ${dayIndex + 1} *$`))[0]; test('in a plan: strip shows the day, a dot per chapter, and the plan translation', () => { withPlan(); render(); expect(screen.getAllByLabelText(/Go to chapter/)).toHaveLength(refs.length); expect(screen.getByText(`${position} of ${refs.length}`)).toBeTruthy(); }); test('end-of-day card ticks the current chapter and moves on', () => { // every other chapter of the day already read, so finishing this one completes the day withPlan({ chaptersRead: { [dayIndex]: keys.filter((k) => k !== 'JHN.3') } }); render(); fireEvent.click(screen.getByText(isLast ? /Mark read · Day/ : /^Next: /)); expect(ctx.toggleChapterRead).toHaveBeenCalledWith('p1', dayIndex, 'JHN.3', keys, 30, true); if (!isLast) expect(ctx.setReaderChapter).toHaveBeenCalledWith(refs[position].chapter); }); test('"mark read" on the last chapter completes the whole day even if earlier chapters were skipped', () => { const last = refs.at(-1); expect(refs.length).toBeGreaterThan(1); withPlan(); // nothing read yet ctx.readerBookAbbrev = last.book; ctx.readerChapter = last.chapter; try { render(); fireEvent.click(screen.getByText(/Mark read · Day/)); expect(ctx.toggleDay).toHaveBeenCalledWith('p1', dayIndex, 30); } finally { ctx.readerBookAbbrev = 'JHN'; ctx.readerChapter = 3; } }); test('plan sheet lists the day, changes the plan translation, and leaves the plan', () => { withPlan(); render(); fireEvent.click(stripButton()); expect(screen.getByText('Mark the whole day read')).toBeTruthy(); fireEvent.change(screen.getByDisplayValue(/^ESV/), { target: { value: 'NLT' } }); expect(ctx.setReaderTranslation).toHaveBeenCalledWith('NLT'); fireEvent.click(screen.getByText(/Leave plan/)); expect(ctx.setReaderPlan).toHaveBeenCalledWith(null); }); test('outside a plan but on a plan chapter: offers to open the plan', () => { withPlan(); ctx.readerPlan = null; render(); fireEvent.click(screen.getByText('Open plan')); expect(ctx.setReaderPlan).toHaveBeenCalledWith({ progressId: 'p1', dayIndex }); });