mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 18:22:42 -08:00
424 lines
11 KiB
TypeScript
424 lines
11 KiB
TypeScript
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 { ConfigService } from "./service";
|
|
import { DEFAULT_CONFIG, RUNTIME_OPTION_REGISTRY } from "./definitions";
|
|
import { generateConfigTemplate } from "./template";
|
|
|
|
function makeTempDir(): string {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), "subminer-config-test-"));
|
|
}
|
|
|
|
test("loads defaults when config is missing", () => {
|
|
const dir = makeTempDir();
|
|
const service = new ConfigService(dir);
|
|
const config = service.getConfig();
|
|
assert.equal(config.websocket.port, DEFAULT_CONFIG.websocket.port);
|
|
assert.equal(config.ankiConnect.behavior.autoUpdateNewCards, true);
|
|
});
|
|
|
|
test("parses jsonc and warns/falls back on invalid value", () => {
|
|
const dir = makeTempDir();
|
|
fs.writeFileSync(
|
|
path.join(dir, "config.jsonc"),
|
|
`{
|
|
// invalid websocket port
|
|
"websocket": { "port": "bad" }
|
|
}`,
|
|
"utf-8",
|
|
);
|
|
|
|
const service = new ConfigService(dir);
|
|
const config = service.getConfig();
|
|
assert.equal(config.websocket.port, DEFAULT_CONFIG.websocket.port);
|
|
assert.ok(service.getWarnings().some((w) => w.path === "websocket.port"));
|
|
});
|
|
|
|
test("accepts valid logging.level", () => {
|
|
const dir = makeTempDir();
|
|
fs.writeFileSync(
|
|
path.join(dir, "config.jsonc"),
|
|
`{
|
|
"logging": {
|
|
"level": "warn"
|
|
}
|
|
}`,
|
|
"utf-8",
|
|
);
|
|
|
|
const service = new ConfigService(dir);
|
|
const config = service.getConfig();
|
|
|
|
assert.equal(config.logging.level, "warn");
|
|
});
|
|
|
|
test("falls back for invalid logging.level and reports warning", () => {
|
|
const dir = makeTempDir();
|
|
fs.writeFileSync(
|
|
path.join(dir, "config.jsonc"),
|
|
`{
|
|
"logging": {
|
|
"level": "trace"
|
|
}
|
|
}`,
|
|
"utf-8",
|
|
);
|
|
|
|
const service = new ConfigService(dir);
|
|
const config = service.getConfig();
|
|
const warnings = service.getWarnings();
|
|
|
|
assert.equal(config.logging.level, DEFAULT_CONFIG.logging.level);
|
|
assert.ok(
|
|
warnings.some((warning) => warning.path === "logging.level"),
|
|
);
|
|
});
|
|
|
|
test("parses invisible overlay config and new global shortcuts", () => {
|
|
const dir = makeTempDir();
|
|
fs.writeFileSync(
|
|
path.join(dir, "config.jsonc"),
|
|
`{
|
|
"shortcuts": {
|
|
"toggleVisibleOverlayGlobal": "Alt+Shift+U",
|
|
"toggleInvisibleOverlayGlobal": "Alt+Shift+I",
|
|
"openJimaku": "Ctrl+Alt+J"
|
|
},
|
|
"invisibleOverlay": {
|
|
"startupVisibility": "hidden"
|
|
},
|
|
"bind_visible_overlay_to_mpv_sub_visibility": false,
|
|
"youtubeSubgen": {
|
|
"primarySubLanguages": ["ja", "jpn", "jp"]
|
|
}
|
|
}`,
|
|
"utf-8",
|
|
);
|
|
|
|
const service = new ConfigService(dir);
|
|
const config = service.getConfig();
|
|
assert.equal(config.shortcuts.toggleVisibleOverlayGlobal, "Alt+Shift+U");
|
|
assert.equal(config.shortcuts.toggleInvisibleOverlayGlobal, "Alt+Shift+I");
|
|
assert.equal(config.shortcuts.openJimaku, "Ctrl+Alt+J");
|
|
assert.equal(config.invisibleOverlay.startupVisibility, "hidden");
|
|
assert.equal(config.bind_visible_overlay_to_mpv_sub_visibility, false);
|
|
assert.deepEqual(config.youtubeSubgen.primarySubLanguages, ["ja", "jpn", "jp"]);
|
|
});
|
|
|
|
test("runtime options registry is centralized", () => {
|
|
const ids = RUNTIME_OPTION_REGISTRY.map((entry) => entry.id);
|
|
assert.deepEqual(ids, [
|
|
"anki.autoUpdateNewCards",
|
|
"anki.nPlusOneMatchMode",
|
|
"anki.kikuFieldGrouping",
|
|
]);
|
|
});
|
|
|
|
test("validates ankiConnect n+1 behavior values", () => {
|
|
const dir = makeTempDir();
|
|
fs.writeFileSync(
|
|
path.join(dir, "config.jsonc"),
|
|
`{
|
|
"ankiConnect": {
|
|
"nPlusOne": {
|
|
"highlightEnabled": "yes",
|
|
"refreshMinutes": -5
|
|
}
|
|
}
|
|
}`,
|
|
"utf-8",
|
|
);
|
|
|
|
const service = new ConfigService(dir);
|
|
const config = service.getConfig();
|
|
const warnings = service.getWarnings();
|
|
|
|
assert.equal(
|
|
config.ankiConnect.nPlusOne.highlightEnabled,
|
|
DEFAULT_CONFIG.ankiConnect.nPlusOne.highlightEnabled,
|
|
);
|
|
assert.equal(
|
|
config.ankiConnect.nPlusOne.refreshMinutes,
|
|
DEFAULT_CONFIG.ankiConnect.nPlusOne.refreshMinutes,
|
|
);
|
|
assert.ok(
|
|
warnings.some(
|
|
(warning) => warning.path === "ankiConnect.nPlusOne.highlightEnabled",
|
|
),
|
|
);
|
|
assert.ok(
|
|
warnings.some(
|
|
(warning) => warning.path === "ankiConnect.nPlusOne.refreshMinutes",
|
|
),
|
|
);
|
|
});
|
|
|
|
test("accepts valid ankiConnect n+1 behavior values", () => {
|
|
const dir = makeTempDir();
|
|
fs.writeFileSync(
|
|
path.join(dir, "config.jsonc"),
|
|
`{
|
|
"ankiConnect": {
|
|
"nPlusOne": {
|
|
"highlightEnabled": true,
|
|
"refreshMinutes": 120
|
|
}
|
|
}
|
|
}`,
|
|
"utf-8",
|
|
);
|
|
|
|
const service = new ConfigService(dir);
|
|
const config = service.getConfig();
|
|
|
|
assert.equal(config.ankiConnect.nPlusOne.highlightEnabled, true);
|
|
assert.equal(config.ankiConnect.nPlusOne.refreshMinutes, 120);
|
|
});
|
|
|
|
test("validates ankiConnect n+1 minimum sentence word count", () => {
|
|
const dir = makeTempDir();
|
|
fs.writeFileSync(
|
|
path.join(dir, "config.jsonc"),
|
|
`{
|
|
"ankiConnect": {
|
|
"nPlusOne": {
|
|
"minSentenceWords": 0
|
|
}
|
|
}
|
|
}`,
|
|
"utf-8",
|
|
);
|
|
|
|
const service = new ConfigService(dir);
|
|
const config = service.getConfig();
|
|
const warnings = service.getWarnings();
|
|
|
|
assert.equal(
|
|
config.ankiConnect.nPlusOne.minSentenceWords,
|
|
DEFAULT_CONFIG.ankiConnect.nPlusOne.minSentenceWords,
|
|
);
|
|
assert.ok(
|
|
warnings.some(
|
|
(warning) => warning.path === "ankiConnect.nPlusOne.minSentenceWords",
|
|
),
|
|
);
|
|
});
|
|
|
|
test("accepts valid ankiConnect n+1 minimum sentence word count", () => {
|
|
const dir = makeTempDir();
|
|
fs.writeFileSync(
|
|
path.join(dir, "config.jsonc"),
|
|
`{
|
|
"ankiConnect": {
|
|
"nPlusOne": {
|
|
"minSentenceWords": 4
|
|
}
|
|
}
|
|
}`,
|
|
"utf-8",
|
|
);
|
|
|
|
const service = new ConfigService(dir);
|
|
const config = service.getConfig();
|
|
|
|
assert.equal(config.ankiConnect.nPlusOne.minSentenceWords, 4);
|
|
});
|
|
|
|
test("validates ankiConnect n+1 match mode values", () => {
|
|
const dir = makeTempDir();
|
|
fs.writeFileSync(
|
|
path.join(dir, "config.jsonc"),
|
|
`{
|
|
"ankiConnect": {
|
|
"nPlusOne": {
|
|
"matchMode": "bad-mode"
|
|
}
|
|
}
|
|
}`,
|
|
"utf-8",
|
|
);
|
|
|
|
const service = new ConfigService(dir);
|
|
const config = service.getConfig();
|
|
const warnings = service.getWarnings();
|
|
|
|
assert.equal(
|
|
config.ankiConnect.nPlusOne.matchMode,
|
|
DEFAULT_CONFIG.ankiConnect.nPlusOne.matchMode,
|
|
);
|
|
assert.ok(
|
|
warnings.some((warning) =>
|
|
warning.path === "ankiConnect.nPlusOne.matchMode",
|
|
),
|
|
);
|
|
});
|
|
|
|
test("accepts valid ankiConnect n+1 match mode values", () => {
|
|
const dir = makeTempDir();
|
|
fs.writeFileSync(
|
|
path.join(dir, "config.jsonc"),
|
|
`{
|
|
"ankiConnect": {
|
|
"nPlusOne": {
|
|
"matchMode": "surface"
|
|
}
|
|
}
|
|
}`,
|
|
"utf-8",
|
|
);
|
|
|
|
const service = new ConfigService(dir);
|
|
const config = service.getConfig();
|
|
|
|
assert.equal(config.ankiConnect.nPlusOne.matchMode, "surface");
|
|
});
|
|
|
|
test("validates ankiConnect n+1 color values", () => {
|
|
const dir = makeTempDir();
|
|
fs.writeFileSync(
|
|
path.join(dir, "config.jsonc"),
|
|
`{
|
|
"ankiConnect": {
|
|
"nPlusOne": {
|
|
"nPlusOne": "not-a-color",
|
|
"knownWord": 123
|
|
}
|
|
}
|
|
}`,
|
|
"utf-8",
|
|
);
|
|
|
|
const service = new ConfigService(dir);
|
|
const config = service.getConfig();
|
|
const warnings = service.getWarnings();
|
|
|
|
assert.equal(
|
|
config.ankiConnect.nPlusOne.nPlusOne,
|
|
DEFAULT_CONFIG.ankiConnect.nPlusOne.nPlusOne,
|
|
);
|
|
assert.equal(
|
|
config.ankiConnect.nPlusOne.knownWord,
|
|
DEFAULT_CONFIG.ankiConnect.nPlusOne.knownWord,
|
|
);
|
|
assert.ok(
|
|
warnings.some((warning) => warning.path === "ankiConnect.nPlusOne.nPlusOne"),
|
|
);
|
|
assert.ok(
|
|
warnings.some((warning) => warning.path === "ankiConnect.nPlusOne.knownWord"),
|
|
);
|
|
});
|
|
|
|
test("accepts valid ankiConnect n+1 color values", () => {
|
|
const dir = makeTempDir();
|
|
fs.writeFileSync(
|
|
path.join(dir, "config.jsonc"),
|
|
`{
|
|
"ankiConnect": {
|
|
"nPlusOne": {
|
|
"nPlusOne": "#c6a0f6",
|
|
"knownWord": "#a6da95"
|
|
}
|
|
}
|
|
}`,
|
|
"utf-8",
|
|
);
|
|
|
|
const service = new ConfigService(dir);
|
|
const config = service.getConfig();
|
|
|
|
assert.equal(config.ankiConnect.nPlusOne.nPlusOne, "#c6a0f6");
|
|
assert.equal(config.ankiConnect.nPlusOne.knownWord, "#a6da95");
|
|
});
|
|
|
|
test("supports legacy ankiConnect.behavior N+1 settings as fallback", () => {
|
|
const dir = makeTempDir();
|
|
fs.writeFileSync(
|
|
path.join(dir, "config.jsonc"),
|
|
`{
|
|
"ankiConnect": {
|
|
"behavior": {
|
|
"nPlusOneHighlightEnabled": true,
|
|
"nPlusOneRefreshMinutes": 90,
|
|
"nPlusOneMatchMode": "surface"
|
|
}
|
|
}
|
|
}`,
|
|
"utf-8",
|
|
);
|
|
|
|
const service = new ConfigService(dir);
|
|
const config = service.getConfig();
|
|
const warnings = service.getWarnings();
|
|
|
|
assert.equal(config.ankiConnect.nPlusOne.highlightEnabled, true);
|
|
assert.equal(config.ankiConnect.nPlusOne.refreshMinutes, 90);
|
|
assert.equal(config.ankiConnect.nPlusOne.matchMode, "surface");
|
|
assert.ok(
|
|
warnings.some(
|
|
(warning) =>
|
|
warning.path === "ankiConnect.behavior.nPlusOneHighlightEnabled" ||
|
|
warning.path === "ankiConnect.behavior.nPlusOneRefreshMinutes" ||
|
|
warning.path === "ankiConnect.behavior.nPlusOneMatchMode",
|
|
),
|
|
);
|
|
});
|
|
|
|
test("accepts valid ankiConnect n+1 deck list", () => {
|
|
const dir = makeTempDir();
|
|
fs.writeFileSync(
|
|
path.join(dir, "config.jsonc"),
|
|
`{
|
|
"ankiConnect": {
|
|
"nPlusOne": {
|
|
"decks": ["Deck One", "Deck Two"]
|
|
}
|
|
}
|
|
}`,
|
|
"utf-8",
|
|
);
|
|
|
|
const service = new ConfigService(dir);
|
|
const config = service.getConfig();
|
|
|
|
assert.deepEqual(config.ankiConnect.nPlusOne.decks, ["Deck One", "Deck Two"]);
|
|
});
|
|
|
|
test("falls back to default when ankiConnect n+1 deck list is invalid", () => {
|
|
const dir = makeTempDir();
|
|
fs.writeFileSync(
|
|
path.join(dir, "config.jsonc"),
|
|
`{
|
|
"ankiConnect": {
|
|
"nPlusOne": {
|
|
"decks": "not-an-array"
|
|
}
|
|
}
|
|
}`,
|
|
"utf-8",
|
|
);
|
|
|
|
const service = new ConfigService(dir);
|
|
const config = service.getConfig();
|
|
const warnings = service.getWarnings();
|
|
|
|
assert.deepEqual(config.ankiConnect.nPlusOne.decks, []);
|
|
assert.ok(
|
|
warnings.some((warning) => warning.path === "ankiConnect.nPlusOne.decks"),
|
|
);
|
|
});
|
|
|
|
test("template generator includes known keys", () => {
|
|
const output = generateConfigTemplate(DEFAULT_CONFIG);
|
|
assert.match(output, /"ankiConnect":/);
|
|
assert.match(output, /"logging":/);
|
|
assert.match(output, /"websocket":/);
|
|
assert.match(output, /"youtubeSubgen":/);
|
|
assert.match(output, /"nPlusOne"\s*:\s*\{/);
|
|
assert.match(output, /"nPlusOne": "#c6a0f6"/);
|
|
assert.match(output, /"knownWord": "#a6da95"/);
|
|
assert.match(output, /"minSentenceWords": 3/);
|
|
assert.match(output, /auto-generated from src\/config\/definitions.ts/);
|
|
});
|