openreader/tests/unit/tts-epub-handoff.spec.ts
Richard R e8d4c11434 refactor(tts,epub,locator): introduce canonical segment keys and locator comparison utilities
Refactor TTS segment identification to support canonical segment keys, decoupling segment IDs from locator and index for more robust deduplication and handoff. Add `segmentKey` to segment types, manifest, and ensure route. Implement natural sorting for EPUB CFI locations and segment locators using new comparison utilities. Update sidebar and manifest helpers to use locator-aware sorting. Add foundational modules for EPUB word highlighting, TTS segment planning, and EPUB handoff logic. Expand types and tests to cover new canonical segment and locator behaviors.

BREAKING CHANGE: TTS segment IDs now support canonical segment keys; APIs and manifest consumers must handle `segmentKey` and updated locator comparison logic.
2026-05-11 05:17:41 -06:00

115 lines
4 KiB
TypeScript

import { expect, test } from '@playwright/test';
import {
completedEpubBoundarySegment,
resolveEpubBoundaryHandoffStartIndex,
resolveEpubReplaySuppressionAction,
shouldSuppressCompletedEpubBoundaryReplay,
} from '../../src/lib/shared/tts-epub-handoff';
import type { CanonicalTtsSegment } from '../../src/lib/shared/tts-segment-plan';
const segment = (
text: string,
overrides: Partial<CanonicalTtsSegment> = {},
): CanonicalTtsSegment => ({
key: `segment:${text}`,
ordinal: 0,
text,
ownerSourceKey: 'str:page-a',
ownerLocator: { location: 'page-a', readerType: 'epub' },
startAnchor: { sourceKey: 'str:page-a', offset: 0 },
endAnchor: { sourceKey: 'str:page-a', offset: text.length },
spansSourceBoundary: false,
...overrides,
});
test.describe('EPUB boundary handoff', () => {
test('records only completed segments that span a source boundary', () => {
expect(completedEpubBoundarySegment(segment('same page'))).toBeNull();
const completed = completedEpubBoundarySegment(segment('cross page', {
spansSourceBoundary: true,
endAnchor: { sourceKey: 'str:page-b', offset: 5 },
}), 100);
expect(completed).toMatchObject({
key: 'segment:cross page',
fingerprint: 'cross page',
completedAt: 100,
});
});
test('skips a leading replay of the completed boundary segment', () => {
const completed = completedEpubBoundarySegment(segment('The same audio crosses the page.', {
key: 'old-key',
spansSourceBoundary: true,
endAnchor: { sourceKey: 'str:page-b', offset: 10 },
}), 1000);
const startIndex = resolveEpubBoundaryHandoffStartIndex([
segment('The same audio crosses the page.', { key: 'new-cfi-key' }),
segment('Fresh audio on the next page.'),
], completed, 1100);
expect(startIndex).toBe(1);
});
test('does not skip stale or non-leading matches', () => {
const completed = completedEpubBoundarySegment(segment('Repeated later.', {
spansSourceBoundary: true,
endAnchor: { sourceKey: 'str:page-b', offset: 10 },
}), 1000);
expect(resolveEpubBoundaryHandoffStartIndex([
segment('Fresh start.'),
segment('Repeated later.'),
], completed, 1100)).toBe(0);
expect(resolveEpubBoundaryHandoffStartIndex([
segment('Repeated later.'),
], completed, 200_000)).toBe(0);
});
test('suppresses a completed boundary segment at playback time even if setText handoff missed it', () => {
const completed = completedEpubBoundarySegment(segment('Boundary audio that already played.', {
key: 'completed-key',
spansSourceBoundary: true,
endAnchor: { sourceKey: 'str:page-b', offset: 10 },
}), 1000);
expect(shouldSuppressCompletedEpubBoundaryReplay(
segment('Boundary audio that already played.', { key: 'different-render-key' }),
completed,
1500,
)).toBe(true);
expect(shouldSuppressCompletedEpubBoundaryReplay(
segment('Actually fresh audio.'),
completed,
1500,
)).toBe(false);
});
test('suppression skips only within the current rendered segment list', () => {
const completed = completedEpubBoundarySegment(segment('Boundary audio that already played.', {
spansSourceBoundary: true,
endAnchor: { sourceKey: 'str:page-b', offset: 10 },
}), 1000);
expect(resolveEpubReplaySuppressionAction([
segment('Boundary audio that already played.', { key: 'rerendered-key' }),
segment('Fresh local audio.'),
], 0, completed, 1100)).toEqual({ kind: 'skip-to-index', index: 1 });
});
test('suppression pauses instead of requesting another page turn when no fresh local segment exists', () => {
const completed = completedEpubBoundarySegment(segment('Boundary audio that already played.', {
spansSourceBoundary: true,
endAnchor: { sourceKey: 'str:page-b', offset: 10 },
}), 1000);
expect(resolveEpubReplaySuppressionAction([
segment('Boundary audio that already played.', { key: 'rerendered-key' }),
], 0, completed, 1100)).toEqual({ kind: 'pause' });
});
});