Hover tooltips, clickable cross-references, easier highlighting

- Tooltips on hover and keyboard focus across the app: icon-only buttons show
  their label, existing titles show in the app's style without the browser's
  delay, and the reader's main buttons show shortcut hints ("Search (/)").
  Touch screens are unaffected.
- Reader: cross-references (under verses and in the Cross-refs tab) open the
  passage's text in the side panel without leaving the chapter; "Open chapter"
  goes there when wanted
- Reader: the verse panel now has a Highlight row of colours for one-tap
  whole-verse highlighting (tap again to remove, Clear for all, with Undo);
  selecting words still highlights just those
- Fix: a cross-reference preview was cleared when React ran the panel's
  effect twice (development builds)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-01 16:31:27 -04:00
1 parent bb80623c45
commit 7f0256be06
9 files changed
+394 -28

No files matched your search

+60 -1
View File
@@ -1,4 +1,5 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { render, screen, fireEvent, within, waitFor } from '@testing-library/react';
import { StrictMode } from 'react';
import { vi, test, expect } from 'vitest';
const noop = () => {};
@@ -58,6 +59,64 @@ test('the compare button next to Aa opens the comparison sheet, and choosing a B
expect(ctx.setReaderParallel).toHaveBeenCalledWith('NET');
});
test('a cross-reference under a verse opens the side panel with that passage, without leaving the chapter', async () => {
const saved = { ...ctx };
Object.assign(ctx, {
readerShowCrossRefs: true, readerSelectedVerse: 1,
readerCrossRefs: { 1: [{ book: 'ROM', chapter: 5, verse: 8, endVerse: 9 }] },
jumpToReaderVerse: vi.fn(),
});
vi.stubGlobal('fetch', vi.fn((url) => Promise.resolve({
ok: true,
json: () => Promise.resolve(String(url).includes('/api/bible/')
? { verses: [{ number: 7, text: 'Seven.' }, { number: 8, text: 'But God shows His love for us.' }, { number: 9, text: 'Much more shall we be saved.' }, { number: 10, text: 'Ten.' }] }
: { chapter: { content: [] } }),
})));
render(<StrictMode><ReaderPage /></StrictMode>); // dev builds run effects twice; the preview must survive that
const chip = within(screen.getByRole('article')).getByRole('button', { name: 'Romans 5:8-9' });
fireEvent.click(chip);
const card = await screen.findByRole('region', { name: /cross-reference romans 5:8-9/i });
await waitFor(() => expect(within(card).getByText(/But God shows His love/)).toBeTruthy());
expect(within(card).getByText(/Much more shall we be saved/)).toBeTruthy();
expect(within(card).queryByText('Seven.')).toBeNull(); // only the verses in the reference
expect(ctx.setReaderSelectedVerse).not.toHaveBeenCalledWith(null); // the click didn't toggle the verse off
fireEvent.click(within(card).getByRole('button', { name: /open chapter/i }));
expect(ctx.jumpToReaderVerse).toHaveBeenCalledWith('ROM', 5, 8); // only now does it leave the chapter
fireEvent.click(within(card).getByRole('button', { name: /close cross-reference/i }));
expect(screen.queryByRole('region', { name: /cross-reference romans/i })).toBeNull();
Object.assign(ctx, saved);
vi.unstubAllGlobals();
});
test('the verse panel has one-tap highlight colours for the whole verse', () => {
const saved = { ...ctx };
Object.assign(ctx, { readerSelectedVerse: 2, addTextHighlight: vi.fn(), removeTextHighlight: vi.fn(), restoreTextHighlight: vi.fn(), readerTextHighlights: [] });
render(<ReaderPage />);
const row = screen.getByRole('group', { name: /highlight this verse/i });
fireEvent.click(within(row).getByRole('button', { name: /highlight verse green/i }));
expect(ctx.addTextHighlight).toHaveBeenCalledWith(expect.objectContaining({
book: 'JHN', chapter: 3, verse: 2, startOffset: 0, endOffset: 'This man came to Jesus by night.'.length, color: '#bbf7d0', text: 'This man came to Jesus by night.',
}));
Object.assign(ctx, saved);
});
test('tapping a verse colour again takes it off, and Clear removes them all (with Undo)', () => {
const saved = { ...ctx };
const text = 'This man came to Jesus by night.';
const hl = (id, color, start = 0, end = text.length) => ({ id, book: 'JHN', chapter: 3, verse: 2, startOffset: start, endOffset: end, color });
Object.assign(ctx, { readerSelectedVerse: 2, addTextHighlight: vi.fn(), removeTextHighlight: vi.fn(), restoreTextHighlight: vi.fn(), readerTextHighlights: [hl('a', '#bbf7d0'), hl('b', '#fef08a', 5, 8)] });
render(<ReaderPage />);
const row = screen.getByRole('group', { name: /highlight this verse/i });
expect(within(row).getByRole('button', { name: /highlight verse green/i })).toHaveAttribute('aria-pressed', 'true');
expect(within(row).getByRole('button', { name: /highlight verse yellow/i })).toHaveAttribute('aria-pressed', 'false'); // a part-verse highlight isn't "the verse"
fireEvent.click(within(row).getByRole('button', { name: /highlight verse green/i }));
expect(ctx.removeTextHighlight).toHaveBeenCalledWith('a');
expect(ctx.addTextHighlight).not.toHaveBeenCalled();
fireEvent.click(within(row).getByRole('button', { name: /^clear$/i }));
expect(ctx.removeTextHighlight).toHaveBeenCalledWith('b');
Object.assign(ctx, saved);
});
test('tapping a verse selects it', () => {
render(<ReaderPage />);
fireEvent.click(screen.getByText(/This man came to Jesus/));