Reading plans: integrated into the Reader, redesigned, with per-plan translation
Build and Push Docker Image / build-and-push (push) Failing after 19s

Reader: plan strip with a dot per chapter, plan sheet (day picker, chapter
ticks, translation, streak, other plans), end-of-day card that steps through
the day and rolls into the next, and chapter-level auto-marking.
Each plan remembers its own translation (BSB, ESV, NLT, CSB, NASB, NET) and the
Reader follows it while a plan is open. Plans home redesigned in the reader's
themes with a Continue card, in-progress plans, a filterable catalog, and
sheets for details and the custom-plan builder.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-09-29 09:47:51 -04:00
1 parent c11b6a60bf
commit b1bc1c8ea4
17 files changed
+1205 -863

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
import { render, screen, fireEvent, within } from '@testing-library/react';
import { vi, test, expect, beforeEach } from 'vitest';
import { getPlanSchedule, formatDayReadings } from '../utils/readingPlans.js';
const fn = () => vi.fn();
const ctx = {};
vi.mock('../context/AppContext.js', () => ({ useApp: () => ctx }));
import ReadingPlansPage from './ReadingPlansPage.jsx';
const record = (over = {}) => ({
id: 'p1', planId: 'gospels-30', startedAt: Date.now(), completedDays: [0, 1], chaptersRead: {}, completedAt: null,
lastActivityAt: Date.now(), translation: 'NLT', ...over,
});
beforeEach(() => {
Object.assign(ctx, {
goHome: fn(), planProgress: [], customPlans: [], readingActivity: [],
startPlan: fn(), toggleDay: fn(), restartPlan: fn(), removePlanProgress: fn(), recalibratePace: fn(), setPlanTranslation: fn(),
addCustomPlan: fn(), removeCustomPlan: fn(), updateCustomPlan: fn(),
openPlanReading: fn(), studyReadingPlanDay: fn(), studyBuildBusy: false,
readingPlanSyncStatus: '', readingPlanSyncError: '', retryReadingPlanSync: fn(),
readerTheme: 'paper', setReaderTheme: fn(), readerTranslation: 'BSB',
});
});
test('with no plans, the catalog is shown and there is no Continue card', () => {
render(<ReadingPlansPage />);
expect(screen.queryByText('Continue reading')).toBeNull();
expect(screen.getByRole('button', { name: /The Gospels in 30 Days/ })).toBeTruthy();
});
test('Continue reading opens the plan at its next unread day, in the plan translation', () => {
ctx.planProgress = [record()];
render(<ReadingPlansPage />);
const day2 = getPlanSchedule('gospels-30')[2];
expect(screen.getAllByText(new RegExp(formatDayReadings(day2)))[0]).toBeTruthy();
expect(screen.getAllByText('NLT').length).toBeGreaterThan(0);
fireEvent.click(screen.getByText('Continue reading'));
expect(ctx.openPlanReading).toHaveBeenCalledWith('p1', 2);
});
test('starting a catalog plan passes the chosen translation', () => {
render(<ReadingPlansPage />);
fireEvent.click(screen.getByRole('button', { name: /The Gospels in 30 Days/ }));
fireEvent.change(screen.getByRole('combobox'), { target: { value: 'CSB' } });
fireEvent.click(screen.getByRole('button', { name: 'Start plan' }));
expect(ctx.startPlan).toHaveBeenCalledWith('gospels-30', 'CSB');
});
test('every reader translation can be chosen for a plan', () => {
render(<ReadingPlansPage />);
fireEvent.click(screen.getByRole('button', { name: /The Gospels in 30 Days/ }));
const options = within(screen.getByRole('combobox')).getAllByRole('option').map((o) => o.value);
expect(options).toEqual(expect.arrayContaining(['BSB', 'ESV', 'NLT', 'CSB', 'NASB', 'NET']));
});
test('an in-progress plan: change its translation, jump into a specific day', () => {
ctx.planProgress = [record()];
render(<ReadingPlansPage />);
fireEvent.click(screen.getAllByRole('button', { name: /The Gospels in 30 Days/ })[0]);
fireEvent.change(screen.getByRole('combobox'), { target: { value: 'ESV' } });
expect(ctx.setPlanTranslation).toHaveBeenCalledWith('p1', 'ESV');
fireEvent.click(screen.getAllByText('Read')[3]); // "Read" on Day 4
expect(ctx.openPlanReading).toHaveBeenCalledWith('p1', 3);
});
test('categories filter the catalog', () => {
render(<ReadingPlansPage />);
fireEvent.click(screen.getByRole('button', { name: 'Old Testament' }));
expect(screen.queryByRole('button', { name: /The Gospels in 30 Days/ })).toBeNull();
});