Outline: per-chunk outlines, richer points, print and share

- One outline for the whole study plus one per chunk (picker at the top);
  chunk outlines can start from the chunk's verses, each carrying its words
- Points can have a type (illustration, application, quote, question,
  transition), minutes (with a running total), a longer note, verse text
  fetched from a passage, and cross-references / word studies attached from
  the chunk
- Share (system share sheet, or copy), Print / PDF, Copy as text, Word and
  Markdown downloads for any one outline
- All of it included in the Markdown, HTML/PDF and Word study exports,
  with each chunk's outline under that chunk

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-01 14:24:28 -04:00
1 parent d273c8cb3e
commit 820886e3ab
12 files changed
+741 -103

No files matched your search

+56
View File
@@ -0,0 +1,56 @@
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
import { outlineShareText, printOutline, shareOutline } from './outlineExport.js';
import { buildOutlineHtml } from './bibleUtils.jsx';
const outline = {
title: 'Titus 1 — Elders', subtitle: 'Titus 1:5–9',
tree: [{ id: 'a', text: 'Appoint elders', ref: 'Titus 1:5', kind: 'application', minutes: 4, note: 'Why it matters', children: [{ id: 'b', text: 'Blameless', ref: '', children: [] }] }],
};
beforeEach(() => { vi.stubGlobal('navigator', { ...navigator, share: undefined, clipboard: { writeText: vi.fn(() => Promise.resolve()) } }); });
afterEach(() => { vi.unstubAllGlobals(); vi.restoreAllMocks(); });
test('the share text has the title, passage and the outline', () => {
const text = outlineShareText(outline);
expect(text.startsWith('Titus 1 — Elders\nTitus 1:5–9\n\nI. [Application] Appoint elders (Titus 1:5) · 4 min')).toBe(true);
expect(text).toContain('A. Blameless');
});
test('uses the system share sheet when there is one', async () => {
const share = vi.fn(() => Promise.resolve());
vi.stubGlobal('navigator', { share, clipboard: { writeText: vi.fn() } });
expect(await shareOutline(outline)).toBe('shared');
expect(share).toHaveBeenCalledWith(expect.objectContaining({ title: 'Titus 1 — Elders — Outline' }));
});
test('closing the share sheet is not an error, and a broken one falls back to copying', async () => {
vi.stubGlobal('navigator', { share: vi.fn(() => Promise.reject(Object.assign(new Error('x'), { name: 'AbortError' }))), clipboard: { writeText: vi.fn() } });
expect(await shareOutline(outline)).toBe('cancelled');
const writeText = vi.fn(() => Promise.resolve());
vi.stubGlobal('navigator', { share: vi.fn(() => Promise.reject(new Error('nope'))), clipboard: { writeText } });
expect(await shareOutline(outline)).toBe('copied');
expect(writeText).toHaveBeenCalled();
});
test('without a share sheet it copies, and says so when even that fails', async () => {
expect(await shareOutline(outline)).toBe('copied');
vi.stubGlobal('navigator', { clipboard: { writeText: vi.fn(() => Promise.reject(new Error('denied'))) } });
expect(await shareOutline(outline)).toBe('failed');
});
test('print writes a page of just the outline, and reports a blocked window', () => {
const doc = { write: vi.fn(), close: vi.fn() };
vi.spyOn(window, 'open').mockReturnValue({ document: doc, focus: vi.fn(), print: vi.fn() });
expect(printOutline(outline)).toBe(true);
const html = doc.write.mock.calls[0][0];
expect(html).toContain('Appoint elders');
expect(html).toContain('About 4 minutes');
window.open.mockReturnValue(null);
expect(printOutline(outline)).toBe(false);
});
test('the printable page escapes text and shows notes', () => {
const html = buildOutlineHtml({ title: 'A <b>', tree: [{ id: 'x', text: '<i>hi</i>', ref: '', note: 'line', children: [] }] });
expect(html).toContain('A &lt;b&gt;');
expect(html).toContain('&lt;i&gt;hi&lt;/i&gt;');
expect(html).toContain('class="o-note"');
});