import { expect, test } from '@playwright/test'; import { planCanonicalTtsSegments } from '../../src/lib/shared/tts-segment-plan'; test.describe('planCanonicalTtsSegments – leading context handling', () => { test('clean boundary: splits block so current source keeps its first sentence', () => { const currentSourceKey = 'str:epubcfi(/6/10!/4/2)'; const plan = planCanonicalTtsSegments([ { sourceKey: `previous:${currentSourceKey}`, text: 'This is the last sentence from the previous page. It ends cleanly here.', locator: null, }, { sourceKey: currentSourceKey, text: 'The first sentence on this page starts fresh. And here is the second sentence.', locator: { location: 'epubcfi(/6/10!/4/2)', readerType: 'epub' as const }, }, ], { readerType: 'epub', maxBlockLength: 450, keyPrefix: 'doc:epub:v1', }); const currentSegments = plan.segments.filter(seg => seg.ownerSourceKey === currentSourceKey); expect(currentSegments.length).toBe(1); expect(currentSegments[0].text).toContain('The first sentence on this page'); expect(currentSegments[0].startAnchor.sourceKey).toBe(currentSourceKey); expect(currentSegments[0].startAnchor.offset).toBe(0); expect(currentSegments[0].spansSourceBoundary).toBe(false); }); test('overlapping sentence: stays owned by context source, filtered out of current page', () => { const currentSourceKey = 'str:epubcfi(/6/10!/4/2)'; const previousSourceKey = `previous:${currentSourceKey}`; const plan = planCanonicalTtsSegments([ { sourceKey: previousSourceKey, text: 'The sentence starts on the previous page and', locator: null, }, { sourceKey: currentSourceKey, text: 'finishes on the current page with enough context. Next sentence here.', locator: { location: 'epubcfi(/6/10!/4/2)', readerType: 'epub' as const }, }, ], { readerType: 'epub', maxBlockLength: 450, keyPrefix: 'doc:epub:v1', }); // The overlapping sentence should stay owned by the context source. const contextSegments = plan.segments.filter(seg => seg.ownerSourceKey === previousSourceKey); expect(contextSegments.length).toBeGreaterThanOrEqual(1); expect(contextSegments[0].text).toContain('The sentence starts on the previous page and finishes on the current page'); expect(contextSegments[0].spansSourceBoundary).toBe(true); // Current page segments should NOT include the overlapping sentence. const currentSegments = plan.segments.filter(seg => seg.ownerSourceKey === currentSourceKey); for (const seg of currentSegments) { expect(seg.text).not.toContain('previous page'); } }); test('clean boundary with longer text: preserves first sentence', () => { const currentSourceKey = 'str:epubcfi(/6/10!/4/2)'; const longPreviousText = 'This is a much longer paragraph from the previous page. It contains several sentences that fill up more space. ' + 'The story continues with more details about what happened. Characters move through the scene with purpose. ' + 'Each step brings them closer to their destination. The sun sets slowly behind the mountains. ' + 'Wind rustles through the leaves of the ancient trees. Birds call out their evening songs.'; const currentText = 'The next morning dawned bright and clear. She stepped outside and breathed in the fresh air. ' + 'The garden was blooming with colorful flowers. Bees buzzed lazily from blossom to blossom. ' + 'It was the kind of day that made everything seem possible. She smiled to herself and walked down the path.'; const plan = planCanonicalTtsSegments([ { sourceKey: `previous:${currentSourceKey}`, text: longPreviousText, locator: null, }, { sourceKey: currentSourceKey, text: currentText, locator: { location: 'epubcfi(/6/10!/4/2)', readerType: 'epub' as const }, }, ], { readerType: 'epub', maxBlockLength: 450, keyPrefix: 'doc:epub:v1', }); const currentSegments = plan.segments.filter(seg => seg.ownerSourceKey === currentSourceKey); const reconstructed = currentSegments.map(s => s.text).join(' '); expect(reconstructed).toContain('The next morning dawned bright and clear'); expect(reconstructed).toContain('She stepped outside'); expect(reconstructed).toContain('She smiled to herself'); }); test('forward-looking boundary between real sources is preserved', () => { const plan = planCanonicalTtsSegments([ { sourceKey: 'page:1', locator: { page: 1, readerType: 'pdf' as const }, text: 'The boundary sentence begins on page one and', }, { sourceKey: 'page:2', locator: { page: 2, readerType: 'pdf' as const }, text: 'finishes on page two with enough words to stand alone. A short follow up.', }, ], { readerType: 'pdf', maxBlockLength: 60, keyPrefix: 'doc:v1', }); // Forward boundary between two real sources stays unified. expect(plan.segments[0]).toMatchObject({ ownerSourceKey: 'page:1', spansSourceBoundary: true, }); expect(plan.segments[0].text).toContain('begins on page one and finishes on page two'); }); test('overlapping sentence with comma: stays owned by context source', () => { const currentSourceKey = 'str:epubcfi(/6/14!/4/2)'; const previousSourceKey = `previous:${currentSourceKey}`; const plan = planCanonicalTtsSegments([ { sourceKey: previousSourceKey, text: 'She walked down the lane, past the old church,', locator: null, }, { sourceKey: currentSourceKey, text: 'and turned left at the river. The bridge was old.', locator: { location: 'epubcfi(/6/14!/4/2)', readerType: 'epub' as const }, }, ], { readerType: 'epub', maxBlockLength: 450, keyPrefix: 'doc:epub:v1', }); // Comma is not sentence-ending punctuation → overlapping → owned by context. const contextSegments = plan.segments.filter(seg => seg.ownerSourceKey === previousSourceKey); expect(contextSegments.length).toBeGreaterThanOrEqual(1); expect(contextSegments[0].text).toContain('She walked down the lane'); expect(contextSegments[0].text).toContain('and turned left at the river'); // Current page should NOT include the overlapping sentence. const currentSegments = plan.segments.filter(seg => seg.ownerSourceKey === currentSourceKey); for (const seg of currentSegments) { expect(seg.text).not.toContain('She walked down the lane'); } }); test('clean boundary with quoted speech ending in period', () => { const currentSourceKey = 'str:epubcfi(/6/16!/4/2)'; const plan = planCanonicalTtsSegments([ { sourceKey: `previous:${currentSourceKey}`, text: '"I will be back tomorrow," she said.', locator: null, }, { sourceKey: currentSourceKey, text: 'The door closed behind her. Silence filled the room.', locator: { location: 'epubcfi(/6/16!/4/2)', readerType: 'epub' as const }, }, ], { readerType: 'epub', maxBlockLength: 450, keyPrefix: 'doc:epub:v1', }); const currentSegments = plan.segments.filter(seg => seg.ownerSourceKey === currentSourceKey); expect(currentSegments.length).toBe(1); // Clean boundary after quoted speech → split correctly. expect(currentSegments[0].text).toContain('The door closed behind her'); expect(currentSegments[0].text).not.toContain('I will be back'); }); });