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 ', tree: [{ id: 'x', text: 'hi', ref: '', note: 'line', children: [] }] }); expect(html).toContain('A <b>'); expect(html).toContain('<i>hi</i>'); expect(html).toContain('class="o-note"'); }); test('the Word document builds, with and without the extras', async () => { const { buildOutlineDocxBlob } = await import('./docxExport.js'); const blob = await buildOutlineDocxBlob({ ...outline, tree: [{ ...outline.tree[0], scripture: { ref: 'Titus 1:5', text: 'appoint elders', translation: 'BSB' }, attachments: [{ id: 'x', type: 'word', label: 'presbyteros', detail: 'elder' }] }, { id: 'z', text: '', ref: '', children: [] }], }); expect(blob.size).toBeGreaterThan(1000); expect((await buildOutlineDocxBlob({ title: 'Empty', tree: [] })).size).toBeGreaterThan(500); });