refactor: extract ipc deps runtime service

This commit is contained in:
2026-02-10 01:03:10 -08:00
parent 3686788a72
commit e95728b4d1
4 changed files with 253 additions and 58 deletions

View File

@@ -0,0 +1,108 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createIpcDepsRuntimeService } from "./ipc-deps-runtime-service";
test("createIpcDepsRuntimeService maps window and mecab helpers", async () => {
let ignoreMouse: { ignore: boolean; forward?: boolean } | null = null;
let toggledDevTools = 0;
let mecabEnabled: boolean | null = null;
const visibleWindow = {
isDestroyed: () => false,
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => {
ignoreMouse = { ignore, forward: options?.forward };
},
webContents: {
toggleDevTools: () => {
toggledDevTools += 1;
},
},
};
const deps = createIpcDepsRuntimeService({
getInvisibleWindow: () => visibleWindow,
getMainWindow: () => visibleWindow,
getVisibleOverlayVisibility: () => true,
getInvisibleOverlayVisibility: () => false,
onOverlayModalClosed: () => {},
openYomitanSettings: () => {},
quitApp: () => {},
toggleVisibleOverlay: () => {},
tokenizeCurrentSubtitle: async () => ({ text: "x" }),
getCurrentSubtitleAss: () => "ass",
getMpvSubtitleRenderMetrics: () => ({ subPos: 100 }),
getSubtitlePosition: () => ({ x: 1, y: 2 }),
getSubtitleStyle: () => null,
saveSubtitlePosition: () => {},
getMecabTokenizer: () => ({
getStatus: () => ({ available: true, enabled: true, path: "/usr/bin/mecab" }),
setEnabled: (enabled: boolean) => {
mecabEnabled = enabled;
},
}),
handleMpvCommand: () => {},
getKeybindings: () => ({ copySubtitle: ["C"] }),
getSecondarySubMode: () => "hidden",
getMpvClient: () => ({ currentSecondarySubText: "secondary" }),
runSubsyncManual: async () => ({ ok: true }),
getAnkiConnectStatus: () => true,
getRuntimeOptions: () => ({ values: {} }),
setRuntimeOption: () => ({ ok: true }),
cycleRuntimeOption: () => ({ ok: true }),
});
deps.setInvisibleIgnoreMouseEvents(true, { forward: true });
deps.toggleDevTools();
deps.setMecabEnabled(false);
assert.deepEqual(ignoreMouse, { ignore: true, forward: true });
assert.equal(toggledDevTools, 1);
assert.equal(mecabEnabled, false);
assert.deepEqual(deps.getMecabStatus(), {
available: true,
enabled: true,
path: "/usr/bin/mecab",
});
assert.equal(deps.getCurrentSecondarySub(), "secondary");
assert.deepEqual(await deps.tokenizeCurrentSubtitle(), { text: "x" });
});
test("createIpcDepsRuntimeService handles missing optional runtime resources", () => {
const deps = createIpcDepsRuntimeService({
getInvisibleWindow: () => null,
getMainWindow: () => null,
getVisibleOverlayVisibility: () => false,
getInvisibleOverlayVisibility: () => false,
onOverlayModalClosed: () => {},
openYomitanSettings: () => {},
quitApp: () => {},
toggleVisibleOverlay: () => {},
tokenizeCurrentSubtitle: async () => null,
getCurrentSubtitleAss: () => "",
getMpvSubtitleRenderMetrics: () => null,
getSubtitlePosition: () => null,
getSubtitleStyle: () => null,
saveSubtitlePosition: () => {},
getMecabTokenizer: () => null,
handleMpvCommand: () => {},
getKeybindings: () => null,
getSecondarySubMode: () => "hidden",
getMpvClient: () => null,
runSubsyncManual: async () => ({ ok: false }),
getAnkiConnectStatus: () => false,
getRuntimeOptions: () => null,
setRuntimeOption: () => ({ ok: false }),
cycleRuntimeOption: () => ({ ok: false }),
});
deps.setInvisibleIgnoreMouseEvents(true, { forward: true });
deps.toggleDevTools();
deps.setMecabEnabled(true);
assert.deepEqual(deps.getMecabStatus(), {
available: false,
enabled: false,
path: null,
});
assert.equal(deps.getCurrentSecondarySub(), "");
});

View File

@@ -0,0 +1,100 @@
import { IpcServiceDeps } from "./ipc-service";
interface WindowLike {
isDestroyed: () => boolean;
setIgnoreMouseEvents: (
ignore: boolean,
options?: { forward?: boolean },
) => void;
webContents: {
toggleDevTools: () => void;
};
}
interface MecabTokenizerLike {
getStatus: () => { available: boolean; enabled: boolean; path: string | null };
setEnabled: (enabled: boolean) => void;
}
interface MpvClientLike {
currentSecondarySubText?: string;
}
export interface IpcDepsRuntimeOptions {
getInvisibleWindow: () => WindowLike | null;
getMainWindow: () => WindowLike | null;
getVisibleOverlayVisibility: () => boolean;
getInvisibleOverlayVisibility: () => boolean;
onOverlayModalClosed: (modal: string) => void;
openYomitanSettings: () => void;
quitApp: () => void;
toggleVisibleOverlay: () => void;
tokenizeCurrentSubtitle: () => Promise<unknown>;
getCurrentSubtitleAss: () => string;
getMpvSubtitleRenderMetrics: () => unknown;
getSubtitlePosition: () => unknown;
getSubtitleStyle: () => unknown;
saveSubtitlePosition: (position: unknown) => void;
getMecabTokenizer: () => MecabTokenizerLike | null;
handleMpvCommand: (command: Array<string | number>) => void;
getKeybindings: () => unknown;
getSecondarySubMode: () => unknown;
getMpvClient: () => MpvClientLike | null;
runSubsyncManual: (request: unknown) => Promise<unknown>;
getAnkiConnectStatus: () => boolean;
getRuntimeOptions: () => unknown;
setRuntimeOption: (id: string, value: unknown) => unknown;
cycleRuntimeOption: (id: string, direction: 1 | -1) => unknown;
}
export function createIpcDepsRuntimeService(
options: IpcDepsRuntimeOptions,
): IpcServiceDeps {
return {
getInvisibleWindow: () => options.getInvisibleWindow() as never,
isVisibleOverlayVisible: options.getVisibleOverlayVisibility,
setInvisibleIgnoreMouseEvents: (ignore, eventsOptions) => {
const invisibleWindow = options.getInvisibleWindow();
if (!invisibleWindow || invisibleWindow.isDestroyed()) return;
invisibleWindow.setIgnoreMouseEvents(ignore, eventsOptions);
},
onOverlayModalClosed: options.onOverlayModalClosed,
openYomitanSettings: options.openYomitanSettings,
quitApp: options.quitApp,
toggleDevTools: () => {
const mainWindow = options.getMainWindow();
if (!mainWindow || mainWindow.isDestroyed()) return;
mainWindow.webContents.toggleDevTools();
},
getVisibleOverlayVisibility: options.getVisibleOverlayVisibility,
toggleVisibleOverlay: options.toggleVisibleOverlay,
getInvisibleOverlayVisibility: options.getInvisibleOverlayVisibility,
tokenizeCurrentSubtitle: options.tokenizeCurrentSubtitle,
getCurrentSubtitleAss: options.getCurrentSubtitleAss,
getMpvSubtitleRenderMetrics: options.getMpvSubtitleRenderMetrics,
getSubtitlePosition: options.getSubtitlePosition,
getSubtitleStyle: options.getSubtitleStyle,
saveSubtitlePosition: options.saveSubtitlePosition,
getMecabStatus: () => {
const mecabTokenizer = options.getMecabTokenizer();
return mecabTokenizer
? mecabTokenizer.getStatus()
: { available: false, enabled: false, path: null };
},
setMecabEnabled: (enabled) => {
const mecabTokenizer = options.getMecabTokenizer();
if (!mecabTokenizer) return;
mecabTokenizer.setEnabled(enabled);
},
handleMpvCommand: options.handleMpvCommand,
getKeybindings: options.getKeybindings,
getSecondarySubMode: options.getSecondarySubMode,
getCurrentSecondarySub: () =>
options.getMpvClient()?.currentSecondarySubText || "",
runSubsyncManual: options.runSubsyncManual,
getAnkiConnectStatus: options.getAnkiConnectStatus,
getRuntimeOptions: options.getRuntimeOptions,
setRuntimeOption: options.setRuntimeOption,
cycleRuntimeOption: options.cycleRuntimeOption,
};
}