import test from "node:test"; import assert from "node:assert/strict"; import { ConfiguredShortcuts } from "../utils/shortcut-config"; import { createOverlayShortcutRuntimeHandlers, OverlayShortcutRuntimeDeps, runOverlayShortcutLocalFallback, } from "./overlay-shortcut-handler"; function makeShortcuts( overrides: Partial = {}, ): ConfiguredShortcuts { return { toggleVisibleOverlayGlobal: null, toggleInvisibleOverlayGlobal: null, copySubtitle: null, copySubtitleMultiple: null, updateLastCardFromClipboard: null, triggerFieldGrouping: null, triggerSubsync: null, mineSentence: null, mineSentenceMultiple: null, multiCopyTimeoutMs: 2500, toggleSecondarySub: null, markAudioCard: null, openRuntimeOptions: null, openJimaku: null, ...overrides, }; } function createDeps(overrides: Partial = {}) { const calls: string[] = []; const osd: string[] = []; const deps: OverlayShortcutRuntimeDeps = { showMpvOsd: (text) => { osd.push(text); }, openRuntimeOptions: () => { calls.push("openRuntimeOptions"); }, openJimaku: () => { calls.push("openJimaku"); }, markAudioCard: async () => { calls.push("markAudioCard"); }, copySubtitleMultiple: (timeoutMs) => { calls.push(`copySubtitleMultiple:${timeoutMs}`); }, copySubtitle: () => { calls.push("copySubtitle"); }, toggleSecondarySub: () => { calls.push("toggleSecondarySub"); }, updateLastCardFromClipboard: async () => { calls.push("updateLastCardFromClipboard"); }, triggerFieldGrouping: async () => { calls.push("triggerFieldGrouping"); }, triggerSubsync: async () => { calls.push("triggerSubsync"); }, mineSentence: async () => { calls.push("mineSentence"); }, mineSentenceMultiple: (timeoutMs) => { calls.push(`mineSentenceMultiple:${timeoutMs}`); }, ...overrides, }; return { deps, calls, osd }; } test("createOverlayShortcutRuntimeHandlers dispatches sync and async handlers", async () => { const { deps, calls } = createDeps(); const { overlayHandlers, fallbackHandlers } = createOverlayShortcutRuntimeHandlers(deps); overlayHandlers.copySubtitle(); overlayHandlers.copySubtitleMultiple(1111); overlayHandlers.toggleSecondarySub(); overlayHandlers.openRuntimeOptions(); overlayHandlers.openJimaku(); overlayHandlers.mineSentenceMultiple(2222); overlayHandlers.updateLastCardFromClipboard(); fallbackHandlers.mineSentence(); await new Promise((resolve) => setImmediate(resolve)); assert.deepEqual(calls, [ "copySubtitle", "copySubtitleMultiple:1111", "toggleSecondarySub", "openRuntimeOptions", "openJimaku", "mineSentenceMultiple:2222", "updateLastCardFromClipboard", "mineSentence", ]); }); test("createOverlayShortcutRuntimeHandlers reports async failures via OSD", async () => { const logs: unknown[][] = []; const originalError = console.error; console.error = (...args: unknown[]) => { logs.push(args); }; try { const { deps, osd } = createDeps({ markAudioCard: async () => { throw new Error("audio boom"); }, }); const { overlayHandlers } = createOverlayShortcutRuntimeHandlers(deps); overlayHandlers.markAudioCard(); await new Promise((resolve) => setImmediate(resolve)); assert.equal(logs.length, 1); assert.equal(logs[0]?.[0], "markLastCardAsAudioCard failed:"); assert.ok(osd.some((entry) => entry.includes("Audio card failed: audio boom"))); } finally { console.error = originalError; } }); test("runOverlayShortcutLocalFallback dispatches matching actions with timeout", () => { const handled: string[] = []; const matched: Array<{ accelerator: string; allowWhenRegistered: boolean }> = []; const shortcuts = makeShortcuts({ copySubtitleMultiple: "Ctrl+M", multiCopyTimeoutMs: 4321, }); const result = runOverlayShortcutLocalFallback( {} as Electron.Input, shortcuts, (_input, accelerator, allowWhenRegistered) => { matched.push({ accelerator, allowWhenRegistered: allowWhenRegistered === true, }); return accelerator === "Ctrl+M"; }, { openRuntimeOptions: () => handled.push("openRuntimeOptions"), openJimaku: () => handled.push("openJimaku"), markAudioCard: () => handled.push("markAudioCard"), copySubtitleMultiple: (timeoutMs) => handled.push(`copySubtitleMultiple:${timeoutMs}`), copySubtitle: () => handled.push("copySubtitle"), toggleSecondarySub: () => handled.push("toggleSecondarySub"), updateLastCardFromClipboard: () => handled.push("updateLastCardFromClipboard"), triggerFieldGrouping: () => handled.push("triggerFieldGrouping"), triggerSubsync: () => handled.push("triggerSubsync"), mineSentence: () => handled.push("mineSentence"), mineSentenceMultiple: (timeoutMs) => handled.push(`mineSentenceMultiple:${timeoutMs}`), }, ); assert.equal(result, true); assert.deepEqual(handled, ["copySubtitleMultiple:4321"]); assert.deepEqual(matched, [{ accelerator: "Ctrl+M", allowWhenRegistered: false }]); }); test("runOverlayShortcutLocalFallback passes allowWhenRegistered for secondary-sub toggle", () => { const matched: Array<{ accelerator: string; allowWhenRegistered: boolean }> = []; const shortcuts = makeShortcuts({ toggleSecondarySub: "Ctrl+2", }); const result = runOverlayShortcutLocalFallback( {} as Electron.Input, shortcuts, (_input, accelerator, allowWhenRegistered) => { matched.push({ accelerator, allowWhenRegistered: allowWhenRegistered === true, }); return accelerator === "Ctrl+2"; }, { openRuntimeOptions: () => {}, openJimaku: () => {}, markAudioCard: () => {}, copySubtitleMultiple: () => {}, copySubtitle: () => {}, toggleSecondarySub: () => {}, updateLastCardFromClipboard: () => {}, triggerFieldGrouping: () => {}, triggerSubsync: () => {}, mineSentence: () => {}, mineSentenceMultiple: () => {}, }, ); assert.equal(result, true); assert.deepEqual(matched, [{ accelerator: "Ctrl+2", allowWhenRegistered: true }]); }); test("runOverlayShortcutLocalFallback allows registered-global jimaku shortcut", () => { const matched: Array<{ accelerator: string; allowWhenRegistered: boolean }> = []; const shortcuts = makeShortcuts({ openJimaku: "Ctrl+J", }); const result = runOverlayShortcutLocalFallback( {} as Electron.Input, shortcuts, (_input, accelerator, allowWhenRegistered) => { matched.push({ accelerator, allowWhenRegistered: allowWhenRegistered === true, }); return accelerator === "Ctrl+J"; }, { openRuntimeOptions: () => {}, openJimaku: () => {}, markAudioCard: () => {}, copySubtitleMultiple: () => {}, copySubtitle: () => {}, toggleSecondarySub: () => {}, updateLastCardFromClipboard: () => {}, triggerFieldGrouping: () => {}, triggerSubsync: () => {}, mineSentence: () => {}, mineSentenceMultiple: () => {}, }, ); assert.equal(result, true); assert.deepEqual(matched, [{ accelerator: "Ctrl+J", allowWhenRegistered: true }]); }); test("runOverlayShortcutLocalFallback returns false when no action matches", () => { const shortcuts = makeShortcuts({ copySubtitle: "Ctrl+C", }); let called = false; const result = runOverlayShortcutLocalFallback( {} as Electron.Input, shortcuts, () => false, { openRuntimeOptions: () => { called = true; }, openJimaku: () => { called = true; }, markAudioCard: () => { called = true; }, copySubtitleMultiple: () => { called = true; }, copySubtitle: () => { called = true; }, toggleSecondarySub: () => { called = true; }, updateLastCardFromClipboard: () => { called = true; }, triggerFieldGrouping: () => { called = true; }, triggerSubsync: () => { called = true; }, mineSentence: () => { called = true; }, mineSentenceMultiple: () => { called = true; }, }, ); assert.equal(result, false); assert.equal(called, false); });