Files
SubMiner/src/core/services/overlay-shortcut-handler.test.ts

292 lines
8.3 KiB
TypeScript

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> = {},
): 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<OverlayShortcutRuntimeDeps> = {}) {
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);
});