mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-06 19:57:26 -08:00
test: align standard commands with maintained test surface
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 }]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user