test(core): expand mpv/subsync/tokenizer and cli coverage

This commit is contained in:
kyasuda
2026-02-10 13:13:47 -08:00
committed by sudacode
parent 9d6b2f840c
commit 3dfe8f7dd2
6 changed files with 749 additions and 1 deletions

View File

@@ -16,7 +16,7 @@
"docs:build": "VITE_EXTRA_EXTENSIONS=jsonc vitepress build docs",
"docs:preview": "VITE_EXTRA_EXTENSIONS=jsonc vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort",
"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 dist/core/services/cli-command-service.test.js dist/core/services/cli-command-deps-runtime-service.test.js dist/core/services/ipc-deps-runtime-service.test.js dist/core/services/field-grouping-overlay-runtime-service.test.js dist/core/services/numeric-shortcut-runtime-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/tokenizer-deps-runtime-service.test.js dist/core/services/shortcut-ui-deps-runtime-service.test.js dist/core/services/secondary-subtitle-service.test.js dist/core/services/mpv-render-metrics-service.test.js dist/core/services/mpv-runtime-service.test.js dist/core/services/runtime-options-runtime-service.test.js dist/core/services/overlay-modal-restore-service.test.js dist/core/services/runtime-config-service.test.js dist/core/services/overlay-bridge-runtime-service.test.js dist/core/services/overlay-broadcast-runtime-service.test.js dist/core/services/overlay-manager-service.test.js dist/core/services/app-ready-runtime-service.test.js dist/core/services/app-shutdown-runtime-service.test.js dist/core/services/app-lifecycle-deps-runtime-service.test.js dist/core/services/runtime-options-manager-runtime-service.test.js dist/core/services/config-warning-runtime-service.test.js dist/core/services/app-logging-runtime-service.test.js dist/core/services/startup-resource-runtime-service.test.js dist/core/services/config-generation-runtime-service.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/field-grouping-overlay-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/secondary-subtitle-service.test.js dist/core/services/mpv-render-metrics-service.test.js dist/core/services/mpv-control-service.test.js dist/core/services/mpv-service.test.js dist/core/services/runtime-options-ipc-service.test.js dist/core/services/runtime-config-service.test.js dist/core/services/tokenizer-service.test.js dist/core/services/subsync-service.test.js dist/core/services/overlay-bridge-service.test.js dist/core/services/overlay-manager-service.test.js dist/core/services/app-ready-service.test.js dist/core/services/startup-bootstrap-service.test.js dist/subsync/utils.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",
"start": "pnpm run build && electron . --start",

View File

@@ -179,3 +179,112 @@ test("handleCliCommandService reports async mine errors to OSD", async () => {
assert.ok(calls.some((value) => value.startsWith("error:mineSentenceCard failed:")));
assert.ok(osd.some((value) => value.includes("Mine sentence failed: boom")));
});
test("handleCliCommandService applies socket path and connects on start", () => {
const { deps, calls } = createDeps();
handleCliCommandService(
makeArgs({ start: true, socketPath: "/tmp/custom.sock" }),
"initial",
deps,
);
assert.ok(calls.includes("setMpvSocketPath:/tmp/custom.sock"));
assert.ok(calls.includes("setMpvClientSocketPath:/tmp/custom.sock"));
assert.ok(calls.includes("connectMpvClient"));
});
test("handleCliCommandService warns when texthooker port override used while running", () => {
const { deps, calls } = createDeps({
isTexthookerRunning: () => true,
});
handleCliCommandService(
makeArgs({ texthookerPort: 9999, texthooker: true }),
"initial",
deps,
);
assert.ok(
calls.includes(
"warn:Ignoring --port override because the texthooker server is already running.",
),
);
assert.equal(calls.some((value) => value === "setTexthookerPort:9999"), false);
});
test("handleCliCommandService prints help and stops app when no window exists", () => {
const { deps, calls } = createDeps({
hasMainWindow: () => false,
});
handleCliCommandService(makeArgs({ help: true }), "initial", deps);
assert.ok(calls.includes("printHelp"));
assert.ok(calls.includes("stopApp"));
});
test("handleCliCommandService reports async trigger-subsync errors to OSD", async () => {
const { deps, calls, osd } = createDeps({
triggerSubsyncFromConfig: async () => {
throw new Error("subsync boom");
},
});
handleCliCommandService(makeArgs({ triggerSubsync: true }), "initial", deps);
await new Promise((resolve) => setImmediate(resolve));
assert.ok(
calls.some((value) => value.startsWith("error:triggerSubsyncFromConfig failed:")),
);
assert.ok(osd.some((value) => value.includes("Subsync failed: subsync boom")));
});
test("handleCliCommandService stops app for --stop command", () => {
const { deps, calls } = createDeps();
handleCliCommandService(makeArgs({ stop: true }), "initial", deps);
assert.ok(calls.includes("log:Stopping SubMiner..."));
assert.ok(calls.includes("stopApp"));
});
test("handleCliCommandService still runs non-start actions on second-instance", () => {
const { deps, calls } = createDeps();
handleCliCommandService(
makeArgs({ start: true, toggleVisibleOverlay: true }),
"second-instance",
deps,
);
assert.ok(calls.includes("toggleVisibleOverlay"));
assert.equal(calls.some((value) => value === "connectMpvClient"), false);
});
test("handleCliCommandService handles visibility and utility command dispatches", () => {
const cases: Array<{
args: Partial<CliArgs>;
expected: string;
}> = [
{ args: { toggleInvisibleOverlay: true }, expected: "toggleInvisibleOverlay" },
{ args: { settings: true }, expected: "openYomitanSettingsDelayed:1000" },
{ args: { showVisibleOverlay: true }, expected: "setVisibleOverlayVisible:true" },
{ args: { hideVisibleOverlay: true }, expected: "setVisibleOverlayVisible:false" },
{ args: { showInvisibleOverlay: true }, expected: "setInvisibleOverlayVisible:true" },
{ args: { hideInvisibleOverlay: true }, expected: "setInvisibleOverlayVisible:false" },
{ args: { copySubtitle: true }, expected: "copyCurrentSubtitle" },
{ args: { copySubtitleMultiple: true }, expected: "startPendingMultiCopy:2500" },
{
args: { mineSentenceMultiple: true },
expected: "startPendingMineSentenceMultiple:2500",
},
{ args: { toggleSecondarySub: true }, expected: "cycleSecondarySubMode" },
{ args: { openRuntimeOptions: true }, expected: "openRuntimeOptionsPalette" },
];
for (const entry of cases) {
const { deps, calls } = createDeps();
handleCliCommandService(makeArgs(entry.args), "initial", deps);
assert.ok(
calls.includes(entry.expected),
`expected call missing for args ${JSON.stringify(entry.args)}: ${entry.expected}`,
);
}
});

View File

@@ -0,0 +1,176 @@
import test from "node:test";
import assert from "node:assert/strict";
import { MpvIpcClient, MpvIpcClientDeps } from "./mpv-service";
function makeDeps(
overrides: Partial<MpvIpcClientDeps> = {},
): MpvIpcClientDeps {
return {
getResolvedConfig: () => ({} as any),
autoStartOverlay: false,
setOverlayVisible: () => {},
shouldBindVisibleOverlayToMpvSubVisibility: () => false,
isVisibleOverlayVisible: () => false,
getReconnectTimer: () => null,
setReconnectTimer: () => {},
getCurrentSubText: () => "",
setCurrentSubText: () => {},
setCurrentSubAssText: () => {},
getSubtitleTimingTracker: () => null,
subtitleWsBroadcast: () => {},
getOverlayWindowsCount: () => 0,
tokenizeSubtitle: async (text) => ({ text, tokens: null }),
broadcastToOverlayWindows: () => {},
updateCurrentMediaPath: () => {},
updateMpvSubtitleRenderMetrics: () => {},
getMpvSubtitleRenderMetrics: () => ({
subPos: 100,
subFontSize: 36,
subScale: 1,
subMarginY: 0,
subMarginX: 0,
subFont: "sans-serif",
subSpacing: 0,
subBold: false,
subItalic: false,
subBorderSize: 0,
subShadowOffset: 0,
subAssOverride: "yes",
subScaleByWindow: true,
subUseMargins: true,
osdHeight: 720,
osdDimensions: null,
}),
setPreviousSecondarySubVisibility: () => {},
showMpvOsd: () => {},
...overrides,
};
}
test("MpvIpcClient resolves pending request by request_id", async () => {
const client = new MpvIpcClient("/tmp/mpv.sock", makeDeps());
let resolved: unknown = null;
(client as any).pendingRequests.set(1234, (msg: unknown) => {
resolved = msg;
});
await (client as any).handleMessage({ request_id: 1234, data: "ok" });
assert.deepEqual(resolved, { request_id: 1234, data: "ok" });
assert.equal((client as any).pendingRequests.size, 0);
});
test("MpvIpcClient handles sub-text property change and broadcasts tokenized subtitle", async () => {
const calls: string[] = [];
const client = new MpvIpcClient(
"/tmp/mpv.sock",
makeDeps({
setCurrentSubText: (text) => {
calls.push(`setCurrentSubText:${text}`);
},
subtitleWsBroadcast: (text) => {
calls.push(`subtitleWsBroadcast:${text}`);
},
getOverlayWindowsCount: () => 1,
tokenizeSubtitle: async (text) => ({ text, tokens: null }),
broadcastToOverlayWindows: (channel, payload) => {
calls.push(`broadcast:${channel}:${String((payload as any).text ?? "")}`);
},
}),
);
await (client as any).handleMessage({
event: "property-change",
name: "sub-text",
data: "字幕",
});
assert.ok(calls.includes("setCurrentSubText:字幕"));
assert.ok(calls.includes("subtitleWsBroadcast:字幕"));
assert.ok(calls.includes("broadcast:subtitle:set:字幕"));
});
test("MpvIpcClient parses JSON line protocol in processBuffer", () => {
const client = new MpvIpcClient("/tmp/mpv.sock", makeDeps());
const seen: Array<Record<string, unknown>> = [];
(client as any).handleMessage = (msg: Record<string, unknown>) => {
seen.push(msg);
};
(client as any).buffer =
"{\"event\":\"property-change\",\"name\":\"path\",\"data\":\"a\"}\n{\"request_id\":1,\"data\":\"ok\"}\n{\"partial\":";
(client as any).processBuffer();
assert.equal(seen.length, 2);
assert.equal(seen[0].name, "path");
assert.equal(seen[1].request_id, 1);
assert.equal((client as any).buffer, "{\"partial\":");
});
test("MpvIpcClient request rejects when disconnected", async () => {
const client = new MpvIpcClient("/tmp/mpv.sock", makeDeps());
await assert.rejects(
async () => client.request(["get_property", "path"]),
/MPV not connected/,
);
});
test("MpvIpcClient requestProperty throws on mpv error response", async () => {
const client = new MpvIpcClient("/tmp/mpv.sock", makeDeps());
(client as any).request = async () => ({ error: "property unavailable" });
await assert.rejects(
async () => client.requestProperty("path"),
/Failed to read MPV property 'path': property unavailable/,
);
});
test("MpvIpcClient failPendingRequests resolves outstanding requests as disconnected", () => {
const client = new MpvIpcClient("/tmp/mpv.sock", makeDeps());
const resolved: unknown[] = [];
(client as any).pendingRequests.set(10, (msg: unknown) => {
resolved.push(msg);
});
(client as any).pendingRequests.set(11, (msg: unknown) => {
resolved.push(msg);
});
(client as any).failPendingRequests();
assert.deepEqual(resolved, [
{ request_id: 10, error: "disconnected" },
{ request_id: 11, error: "disconnected" },
]);
assert.equal((client as any).pendingRequests.size, 0);
});
test("MpvIpcClient scheduleReconnect schedules timer and invokes connect", () => {
const timers: Array<ReturnType<typeof setTimeout> | null> = [];
const client = new MpvIpcClient(
"/tmp/mpv.sock",
makeDeps({
getReconnectTimer: () => null,
setReconnectTimer: (timer) => {
timers.push(timer);
},
}),
);
let connectCalled = false;
(client as any).connect = () => {
connectCalled = true;
};
const originalSetTimeout = globalThis.setTimeout;
(globalThis as any).setTimeout = (handler: () => void, _delay: number) => {
handler();
return 1 as unknown as ReturnType<typeof setTimeout>;
};
try {
(client as any).scheduleReconnect();
} finally {
(globalThis as any).setTimeout = originalSetTimeout;
}
assert.equal(timers.length, 1);
assert.equal(connectCalled, true);
});

View File

@@ -0,0 +1,300 @@
import test from "node:test";
import assert from "node:assert/strict";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import {
TriggerSubsyncFromConfigDeps,
runSubsyncManualService,
triggerSubsyncFromConfigService,
} from "./subsync-service";
function makeDeps(
overrides: Partial<TriggerSubsyncFromConfigDeps> = {},
): TriggerSubsyncFromConfigDeps {
const mpvClient = {
connected: true,
currentAudioStreamIndex: null,
send: () => {},
requestProperty: async (name: string) => {
if (name === "path") return "/tmp/video.mkv";
if (name === "sid") return 1;
if (name === "secondary-sid") return null;
if (name === "track-list") {
return [
{ id: 1, type: "sub", selected: true, lang: "jpn" },
{
id: 2,
type: "sub",
selected: false,
external: true,
lang: "eng",
"external-filename": "/tmp/ref.srt",
},
{ id: 3, type: "audio", selected: true, "ff-index": 1 },
];
}
return null;
},
};
return {
getMpvClient: () => mpvClient,
getResolvedConfig: () => ({
defaultMode: "manual",
alassPath: "/usr/bin/alass",
ffsubsyncPath: "/usr/bin/ffsubsync",
ffmpegPath: "/usr/bin/ffmpeg",
}),
isSubsyncInProgress: () => false,
setSubsyncInProgress: () => {},
showMpvOsd: () => {},
runWithSubsyncSpinner: async <T>(task: () => Promise<T>) => task(),
openManualPicker: () => {},
...overrides,
};
}
test("triggerSubsyncFromConfigService returns early when already in progress", async () => {
const osd: string[] = [];
await triggerSubsyncFromConfigService(
makeDeps({
isSubsyncInProgress: () => true,
showMpvOsd: (text) => {
osd.push(text);
},
}),
);
assert.deepEqual(osd, ["Subsync already running"]);
});
test("triggerSubsyncFromConfigService opens manual picker in manual mode", async () => {
const osd: string[] = [];
let payloadTrackCount = 0;
let inProgressState: boolean | null = null;
await triggerSubsyncFromConfigService(
makeDeps({
openManualPicker: (payload) => {
payloadTrackCount = payload.sourceTracks.length;
},
showMpvOsd: (text) => {
osd.push(text);
},
setSubsyncInProgress: (value) => {
inProgressState = value;
},
}),
);
assert.equal(payloadTrackCount, 1);
assert.ok(osd.includes("Subsync: choose engine and source"));
assert.equal(inProgressState, false);
});
test("triggerSubsyncFromConfigService reports failures to OSD", async () => {
const osd: string[] = [];
await triggerSubsyncFromConfigService(
makeDeps({
getMpvClient: () => null,
showMpvOsd: (text) => {
osd.push(text);
},
}),
);
assert.ok(osd.some((line) => line.startsWith("Subsync failed: MPV not connected")));
});
test("runSubsyncManualService requires a source track for alass", async () => {
const result = await runSubsyncManualService(
{ engine: "alass", sourceTrackId: null },
makeDeps(),
);
assert.deepEqual(result, {
ok: false,
message: "Select a subtitle source track for alass",
});
});
test("triggerSubsyncFromConfigService reports path validation failures", async () => {
const osd: string[] = [];
const inProgress: boolean[] = [];
await triggerSubsyncFromConfigService(
makeDeps({
getResolvedConfig: () => ({
defaultMode: "auto",
alassPath: "/missing/alass",
ffsubsyncPath: "/missing/ffsubsync",
ffmpegPath: "/missing/ffmpeg",
}),
setSubsyncInProgress: (value) => {
inProgress.push(value);
},
showMpvOsd: (text) => {
osd.push(text);
},
}),
);
assert.deepEqual(inProgress, [true, false]);
assert.ok(
osd.some((line) =>
line.startsWith("Subsync failed: Configured ffmpeg executable not found"),
),
);
});
function writeExecutableScript(filePath: string, content: string): void {
fs.writeFileSync(filePath, content, { encoding: "utf8", mode: 0o755 });
fs.chmodSync(filePath, 0o755);
}
test("runSubsyncManualService constructs ffsubsync command and returns success", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "subsync-ffsubsync-"));
const ffsubsyncLogPath = path.join(tmpDir, "ffsubsync-args.log");
const ffsubsyncPath = path.join(tmpDir, "ffsubsync.sh");
const ffmpegPath = path.join(tmpDir, "ffmpeg.sh");
const alassPath = path.join(tmpDir, "alass.sh");
const videoPath = path.join(tmpDir, "video.mkv");
const primaryPath = path.join(tmpDir, "primary.srt");
fs.writeFileSync(videoPath, "video");
fs.writeFileSync(primaryPath, "sub");
writeExecutableScript(
ffmpegPath,
"#!/bin/sh\nexit 0\n",
);
writeExecutableScript(
alassPath,
"#!/bin/sh\nexit 0\n",
);
writeExecutableScript(
ffsubsyncPath,
`#!/bin/sh\n: > "${ffsubsyncLogPath}"\nfor arg in "$@"; do printf '%s\\n' "$arg" >> "${ffsubsyncLogPath}"; done\nout=\"\"\nprev=\"\"\nfor arg in \"$@\"; do\n if [ \"$prev\" = \"-o\" ]; then out=\"$arg\"; fi\n prev=\"$arg\"\ndone\nif [ -n \"$out\" ]; then : > \"$out\"; fi\nexit 0\n`,
);
const sentCommands: Array<Array<string | number>> = [];
const deps = makeDeps({
getMpvClient: () => ({
connected: true,
currentAudioStreamIndex: 2,
send: (payload) => {
sentCommands.push(payload.command);
},
requestProperty: async (name: string) => {
if (name === "path") return videoPath;
if (name === "sid") return 1;
if (name === "secondary-sid") return null;
if (name === "track-list") {
return [
{
id: 1,
type: "sub",
selected: true,
external: true,
"external-filename": primaryPath,
},
];
}
return null;
},
}),
getResolvedConfig: () => ({
defaultMode: "manual",
alassPath,
ffsubsyncPath,
ffmpegPath,
}),
});
const result = await runSubsyncManualService(
{ engine: "ffsubsync", sourceTrackId: null },
deps,
);
assert.equal(result.ok, true);
assert.equal(result.message, "Subtitle synchronized with ffsubsync");
const ffArgs = fs.readFileSync(ffsubsyncLogPath, "utf8").trim().split("\n");
assert.equal(ffArgs[0], videoPath);
assert.ok(ffArgs.includes("-i"));
assert.ok(ffArgs.includes(primaryPath));
assert.ok(ffArgs.includes("--reference-stream"));
assert.ok(ffArgs.includes("0:2"));
assert.equal(sentCommands[0]?.[0], "sub_add");
assert.deepEqual(sentCommands[1], ["set_property", "sub-delay", 0]);
});
test("runSubsyncManualService constructs alass command and returns failure on non-zero exit", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "subsync-alass-"));
const alassLogPath = path.join(tmpDir, "alass-args.log");
const alassPath = path.join(tmpDir, "alass.sh");
const ffmpegPath = path.join(tmpDir, "ffmpeg.sh");
const ffsubsyncPath = path.join(tmpDir, "ffsubsync.sh");
const videoPath = path.join(tmpDir, "video.mkv");
const primaryPath = path.join(tmpDir, "primary.srt");
const sourcePath = path.join(tmpDir, "source.srt");
fs.writeFileSync(videoPath, "video");
fs.writeFileSync(primaryPath, "sub");
fs.writeFileSync(sourcePath, "sub2");
writeExecutableScript(ffmpegPath, "#!/bin/sh\nexit 0\n");
writeExecutableScript(ffsubsyncPath, "#!/bin/sh\nexit 0\n");
writeExecutableScript(
alassPath,
`#!/bin/sh\n: > "${alassLogPath}"\nfor arg in "$@"; do printf '%s\\n' "$arg" >> "${alassLogPath}"; done\nexit 1\n`,
);
const deps = makeDeps({
getMpvClient: () => ({
connected: true,
currentAudioStreamIndex: null,
send: () => {},
requestProperty: async (name: string) => {
if (name === "path") return videoPath;
if (name === "sid") return 1;
if (name === "secondary-sid") return null;
if (name === "track-list") {
return [
{
id: 1,
type: "sub",
selected: true,
external: true,
"external-filename": primaryPath,
},
{
id: 2,
type: "sub",
selected: false,
external: true,
"external-filename": sourcePath,
},
];
}
return null;
},
}),
getResolvedConfig: () => ({
defaultMode: "manual",
alassPath,
ffsubsyncPath,
ffmpegPath,
}),
});
const result = await runSubsyncManualService(
{ engine: "alass", sourceTrackId: 2 },
deps,
);
assert.deepEqual(result, {
ok: false,
message: "alass synchronization failed",
});
const alassArgs = fs.readFileSync(alassLogPath, "utf8").trim().split("\n");
assert.equal(alassArgs[0], sourcePath);
assert.equal(alassArgs[1], primaryPath);
});

View File

@@ -0,0 +1,129 @@
import test from "node:test";
import assert from "node:assert/strict";
import { PartOfSpeech } from "../../types";
import { tokenizeSubtitleService, TokenizerServiceDeps } from "./tokenizer-service";
function makeDeps(
overrides: Partial<TokenizerServiceDeps> = {},
): TokenizerServiceDeps {
return {
getYomitanExt: () => null,
getYomitanParserWindow: () => null,
setYomitanParserWindow: () => {},
getYomitanParserReadyPromise: () => null,
setYomitanParserReadyPromise: () => {},
getYomitanParserInitPromise: () => null,
setYomitanParserInitPromise: () => {},
tokenizeWithMecab: async () => null,
...overrides,
};
}
test("tokenizeSubtitleService returns null tokens for empty normalized text", async () => {
const result = await tokenizeSubtitleService(" \\n ", makeDeps());
assert.deepEqual(result, { text: " \\n ", tokens: null });
});
test("tokenizeSubtitleService normalizes newlines before mecab fallback", async () => {
let tokenizeInput = "";
const result = await tokenizeSubtitleService(
"猫\\Nです\nね",
makeDeps({
tokenizeWithMecab: async (text) => {
tokenizeInput = text;
return [
{
surface: "猫ですね",
reading: "ネコデスネ",
headword: "猫ですね",
startPos: 0,
endPos: 4,
partOfSpeech: PartOfSpeech.other,
isMerged: true,
},
];
},
}),
);
assert.equal(tokenizeInput, "猫 です ね");
assert.equal(result.text, "猫\nです\nね");
assert.equal(result.tokens?.[0]?.surface, "猫ですね");
});
test("tokenizeSubtitleService falls back to mecab tokens when available", async () => {
const result = await tokenizeSubtitleService(
"猫です",
makeDeps({
tokenizeWithMecab: async () => [
{
surface: "猫",
reading: "ネコ",
headword: "猫",
startPos: 0,
endPos: 1,
partOfSpeech: PartOfSpeech.noun,
isMerged: false,
},
],
}),
);
assert.equal(result.text, "猫です");
assert.equal(result.tokens?.length, 1);
assert.equal(result.tokens?.[0]?.surface, "猫");
});
test("tokenizeSubtitleService returns null tokens when mecab throws", async () => {
const result = await tokenizeSubtitleService(
"猫です",
makeDeps({
tokenizeWithMecab: async () => {
throw new Error("mecab failed");
},
}),
);
assert.deepEqual(result, { text: "猫です", tokens: null });
});
test("tokenizeSubtitleService uses Yomitan parser result when available", async () => {
const parserWindow = {
isDestroyed: () => false,
webContents: {
executeJavaScript: async () => [
{
source: "scanning-parser",
index: 0,
content: [
[
{
text: "猫",
reading: "ねこ",
headwords: [[{ term: "猫" }]],
},
{
text: "です",
reading: "です",
},
],
],
},
],
},
} as unknown as Electron.BrowserWindow;
const result = await tokenizeSubtitleService(
"猫です",
makeDeps({
getYomitanExt: () => ({ id: "dummy-ext" } as any),
getYomitanParserWindow: () => parserWindow,
tokenizeWithMecab: async () => null,
}),
);
assert.equal(result.text, "猫です");
assert.equal(result.tokens?.length, 1);
assert.equal(result.tokens?.[0]?.surface, "猫です");
assert.equal(result.tokens?.[0]?.reading, "ねこです");
});

34
src/subsync/utils.test.ts Normal file
View File

@@ -0,0 +1,34 @@
import test from "node:test";
import assert from "node:assert/strict";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { getSubsyncConfig, runCommand } from "./utils";
test("getSubsyncConfig applies fallback executable paths for blank values", () => {
const config = getSubsyncConfig({
defaultMode: "manual",
alass_path: " ",
ffsubsync_path: "",
ffmpeg_path: undefined,
});
assert.equal(config.defaultMode, "manual");
assert.equal(config.alassPath, "/usr/bin/alass");
assert.equal(config.ffsubsyncPath, "/usr/bin/ffsubsync");
assert.equal(config.ffmpegPath, "/usr/bin/ffmpeg");
});
test("runCommand returns failure on timeout", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "subsync-utils-"));
const sleeperPath = path.join(tmpDir, "sleeper.sh");
fs.writeFileSync(sleeperPath, "#!/bin/sh\nsleep 2\n", {
encoding: "utf8",
mode: 0o755,
});
fs.chmodSync(sleeperPath, 0o755);
const result = await runCommand(sleeperPath, [], 50);
assert.equal(result.ok, false);
});