mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-09 15:13:32 -07:00
9d77907877
- Show mpv OSD spinner from start-file until subminer-overlay-loading-ready; force-shown for visible-overlay startup regardless of osd_messages setting - Gate non-macOS overlay visibility on content-ready so first subtitle line is immediately hoverable and clickable - Queue startup notifications in main process until overlay window finishes loading; upsert progress cards by id to avoid cold-start floods - Defer background warmups until after overlay runtime init so queued notifications can deliver promptly - Preserve character dictionary checking/building/importing/ready phases as distinct history entries; route building and importing to system notifications when notificationType is both
440 lines
14 KiB
TypeScript
440 lines
14 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
getPlaybackFeedbackNotificationOptions,
|
|
notifyConfiguredStatus,
|
|
} from './configured-status-notification';
|
|
import { createOverlayNotificationDelivery } from './overlay-notification-delivery';
|
|
|
|
test('notifyConfiguredStatus routes both to overlay and system without osd', () => {
|
|
const calls: string[] = [];
|
|
|
|
notifyConfiguredStatus('Subsync: choose engine and source', {
|
|
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 ?? ''}`),
|
|
});
|
|
|
|
assert.deepEqual(calls, [
|
|
'overlay::SubMiner:Subsync: choose engine and source:info:auto',
|
|
'desktop:SubMiner:Subsync: choose engine and source',
|
|
]);
|
|
});
|
|
|
|
test('notifyConfiguredStatus queues pre-overlay both status through overlay sender and desktop', () => {
|
|
const calls: string[] = [];
|
|
|
|
notifyConfiguredStatus('Overlay loading...', {
|
|
getNotificationType: () => 'both',
|
|
isOverlayReady: () => false,
|
|
showOsd: (message) => {
|
|
calls.push(`osd:${message}`);
|
|
},
|
|
showOverlayNotification: (payload) =>
|
|
calls.push(`overlay:${payload.id ?? ''}:${payload.body ?? ''}`),
|
|
showDesktopNotification: (title, options) =>
|
|
calls.push(`desktop:${title}:${options.body ?? ''}`),
|
|
});
|
|
|
|
assert.deepEqual(calls, ['overlay::Overlay loading...', 'desktop:SubMiner:Overlay loading...']);
|
|
});
|
|
|
|
test('notifyConfiguredStatus queues pre-overlay overlay-only status without osd fallback', () => {
|
|
const calls: string[] = [];
|
|
|
|
notifyConfiguredStatus('Overlay loading...', {
|
|
getNotificationType: () => 'overlay',
|
|
isOverlayReady: () => false,
|
|
showOsd: (message) => {
|
|
calls.push(`osd:${message}`);
|
|
},
|
|
showOverlayNotification: (payload) =>
|
|
calls.push(`overlay:${payload.id ?? ''}:${payload.body ?? ''}`),
|
|
showDesktopNotification: (title, options) =>
|
|
calls.push(`desktop:${title}:${options.body ?? ''}`),
|
|
});
|
|
|
|
assert.deepEqual(calls, ['overlay::Overlay loading...']);
|
|
});
|
|
|
|
test('notifyConfiguredStatus routes pre-overlay system status to desktop only', () => {
|
|
const calls: string[] = [];
|
|
|
|
notifyConfiguredStatus('Overlay loading...', {
|
|
getNotificationType: () => 'system',
|
|
isOverlayReady: () => false,
|
|
showOsd: (message) => {
|
|
calls.push(`osd:${message}`);
|
|
},
|
|
showOverlayNotification: (payload) =>
|
|
calls.push(`overlay:${payload.id ?? ''}:${payload.body ?? ''}`),
|
|
showDesktopNotification: (title, options) =>
|
|
calls.push(`desktop:${title}:${options.body ?? ''}`),
|
|
});
|
|
|
|
assert.deepEqual(calls, ['desktop:SubMiner:Overlay loading...']);
|
|
});
|
|
|
|
test('notifyConfiguredStatus keeps osd-system on legacy surfaces', () => {
|
|
const calls: string[] = [];
|
|
|
|
notifyConfiguredStatus('Overlay loading...', {
|
|
getNotificationType: () => 'osd-system',
|
|
showOsd: (message) => {
|
|
calls.push(`osd:${message}`);
|
|
},
|
|
showDesktopNotification: (title, options) =>
|
|
calls.push(`desktop:${title}:${options.body ?? ''}`),
|
|
});
|
|
|
|
assert.deepEqual(calls, ['osd:Overlay loading...', 'desktop:SubMiner:Overlay loading...']);
|
|
});
|
|
|
|
test('notifyConfiguredStatus can suppress desktop delivery for progress ticks', () => {
|
|
const calls: string[] = [];
|
|
|
|
notifyConfiguredStatus(
|
|
'Subsync: syncing |',
|
|
{
|
|
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 ?? ''}`),
|
|
},
|
|
{
|
|
id: 'subsync-status',
|
|
title: 'Subsync',
|
|
variant: 'progress',
|
|
persistent: true,
|
|
desktop: false,
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(calls, ['overlay:subsync-status:Subsync:Subsync: syncing |:progress:pin']);
|
|
});
|
|
|
|
test('notifyConfiguredStatus routes feedback through overlay without desktop delivery', () => {
|
|
const calls: string[] = [];
|
|
|
|
notifyConfiguredStatus(
|
|
'Primary subtitle: hover',
|
|
{
|
|
getNotificationType: () => 'both',
|
|
showOsd: (message) => {
|
|
calls.push(`osd:${message}`);
|
|
},
|
|
showOverlayNotification: (payload) =>
|
|
calls.push(`overlay:${payload.title}:${payload.body ?? ''}`),
|
|
showDesktopNotification: (title, options) =>
|
|
calls.push(`desktop:${title}:${options.body ?? ''}`),
|
|
},
|
|
{ delivery: 'feedback' },
|
|
);
|
|
|
|
assert.deepEqual(calls, ['overlay:SubMiner:Primary subtitle: hover']);
|
|
});
|
|
|
|
test('notifyConfiguredStatus routes osd-system feedback through osd only', () => {
|
|
const calls: string[] = [];
|
|
|
|
notifyConfiguredStatus(
|
|
'Secondary subtitle: visible',
|
|
{
|
|
getNotificationType: () => 'osd-system',
|
|
showOsd: (message) => {
|
|
calls.push(`osd:${message}`);
|
|
},
|
|
showDesktopNotification: (title, options) =>
|
|
calls.push(`desktop:${title}:${options.body ?? ''}`),
|
|
},
|
|
{ delivery: 'feedback' },
|
|
);
|
|
|
|
assert.deepEqual(calls, ['osd:Secondary subtitle: visible']);
|
|
});
|
|
|
|
test('notifyConfiguredStatus suppresses system-only feedback', () => {
|
|
const calls: string[] = [];
|
|
|
|
notifyConfiguredStatus(
|
|
'Primary subtitle: visible',
|
|
{
|
|
getNotificationType: () => 'system',
|
|
showOsd: (message) => {
|
|
calls.push(`osd:${message}`);
|
|
},
|
|
showDesktopNotification: (title, options) =>
|
|
calls.push(`desktop:${title}:${options.body ?? ''}`),
|
|
},
|
|
{ delivery: 'feedback' },
|
|
);
|
|
|
|
assert.deepEqual(calls, []);
|
|
});
|
|
|
|
test('playback feedback options reuse subtitle mode notification ids', () => {
|
|
assert.deepEqual(getPlaybackFeedbackNotificationOptions('Primary subtitle: hover'), {
|
|
id: 'primary-subtitle-mode-feedback',
|
|
});
|
|
assert.deepEqual(getPlaybackFeedbackNotificationOptions('Secondary subtitle: hidden'), {
|
|
id: 'secondary-subtitle-mode-feedback',
|
|
});
|
|
assert.deepEqual(getPlaybackFeedbackNotificationOptions('Secondary subtitle track: English'), {});
|
|
});
|
|
|
|
test('notifyConfiguredStatus falls back to desktop if overlay is unavailable', () => {
|
|
const calls: string[] = [];
|
|
|
|
notifyConfiguredStatus('Overlay unavailable.', {
|
|
getNotificationType: () => 'overlay',
|
|
showOsd: (message) => {
|
|
calls.push(`osd:${message}`);
|
|
},
|
|
showDesktopNotification: (title, options) =>
|
|
calls.push(`desktop:${title}:${options.body ?? ''}`),
|
|
});
|
|
|
|
assert.deepEqual(calls, ['desktop:SubMiner:Overlay unavailable.']);
|
|
});
|
|
|
|
test('overlay notification delivery queues until an overlay window is ready', () => {
|
|
const sent: string[] = [];
|
|
let ready = false;
|
|
const delivery = createOverlayNotificationDelivery({
|
|
hasReadyOverlayWindow: () => ready,
|
|
send: (payload) => sent.push(`${payload.id ?? ''}:${'body' in payload ? payload.body : ''}`),
|
|
});
|
|
|
|
delivery.send({ id: 'startup-tokenization', title: 'Subtitle tokenization', body: 'Loading' });
|
|
delivery.send({ id: 'character-dictionary-auto-sync', title: 'Dictionary', body: 'Building' });
|
|
|
|
assert.equal(delivery.getQueuedCount(), 2);
|
|
assert.deepEqual(sent, []);
|
|
|
|
ready = true;
|
|
delivery.flush();
|
|
|
|
assert.equal(delivery.getQueuedCount(), 0);
|
|
assert.deepEqual(sent, [
|
|
'startup-tokenization:Loading',
|
|
'character-dictionary-auto-sync:Building',
|
|
]);
|
|
});
|
|
|
|
test('overlay notification delivery upserts queued progress by notification id', () => {
|
|
const sent: string[] = [];
|
|
let ready = false;
|
|
const delivery = createOverlayNotificationDelivery({
|
|
hasReadyOverlayWindow: () => ready,
|
|
send: (payload) => sent.push(`${payload.id ?? ''}:${'body' in payload ? payload.body : ''}`),
|
|
});
|
|
|
|
delivery.send({ id: 'startup-subtitle-annotations', title: 'Subtitle annotations', body: '|' });
|
|
delivery.send({ id: 'startup-subtitle-annotations', title: 'Subtitle annotations', body: '/' });
|
|
delivery.send({ id: 'startup-tokenization', title: 'Subtitle tokenization', body: 'Ready' });
|
|
|
|
ready = true;
|
|
delivery.flush();
|
|
|
|
assert.deepEqual(sent, ['startup-subtitle-annotations:/', 'startup-tokenization:Ready']);
|
|
});
|
|
|
|
test('overlay notification delivery preserves queued events with distinct history ids', () => {
|
|
const sent: string[] = [];
|
|
let ready = false;
|
|
const delivery = createOverlayNotificationDelivery({
|
|
hasReadyOverlayWindow: () => ready,
|
|
send: (payload) =>
|
|
sent.push(
|
|
`${payload.id ?? ''}:${'historyId' in payload ? payload.historyId : ''}:${'body' in payload ? payload.body : ''}`,
|
|
),
|
|
});
|
|
|
|
delivery.send({
|
|
id: 'character-dictionary-auto-sync',
|
|
historyId: 'character-dictionary-auto-sync-checking',
|
|
title: 'Character dictionary',
|
|
body: 'Checking character dictionary...',
|
|
persistent: true,
|
|
});
|
|
delivery.send({
|
|
id: 'character-dictionary-auto-sync',
|
|
historyId: 'character-dictionary-auto-sync-building',
|
|
title: 'Character dictionary',
|
|
body: 'Building character dictionary...',
|
|
persistent: true,
|
|
});
|
|
|
|
ready = true;
|
|
delivery.flush();
|
|
|
|
assert.deepEqual(sent, [
|
|
'character-dictionary-auto-sync:character-dictionary-auto-sync-checking:Checking character dictionary...',
|
|
'character-dictionary-auto-sync:character-dictionary-auto-sync-building:Building character dictionary...',
|
|
]);
|
|
});
|
|
|
|
test('overlay notification delivery preserves queued startup progress before terminal update', () => {
|
|
const sent: string[] = [];
|
|
const scheduled: Array<() => void> = [];
|
|
let ready = false;
|
|
const delivery = createOverlayNotificationDelivery({
|
|
hasReadyOverlayWindow: () => ready,
|
|
send: (payload) =>
|
|
sent.push(
|
|
`${payload.id ?? ''}:${'body' in payload ? payload.body : ''}:${'persistent' in payload && payload.persistent ? 'pin' : 'auto'}`,
|
|
),
|
|
scheduleFlushRetry: (callback) => {
|
|
scheduled.push(callback);
|
|
},
|
|
});
|
|
|
|
delivery.send({
|
|
id: 'startup-tokenization',
|
|
title: 'Subtitle tokenization',
|
|
body: 'Loading subtitle tokenization...',
|
|
variant: 'progress',
|
|
persistent: true,
|
|
});
|
|
delivery.send({
|
|
id: 'startup-tokenization',
|
|
title: 'Subtitle tokenization',
|
|
body: 'Subtitle tokenization ready',
|
|
variant: 'success',
|
|
persistent: false,
|
|
});
|
|
|
|
ready = true;
|
|
delivery.flush();
|
|
scheduled.shift()?.();
|
|
|
|
assert.deepEqual(sent, [
|
|
'startup-tokenization:Loading subtitle tokenization...:pin',
|
|
'startup-tokenization:Subtitle tokenization ready:auto',
|
|
]);
|
|
});
|
|
|
|
test('overlay notification delivery defers terminal update after first queued progress paint', () => {
|
|
const sent: string[] = [];
|
|
const scheduled: Array<() => void> = [];
|
|
const delays: number[] = [];
|
|
let ready = false;
|
|
const delivery = createOverlayNotificationDelivery({
|
|
hasReadyOverlayWindow: () => ready,
|
|
send: (payload) =>
|
|
sent.push(
|
|
`${payload.id ?? ''}:${'body' in payload ? payload.body : ''}:${'persistent' in payload && payload.persistent ? 'pin' : 'auto'}`,
|
|
),
|
|
scheduleFlushRetry: (callback, delayMs) => {
|
|
scheduled.push(callback);
|
|
delays.push(delayMs);
|
|
},
|
|
terminalUpdateDelayMs: 750,
|
|
});
|
|
|
|
delivery.send({
|
|
id: 'startup-subtitle-annotations',
|
|
title: 'Subtitle annotations',
|
|
body: 'Loading subtitle annotations |',
|
|
variant: 'progress',
|
|
persistent: true,
|
|
});
|
|
delivery.send({
|
|
id: 'startup-subtitle-annotations',
|
|
title: 'Subtitle annotations',
|
|
body: 'Subtitle annotations loaded',
|
|
variant: 'success',
|
|
persistent: false,
|
|
});
|
|
|
|
ready = true;
|
|
delivery.flush();
|
|
|
|
assert.deepEqual(sent, ['startup-subtitle-annotations:Loading subtitle annotations |:pin']);
|
|
assert.equal(delivery.getQueuedCount(), 1);
|
|
assert.deepEqual(delays, [750]);
|
|
|
|
scheduled.shift()?.();
|
|
|
|
assert.equal(delivery.getQueuedCount(), 0);
|
|
assert.deepEqual(sent, [
|
|
'startup-subtitle-annotations:Loading subtitle annotations |:pin',
|
|
'startup-subtitle-annotations:Subtitle annotations loaded:auto',
|
|
]);
|
|
});
|
|
|
|
test('overlay notification delivery retries flush when lifecycle fires before window readiness settles', () => {
|
|
const sent: string[] = [];
|
|
const scheduled: Array<() => void> = [];
|
|
let ready = false;
|
|
const delivery = createOverlayNotificationDelivery({
|
|
hasReadyOverlayWindow: () => ready,
|
|
send: (payload) => sent.push(`${payload.id ?? ''}:${'body' in payload ? payload.body : ''}`),
|
|
scheduleFlushRetry: (callback) => {
|
|
scheduled.push(callback);
|
|
},
|
|
});
|
|
|
|
delivery.send({ id: 'startup-tokenization', title: 'Subtitle tokenization', body: 'Loading' });
|
|
delivery.flush();
|
|
|
|
assert.equal(delivery.getQueuedCount(), 1);
|
|
assert.equal(scheduled.length, 1);
|
|
assert.deepEqual(sent, []);
|
|
|
|
ready = true;
|
|
scheduled.shift()?.();
|
|
|
|
assert.equal(delivery.getQueuedCount(), 0);
|
|
assert.deepEqual(sent, ['startup-tokenization:Loading']);
|
|
});
|
|
|
|
test('overlay notification delivery drops queued notification when dismissed before flush', () => {
|
|
const sent: string[] = [];
|
|
let ready = false;
|
|
const delivery = createOverlayNotificationDelivery({
|
|
hasReadyOverlayWindow: () => ready,
|
|
send: (payload) =>
|
|
sent.push('dismiss' in payload ? `dismiss:${payload.id}` : `show:${payload.id ?? ''}`),
|
|
});
|
|
|
|
delivery.send({ id: 'overlay-loading-status', title: 'SubMiner', body: 'Overlay loading' });
|
|
delivery.send({ id: 'overlay-loading-status', dismiss: true });
|
|
|
|
ready = true;
|
|
delivery.flush();
|
|
|
|
assert.deepEqual(sent, []);
|
|
});
|
|
|
|
test('overlay notification delivery removes queued notification when dismissed at readiness', () => {
|
|
const sent: string[] = [];
|
|
let ready = false;
|
|
const delivery = createOverlayNotificationDelivery({
|
|
hasReadyOverlayWindow: () => ready,
|
|
send: (payload) =>
|
|
sent.push('dismiss' in payload ? `dismiss:${payload.id}` : `show:${payload.id ?? ''}`),
|
|
});
|
|
|
|
delivery.send({ id: 'overlay-loading-status', title: 'SubMiner', body: 'Overlay loading' });
|
|
|
|
ready = true;
|
|
delivery.send({ id: 'overlay-loading-status', dismiss: true });
|
|
delivery.flush();
|
|
|
|
assert.deepEqual(sent, ['dismiss:overlay-loading-status']);
|
|
});
|