Split Home: local UI state, useProjectLibrary, useReadingPlan

- Search, tag filter, full-text note search, and rename state live in
  HomePage. The sort choice is remembered in localStorage.
- useProjectLibrary owns opening/renaming/deleting projects and the
  server reconcile (auto-restore + newer-on-server list).
- useReadingPlan owns the reading plan shared by Home and Reader.
- Tests: note search, rename, delete, sort memory, reading plan. Test
  setup now resets the storage namespace between tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Opus 5.5 committed 2026-09-22 14:52:46 -04:00
1 parent 8bb3b104c8
commit a7e00a76b5
5 files changed
+291 -178

No files matched your search

+74
View File
@@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
import { vi, beforeEach, afterEach, describe, test, expect } from 'vitest';
import App from './App';
import { resetLexiconCaches } from './utils/wordStudy.js';
import { switchStorageUser } from './utils/bibleUtils.jsx';
// ---------------------------------------------------------------------------
// Shared mock data
@@ -51,6 +52,7 @@ function buildFetchMock({ chapterData = mockChapterData, greekData = null } = {}
beforeEach(() => {
localStorage.clear();
resetLexiconCaches();
switchStorageUser(null); // a signed-in test leaves the storage namespace switched
vi.stubGlobal('fetch', buildFetchMock());
vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock');
vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {});
@@ -512,3 +514,75 @@ describe('Import page', () => {
expect(await screen.findByRole('button', { name: /import session list/i })).toBeInTheDocument();
});
});
// ---------------------------------------------------------------------------
// Home page (search/sort/rename local to HomePage; library + reading plan hooks)
// ---------------------------------------------------------------------------
describe('Home page', () => {
// Tests run in local-only mode (the mock API is unreachable), which uses the
// pre-account localStorage keys.
function seedProject({ id = 'p1', title = 'Titus Notes', observation = 'Grace abounds to all.' } = {}) {
const project = {
id, title, translation: 'BSB', lastEdited: Date.now(), selectedChunkId: null,
chapters: [{
book: 'Titus', bookAbbrev: 'TIT', chapter: '1', verses: [{ number: 1, text: 'Paul, a servant of God.' }],
chunks: [{ id: 'c1', startVerse: 1, endVerse: 1, observation, interpretation: '', application: '',
crossReferences: [], greekWords: [], tags: [] }],
}],
};
const index = JSON.parse(localStorage.getItem('bible-study-index') || '[]');
index.push({ id, title, lastEdited: project.lastEdited, chapterSummary: 'Titus 1', tags: [] });
localStorage.setItem(`bible-study-project-${id}`, JSON.stringify(project));
localStorage.setItem('bible-study-index', JSON.stringify(index));
}
test('searching 3+ characters finds matches inside study notes', async () => {
seedProject();
const user = userEvent.setup();
await renderApp();
await user.type(screen.getByPlaceholderText(/search projects/i), 'grace');
expect(await screen.findByText(/notes matching "grace"/i)).toBeInTheDocument();
});
test('renaming a project updates its title and saved index', async () => {
seedProject();
const user = userEvent.setup();
await renderApp();
await user.click(screen.getByRole('button', { name: /rename/i }));
const input = screen.getByDisplayValue('Titus Notes');
await user.clear(input);
await user.type(input, 'Titus Deep Dive');
await user.click(screen.getByRole('button', { name: /^save$/i }));
expect(await screen.findByText('Titus Deep Dive')).toBeInTheDocument();
expect(JSON.parse(localStorage.getItem('bible-study-index'))[0].title).toBe('Titus Deep Dive');
});
test('deleting a project removes it after confirmation', async () => {
seedProject();
window.confirm.mockReturnValue(true);
const user = userEvent.setup();
await renderApp();
await user.click(screen.getByRole('button', { name: /^delete$/i }));
await waitFor(() => expect(screen.queryByText('Titus Notes')).not.toBeInTheDocument());
expect(localStorage.getItem('bible-study-project-p1')).toBeNull();
});
test('sort choice is remembered after leaving Home', async () => {
seedProject();
const user = userEvent.setup();
await renderApp();
await user.selectOptions(screen.getByDisplayValue('Last edited'), 'title');
await user.click(screen.getByRole('button', { name: /read bible/i }));
await user.click(await screen.findByRole('button', { name: /back to studies/i }));
expect(await screen.findByDisplayValue('Title')).toBeInTheDocument();
});
test('a reading plan can be started from Home', async () => {
seedProject(); // the plan card shows once there's at least one project
const user = userEvent.setup();
await renderApp();
await user.click(screen.getByRole('button', { name: /set goal/i }));
await user.click(screen.getByRole('button', { name: /start plan/i }));
await waitFor(() => expect(JSON.parse(localStorage.getItem('readingPlan'))?.bookAbbrev).toBe('GEN'));
});
});