mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
202 lines
6.2 KiB
TypeScript
202 lines
6.2 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import type { MpvSubtitleRenderMetrics } from "../../types";
|
|
import {
|
|
dispatchMpvProtocolMessage,
|
|
MPV_REQUEST_ID_TRACK_LIST_SECONDARY,
|
|
MpvProtocolHandleMessageDeps,
|
|
splitMpvMessagesFromBuffer,
|
|
parseVisibilityProperty,
|
|
asBoolean,
|
|
} from "./mpv-protocol";
|
|
|
|
function createDeps(overrides: Partial<MpvProtocolHandleMessageDeps> = {}): {
|
|
deps: MpvProtocolHandleMessageDeps;
|
|
state: {
|
|
subText: string;
|
|
secondarySubText: string;
|
|
events: Array<unknown>;
|
|
commands: unknown[];
|
|
mediaPath: string;
|
|
restored: number;
|
|
};
|
|
} {
|
|
const state = {
|
|
subText: "",
|
|
secondarySubText: "",
|
|
events: [] as Array<unknown>,
|
|
commands: [] as unknown[],
|
|
mediaPath: "",
|
|
restored: 0,
|
|
};
|
|
const metrics: MpvSubtitleRenderMetrics = {
|
|
subPos: 100,
|
|
subFontSize: 36,
|
|
subScale: 1,
|
|
subMarginY: 0,
|
|
subMarginX: 0,
|
|
subFont: "",
|
|
subSpacing: 0,
|
|
subBold: false,
|
|
subItalic: false,
|
|
subBorderSize: 0,
|
|
subShadowOffset: 0,
|
|
subAssOverride: "yes",
|
|
subScaleByWindow: true,
|
|
subUseMargins: true,
|
|
osdHeight: 0,
|
|
osdDimensions: null,
|
|
};
|
|
|
|
return {
|
|
state,
|
|
deps: {
|
|
getResolvedConfig: () => ({ secondarySub: { secondarySubLanguages: ["ja"] } }),
|
|
getSubtitleMetrics: () => metrics,
|
|
isVisibleOverlayVisible: () => false,
|
|
emitSubtitleChange: (payload) => state.events.push(payload),
|
|
emitSubtitleAssChange: (payload) => state.events.push(payload),
|
|
emitSubtitleTiming: (payload) => state.events.push(payload),
|
|
emitSecondarySubtitleChange: (payload) => state.events.push(payload),
|
|
getCurrentSubText: () => state.subText,
|
|
setCurrentSubText: (text) => {
|
|
state.subText = text;
|
|
},
|
|
setCurrentSubStart: () => {},
|
|
getCurrentSubStart: () => 0,
|
|
setCurrentSubEnd: () => {},
|
|
getCurrentSubEnd: () => 0,
|
|
emitMediaPathChange: (payload) => {
|
|
state.mediaPath = payload.path;
|
|
},
|
|
emitMediaTitleChange: (payload) => state.events.push(payload),
|
|
emitSubtitleMetricsChange: (payload) => state.events.push(payload),
|
|
setCurrentSecondarySubText: (text) => {
|
|
state.secondarySubText = text;
|
|
},
|
|
resolvePendingRequest: () => false,
|
|
setSecondarySubVisibility: () => {},
|
|
syncCurrentAudioStreamIndex: () => {},
|
|
setCurrentAudioTrackId: () => {},
|
|
setCurrentTimePos: () => {},
|
|
getCurrentTimePos: () => 0,
|
|
getPendingPauseAtSubEnd: () => false,
|
|
setPendingPauseAtSubEnd: () => {},
|
|
getPauseAtTime: () => null,
|
|
setPauseAtTime: () => {},
|
|
autoLoadSecondarySubTrack: () => {},
|
|
setCurrentVideoPath: () => {},
|
|
emitSecondarySubtitleVisibility: (payload) => state.events.push(payload),
|
|
setCurrentAudioStreamIndex: () => {},
|
|
sendCommand: (payload) => {
|
|
state.commands.push(payload);
|
|
return true;
|
|
},
|
|
restorePreviousSecondarySubVisibility: () => {
|
|
state.restored += 1;
|
|
},
|
|
setPreviousSecondarySubVisibility: () => {
|
|
// intentionally not tracked in this unit test
|
|
},
|
|
...overrides,
|
|
},
|
|
};
|
|
}
|
|
|
|
test("dispatchMpvProtocolMessage emits subtitle text on property change", async () => {
|
|
const { deps, state } = createDeps();
|
|
|
|
await dispatchMpvProtocolMessage(
|
|
{ event: "property-change", name: "sub-text", data: "字幕" },
|
|
deps,
|
|
);
|
|
|
|
assert.equal(state.subText, "字幕");
|
|
assert.deepEqual(state.events, [{ text: "字幕", isOverlayVisible: false }]);
|
|
});
|
|
|
|
test("dispatchMpvProtocolMessage sets secondary subtitle track based on track list response", async () => {
|
|
const { deps, state } = createDeps();
|
|
|
|
await dispatchMpvProtocolMessage(
|
|
{
|
|
request_id: MPV_REQUEST_ID_TRACK_LIST_SECONDARY,
|
|
data: [
|
|
{ type: "audio", id: 1, lang: "eng" },
|
|
{ type: "sub", id: 2, lang: "ja" },
|
|
],
|
|
},
|
|
deps,
|
|
);
|
|
|
|
assert.deepEqual(state.commands, [{ command: ["set_property", "secondary-sid", 2] }]);
|
|
});
|
|
|
|
test("dispatchMpvProtocolMessage restores secondary visibility on shutdown", async () => {
|
|
const { deps, state } = createDeps();
|
|
|
|
await dispatchMpvProtocolMessage({ event: "shutdown" }, deps);
|
|
|
|
assert.equal(state.restored, 1);
|
|
});
|
|
|
|
test("dispatchMpvProtocolMessage pauses on sub-end when pendingPauseAtSubEnd is set", async () => {
|
|
let pendingPauseAtSubEnd = true;
|
|
let pauseAtTime: number | null = null;
|
|
const { deps, state } = createDeps({
|
|
getPendingPauseAtSubEnd: () => pendingPauseAtSubEnd,
|
|
setPendingPauseAtSubEnd: (next) => {
|
|
pendingPauseAtSubEnd = next;
|
|
},
|
|
getCurrentSubText: () => "字幕",
|
|
setCurrentSubEnd: () => {},
|
|
getCurrentSubEnd: () => 0,
|
|
setPauseAtTime: (next) => {
|
|
pauseAtTime = next;
|
|
},
|
|
});
|
|
|
|
await dispatchMpvProtocolMessage(
|
|
{ event: "property-change", name: "sub-end", data: 42 },
|
|
deps,
|
|
);
|
|
|
|
assert.equal(pendingPauseAtSubEnd, false);
|
|
assert.equal(pauseAtTime, 42);
|
|
assert.deepEqual(state.events, [{ text: "字幕", start: 0, end: 0 }]);
|
|
assert.deepEqual(
|
|
state.commands[state.commands.length - 1],
|
|
{ command: ["set_property", "pause", false] },
|
|
);
|
|
});
|
|
|
|
test("splitMpvMessagesFromBuffer parses complete lines and preserves partial buffer", () => {
|
|
const parsed = splitMpvMessagesFromBuffer(
|
|
'{"event":"shutdown"}\n{"event":"property-change","name":"media-title","data":"x"}\n{"partial"',
|
|
);
|
|
|
|
assert.equal(parsed.messages.length, 2);
|
|
assert.equal(parsed.nextBuffer, "{\"partial\"");
|
|
assert.equal(parsed.messages[0].event, "shutdown");
|
|
assert.equal(parsed.messages[1].name, "media-title");
|
|
});
|
|
|
|
test("splitMpvMessagesFromBuffer reports invalid JSON lines", () => {
|
|
const errors: Array<{ line: string; error?: string }> = [];
|
|
|
|
splitMpvMessagesFromBuffer('{"event":"x"}\n{invalid}\n', undefined, (line, error) => {
|
|
errors.push({ line, error: String(error) });
|
|
});
|
|
|
|
assert.equal(errors.length, 1);
|
|
assert.equal(errors[0].line, "{invalid}");
|
|
});
|
|
|
|
test("visibility and boolean parsers handle text values", () => {
|
|
assert.equal(parseVisibilityProperty("true"), true);
|
|
assert.equal(parseVisibilityProperty("0"), false);
|
|
assert.equal(parseVisibilityProperty("unknown"), null);
|
|
assert.equal(asBoolean("yes", false), true);
|
|
assert.equal(asBoolean("0", true), false);
|
|
});
|