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:
1 parent
8bb3b104c8
commit
a7e00a76b5
5 files changed
+291
-178
No files matched your search
@@ -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'));
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user