mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 12:11:28 -07:00
feat(core): add Electron runtime, services, and app composition
This commit is contained in:
201
src/core/services/mpv-protocol.test.ts
Normal file
201
src/core/services/mpv-protocol.test.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
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: () => {},
|
||||
emitTimePosChange: () => {},
|
||||
emitPauseChange: () => {},
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user