mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-22 12:11:27 -07:00
237 lines
7.1 KiB
TypeScript
237 lines
7.1 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import type { SubtitlePrefetchService } from '../../core/services/subtitle-prefetch';
|
|
import type { SubtitleCue } from '../../types';
|
|
import { createSubtitlePrefetchInitController } from './subtitle-prefetch-init';
|
|
|
|
function createDeferred<T>(): {
|
|
promise: Promise<T>;
|
|
resolve: (value: T) => void;
|
|
reject: (error: unknown) => void;
|
|
} {
|
|
let resolve!: (value: T) => void;
|
|
let reject!: (error: unknown) => void;
|
|
const promise = new Promise<T>((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
return { promise, resolve, reject };
|
|
}
|
|
|
|
function flushMicrotasks(): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
}
|
|
|
|
test('latest subtitle prefetch init wins over stale async loads', async () => {
|
|
const loads = new Map<string, ReturnType<typeof createDeferred<string>>>();
|
|
const started: string[] = [];
|
|
const stopped: string[] = [];
|
|
let currentService: SubtitlePrefetchService | null = null;
|
|
|
|
const controller = createSubtitlePrefetchInitController({
|
|
getCurrentService: () => currentService,
|
|
setCurrentService: (service) => {
|
|
currentService = service;
|
|
},
|
|
loadSubtitleSourceText: async (source) => {
|
|
const deferred = createDeferred<string>();
|
|
loads.set(source, deferred);
|
|
return await deferred.promise;
|
|
},
|
|
parseSubtitleCues: (_content, filename): SubtitleCue[] => [
|
|
{ startTime: 0, endTime: 1, text: filename },
|
|
],
|
|
createSubtitlePrefetchService: ({ cues }) => ({
|
|
start: () => {
|
|
started.push(cues[0]!.text);
|
|
},
|
|
stop: () => {
|
|
stopped.push(cues[0]!.text);
|
|
},
|
|
onSeek: () => {},
|
|
pause: () => {},
|
|
resume: () => {},
|
|
}),
|
|
tokenizeSubtitle: async () => null,
|
|
preCacheTokenization: () => {},
|
|
isCacheFull: () => false,
|
|
logInfo: () => {},
|
|
logWarn: () => {},
|
|
});
|
|
|
|
const firstInit = controller.initSubtitlePrefetch('old.ass', 1);
|
|
const secondInit = controller.initSubtitlePrefetch('new.ass', 2);
|
|
|
|
loads.get('new.ass')!.resolve('new');
|
|
await flushMicrotasks();
|
|
|
|
assert.deepEqual(started, ['new.ass']);
|
|
|
|
loads.get('old.ass')!.resolve('old');
|
|
await Promise.all([firstInit, secondInit]);
|
|
|
|
assert.deepEqual(started, ['new.ass']);
|
|
assert.deepEqual(stopped, []);
|
|
});
|
|
|
|
test('cancelPendingInit prevents an in-flight load from attaching a stale service', async () => {
|
|
const deferred = createDeferred<string>();
|
|
let currentService: SubtitlePrefetchService | null = null;
|
|
const started: string[] = [];
|
|
|
|
const controller = createSubtitlePrefetchInitController({
|
|
getCurrentService: () => currentService,
|
|
setCurrentService: (service) => {
|
|
currentService = service;
|
|
},
|
|
loadSubtitleSourceText: async () => await deferred.promise,
|
|
parseSubtitleCues: (_content, filename): SubtitleCue[] => [
|
|
{ startTime: 0, endTime: 1, text: filename },
|
|
],
|
|
createSubtitlePrefetchService: ({ cues }) => ({
|
|
start: () => {
|
|
started.push(cues[0]!.text);
|
|
},
|
|
stop: () => {},
|
|
onSeek: () => {},
|
|
pause: () => {},
|
|
resume: () => {},
|
|
}),
|
|
tokenizeSubtitle: async () => null,
|
|
preCacheTokenization: () => {},
|
|
isCacheFull: () => false,
|
|
logInfo: () => {},
|
|
logWarn: () => {},
|
|
});
|
|
|
|
const initPromise = controller.initSubtitlePrefetch('stale.ass', 1);
|
|
controller.cancelPendingInit();
|
|
deferred.resolve('stale');
|
|
await initPromise;
|
|
|
|
assert.equal(currentService, null);
|
|
assert.deepEqual(started, []);
|
|
});
|
|
|
|
test('subtitle prefetch init publishes parsed cues and clears them on cancel', async () => {
|
|
const deferred = createDeferred<string>();
|
|
let currentService: SubtitlePrefetchService | null = null;
|
|
const cueUpdates: Array<SubtitleCue[] | null> = [];
|
|
|
|
const controller = createSubtitlePrefetchInitController({
|
|
getCurrentService: () => currentService,
|
|
setCurrentService: (service) => {
|
|
currentService = service;
|
|
},
|
|
loadSubtitleSourceText: async () => await deferred.promise,
|
|
parseSubtitleCues: () => [
|
|
{ startTime: 1, endTime: 2, text: 'first' },
|
|
{ startTime: 3, endTime: 4, text: 'second' },
|
|
],
|
|
createSubtitlePrefetchService: () => ({
|
|
start: () => {},
|
|
stop: () => {},
|
|
onSeek: () => {},
|
|
pause: () => {},
|
|
resume: () => {},
|
|
}),
|
|
tokenizeSubtitle: async () => null,
|
|
preCacheTokenization: () => {},
|
|
isCacheFull: () => false,
|
|
logInfo: () => {},
|
|
logWarn: () => {},
|
|
onParsedSubtitleCuesChanged: (cues) => {
|
|
cueUpdates.push(cues);
|
|
},
|
|
});
|
|
|
|
const initPromise = controller.initSubtitlePrefetch('episode.ass', 12);
|
|
deferred.resolve('content');
|
|
await initPromise;
|
|
|
|
controller.cancelPendingInit();
|
|
|
|
assert.deepEqual(cueUpdates, [
|
|
[
|
|
{ startTime: 1, endTime: 2, text: 'first' },
|
|
{ startTime: 3, endTime: 4, text: 'second' },
|
|
],
|
|
null,
|
|
]);
|
|
});
|
|
|
|
test('subtitle prefetch init publishes the provided stable source key instead of the load path', async () => {
|
|
const deferred = createDeferred<string>();
|
|
let currentService: SubtitlePrefetchService | null = null;
|
|
const sourceUpdates: Array<string | null> = [];
|
|
|
|
const controller = createSubtitlePrefetchInitController({
|
|
getCurrentService: () => currentService,
|
|
setCurrentService: (service) => {
|
|
currentService = service;
|
|
},
|
|
loadSubtitleSourceText: async () => await deferred.promise,
|
|
parseSubtitleCues: () => [{ startTime: 1, endTime: 2, text: 'first' }],
|
|
createSubtitlePrefetchService: () => ({
|
|
start: () => {},
|
|
stop: () => {},
|
|
onSeek: () => {},
|
|
pause: () => {},
|
|
resume: () => {},
|
|
}),
|
|
tokenizeSubtitle: async () => null,
|
|
preCacheTokenization: () => {},
|
|
isCacheFull: () => false,
|
|
logInfo: () => {},
|
|
logWarn: () => {},
|
|
onParsedSubtitleCuesChanged: (_cues, source) => {
|
|
sourceUpdates.push(source);
|
|
},
|
|
});
|
|
|
|
const initPromise = controller.initSubtitlePrefetch(
|
|
'/tmp/subminer-sidebar-123/track_7.ass',
|
|
12,
|
|
'internal:/media/episode01.mkv:track:3:ff:7',
|
|
);
|
|
deferred.resolve('content');
|
|
await initPromise;
|
|
|
|
assert.deepEqual(sourceUpdates, ['internal:/media/episode01.mkv:track:3:ff:7']);
|
|
});
|
|
|
|
test('subtitle prefetch init clears parsed cues when initialization fails', async () => {
|
|
const cueUpdates: Array<SubtitleCue[] | null> = [];
|
|
let currentService: SubtitlePrefetchService | null = null;
|
|
|
|
const controller = createSubtitlePrefetchInitController({
|
|
getCurrentService: () => currentService,
|
|
setCurrentService: (service) => {
|
|
currentService = service;
|
|
},
|
|
loadSubtitleSourceText: async () => {
|
|
throw new Error('boom');
|
|
},
|
|
parseSubtitleCues: () => [{ startTime: 1, endTime: 2, text: 'first' }],
|
|
createSubtitlePrefetchService: () => ({
|
|
start: () => {},
|
|
stop: () => {},
|
|
onSeek: () => {},
|
|
pause: () => {},
|
|
resume: () => {},
|
|
}),
|
|
tokenizeSubtitle: async () => null,
|
|
preCacheTokenization: () => {},
|
|
isCacheFull: () => false,
|
|
logInfo: () => {},
|
|
logWarn: () => {},
|
|
onParsedSubtitleCuesChanged: (cues) => {
|
|
cueUpdates.push(cues);
|
|
},
|
|
});
|
|
|
|
await controller.initSubtitlePrefetch('episode.ass', 12);
|
|
|
|
assert.deepEqual(cueUpdates, [null]);
|
|
});
|