import assert from 'node:assert/strict'; import test from 'node:test'; import { createStartupOsdSequencer, type StartupOsdSequencerCharacterDictionaryEvent, } from './startup-osd-sequencer'; function makeDictionaryEvent( phase: StartupOsdSequencerCharacterDictionaryEvent['phase'], message: string, ): StartupOsdSequencerCharacterDictionaryEvent { return { phase, message, }; } test('startup OSD keeps dictionary progress hidden until tokenization and annotation loading finish', () => { const osdMessages: string[] = []; const sequencer = createStartupOsdSequencer({ showOsd: (message) => { osdMessages.push(message); }, }); sequencer.notifyCharacterDictionaryStatus( makeDictionaryEvent('syncing', 'Updating character dictionary for Frieren...'), ); sequencer.showAnnotationLoading('Loading subtitle annotations |'); sequencer.markTokenizationReady(); assert.deepEqual(osdMessages, ['Loading subtitle annotations |']); sequencer.showAnnotationLoading('Loading subtitle annotations /'); assert.deepEqual(osdMessages, [ 'Loading subtitle annotations |', 'Loading subtitle annotations /', ]); sequencer.markAnnotationLoadingComplete('Subtitle annotations loaded'); assert.deepEqual(osdMessages, [ 'Loading subtitle annotations |', 'Loading subtitle annotations /', 'Updating character dictionary for Frieren...', ]); }); test('startup OSD buffers checking behind annotations and replaces it with later generating progress', () => { const osdMessages: string[] = []; const sequencer = createStartupOsdSequencer({ showOsd: (message) => { osdMessages.push(message); }, }); sequencer.notifyCharacterDictionaryStatus( makeDictionaryEvent('checking', 'Checking character dictionary for Frieren...'), ); sequencer.showAnnotationLoading('Loading subtitle annotations |'); sequencer.markTokenizationReady(); sequencer.notifyCharacterDictionaryStatus( makeDictionaryEvent('generating', 'Generating character dictionary for Frieren...'), ); assert.deepEqual(osdMessages, [ 'Loading subtitle annotations |', 'Generating character dictionary for Frieren...', ]); sequencer.markAnnotationLoadingComplete('Subtitle annotations loaded'); assert.deepEqual(osdMessages, [ 'Loading subtitle annotations |', 'Generating character dictionary for Frieren...', ]); }); test('startup OSD replaces earlier dictionary progress with later building progress', () => { const osdMessages: string[] = []; const sequencer = createStartupOsdSequencer({ showOsd: (message) => { osdMessages.push(message); }, }); sequencer.notifyCharacterDictionaryStatus( makeDictionaryEvent('syncing', 'Updating character dictionary for Frieren...'), ); sequencer.showAnnotationLoading('Loading subtitle annotations |'); sequencer.markTokenizationReady(); sequencer.notifyCharacterDictionaryStatus( makeDictionaryEvent('building', 'Building character dictionary for Frieren...'), ); sequencer.markAnnotationLoadingComplete('Subtitle annotations loaded'); assert.deepEqual(osdMessages, [ 'Loading subtitle annotations |', 'Building character dictionary for Frieren...', ]); }); test('startup OSD shows dictionary ready when progress completed before it became visible', () => { const osdMessages: string[] = []; const sequencer = createStartupOsdSequencer({ showOsd: (message) => { osdMessages.push(message); }, }); sequencer.notifyCharacterDictionaryStatus( makeDictionaryEvent('syncing', 'Updating character dictionary for Frieren...'), ); sequencer.notifyCharacterDictionaryStatus( makeDictionaryEvent('ready', 'Character dictionary ready for Frieren'), ); sequencer.markTokenizationReady(); sequencer.markAnnotationLoadingComplete('Subtitle annotations loaded'); assert.deepEqual(osdMessages, [ 'Character dictionary ready for Frieren', 'Subtitle annotations loaded', ]); }); test('startup OSD shows dictionary failure after annotation loading completes', () => { const osdMessages: string[] = []; const sequencer = createStartupOsdSequencer({ showOsd: (message) => { osdMessages.push(message); }, }); sequencer.showAnnotationLoading('Loading subtitle annotations |'); sequencer.notifyCharacterDictionaryStatus( makeDictionaryEvent('failed', 'Character dictionary sync failed for Frieren: boom'), ); sequencer.markTokenizationReady(); sequencer.markAnnotationLoadingComplete('Subtitle annotations loaded'); assert.deepEqual(osdMessages, [ 'Loading subtitle annotations |', 'Character dictionary sync failed for Frieren: boom', ]); }); test('startup OSD reset keeps tokenization ready after first warmup', () => { const osdMessages: string[] = []; const sequencer = createStartupOsdSequencer({ showOsd: (message) => { osdMessages.push(message); }, }); sequencer.markTokenizationReady(); sequencer.reset(); sequencer.notifyCharacterDictionaryStatus( makeDictionaryEvent('syncing', 'Updating character dictionary for Frieren...'), ); assert.deepEqual(osdMessages, ['Updating character dictionary for Frieren...']); }); test('startup OSD reset preserves in-flight tokenization loading for ready update', () => { const calls: string[] = []; const sequencer = createStartupOsdSequencer({ getNotificationType: () => 'both', showOsd: (message) => { calls.push(`osd:${message}`); }, showOverlayNotification: (payload) => { calls.push( `overlay:${payload.id}:${payload.title}:${payload.body}:${payload.variant}:${payload.persistent ? 'pin' : 'auto'}`, ); }, showDesktopNotification: (title, options) => { calls.push(`desktop:${title}:${options.body ?? ''}`); }, }); sequencer.showTokenizationLoading('Loading subtitle tokenization...'); sequencer.reset(); sequencer.markTokenizationReady(); assert.deepEqual(calls, [ 'overlay:startup-tokenization:Subtitle tokenization:Loading subtitle tokenization...:progress:pin', 'overlay:startup-tokenization:Subtitle tokenization:Subtitle tokenization ready:success:auto', 'desktop:SubMiner:Subtitle tokenization ready', ]); }); test('startup OSD shows later dictionary progress immediately once tokenization is ready', () => { const osdMessages: string[] = []; const sequencer = createStartupOsdSequencer({ showOsd: (message) => { osdMessages.push(message); }, }); sequencer.showAnnotationLoading('Loading subtitle annotations |'); sequencer.markTokenizationReady(); sequencer.notifyCharacterDictionaryStatus( makeDictionaryEvent('generating', 'Generating character dictionary for Frieren...'), ); assert.deepEqual(osdMessages, [ 'Loading subtitle annotations |', 'Generating character dictionary for Frieren...', ]); sequencer.markAnnotationLoadingComplete('Subtitle annotations loaded'); assert.deepEqual(osdMessages, [ 'Loading subtitle annotations |', 'Generating character dictionary for Frieren...', ]); }); test('startup OSD keeps dictionary progress pending when mpv osd is unavailable', () => { const osdMessages: string[] = []; let osdAvailable = false; const sequencer = createStartupOsdSequencer({ showOsd: (message) => { osdMessages.push(message); return osdAvailable; }, }); sequencer.markTokenizationReady(); sequencer.notifyCharacterDictionaryStatus( makeDictionaryEvent('generating', 'Generating character dictionary for Frieren...'), ); sequencer.notifyCharacterDictionaryStatus( makeDictionaryEvent('ready', 'Character dictionary ready for Frieren'), ); assert.deepEqual(osdMessages, [ 'Generating character dictionary for Frieren...', 'Character dictionary ready for Frieren', ]); osdAvailable = true; sequencer.notifyCharacterDictionaryStatus( makeDictionaryEvent('ready', 'Character dictionary ready for Frieren'), ); assert.deepEqual(osdMessages, [ 'Generating character dictionary for Frieren...', 'Character dictionary ready for Frieren', 'Character dictionary ready for Frieren', ]); }); test('startup notifications route tokenization and annotation status to overlay and system without osd for both', () => { const calls: string[] = []; const sequencer = createStartupOsdSequencer({ getNotificationType: () => 'both', showOsd: (message) => { calls.push(`osd:${message}`); }, showOverlayNotification: (payload) => { calls.push( `overlay:${payload.id}:${payload.title}:${payload.body}:${payload.variant}:${payload.persistent ? 'pin' : 'auto'}`, ); }, showDesktopNotification: (title, options) => { calls.push(`desktop:${title}:${options.body ?? ''}`); }, }); sequencer.showTokenizationLoading('Loading subtitle tokenization...'); sequencer.markTokenizationReady(); sequencer.showAnnotationLoading('Loading subtitle annotations |'); sequencer.markAnnotationLoadingComplete('Subtitle annotations loaded'); assert.deepEqual(calls, [ 'overlay:startup-tokenization:Subtitle tokenization:Loading subtitle tokenization...:progress:pin', 'overlay:startup-tokenization:Subtitle tokenization:Subtitle tokenization ready:success:auto', 'desktop:SubMiner:Subtitle tokenization ready', 'overlay:startup-subtitle-annotations:Subtitle annotations:Loading subtitle annotations |:progress:pin', 'overlay:startup-subtitle-annotations:Subtitle annotations:Subtitle annotations loaded:success:auto', 'desktop:SubMiner:Subtitle annotations loaded', ]); });