mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-11-13 16:46:08 -08:00
test: cut low-value tests, rewrite the rest around behavior, parallelize lanes (#285)
This commit is contained in:
1 parent
4b6842d034
commit
b48083ddd8
368 files changed
+18045
-60331
No files matched your search
@@ -6,6 +6,7 @@ import {
|
||||
createHandleJellyfinRemotePlaystate,
|
||||
getConfiguredJellyfinSession,
|
||||
type ActiveJellyfinRemotePlaybackState,
|
||||
type JellyfinRemotePlayHandlerDeps,
|
||||
} from './jellyfin-remote-commands';
|
||||
|
||||
test('getConfiguredJellyfinSession returns null for incomplete config', () => {
|
||||
@@ -20,134 +21,17 @@ test('getConfiguredJellyfinSession returns null for incomplete config', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('createHandleJellyfinRemotePlay forwards parsed payload to play runtime', async () => {
|
||||
const calls: Array<{
|
||||
itemId: string;
|
||||
audio?: number;
|
||||
subtitle?: number;
|
||||
start?: number;
|
||||
fallback?: boolean;
|
||||
}> = [];
|
||||
const handlePlay = createHandleJellyfinRemotePlay({
|
||||
getConfiguredSession: () => ({
|
||||
serverUrl: 'https://jellyfin.local',
|
||||
accessToken: 'token',
|
||||
userId: 'user',
|
||||
username: 'name',
|
||||
}),
|
||||
getClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'abc' }),
|
||||
getJellyfinConfig: () => ({ enabled: true }),
|
||||
playJellyfinItem: async (params) => {
|
||||
calls.push({
|
||||
itemId: params.itemId,
|
||||
audio: params.audioStreamIndex,
|
||||
subtitle: params.subtitleStreamIndex,
|
||||
start: params.startTimeTicksOverride,
|
||||
fallback: params.fallbackToPlanStartTimeOnZeroOverride,
|
||||
});
|
||||
},
|
||||
logWarn: () => {},
|
||||
});
|
||||
|
||||
await handlePlay({
|
||||
ItemIds: ['item-1'],
|
||||
AudioStreamIndex: 3,
|
||||
SubtitleStreamIndex: 7,
|
||||
StartPositionTicks: 1000,
|
||||
});
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
{ itemId: 'item-1', audio: 3, subtitle: 7, start: 1000, fallback: true },
|
||||
]);
|
||||
});
|
||||
|
||||
test('createHandleJellyfinRemotePlay parses string StartPositionTicks', async () => {
|
||||
const calls: Array<{ itemId: string; start?: number; fallback?: boolean }> = [];
|
||||
const handlePlay = createHandleJellyfinRemotePlay({
|
||||
getConfiguredSession: () => ({
|
||||
serverUrl: 'https://jellyfin.local',
|
||||
accessToken: 'token',
|
||||
userId: 'user',
|
||||
username: 'name',
|
||||
}),
|
||||
getClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'abc' }),
|
||||
getJellyfinConfig: () => ({ enabled: true }),
|
||||
playJellyfinItem: async (params) => {
|
||||
calls.push({
|
||||
itemId: params.itemId,
|
||||
start: params.startTimeTicksOverride,
|
||||
fallback: params.fallbackToPlanStartTimeOnZeroOverride,
|
||||
});
|
||||
},
|
||||
logWarn: () => {},
|
||||
});
|
||||
|
||||
await handlePlay({
|
||||
ItemIds: ['item-2'],
|
||||
StartPositionTicks: '12345',
|
||||
});
|
||||
|
||||
assert.deepEqual(calls, [{ itemId: 'item-2', start: 12345, fallback: true }]);
|
||||
});
|
||||
|
||||
test('createHandleJellyfinRemotePlay starts from beginning when StartPositionTicks is omitted', async () => {
|
||||
const calls: Array<{ itemId: string; start?: number; fallback?: boolean }> = [];
|
||||
const handlePlay = createHandleJellyfinRemotePlay({
|
||||
getConfiguredSession: () => ({
|
||||
serverUrl: 'https://jellyfin.local',
|
||||
accessToken: 'token',
|
||||
userId: 'user',
|
||||
username: 'name',
|
||||
}),
|
||||
getClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'abc' }),
|
||||
getJellyfinConfig: () => ({ enabled: true }),
|
||||
playJellyfinItem: async (params) => {
|
||||
calls.push({
|
||||
itemId: params.itemId,
|
||||
start: params.startTimeTicksOverride,
|
||||
fallback: params.fallbackToPlanStartTimeOnZeroOverride,
|
||||
});
|
||||
},
|
||||
logWarn: () => {},
|
||||
});
|
||||
|
||||
await handlePlay({
|
||||
ItemIds: ['item-3'],
|
||||
});
|
||||
|
||||
assert.deepEqual(calls, [{ itemId: 'item-3', start: 0, fallback: false }]);
|
||||
});
|
||||
|
||||
test('createHandleJellyfinRemotePlay lets explicit zero fall back to Jellyfin item progress', async () => {
|
||||
const calls: Array<{ itemId: string; start?: number; fallback?: boolean }> = [];
|
||||
const handlePlay = createHandleJellyfinRemotePlay({
|
||||
getConfiguredSession: () => ({
|
||||
serverUrl: 'https://jellyfin.local',
|
||||
accessToken: 'token',
|
||||
userId: 'user',
|
||||
username: 'name',
|
||||
}),
|
||||
getClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'abc' }),
|
||||
getJellyfinConfig: () => ({ enabled: true }),
|
||||
playJellyfinItem: async (params) => {
|
||||
calls.push({
|
||||
itemId: params.itemId,
|
||||
start: params.startTimeTicksOverride,
|
||||
fallback: params.fallbackToPlanStartTimeOnZeroOverride,
|
||||
});
|
||||
},
|
||||
logWarn: () => {},
|
||||
});
|
||||
|
||||
await handlePlay({
|
||||
ItemIds: ['item-4'],
|
||||
StartPositionTicks: 0,
|
||||
});
|
||||
|
||||
assert.deepEqual(calls, [{ itemId: 'item-4', start: 0, fallback: true }]);
|
||||
});
|
||||
|
||||
test('createHandleJellyfinRemotePlay logs and skips payload without item id', async () => {
|
||||
function makePlayHandler(overrides: Partial<JellyfinRemotePlayHandlerDeps> = {}) {
|
||||
const played: Array<
|
||||
Pick<
|
||||
Parameters<JellyfinRemotePlayHandlerDeps['playJellyfinItem']>[0],
|
||||
| 'itemId'
|
||||
| 'audioStreamIndex'
|
||||
| 'subtitleStreamIndex'
|
||||
| 'startTimeTicksOverride'
|
||||
| 'fallbackToPlanStartTimeOnZeroOverride'
|
||||
>
|
||||
> = [];
|
||||
const warnings: string[] = [];
|
||||
const handlePlay = createHandleJellyfinRemotePlay({
|
||||
getConfiguredSession: () => ({
|
||||
@@ -158,40 +42,92 @@ test('createHandleJellyfinRemotePlay logs and skips payload without item id', as
|
||||
}),
|
||||
getClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'abc' }),
|
||||
getJellyfinConfig: () => ({}),
|
||||
playJellyfinItem: async () => {
|
||||
throw new Error('should not be called');
|
||||
playJellyfinItem: async (params) => {
|
||||
played.push({
|
||||
itemId: params.itemId,
|
||||
audioStreamIndex: params.audioStreamIndex,
|
||||
subtitleStreamIndex: params.subtitleStreamIndex,
|
||||
startTimeTicksOverride: params.startTimeTicksOverride,
|
||||
fallbackToPlanStartTimeOnZeroOverride: params.fallbackToPlanStartTimeOnZeroOverride,
|
||||
});
|
||||
},
|
||||
logWarn: (message) => warnings.push(message),
|
||||
...overrides,
|
||||
});
|
||||
return { handlePlay, played, warnings };
|
||||
}
|
||||
|
||||
const PLAY_PAYLOAD_CASES: Array<{
|
||||
name: string;
|
||||
payload: Record<string, unknown>;
|
||||
expected: {
|
||||
audioStreamIndex?: number;
|
||||
subtitleStreamIndex?: number;
|
||||
startTimeTicksOverride: number;
|
||||
fallbackToPlanStartTimeOnZeroOverride: boolean;
|
||||
};
|
||||
}> = [
|
||||
{
|
||||
name: 'forwards audio, subtitle and numeric StartPositionTicks',
|
||||
payload: { AudioStreamIndex: 3, SubtitleStreamIndex: 7, StartPositionTicks: 1000 },
|
||||
expected: {
|
||||
audioStreamIndex: 3,
|
||||
subtitleStreamIndex: 7,
|
||||
startTimeTicksOverride: 1000,
|
||||
fallbackToPlanStartTimeOnZeroOverride: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'parses string StartPositionTicks',
|
||||
payload: { StartPositionTicks: '12345' },
|
||||
expected: { startTimeTicksOverride: 12345, fallbackToPlanStartTimeOnZeroOverride: true },
|
||||
},
|
||||
{
|
||||
name: 'starts from beginning when StartPositionTicks is omitted',
|
||||
payload: {},
|
||||
expected: { startTimeTicksOverride: 0, fallbackToPlanStartTimeOnZeroOverride: false },
|
||||
},
|
||||
{
|
||||
name: 'lets explicit zero fall back to Jellyfin item progress',
|
||||
payload: { StartPositionTicks: 0 },
|
||||
expected: { startTimeTicksOverride: 0, fallbackToPlanStartTimeOnZeroOverride: true },
|
||||
},
|
||||
];
|
||||
|
||||
for (const c of PLAY_PAYLOAD_CASES) {
|
||||
test(`createHandleJellyfinRemotePlay ${c.name}`, async () => {
|
||||
const { handlePlay, played } = makePlayHandler();
|
||||
|
||||
await handlePlay({ ItemIds: ['item-1'], ...c.payload });
|
||||
|
||||
assert.deepEqual(played, [
|
||||
{
|
||||
itemId: 'item-1',
|
||||
audioStreamIndex: undefined,
|
||||
subtitleStreamIndex: undefined,
|
||||
...c.expected,
|
||||
},
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
test('createHandleJellyfinRemotePlay logs and skips payload without item id', async () => {
|
||||
const { handlePlay, played, warnings } = makePlayHandler();
|
||||
|
||||
await handlePlay({ ItemIds: [] });
|
||||
|
||||
assert.deepEqual(warnings, ['Ignoring Jellyfin remote Play event without ItemIds.']);
|
||||
assert.deepEqual(played, []);
|
||||
});
|
||||
|
||||
test('createHandleJellyfinRemotePlay ignores duplicate play for active item', async () => {
|
||||
let playCalls = 0;
|
||||
const handlePlay = createHandleJellyfinRemotePlay({
|
||||
getConfiguredSession: () => ({
|
||||
serverUrl: 'https://jellyfin.local',
|
||||
accessToken: 'token',
|
||||
userId: 'user',
|
||||
username: 'name',
|
||||
}),
|
||||
getClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'abc' }),
|
||||
getJellyfinConfig: () => ({}),
|
||||
getActivePlayback: () => ({
|
||||
itemId: 'item-1',
|
||||
playMethod: 'DirectPlay',
|
||||
}),
|
||||
playJellyfinItem: async () => {
|
||||
playCalls += 1;
|
||||
},
|
||||
logWarn: () => {},
|
||||
const { handlePlay, played } = makePlayHandler({
|
||||
getActivePlayback: () => ({ itemId: 'item-1', playMethod: 'DirectPlay' }),
|
||||
});
|
||||
|
||||
await handlePlay({ ItemIds: ['item-1'] });
|
||||
|
||||
assert.equal(playCalls, 0);
|
||||
assert.deepEqual(played, []);
|
||||
});
|
||||
|
||||
test('createHandleJellyfinRemotePlaystate dispatches pause/seek/stop flows', async () => {
|
||||
@@ -222,6 +158,49 @@ test('createHandleJellyfinRemotePlaystate dispatches pause/seek/stop flows', asy
|
||||
assert.deepEqual(calls, ['progress:true', 'progress:true', 'stopped']);
|
||||
});
|
||||
|
||||
test('createHandleJellyfinRemotePlaystate maps Unpause and PlayPause and reports progress', async () => {
|
||||
const commands: Array<(string | number)[]> = [];
|
||||
const progress: boolean[] = [];
|
||||
const handlePlaystate = createHandleJellyfinRemotePlaystate({
|
||||
getMpvClient: () => ({}),
|
||||
sendMpvCommand: (_client, command) => commands.push(command),
|
||||
reportJellyfinRemoteProgress: async (force) => {
|
||||
progress.push(force);
|
||||
},
|
||||
reportJellyfinRemoteStopped: async () => {},
|
||||
jellyfinTicksToSeconds: (ticks) => ticks / 10,
|
||||
});
|
||||
|
||||
await handlePlaystate({ Command: 'Unpause' });
|
||||
await handlePlaystate({ Command: 'PlayPause' });
|
||||
|
||||
assert.deepEqual(commands, [
|
||||
['set_property', 'pause', 'no'],
|
||||
['cycle', 'pause'],
|
||||
]);
|
||||
assert.deepEqual(progress, [true, true]);
|
||||
});
|
||||
|
||||
test('createHandleJellyfinRemotePlaystate ignores Seek without a valid tick position', async () => {
|
||||
const commands: Array<(string | number)[]> = [];
|
||||
const progress: boolean[] = [];
|
||||
const handlePlaystate = createHandleJellyfinRemotePlaystate({
|
||||
getMpvClient: () => ({}),
|
||||
sendMpvCommand: (_client, command) => commands.push(command),
|
||||
reportJellyfinRemoteProgress: async (force) => {
|
||||
progress.push(force);
|
||||
},
|
||||
reportJellyfinRemoteStopped: async () => {},
|
||||
jellyfinTicksToSeconds: (ticks) => ticks / 10,
|
||||
});
|
||||
|
||||
await handlePlaystate({ Command: 'Seek', SeekPositionTicks: 'soon' });
|
||||
await handlePlaystate({ Command: 'Seek' });
|
||||
|
||||
assert.deepEqual(commands, []);
|
||||
assert.deepEqual(progress, []);
|
||||
});
|
||||
|
||||
test('createHandleJellyfinRemoteGeneralCommand mutates active playback indices', async () => {
|
||||
const mpvClient = {};
|
||||
const commands: Array<(string | number)[]> = [];
|
||||
|
||||
Reference in new issue
Block a user