openreader/tests/unit/tts-segments-manifest.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

125 lines
4 KiB
TypeScript

import { expect, test } from '@playwright/test';
import {
compareManifestSegments,
decodeManifestCursor,
dedupeManifestVariants,
encodeManifestCursor,
parseManifestPageSize,
} from '../../src/lib/server/tts/segments-manifest';
test.describe('tts segments manifest helpers', () => {
test('dedupe prefers completed variant over newer pending for same settings key', () => {
const variants = dedupeManifestVariants([
{
dedupeKey: 'settings:abc',
variant: {
segmentId: 'old-completed',
settings: null,
audioPresignUrl: '/audio/old',
audioFallbackUrl: '/audio/old/fallback',
durationMs: 1100,
status: 'completed',
textLength: 12,
alignmentWordCount: 2,
audioKey: 'old',
updatedAt: 100,
},
},
{
dedupeKey: 'settings:abc',
variant: {
segmentId: 'new-pending',
settings: null,
audioPresignUrl: null,
audioFallbackUrl: null,
durationMs: null,
status: 'pending',
textLength: 15,
alignmentWordCount: 0,
audioKey: null,
updatedAt: 200,
},
},
]);
expect(variants).toHaveLength(1);
expect(variants[0].segmentId).toBe('old-completed');
expect(variants[0].status).toBe('completed');
});
test('dedupe uses status rank as tiebreaker when updatedAt is equal', () => {
const variants = dedupeManifestVariants([
{
dedupeKey: 'settings:abc',
variant: {
segmentId: 'pending',
settings: null,
audioPresignUrl: null,
audioFallbackUrl: null,
durationMs: null,
status: 'pending',
textLength: 12,
alignmentWordCount: 0,
audioKey: null,
updatedAt: 123,
},
},
{
dedupeKey: 'settings:abc',
variant: {
segmentId: 'completed',
settings: null,
audioPresignUrl: '/audio/new',
audioFallbackUrl: '/audio/new/fallback',
durationMs: 1200,
status: 'completed',
textLength: 12,
alignmentWordCount: 2,
audioKey: 'new',
updatedAt: 123,
},
},
]);
expect(variants).toHaveLength(1);
expect(variants[0].segmentId).toBe('completed');
expect(variants[0].status).toBe('completed');
});
test('sorts segments deterministically by page, location, and segment index', () => {
const rows = [
{ groupKey: 'c', segmentIndex: 4, locator: { page: 2, location: 'z' } },
{ groupKey: 'a', segmentIndex: 1, locator: { page: 1, location: 'b' } },
{ groupKey: 'b', segmentIndex: 0, locator: { page: 1, location: 'a' } },
{ groupKey: 'd', segmentIndex: 2, locator: { page: 1, location: 'a' } },
];
const sorted = rows.sort(compareManifestSegments);
expect(sorted.map((row) => row.groupKey)).toEqual(['b', 'd', 'a', 'c']);
});
test('sorts EPUB CFI locations naturally instead of lexicographically', () => {
const rows = [
{ groupKey: 'ten', segmentIndex: 0, locator: { location: 'epubcfi(/6/10!/4/2)', readerType: 'epub' as const } },
{ groupKey: 'two-b', segmentIndex: 1, locator: { location: 'epubcfi(/6/2!/4/2)', readerType: 'epub' as const } },
{ groupKey: 'two-a', segmentIndex: 0, locator: { location: 'epubcfi(/6/2!/4/2)', readerType: 'epub' as const } },
];
const sorted = rows.sort(compareManifestSegments);
expect(sorted.map((row) => row.groupKey)).toEqual(['two-a', 'two-b', 'ten']);
});
test('encodes and decodes cursors', () => {
const raw = '3|p:2|l:epubcfi(/6/2)|r:epub';
const encoded = encodeManifestCursor(raw);
expect(decodeManifestCursor(encoded)).toBe(raw);
expect(decodeManifestCursor('not-base64')).toBeNull();
});
test('clamps page size bounds', () => {
expect(parseManifestPageSize(null)).toBe(150);
expect(parseManifestPageSize('10')).toBe(25);
expect(parseManifestPageSize('900')).toBe(500);
expect(parseManifestPageSize('200')).toBe(200);
});
});