mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
test: add core service coverage for cli, shortcuts, and secondary subtitle
This commit is contained in:
181
src/core/services/cli-command-service.test.ts
Normal file
181
src/core/services/cli-command-service.test.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { CliArgs } from "../../cli/args";
|
||||
import { CliCommandServiceDeps, handleCliCommandService } from "./cli-command-service";
|
||||
|
||||
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
return {
|
||||
start: false,
|
||||
stop: false,
|
||||
toggle: false,
|
||||
toggleVisibleOverlay: false,
|
||||
toggleInvisibleOverlay: false,
|
||||
settings: false,
|
||||
show: false,
|
||||
hide: false,
|
||||
showVisibleOverlay: false,
|
||||
hideVisibleOverlay: false,
|
||||
showInvisibleOverlay: false,
|
||||
hideInvisibleOverlay: false,
|
||||
copySubtitle: false,
|
||||
copySubtitleMultiple: false,
|
||||
mineSentence: false,
|
||||
mineSentenceMultiple: false,
|
||||
updateLastCardFromClipboard: false,
|
||||
toggleSecondarySub: false,
|
||||
triggerFieldGrouping: false,
|
||||
triggerSubsync: false,
|
||||
markAudioCard: false,
|
||||
openRuntimeOptions: false,
|
||||
texthooker: false,
|
||||
help: false,
|
||||
autoStartOverlay: false,
|
||||
generateConfig: false,
|
||||
backupOverwrite: false,
|
||||
verbose: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createDeps(overrides: Partial<CliCommandServiceDeps> = {}) {
|
||||
const calls: string[] = [];
|
||||
let mpvSocketPath = "/tmp/subminer.sock";
|
||||
let texthookerPort = 5174;
|
||||
const osd: string[] = [];
|
||||
|
||||
const deps: CliCommandServiceDeps = {
|
||||
getMpvSocketPath: () => mpvSocketPath,
|
||||
setMpvSocketPath: (socketPath) => {
|
||||
mpvSocketPath = socketPath;
|
||||
calls.push(`setMpvSocketPath:${socketPath}`);
|
||||
},
|
||||
setMpvClientSocketPath: (socketPath) => {
|
||||
calls.push(`setMpvClientSocketPath:${socketPath}`);
|
||||
},
|
||||
hasMpvClient: () => true,
|
||||
connectMpvClient: () => {
|
||||
calls.push("connectMpvClient");
|
||||
},
|
||||
isTexthookerRunning: () => false,
|
||||
setTexthookerPort: (port) => {
|
||||
texthookerPort = port;
|
||||
calls.push(`setTexthookerPort:${port}`);
|
||||
},
|
||||
getTexthookerPort: () => texthookerPort,
|
||||
shouldOpenTexthookerBrowser: () => true,
|
||||
ensureTexthookerRunning: (port) => {
|
||||
calls.push(`ensureTexthookerRunning:${port}`);
|
||||
},
|
||||
openTexthookerInBrowser: (url) => {
|
||||
calls.push(`openTexthookerInBrowser:${url}`);
|
||||
},
|
||||
stopApp: () => {
|
||||
calls.push("stopApp");
|
||||
},
|
||||
isOverlayRuntimeInitialized: () => false,
|
||||
initializeOverlayRuntime: () => {
|
||||
calls.push("initializeOverlayRuntime");
|
||||
},
|
||||
toggleVisibleOverlay: () => {
|
||||
calls.push("toggleVisibleOverlay");
|
||||
},
|
||||
toggleInvisibleOverlay: () => {
|
||||
calls.push("toggleInvisibleOverlay");
|
||||
},
|
||||
openYomitanSettingsDelayed: (delayMs) => {
|
||||
calls.push(`openYomitanSettingsDelayed:${delayMs}`);
|
||||
},
|
||||
setVisibleOverlayVisible: (visible) => {
|
||||
calls.push(`setVisibleOverlayVisible:${visible}`);
|
||||
},
|
||||
setInvisibleOverlayVisible: (visible) => {
|
||||
calls.push(`setInvisibleOverlayVisible:${visible}`);
|
||||
},
|
||||
copyCurrentSubtitle: () => {
|
||||
calls.push("copyCurrentSubtitle");
|
||||
},
|
||||
startPendingMultiCopy: (timeoutMs) => {
|
||||
calls.push(`startPendingMultiCopy:${timeoutMs}`);
|
||||
},
|
||||
mineSentenceCard: async () => {
|
||||
calls.push("mineSentenceCard");
|
||||
},
|
||||
startPendingMineSentenceMultiple: (timeoutMs) => {
|
||||
calls.push(`startPendingMineSentenceMultiple:${timeoutMs}`);
|
||||
},
|
||||
updateLastCardFromClipboard: async () => {
|
||||
calls.push("updateLastCardFromClipboard");
|
||||
},
|
||||
cycleSecondarySubMode: () => {
|
||||
calls.push("cycleSecondarySubMode");
|
||||
},
|
||||
triggerFieldGrouping: async () => {
|
||||
calls.push("triggerFieldGrouping");
|
||||
},
|
||||
triggerSubsyncFromConfig: async () => {
|
||||
calls.push("triggerSubsyncFromConfig");
|
||||
},
|
||||
markLastCardAsAudioCard: async () => {
|
||||
calls.push("markLastCardAsAudioCard");
|
||||
},
|
||||
openRuntimeOptionsPalette: () => {
|
||||
calls.push("openRuntimeOptionsPalette");
|
||||
},
|
||||
printHelp: () => {
|
||||
calls.push("printHelp");
|
||||
},
|
||||
hasMainWindow: () => true,
|
||||
getMultiCopyTimeoutMs: () => 2500,
|
||||
showMpvOsd: (text) => {
|
||||
osd.push(text);
|
||||
},
|
||||
log: (message) => {
|
||||
calls.push(`log:${message}`);
|
||||
},
|
||||
warn: (message) => {
|
||||
calls.push(`warn:${message}`);
|
||||
},
|
||||
error: (message) => {
|
||||
calls.push(`error:${message}`);
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
|
||||
return { deps, calls, osd };
|
||||
}
|
||||
|
||||
test("handleCliCommandService ignores --start for second-instance without actions", () => {
|
||||
const { deps, calls } = createDeps();
|
||||
const args = makeArgs({ start: true });
|
||||
|
||||
handleCliCommandService(args, "second-instance", deps);
|
||||
|
||||
assert.ok(calls.includes("log:Ignoring --start because SubMiner is already running."));
|
||||
assert.equal(calls.some((value) => value.includes("connectMpvClient")), false);
|
||||
});
|
||||
|
||||
test("handleCliCommandService runs texthooker flow with browser open", () => {
|
||||
const { deps, calls } = createDeps();
|
||||
const args = makeArgs({ texthooker: true });
|
||||
|
||||
handleCliCommandService(args, "initial", deps);
|
||||
|
||||
assert.ok(calls.includes("ensureTexthookerRunning:5174"));
|
||||
assert.ok(
|
||||
calls.includes("openTexthookerInBrowser:http://127.0.0.1:5174"),
|
||||
);
|
||||
});
|
||||
|
||||
test("handleCliCommandService reports async mine errors to OSD", async () => {
|
||||
const { deps, calls, osd } = createDeps({
|
||||
mineSentenceCard: async () => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
});
|
||||
|
||||
handleCliCommandService(makeArgs({ mineSentence: true }), "initial", deps);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.ok(calls.some((value) => value.startsWith("error:mineSentenceCard failed:")));
|
||||
assert.ok(osd.some((value) => value.includes("Mine sentence failed: boom")));
|
||||
});
|
||||
108
src/core/services/numeric-shortcut-session-service.test.ts
Normal file
108
src/core/services/numeric-shortcut-session-service.test.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createNumericShortcutSessionService } from "./numeric-shortcut-session-service";
|
||||
|
||||
test("numeric shortcut session handles digit selection and unregisters shortcuts", () => {
|
||||
const handlers = new Map<string, () => void>();
|
||||
const unregistered: string[] = [];
|
||||
const osd: string[] = [];
|
||||
const session = createNumericShortcutSessionService({
|
||||
registerShortcut: (accelerator, handler) => {
|
||||
handlers.set(accelerator, handler);
|
||||
return true;
|
||||
},
|
||||
unregisterShortcut: (accelerator) => {
|
||||
unregistered.push(accelerator);
|
||||
handlers.delete(accelerator);
|
||||
},
|
||||
setTimer: () => setTimeout(() => {}, 0),
|
||||
clearTimer: (timer) => clearTimeout(timer),
|
||||
showMpvOsd: (text) => {
|
||||
osd.push(text);
|
||||
},
|
||||
});
|
||||
|
||||
const digits: number[] = [];
|
||||
session.start({
|
||||
timeoutMs: 5000,
|
||||
onDigit: (digit) => {
|
||||
digits.push(digit);
|
||||
},
|
||||
messages: {
|
||||
prompt: "Pick a digit",
|
||||
timeout: "Timed out",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(session.isActive(), true);
|
||||
assert.equal(osd[0], "Pick a digit");
|
||||
assert.ok(handlers.has("3"));
|
||||
handlers.get("3")?.();
|
||||
|
||||
assert.deepEqual(digits, [3]);
|
||||
assert.equal(session.isActive(), false);
|
||||
assert.ok(unregistered.includes("Escape"));
|
||||
assert.ok(unregistered.includes("1"));
|
||||
assert.ok(unregistered.includes("9"));
|
||||
});
|
||||
|
||||
test("numeric shortcut session emits timeout message", () => {
|
||||
const osd: string[] = [];
|
||||
const session = createNumericShortcutSessionService({
|
||||
registerShortcut: () => true,
|
||||
unregisterShortcut: () => {},
|
||||
setTimer: (handler) => {
|
||||
handler();
|
||||
return setTimeout(() => {}, 0);
|
||||
},
|
||||
clearTimer: (timer) => clearTimeout(timer),
|
||||
showMpvOsd: (text) => {
|
||||
osd.push(text);
|
||||
},
|
||||
});
|
||||
|
||||
session.start({
|
||||
timeoutMs: 5000,
|
||||
onDigit: () => {},
|
||||
messages: {
|
||||
prompt: "Pick a digit",
|
||||
timeout: "Timed out",
|
||||
cancelled: "Aborted",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(session.isActive(), false);
|
||||
assert.ok(osd.includes("Timed out"));
|
||||
});
|
||||
|
||||
test("numeric shortcut session handles escape cancellation", () => {
|
||||
const handlers = new Map<string, () => void>();
|
||||
const osd: string[] = [];
|
||||
const session = createNumericShortcutSessionService({
|
||||
registerShortcut: (accelerator, handler) => {
|
||||
handlers.set(accelerator, handler);
|
||||
return true;
|
||||
},
|
||||
unregisterShortcut: (accelerator) => {
|
||||
handlers.delete(accelerator);
|
||||
},
|
||||
setTimer: () => setTimeout(() => {}, 10000),
|
||||
clearTimer: (timer) => clearTimeout(timer),
|
||||
showMpvOsd: (text) => {
|
||||
osd.push(text);
|
||||
},
|
||||
});
|
||||
|
||||
session.start({
|
||||
timeoutMs: 5000,
|
||||
onDigit: () => {},
|
||||
messages: {
|
||||
prompt: "Pick a digit",
|
||||
timeout: "Timed out",
|
||||
cancelled: "Aborted",
|
||||
},
|
||||
});
|
||||
handlers.get("Escape")?.();
|
||||
assert.equal(session.isActive(), false);
|
||||
assert.ok(osd.includes("Aborted"));
|
||||
});
|
||||
64
src/core/services/secondary-subtitle-service.test.ts
Normal file
64
src/core/services/secondary-subtitle-service.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { SecondarySubMode } from "../../types";
|
||||
import { cycleSecondarySubModeService } from "./secondary-subtitle-service";
|
||||
|
||||
test("cycleSecondarySubModeService cycles and emits broadcast + OSD", () => {
|
||||
let mode: SecondarySubMode = "hover";
|
||||
let lastToggleAt = 0;
|
||||
const broadcasts: SecondarySubMode[] = [];
|
||||
const osd: string[] = [];
|
||||
|
||||
cycleSecondarySubModeService({
|
||||
getSecondarySubMode: () => mode,
|
||||
setSecondarySubMode: (next) => {
|
||||
mode = next;
|
||||
},
|
||||
getLastSecondarySubToggleAtMs: () => lastToggleAt,
|
||||
setLastSecondarySubToggleAtMs: (value) => {
|
||||
lastToggleAt = value;
|
||||
},
|
||||
broadcastSecondarySubMode: (next) => {
|
||||
broadcasts.push(next);
|
||||
},
|
||||
showMpvOsd: (text) => {
|
||||
osd.push(text);
|
||||
},
|
||||
now: () => 1000,
|
||||
});
|
||||
|
||||
assert.equal(mode, "hidden");
|
||||
assert.deepEqual(broadcasts, ["hidden"]);
|
||||
assert.deepEqual(osd, ["Secondary subtitle: hidden"]);
|
||||
assert.equal(lastToggleAt, 1000);
|
||||
});
|
||||
|
||||
test("cycleSecondarySubModeService obeys debounce window", () => {
|
||||
let mode: SecondarySubMode = "visible";
|
||||
let lastToggleAt = 950;
|
||||
let broadcasted = false;
|
||||
let osdShown = false;
|
||||
|
||||
cycleSecondarySubModeService({
|
||||
getSecondarySubMode: () => mode,
|
||||
setSecondarySubMode: (next) => {
|
||||
mode = next;
|
||||
},
|
||||
getLastSecondarySubToggleAtMs: () => lastToggleAt,
|
||||
setLastSecondarySubToggleAtMs: (value) => {
|
||||
lastToggleAt = value;
|
||||
},
|
||||
broadcastSecondarySubMode: () => {
|
||||
broadcasted = true;
|
||||
},
|
||||
showMpvOsd: () => {
|
||||
osdShown = true;
|
||||
},
|
||||
now: () => 1000,
|
||||
});
|
||||
|
||||
assert.equal(mode, "visible");
|
||||
assert.equal(lastToggleAt, 950);
|
||||
assert.equal(broadcasted, false);
|
||||
assert.equal(osdShown, false);
|
||||
});
|
||||
Reference in New Issue
Block a user