test: add behavioral assertions to composer tests

Upgrade 8 composer test files from shape-only typeof checks to behavioral
assertions that invoke returned handlers and verify injected dependencies are
actually called, following the mpv-runtime-composer pattern.
This commit is contained in:
2026-03-28 11:22:58 -07:00
parent 4779ac85dc
commit 99b30c4cf0
8 changed files with 101 additions and 12 deletions

View File

@@ -3,11 +3,14 @@ import assert from 'node:assert/strict';
import { composeAnilistSetupHandlers } from './anilist-setup-composer'; import { composeAnilistSetupHandlers } from './anilist-setup-composer';
test('composeAnilistSetupHandlers returns callable setup handlers', () => { test('composeAnilistSetupHandlers returns callable setup handlers', () => {
const calls: string[] = [];
const composed = composeAnilistSetupHandlers({ const composed = composeAnilistSetupHandlers({
notifyDeps: { notifyDeps: {
hasMpvClient: () => false, hasMpvClient: () => false,
showMpvOsd: () => {}, showMpvOsd: () => {},
showDesktopNotification: () => {}, showDesktopNotification: (title, opts) => {
calls.push(`notify:${opts.body}`);
},
logInfo: () => {}, logInfo: () => {},
}, },
consumeTokenDeps: { consumeTokenDeps: {
@@ -37,4 +40,16 @@ test('composeAnilistSetupHandlers returns callable setup handlers', () => {
assert.equal(typeof composed.consumeAnilistSetupTokenFromUrl, 'function'); assert.equal(typeof composed.consumeAnilistSetupTokenFromUrl, 'function');
assert.equal(typeof composed.handleAnilistSetupProtocolUrl, 'function'); assert.equal(typeof composed.handleAnilistSetupProtocolUrl, 'function');
assert.equal(typeof composed.registerSubminerProtocolClient, 'function'); assert.equal(typeof composed.registerSubminerProtocolClient, 'function');
// notifyAnilistSetup forwards to showDesktopNotification when no MPV client
composed.notifyAnilistSetup('Setup complete');
assert.deepEqual(calls, ['notify:Setup complete']);
// handleAnilistSetupProtocolUrl returns false for non-subminer URLs
const handled = composed.handleAnilistSetupProtocolUrl('https://other.example.com/');
assert.equal(handled, false);
// handleAnilistSetupProtocolUrl returns true for subminer:// URLs
const handledProtocol = composed.handleAnilistSetupProtocolUrl('subminer://anilist-setup?code=abc');
assert.equal(handledProtocol, true);
}); });

View File

@@ -3,9 +3,13 @@ import test from 'node:test';
import { composeAppReadyRuntime } from './app-ready-composer'; import { composeAppReadyRuntime } from './app-ready-composer';
test('composeAppReadyRuntime returns reload/critical/app-ready handlers', () => { test('composeAppReadyRuntime returns reload/critical/app-ready handlers', () => {
const calls: string[] = [];
const composed = composeAppReadyRuntime({ const composed = composeAppReadyRuntime({
reloadConfigMainDeps: { reloadConfigMainDeps: {
reloadConfigStrict: () => ({ ok: true, path: '/tmp/config.jsonc', warnings: [] }), reloadConfigStrict: () => {
calls.push('reloadConfigStrict');
return { ok: true, path: '/tmp/config.jsonc', warnings: [] };
},
logInfo: () => {}, logInfo: () => {},
logWarning: () => {}, logWarning: () => {},
showDesktopNotification: () => {}, showDesktopNotification: () => {},
@@ -79,4 +83,8 @@ test('composeAppReadyRuntime returns reload/critical/app-ready handlers', () =>
assert.equal(typeof composed.reloadConfig, 'function'); assert.equal(typeof composed.reloadConfig, 'function');
assert.equal(typeof composed.criticalConfigError, 'function'); assert.equal(typeof composed.criticalConfigError, 'function');
assert.equal(typeof composed.appReadyRuntimeRunner, 'function'); assert.equal(typeof composed.appReadyRuntimeRunner, 'function');
// reloadConfig invokes the injected reloadConfigStrict dep
composed.reloadConfig();
assert.deepEqual(calls, ['reloadConfigStrict']);
}); });

View File

@@ -1,8 +1,10 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import type { CliArgs } from '../../../cli/args';
import { composeCliStartupHandlers } from './cli-startup-composer'; import { composeCliStartupHandlers } from './cli-startup-composer';
test('composeCliStartupHandlers returns callable CLI startup handlers', () => { test('composeCliStartupHandlers returns callable CLI startup handlers', () => {
const calls: string[] = [];
const handlers = composeCliStartupHandlers({ const handlers = composeCliStartupHandlers({
cliCommandContextMainDeps: { cliCommandContextMainDeps: {
appState: {} as never, appState: {} as never,
@@ -57,7 +59,9 @@ test('composeCliStartupHandlers returns callable CLI startup handlers', () => {
startBackgroundWarmups: () => {}, startBackgroundWarmups: () => {},
logInfo: () => {}, logInfo: () => {},
}, },
handleCliCommandRuntimeServiceWithContext: () => {}, handleCliCommandRuntimeServiceWithContext: (args, _source, _ctx) => {
calls.push(`handleCommand:${(args as { command?: string }).command ?? 'unknown'}`);
},
}, },
initialArgsRuntimeHandlerMainDeps: { initialArgsRuntimeHandlerMainDeps: {
getInitialArgs: () => null, getInitialArgs: () => null,
@@ -80,4 +84,8 @@ test('composeCliStartupHandlers returns callable CLI startup handlers', () => {
assert.equal(typeof handlers.createCliCommandContext, 'function'); assert.equal(typeof handlers.createCliCommandContext, 'function');
assert.equal(typeof handlers.handleCliCommand, 'function'); assert.equal(typeof handlers.handleCliCommand, 'function');
assert.equal(typeof handlers.handleInitialArgs, 'function'); assert.equal(typeof handlers.handleInitialArgs, 'function');
// handleCliCommand routes to the injected handleCliCommandRuntimeServiceWithContext dep
handlers.handleCliCommand({ command: 'start' } as unknown as CliArgs);
assert.deepEqual(calls, ['handleCommand:start']);
}); });

View File

@@ -2,8 +2,11 @@ import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { composeJellyfinRemoteHandlers } from './jellyfin-remote-composer'; import { composeJellyfinRemoteHandlers } from './jellyfin-remote-composer';
test('composeJellyfinRemoteHandlers returns callable jellyfin remote handlers', () => { test('composeJellyfinRemoteHandlers returns callable jellyfin remote handlers', async () => {
let lastProgressAt = 0; let lastProgressAt = 0;
let activePlayback: unknown = { itemId: 'item-1', mediaSourceId: 'src-1', playMethod: 'DirectPlay', audioStreamIndex: null, subtitleStreamIndex: null };
const calls: string[] = [];
const composed = composeJellyfinRemoteHandlers({ const composed = composeJellyfinRemoteHandlers({
getConfiguredSession: () => null, getConfiguredSession: () => null,
getClientInfo: () => getClientInfo: () =>
@@ -14,8 +17,11 @@ test('composeJellyfinRemoteHandlers returns callable jellyfin remote handlers',
getMpvClient: () => null, getMpvClient: () => null,
sendMpvCommand: () => {}, sendMpvCommand: () => {},
jellyfinTicksToSeconds: () => 0, jellyfinTicksToSeconds: () => 0,
getActivePlayback: () => null, getActivePlayback: () => activePlayback as never,
clearActivePlayback: () => {}, clearActivePlayback: () => {
activePlayback = null;
calls.push('clearActivePlayback');
},
getSession: () => null, getSession: () => null,
getNow: () => 0, getNow: () => 0,
getLastProgressAtMs: () => lastProgressAt, getLastProgressAtMs: () => lastProgressAt,
@@ -32,4 +38,9 @@ test('composeJellyfinRemoteHandlers returns callable jellyfin remote handlers',
assert.equal(typeof composed.handleJellyfinRemotePlay, 'function'); assert.equal(typeof composed.handleJellyfinRemotePlay, 'function');
assert.equal(typeof composed.handleJellyfinRemotePlaystate, 'function'); assert.equal(typeof composed.handleJellyfinRemotePlaystate, 'function');
assert.equal(typeof composed.handleJellyfinRemoteGeneralCommand, 'function'); assert.equal(typeof composed.handleJellyfinRemoteGeneralCommand, 'function');
// reportJellyfinRemoteStopped clears active playback when there is no connected session
await composed.reportJellyfinRemoteStopped();
assert.equal(activePlayback, null);
assert.deepEqual(calls, ['clearActivePlayback']);
}); });

View File

@@ -190,4 +190,9 @@ test('composeJellyfinRuntimeHandlers returns callable jellyfin runtime handlers'
assert.equal(typeof composed.stopJellyfinRemoteSession, 'function'); assert.equal(typeof composed.stopJellyfinRemoteSession, 'function');
assert.equal(typeof composed.runJellyfinCommand, 'function'); assert.equal(typeof composed.runJellyfinCommand, 'function');
assert.equal(typeof composed.openJellyfinSetupWindow, 'function'); assert.equal(typeof composed.openJellyfinSetupWindow, 'function');
// getResolvedJellyfinConfig forwards to the injected getResolvedConfig dep
const jellyfinConfig = composed.getResolvedJellyfinConfig();
assert.equal(jellyfinConfig.enabled, false);
assert.equal(jellyfinConfig.serverUrl, '');
}); });

View File

@@ -3,15 +3,20 @@ import test from 'node:test';
import { composeOverlayVisibilityRuntime } from './overlay-visibility-runtime-composer'; import { composeOverlayVisibilityRuntime } from './overlay-visibility-runtime-composer';
test('composeOverlayVisibilityRuntime returns overlay visibility handlers', () => { test('composeOverlayVisibilityRuntime returns overlay visibility handlers', () => {
const calls: string[] = [];
const composed = composeOverlayVisibilityRuntime({ const composed = composeOverlayVisibilityRuntime({
overlayVisibilityRuntime: { overlayVisibilityRuntime: {
updateVisibleOverlayVisibility: () => {}, updateVisibleOverlayVisibility: () => {
calls.push('updateVisibleOverlayVisibility');
},
}, },
restorePreviousSecondarySubVisibilityMainDeps: { restorePreviousSecondarySubVisibilityMainDeps: {
getMpvClient: () => null, getMpvClient: () => null,
}, },
broadcastRuntimeOptionsChangedMainDeps: { broadcastRuntimeOptionsChangedMainDeps: {
broadcastRuntimeOptionsChangedRuntime: () => {}, broadcastRuntimeOptionsChangedRuntime: () => {
calls.push('broadcastRuntimeOptionsChangedRuntime');
},
getRuntimeOptionsState: () => [], getRuntimeOptionsState: () => [],
broadcastToOverlayWindows: () => {}, broadcastToOverlayWindows: () => {},
}, },
@@ -24,7 +29,9 @@ test('composeOverlayVisibilityRuntime returns overlay visibility handlers', () =
setCurrentEnabled: () => {}, setCurrentEnabled: () => {},
}, },
openRuntimeOptionsPaletteMainDeps: { openRuntimeOptionsPaletteMainDeps: {
openRuntimeOptionsPaletteRuntime: () => {}, openRuntimeOptionsPaletteRuntime: () => {
calls.push('openRuntimeOptionsPaletteRuntime');
},
}, },
}); });
@@ -34,4 +41,16 @@ test('composeOverlayVisibilityRuntime returns overlay visibility handlers', () =
assert.equal(typeof composed.sendToActiveOverlayWindow, 'function'); assert.equal(typeof composed.sendToActiveOverlayWindow, 'function');
assert.equal(typeof composed.setOverlayDebugVisualizationEnabled, 'function'); assert.equal(typeof composed.setOverlayDebugVisualizationEnabled, 'function');
assert.equal(typeof composed.openRuntimeOptionsPalette, 'function'); assert.equal(typeof composed.openRuntimeOptionsPalette, 'function');
// updateVisibleOverlayVisibility passes through to the injected runtime dep
composed.updateVisibleOverlayVisibility();
assert.deepEqual(calls, ['updateVisibleOverlayVisibility']);
// openRuntimeOptionsPalette forwards to the injected runtime dep
composed.openRuntimeOptionsPalette();
assert.deepEqual(calls, ['updateVisibleOverlayVisibility', 'openRuntimeOptionsPaletteRuntime']);
// broadcastRuntimeOptionsChanged forwards to the injected runtime dep
composed.broadcastRuntimeOptionsChanged();
assert.ok(calls.includes('broadcastRuntimeOptionsChangedRuntime'));
}); });

View File

@@ -3,6 +3,7 @@ import test from 'node:test';
import { composeShortcutRuntimes } from './shortcuts-runtime-composer'; import { composeShortcutRuntimes } from './shortcuts-runtime-composer';
test('composeShortcutRuntimes returns callable shortcut runtime handlers', () => { test('composeShortcutRuntimes returns callable shortcut runtime handlers', () => {
const calls: string[] = [];
const composed = composeShortcutRuntimes({ const composed = composeShortcutRuntimes({
globalShortcuts: { globalShortcuts: {
getConfiguredShortcutsMainDeps: { getConfiguredShortcutsMainDeps: {
@@ -39,9 +40,13 @@ test('composeShortcutRuntimes returns callable shortcut runtime handlers', () =>
}, },
overlayShortcutsRuntimeMainDeps: { overlayShortcutsRuntimeMainDeps: {
overlayShortcutsRuntime: { overlayShortcutsRuntime: {
registerOverlayShortcuts: () => {}, registerOverlayShortcuts: () => {
calls.push('registerOverlayShortcuts');
},
unregisterOverlayShortcuts: () => {}, unregisterOverlayShortcuts: () => {},
syncOverlayShortcuts: () => {}, syncOverlayShortcuts: () => {
calls.push('syncOverlayShortcuts');
},
refreshOverlayShortcuts: () => {}, refreshOverlayShortcuts: () => {},
}, },
}, },
@@ -58,4 +63,12 @@ test('composeShortcutRuntimes returns callable shortcut runtime handlers', () =>
assert.equal(typeof composed.unregisterOverlayShortcuts, 'function'); assert.equal(typeof composed.unregisterOverlayShortcuts, 'function');
assert.equal(typeof composed.syncOverlayShortcuts, 'function'); assert.equal(typeof composed.syncOverlayShortcuts, 'function');
assert.equal(typeof composed.refreshOverlayShortcuts, 'function'); assert.equal(typeof composed.refreshOverlayShortcuts, 'function');
// registerOverlayShortcuts forwards to the injected overlayShortcutsRuntime dep
composed.registerOverlayShortcuts();
assert.deepEqual(calls, ['registerOverlayShortcuts']);
// syncOverlayShortcuts forwards to the injected overlayShortcutsRuntime dep
composed.syncOverlayShortcuts();
assert.deepEqual(calls, ['registerOverlayShortcuts', 'syncOverlayShortcuts']);
}); });

View File

@@ -3,6 +3,7 @@ import test from 'node:test';
import { composeStartupLifecycleHandlers } from './startup-lifecycle-composer'; import { composeStartupLifecycleHandlers } from './startup-lifecycle-composer';
test('composeStartupLifecycleHandlers returns callable startup lifecycle handlers', () => { test('composeStartupLifecycleHandlers returns callable startup lifecycle handlers', () => {
const calls: string[] = [];
const composed = composeStartupLifecycleHandlers({ const composed = composeStartupLifecycleHandlers({
registerProtocolUrlHandlersMainDeps: { registerProtocolUrlHandlersMainDeps: {
registerOpenUrl: () => {}, registerOpenUrl: () => {},
@@ -51,7 +52,9 @@ test('composeStartupLifecycleHandlers returns callable startup lifecycle handler
getAllWindowCount: () => 0, getAllWindowCount: () => 0,
}, },
restoreWindowsOnActivateMainDeps: { restoreWindowsOnActivateMainDeps: {
createMainWindow: () => {}, createMainWindow: () => {
calls.push('createMainWindow');
},
updateVisibleOverlayVisibility: () => {}, updateVisibleOverlayVisibility: () => {},
syncOverlayMpvSubtitleSuppression: () => {}, syncOverlayMpvSubtitleSuppression: () => {},
}, },
@@ -61,4 +64,11 @@ test('composeStartupLifecycleHandlers returns callable startup lifecycle handler
assert.equal(typeof composed.onWillQuitCleanup, 'function'); assert.equal(typeof composed.onWillQuitCleanup, 'function');
assert.equal(typeof composed.shouldRestoreWindowsOnActivate, 'function'); assert.equal(typeof composed.shouldRestoreWindowsOnActivate, 'function');
assert.equal(typeof composed.restoreWindowsOnActivate, 'function'); assert.equal(typeof composed.restoreWindowsOnActivate, 'function');
// shouldRestoreWindowsOnActivate returns false when overlay runtime is not initialized
assert.equal(composed.shouldRestoreWindowsOnActivate(), false);
// restoreWindowsOnActivate invokes the injected createMainWindow dep
composed.restoreWindowsOnActivate();
assert.deepEqual(calls, ['createMainWindow']);
}); });