Files
SubMiner/src/core/services/subtitle-cue-navigation.test.ts
T
sudacode 88bb3edfa4 fix(subtitles): preserve lyric selection and deduplicate secondary text
- Seek sidebar selections past overlapping karaoke exit spans
- Collapse duplicate long ASS lines and omit reconstructed sign grids
2026-08-21 01:28:00 -07:00

109 lines
3.3 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
resolveSanitizedSubtitleSeekCommand,
subtitleCueListSeekTime,
subtitleCueSeekTime,
} from './subtitle-cue-navigation';
test('next subtitle navigation skips generated ASS events and seeks to the next sanitized cue', () => {
const cues = [
{
startTime: 10,
endTime: 13,
text: 'first lyric',
source: 'canonical-ass' as const,
animationStartTime: 9.7,
animationEndTime: 13.4,
},
{
startTime: 13,
endTime: 16,
text: 'second lyric',
source: 'canonical-ass' as const,
animationStartTime: 12.7,
animationEndTime: 16.4,
},
];
assert.deepEqual(resolveSanitizedSubtitleSeekCommand(['sub-seek', 1], cues, 10.2), [
'seek',
13.08,
'absolute+exact',
]);
});
test('next subtitle navigation treats simultaneous sanitized cues as one line boundary', () => {
const cues = [
{ startTime: 10, endTime: 13, text: 'romaji' },
{ startTime: 10.02, endTime: 13, text: 'English' },
{ startTime: 13, endTime: 16, text: 'next romaji' },
{ startTime: 13.02, endTime: 16, text: 'Next English' },
];
assert.deepEqual(resolveSanitizedSubtitleSeekCommand(['sub-seek', 1], cues, 10.1), [
'seek',
13.08,
'absolute+exact',
]);
});
test('next subtitle navigation advances past the latest overlapping lyric', () => {
const cues = [
{ startTime: 10, endTime: 14, text: 'exiting lyric' },
{ startTime: 13, endTime: 16, text: 'current lyric' },
{ startTime: 16, endTime: 19, text: 'next lyric' },
];
assert.deepEqual(resolveSanitizedSubtitleSeekCommand(['sub-seek', 1], cues, 13.2), [
'seek',
16.08,
'absolute+exact',
]);
});
test('previous subtitle navigation leaves the current cue and seeks to the prior cue', () => {
const cues = [
{ startTime: 10, endTime: 12, text: 'first line' },
{ startTime: 13, endTime: 16, text: 'current line' },
];
assert.deepEqual(resolveSanitizedSubtitleSeekCommand(['sub-seek', -1], cues, 14.5), [
'seek',
10.08,
'absolute+exact',
]);
});
test('subtitle navigation falls back when no sanitized destination exists', () => {
const cues = [{ startTime: 10, endTime: 13, text: 'only line' }];
assert.equal(resolveSanitizedSubtitleSeekCommand(['sub-seek', 1], cues, 10.2), null);
assert.equal(resolveSanitizedSubtitleSeekCommand(['seek', 5], cues, 10.2), null);
});
test('sidebar cue seeks share the boundary-safe sanitized cue timestamp', () => {
assert.equal(subtitleCueSeekTime({ startTime: 1, endTime: 2, text: 'line' }), 1.08);
assert.equal(subtitleCueSeekTime({ startTime: 1, endTime: 1.04, text: 'short' }), 1.03);
});
test('sidebar cue selection clears an overlapping previous lyric', () => {
const cues = [
{ startTime: 1, endTime: 3.4, text: 'previous lyric' },
{ startTime: 3, endTime: 5, text: 'selected lyric' },
];
assert.equal(subtitleCueListSeekTime(cues, cues[1]!), 3.48);
});
test('sidebar cue selection remains inside a short cue when overlap cannot be cleared', () => {
const cues = [
{ startTime: 1, endTime: 3.4, text: 'previous lyric' },
{ startTime: 3, endTime: 3.2, text: 'selected lyric' },
];
const seekTime = subtitleCueListSeekTime(cues, cues[1]!);
assert.ok(seekTime >= 3.19);
assert.ok(seekTime < cues[1]!.endTime);
});