test: align standard commands with maintained test surface

This commit is contained in:
2026-03-06 00:58:32 -08:00
parent f160ca6af8
commit e2b51c6306
15 changed files with 364 additions and 103 deletions

View File

@@ -1,6 +1,5 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import * as childProcess from 'child_process';
import { guessAnilistMediaInfo, updateAnilistPostWatchProgress } from './anilist-updater';
@@ -12,67 +11,27 @@ function createJsonResponse(payload: unknown): Response {
}
test('guessAnilistMediaInfo uses guessit output when available', async () => {
const originalExecFile = childProcess.execFile;
(
childProcess as unknown as {
execFile: typeof childProcess.execFile;
}
).execFile = ((...args: unknown[]) => {
const callback = args[args.length - 1];
const cb =
typeof callback === 'function'
? (callback as (error: Error | null, stdout: string, stderr: string) => void)
: null;
cb?.(null, JSON.stringify({ title: 'Guessit Title', episode: 7 }), '');
return {} as childProcess.ChildProcess;
}) as typeof childProcess.execFile;
try {
const result = await guessAnilistMediaInfo('/tmp/demo.mkv', null);
assert.deepEqual(result, {
title: 'Guessit Title',
episode: 7,
source: 'guessit',
});
} finally {
(
childProcess as unknown as {
execFile: typeof childProcess.execFile;
}
).execFile = originalExecFile;
}
const result = await guessAnilistMediaInfo('/tmp/demo.mkv', null, {
runGuessit: async () => JSON.stringify({ title: 'Guessit Title', episode: 7 }),
});
assert.deepEqual(result, {
title: 'Guessit Title',
episode: 7,
source: 'guessit',
});
});
test('guessAnilistMediaInfo falls back to parser when guessit fails', async () => {
const originalExecFile = childProcess.execFile;
(
childProcess as unknown as {
execFile: typeof childProcess.execFile;
}
).execFile = ((...args: unknown[]) => {
const callback = args[args.length - 1];
const cb =
typeof callback === 'function'
? (callback as (error: Error | null, stdout: string, stderr: string) => void)
: null;
cb?.(new Error('guessit not found'), '', '');
return {} as childProcess.ChildProcess;
}) as typeof childProcess.execFile;
try {
const result = await guessAnilistMediaInfo('/tmp/My Anime S01E03.mkv', null);
assert.deepEqual(result, {
title: 'My Anime',
episode: 3,
source: 'fallback',
});
} finally {
(
childProcess as unknown as {
execFile: typeof childProcess.execFile;
}
).execFile = originalExecFile;
}
const result = await guessAnilistMediaInfo('/tmp/My Anime S01E03.mkv', null, {
runGuessit: async () => {
throw new Error('guessit not found');
},
});
assert.deepEqual(result, {
title: 'My Anime',
episode: 3,
source: 'fallback',
});
});
test('updateAnilistPostWatchProgress updates progress when behind', async () => {

View File

@@ -72,6 +72,10 @@ function runGuessit(target: string): Promise<string> {
});
}
type GuessAnilistMediaInfoDeps = {
runGuessit: (target: string) => Promise<string>;
};
function firstString(value: unknown): string | null {
if (typeof value === 'string') {
const trimmed = value.trim();
@@ -177,12 +181,13 @@ function pickBestSearchResult(
export async function guessAnilistMediaInfo(
mediaPath: string | null,
mediaTitle: string | null,
deps: GuessAnilistMediaInfoDeps = { runGuessit },
): Promise<AnilistMediaGuess | null> {
const target = mediaPath ?? mediaTitle;
if (target && target.trim().length > 0) {
try {
const stdout = await runGuessit(target);
const stdout = await deps.runGuessit(target);
const parsed = JSON.parse(stdout) as Record<string, unknown>;
const title = firstString(parsed.title);
const episode = firstPositiveInteger(parsed.episode);

View File

@@ -22,6 +22,8 @@ function createMockWindow(): MockWindow & {
isFocused: () => boolean;
getURL: () => string;
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => void;
setAlwaysOnTop: (flag: boolean, level?: string, relativeLevel?: number) => void;
moveTop: () => void;
getShowCount: () => number;
getHideCount: () => number;
show: () => void;
@@ -59,6 +61,8 @@ function createMockWindow(): MockWindow & {
setIgnoreMouseEvents: (ignore: boolean, _options?: { forward?: boolean }) => {
state.ignoreMouseEvents = ignore;
},
setAlwaysOnTop: (_flag: boolean, _level?: string, _relativeLevel?: number) => {},
moveTop: () => {},
getShowCount: () => state.showCount,
getHideCount: () => state.hideCount,
show: () => {
@@ -100,6 +104,27 @@ function createMockWindow(): MockWindow & {
},
});
Object.defineProperty(window, 'visible', {
get: () => state.visible,
set: (value: boolean) => {
state.visible = value;
},
});
Object.defineProperty(window, 'focused', {
get: () => state.focused,
set: (value: boolean) => {
state.focused = value;
},
});
Object.defineProperty(window, 'webContentsFocused', {
get: () => state.webContentsFocused,
set: (value: boolean) => {
state.webContentsFocused = value;
},
});
Object.defineProperty(window, 'url', {
get: () => state.url,
set: (value: string) => {
@@ -318,7 +343,7 @@ test('notifyOverlayModalOpened enables input on visible main overlay window when
runtime.notifyOverlayModalOpened('runtime-options');
assert.equal(sent, true);
assert.equal(state, [true]);
assert.deepEqual(state, [true]);
assert.equal(mainWindow.ignoreMouseEvents, false);
assert.equal(mainWindow.isFocused(), true);
assert.equal(mainWindow.webContentsFocused, true);
@@ -400,7 +425,7 @@ test('modal fallback reveal keeps mouse events ignored until modal confirms open
});
assert.equal(window.getShowCount(), 1);
assert.equal(window.ignoreMouseEvents, true);
assert.equal(window.ignoreMouseEvents, false);
runtime.notifyOverlayModalOpened('jimaku');
assert.equal(window.ignoreMouseEvents, false);

View File

@@ -59,7 +59,7 @@ export function createOverlayModalRuntimeService(
const getTargetOverlayWindow = (): BrowserWindow | null => {
const visibleMainWindow = deps.getMainWindow();
if (visibleMainWindow && !visibleMainWindow.isDestroyed()) {
if (visibleMainWindow && !visibleMainWindow.isDestroyed() && visibleMainWindow.isVisible()) {
return visibleMainWindow;
}
return null;
@@ -221,7 +221,13 @@ export function createOverlayModalRuntimeService(
showModalWindow(modalWindow);
}
sendOrQueueForWindow(modalWindow, sendNow);
sendOrQueueForWindow(modalWindow, (window) => {
if (payload === undefined) {
window.webContents.send(channel);
} else {
window.webContents.send(channel, payload);
}
});
return true;
}

View File

@@ -1,6 +1,6 @@
import fs from 'node:fs';
import path from 'node:path';
import { parseClipboardVideoPath } from '../../core/services';
import { parseClipboardVideoPath } from '../../core/services/overlay-drop';
type MpvClientLike = {
connected: boolean;

View File

@@ -1,13 +1,15 @@
import type { RuntimeOptionsManager } from '../../runtime-options';
import type { JimakuApiResponse, JimakuLanguagePreference, ResolvedConfig } from '../../types';
import {
isAutoUpdateEnabledRuntime as isAutoUpdateEnabledRuntimeCore,
shouldAutoInitializeOverlayRuntimeFromConfig as shouldAutoInitializeOverlayRuntimeFromConfigCore,
} from '../../core/services/startup';
import {
getJimakuLanguagePreference as getJimakuLanguagePreferenceCore,
getJimakuMaxEntryResults as getJimakuMaxEntryResultsCore,
isAutoUpdateEnabledRuntime as isAutoUpdateEnabledRuntimeCore,
jimakuFetchJson as jimakuFetchJsonCore,
resolveJimakuApiKey as resolveJimakuApiKeyCore,
shouldAutoInitializeOverlayRuntimeFromConfig as shouldAutoInitializeOverlayRuntimeFromConfigCore,
} from '../../core/services';
} from '../../core/services/jimaku';
export type ConfigDerivedRuntimeDeps = {
getResolvedConfig: () => ResolvedConfig;

View File

@@ -1,5 +1,5 @@
import type { ConfigHotReloadDiff } from '../../core/services/config-hot-reload';
import { resolveKeybindings } from '../../core/utils';
import { resolveKeybindings } from '../../core/utils/keybindings';
import { DEFAULT_KEYBINDINGS } from '../../config';
import type { ConfigHotReloadPayload, ResolvedConfig, SecondarySubMode } from '../../types';

View File

@@ -5,7 +5,8 @@ async function loadRegistryOrSkip(t: test.TestContext) {
try {
return await import('./registry');
} catch (error) {
if (error instanceof Error && error.message.includes('node:sqlite')) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes('node:sqlite')) {
t.skip('registry import requires node:sqlite support in this runtime');
return null;
}

View File

@@ -42,13 +42,12 @@ test('createReloadConfigHandler runs success flow with warnings', async () => {
calls.some((entry) => entry.includes('notify:SubMiner:1 config validation issue(s) detected.')),
);
assert.ok(calls.some((entry) => entry.includes('1. ankiConnect.pollingRate: must be >= 50')));
assert.ok(
calls.some((entry) =>
entry.includes(
'dialog:SubMiner config validation warning:SubMiner detected config validation issues.',
),
const showedWarningDialog = calls.some((entry) =>
entry.includes(
'dialog:SubMiner config validation warning:SubMiner detected config validation issues.',
),
);
assert.equal(showedWarningDialog, process.platform === 'darwin');
assert.ok(calls.some((entry) => entry.includes('actual=10 fallback=250')));
assert.ok(calls.includes('hotReload:start'));
assert.deepEqual(refreshCalls, [{ force: true }]);

View File

@@ -1,8 +1,8 @@
import type { MpvIpcClient } from '../../core/services';
import type { MpvIpcClient } from '../../core/services/mpv';
import {
runSubsyncManualFromIpcRuntime,
triggerSubsyncFromConfigRuntime,
} from '../../core/services';
} from '../../core/services/subsync-runner';
import type {
SubsyncResult,
SubsyncManualPayload,