mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
test: add core service coverage for cli, shortcuts, and secondary subtitle
This commit is contained in:
@@ -16,7 +16,7 @@
|
|||||||
"docs:build": "vitepress build docs",
|
"docs:build": "vitepress build docs",
|
||||||
"docs:preview": "vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort",
|
"docs:preview": "vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort",
|
||||||
"test:config": "pnpm run build && node --test dist/config/config.test.js",
|
"test:config": "pnpm run build && node --test dist/config/config.test.js",
|
||||||
"test:core": "pnpm run build && node --test dist/cli/args.test.js dist/cli/help.test.js",
|
"test:core": "pnpm run build && node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/secondary-subtitle-service.test.js",
|
||||||
"test:subtitle": "pnpm run build && node --test dist/subtitle/stages.test.js dist/subtitle/pipeline.test.js",
|
"test:subtitle": "pnpm run build && node --test dist/subtitle/stages.test.js dist/subtitle/pipeline.test.js",
|
||||||
"generate:config-example": "pnpm run build && node dist/generate-config-example.js",
|
"generate:config-example": "pnpm run build && node dist/generate-config-example.js",
|
||||||
"start": "pnpm run build && electron . --start",
|
"start": "pnpm run build && electron . --start",
|
||||||
|
|||||||
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