mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-15 08:12:53 -07:00
430373f010
* feat(tokenizer): use Yomitan word classes for subtitle POS filtering - Carry matched headword wordClasses from termsFind into YomitanScanToken - Map recognized Yomitan wordClasses to SubMiner coarse POS before annotation - MeCab enrichment now fills only missing POS fields, preserving existing coarse pos1 - Exclude standalone grammar particles, して helper fragments, and single-kana surfaces from annotations - Respect source-text punctuation gaps when counting N+1 sentence words - Preserve known-word highlight on excluded kanji-containing tokens - Add backlog tasks 304 (N+1 boundary bug) and 305 (wordClasses POS, done) * fix(tokenizer): preserve annotation and enrichment behavior * fix: restore jlpt subtitle underlines * fix: exclude kana-only n+1 targets * fix: refresh overlay on Hyprland fullscreen * fix: address fullscreen and n-plus-one review notes * fix: address CodeRabbit review comments * fix: accept modified digits for multi-line sentence mining * Cancel pending Linux MPV fullscreen overlay refresh bursts - return a cancel handle from the Linux refresh burst scheduler - clear pending refresh bursts when overlays hide or windows close - tighten the burst test polling to wait for the async refresh * fix: suppress N+1 for kana-only candidates and fix minSentenceWords coun - Treat kana-only tokens with surrounding subtitle punctuation (…, ―, etc.) as kana-only so they are not promoted to N+1 targets - Exclude unknown tokens filtered from N+1 targeting from the minSentenceWords count so filtered kana-only unknowns cannot satisfy sentence length threshold - Add regression tests for kana-only candidate suppression and filtered-unknown padding cases * Suppress subtitle annotations for grammar fragments - Hide annotation metadata for auxiliary inflection and ja-nai endings - Preserve lexical `くれる` forms and add regression coverage * Fix kana-only N+1 tokenizer regression test - Use a pure-kana fixture for the subtitle token N+1 case - Update task notes for the latest CodeRabbit follow-up * Fix managed playback exit and tokenizer grammar splits - Ignore background stats daemons during regular app startup - Split standalone grammar endings before applying annotations - Clear helper-span annotations for auxiliary-only tokens * fix: refresh current subtitle after known-word mining * fix: suppress sigh interjection annotations * fix: preserve jlpt underline color after lookup * Replace grammar-ending permutations with shared matcher; preserve word a - Extract `grammar-ending.ts` with `isStandaloneGrammarEndingText` / `isSubtitleGrammarEndingText` pattern matchers - Replace `STANDALONE_GRAMMAR_ENDINGS` set in parser-selection-stage with shared matcher - Replace generated phrase sets in subtitle-annotation-filter with shared matcher - Remove stale duplicate subtitle-exclusion constants and helpers from annotation-stage - Manual clipboard card updates now write only to the sentence audio field, leaving word/expression audio untouched * fix: CI changelog, annotation options threading, and Jellyfin quit - Add `type: fixed` / `area:` frontmatter to `changes/319` to pass `changelog:lint` - Thread `TokenizerAnnotationOptions` through `stripSubtitleAnnotationMetadata` so `sourceText` is honored - Include `jellyfinPlay` in `shouldQuitOnDisconnectWhenOverlayRuntimeInitialized` predicate - Make mouse test `elementFromPoint` stubs coordinate-sensitive - Make Lua test `.tmp` mkdir portable on Windows * Preserve overlay across macOS flaps and mpv playlist changes - keep visible overlays alive during transient macOS tracker loss - reuse the running mpv overlay path on playlist navigation - update regression coverage and changelog fragments * fix: restore stats daemon deferral * fix: keep subtitle prefetch alive after cache hits * Fix JLPT underline color drift and AniList skipped-threshold sync - Replace JLPT `text-decoration` underlines with `border-bottom` so Chromium selection/hover cannot repaint them to another annotation's color - Lock JLPT underline color for combined annotation selectors (known, n+1, frequency) and character hover/selection states - Trigger AniList post-watch check on every mpv time-position update to catch skipped completion thresholds - Fall back to filename-parser season/episode when guessit omits them * fix: address coderabbit feedback * fix: sync AniList after seeked completion * fix: preserve ordinal frequency annotations * fix: preserve known highlighting for filtered tokens * fix: address PR #57 CodeRabbit feedback - Acquire AniList post-watch in-flight lock before async gating to prevent duplicate writes - Isolate manual watched mark result from AniList post-watch callback failures - Report known-word cache clears as mutations during immediate append when state existed - Add regression tests for each fix * fix: stop AniList setup reopening on Linux when keyring token exists - Gate setup success on token persistence: `saveToken` now returns `boolean`; on failure, keeps the setup window open instead of reporting success - Config reload passes `allowSetupPrompt: false` so playback reloads don't re-open the setup window - Add regression test for persistence-failure path * fix: suppress known highlights for subtitle particles * fix: retry transient AniList safeStorage failures * fix: hide overlay focus ring * fix: align Hyprland fullscreen overlays * fix: restore subtitle playback keybindings * fix: align Hyprland overlay windows to mpv and stop pinning them - Force-apply exact Hyprland move/resize/setprop dispatches when bounds are provided - Stop pinning overlay windows; toggle pin off when Hyprland reports pinned=true - Compensate stats overlay outer placement for Electron/Wayland content insets - Make stats overlay window and page opaque so mpv cannot show through transparent insets - Constrain stats app to h-screen with internal scroll so content covers mpv from y=0 - Lock overlay/stats window titles against page-title-updated events - Add regression coverage for placement dispatches, inset compensation, and CSS overlay mode * fix: retain frequency rank for honorific prefix-noun tokens - Add `shouldAllowHonorificPrefixNounFrequency` to exempt お/ご/御 + noun merged tokens from frequency exclusion - Add regression test for `ご機嫌` asserting rank 5484 is preserved after MeCab enrichment and annotation - Close TASK-341 * fix: map openCharacterDictionary session action to --open-character-dict - Add missing Lua CLI dispatch entry for openCharacterDictionary - Add regression test for Alt+Meta+A binding and CLI flag forwarding * fix: keep macOS overlay interactive while mpv remains active - Overlay no longer hides or becomes click-through during tracker refreshes when mpv is the focused window - Preserve already-visible overlay when tracker is temporarily not ready but mpv target signal is active - Add regression tests for active-mpv tracker refresh and transient tracker-not-ready paths * fix: address coderabbit subtitle follow-ups * fix: resolve media detail from sessions when lifetime summary is absent - Change `getMediaDetail` JOIN to LEFT JOIN on `imm_lifetime_media` and fall back to aggregated session metrics when no lifetime row exists - Add filter `AND (lm.video_id IS NOT NULL OR s.session_id IS NOT NULL)` to keep results valid - Add regression test covering the session-visible / media-detail-missing mismatch * fix: address PR-57 CodeRabbit findings and CI failures - use filtered word counts in media detail session token aggregation - cancel fullscreen refresh burst on exit via updateLinuxMpvFullscreenOverlayRefreshBurst - guard Hyprland JSON.parse in try/catch; exclude windowtitle from geometry events - narrow focus suppression from :focus to :focus-visible - apply JLPT lock selectors to word-name-match tokens (N1–N5) * fix: macOS overlay z-order and Yomitan compound token known highlighting - Release always-on-top when tracked mpv loses foreground on macOS - Skip visible overlay blur restacking on macOS to avoid covering unrelated windows - Prefer Yomitan internal parse tokens over fragmented scanner output for known-word decisions - Add regression tests for both behaviors * fix: macOS visible-overlay blur no longer invokes Windows-only blur call - Split win32/darwin branches in handleOverlayWindowBlurred so darwin visible blur returns early without calling onWindowsVisibleOverlayBlur - Add regression test asserting Windows callback stays inactive on macOS visible overlay blur - Close TASK-347
1576 lines
46 KiB
TypeScript
1576 lines
46 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import { OVERLAY_WINDOW_CONTENT_READY_FLAG } from './overlay-window-flags';
|
|
import { setVisibleOverlayVisible, updateVisibleOverlayVisibility } from './overlay-visibility';
|
|
|
|
type WindowTrackerStub = {
|
|
isTracking: () => boolean;
|
|
getGeometry: () => { x: number; y: number; width: number; height: number } | null;
|
|
isTargetWindowFocused?: () => boolean;
|
|
isTargetWindowMinimized?: () => boolean;
|
|
};
|
|
|
|
function createMainWindowRecorder() {
|
|
const calls: string[] = [];
|
|
let visible = false;
|
|
let focused = false;
|
|
let opacity = 1;
|
|
let contentReady = true;
|
|
const window = {
|
|
webContents: {},
|
|
isDestroyed: () => false,
|
|
isVisible: () => visible,
|
|
isFocused: () => focused,
|
|
hide: () => {
|
|
visible = false;
|
|
focused = false;
|
|
calls.push('hide');
|
|
},
|
|
show: () => {
|
|
visible = true;
|
|
calls.push('show');
|
|
},
|
|
showInactive: () => {
|
|
visible = true;
|
|
calls.push('show-inactive');
|
|
},
|
|
focus: () => {
|
|
focused = true;
|
|
calls.push('focus');
|
|
},
|
|
setAlwaysOnTop: (flag: boolean) => {
|
|
calls.push(`always-on-top:${flag}`);
|
|
},
|
|
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => {
|
|
calls.push(`mouse-ignore:${ignore}:${options?.forward === true ? 'forward' : 'plain'}`);
|
|
},
|
|
setOpacity: (nextOpacity: number) => {
|
|
opacity = nextOpacity;
|
|
calls.push(`opacity:${nextOpacity}`);
|
|
},
|
|
moveTop: () => {
|
|
calls.push('move-top');
|
|
},
|
|
};
|
|
(
|
|
window as {
|
|
[OVERLAY_WINDOW_CONTENT_READY_FLAG]?: boolean;
|
|
}
|
|
)[OVERLAY_WINDOW_CONTENT_READY_FLAG] = contentReady;
|
|
|
|
return {
|
|
window,
|
|
calls,
|
|
getOpacity: () => opacity,
|
|
setContentReady: (nextContentReady: boolean) => {
|
|
contentReady = nextContentReady;
|
|
(
|
|
window as {
|
|
[OVERLAY_WINDOW_CONTENT_READY_FLAG]?: boolean;
|
|
}
|
|
)[OVERLAY_WINDOW_CONTENT_READY_FLAG] = contentReady;
|
|
},
|
|
setFocused: (nextFocused: boolean) => {
|
|
focused = nextFocused;
|
|
},
|
|
};
|
|
}
|
|
|
|
test('macOS keeps visible overlay hidden while tracker is not ready and emits one loading OSD', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
let trackerWarning = false;
|
|
const osdMessages: string[] = [];
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => false,
|
|
getGeometry: () => null,
|
|
};
|
|
|
|
const run = () =>
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: trackerWarning,
|
|
setTrackerNotReadyWarningShown: (shown: boolean) => {
|
|
trackerWarning = shown;
|
|
},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: true,
|
|
showOverlayLoadingOsd: (message: string) => {
|
|
osdMessages.push(message);
|
|
},
|
|
} as never);
|
|
|
|
run();
|
|
run();
|
|
|
|
assert.equal(trackerWarning, true);
|
|
assert.deepEqual(osdMessages, ['Overlay loading...']);
|
|
assert.ok(calls.includes('hide'));
|
|
assert.ok(!calls.includes('show'));
|
|
});
|
|
|
|
test('tracked non-macOS overlay stays hidden while tracker is not ready', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
let trackerWarning = false;
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => false,
|
|
getGeometry: () => null,
|
|
};
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: trackerWarning,
|
|
setTrackerNotReadyWarningShown: (shown: boolean) => {
|
|
trackerWarning = shown;
|
|
},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
showOverlayLoadingOsd: () => {
|
|
calls.push('osd');
|
|
},
|
|
resolveFallbackBounds: () => ({ x: 12, y: 24, width: 640, height: 360 }),
|
|
} as never);
|
|
|
|
assert.equal(trackerWarning, true);
|
|
assert.ok(calls.includes('hide'));
|
|
assert.ok(!calls.includes('update-bounds'));
|
|
assert.ok(!calls.includes('show'));
|
|
assert.ok(!calls.includes('focus'));
|
|
assert.ok(!calls.includes('osd'));
|
|
});
|
|
|
|
test('untracked non-macOS overlay keeps fallback visible behavior when no tracker exists', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
let trackerWarning = false;
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: null,
|
|
trackerNotReadyWarningShown: trackerWarning,
|
|
setTrackerNotReadyWarningShown: (shown: boolean) => {
|
|
trackerWarning = shown;
|
|
},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
showOverlayLoadingOsd: () => {
|
|
calls.push('osd');
|
|
},
|
|
resolveFallbackBounds: () => ({ x: 12, y: 24, width: 640, height: 360 }),
|
|
} as never);
|
|
|
|
assert.equal(trackerWarning, false);
|
|
assert.ok(calls.includes('show'));
|
|
assert.ok(calls.includes('focus'));
|
|
assert.ok(!calls.includes('osd'));
|
|
});
|
|
|
|
test('Windows visible overlay stays click-through and binds to mpv while tracked', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => true,
|
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
|
};
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncWindowsOverlayToMpvZOrder: () => {
|
|
calls.push('sync-windows-z-order');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
isWindowsPlatform: true,
|
|
} as never);
|
|
|
|
assert.ok(calls.includes('opacity:0'));
|
|
assert.ok(calls.includes('mouse-ignore:true:forward'));
|
|
assert.ok(calls.includes('show-inactive'));
|
|
assert.ok(calls.includes('sync-windows-z-order'));
|
|
assert.ok(!calls.includes('move-top'));
|
|
assert.ok(!calls.includes('ensure-level'));
|
|
assert.ok(!calls.includes('enforce-order'));
|
|
assert.ok(!calls.includes('focus'));
|
|
});
|
|
|
|
test('Windows visible overlay restores opacity after the deferred reveal delay', async () => {
|
|
const { window, calls, getOpacity } = createMainWindowRecorder();
|
|
let syncWindowsZOrderCalls = 0;
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => true,
|
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
|
};
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncWindowsOverlayToMpvZOrder: () => {
|
|
syncWindowsZOrderCalls += 1;
|
|
calls.push('sync-windows-z-order');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
isWindowsPlatform: true,
|
|
} as never);
|
|
|
|
assert.equal(getOpacity(), 0);
|
|
assert.equal(syncWindowsZOrderCalls, 1);
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 60));
|
|
assert.equal(getOpacity(), 1);
|
|
assert.equal(syncWindowsZOrderCalls, 2);
|
|
assert.ok(calls.includes('opacity:1'));
|
|
});
|
|
|
|
test('Windows visible overlay waits for content-ready before first reveal', () => {
|
|
const { window, calls, setContentReady } = createMainWindowRecorder();
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => true,
|
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
|
};
|
|
setContentReady(false);
|
|
|
|
const run = () =>
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncWindowsOverlayToMpvZOrder: () => {
|
|
calls.push('sync-windows-z-order');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
isWindowsPlatform: true,
|
|
} as never);
|
|
|
|
run();
|
|
|
|
assert.ok(!calls.includes('show-inactive'));
|
|
assert.ok(!calls.includes('show'));
|
|
|
|
setContentReady(true);
|
|
run();
|
|
|
|
assert.ok(calls.includes('show-inactive'));
|
|
});
|
|
|
|
test('tracked Windows overlay refresh rebinds while already visible', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => true,
|
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
|
};
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncWindowsOverlayToMpvZOrder: () => {
|
|
calls.push('sync-windows-z-order');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
isWindowsPlatform: true,
|
|
} as never);
|
|
|
|
calls.length = 0;
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncWindowsOverlayToMpvZOrder: () => {
|
|
calls.push('sync-windows-z-order');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
isWindowsPlatform: true,
|
|
} as never);
|
|
|
|
assert.ok(calls.includes('mouse-ignore:true:forward'));
|
|
assert.ok(calls.includes('sync-windows-z-order'));
|
|
assert.ok(!calls.includes('move-top'));
|
|
assert.ok(!calls.includes('show'));
|
|
assert.ok(!calls.includes('ensure-level'));
|
|
assert.ok(calls.includes('sync-shortcuts'));
|
|
});
|
|
|
|
test('forced passthrough still reapplies while visible on Windows', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => true,
|
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
|
};
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncWindowsOverlayToMpvZOrder: () => {
|
|
calls.push('sync-windows-z-order');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
isWindowsPlatform: true,
|
|
} as never);
|
|
|
|
calls.length = 0;
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncWindowsOverlayToMpvZOrder: () => {
|
|
calls.push('sync-windows-z-order');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
isWindowsPlatform: true,
|
|
forceMousePassthrough: true,
|
|
} as never);
|
|
|
|
assert.ok(calls.includes('mouse-ignore:true:forward'));
|
|
assert.ok(!calls.includes('always-on-top:false'));
|
|
assert.ok(!calls.includes('move-top'));
|
|
assert.ok(calls.includes('sync-windows-z-order'));
|
|
assert.ok(!calls.includes('ensure-level'));
|
|
assert.ok(!calls.includes('enforce-order'));
|
|
});
|
|
|
|
test('forced passthrough still shows tracked overlay while bound to mpv on Windows', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => true,
|
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
|
};
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncWindowsOverlayToMpvZOrder: () => {
|
|
calls.push('sync-windows-z-order');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
isWindowsPlatform: true,
|
|
forceMousePassthrough: true,
|
|
} as never);
|
|
|
|
assert.ok(calls.includes('show-inactive'));
|
|
assert.ok(!calls.includes('always-on-top:false'));
|
|
assert.ok(!calls.includes('move-top'));
|
|
assert.ok(calls.includes('sync-windows-z-order'));
|
|
});
|
|
|
|
test('forced mouse passthrough drops macOS tracked overlay below higher-priority windows', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => true,
|
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
|
};
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: true,
|
|
isWindowsPlatform: false,
|
|
forceMousePassthrough: true,
|
|
} as never);
|
|
|
|
assert.ok(calls.includes('mouse-ignore:true:forward'));
|
|
assert.ok(calls.includes('always-on-top:false'));
|
|
assert.ok(!calls.includes('ensure-level'));
|
|
assert.ok(!calls.includes('enforce-order'));
|
|
});
|
|
|
|
test('tracked Windows overlay rebinds without hiding when tracker focus changes', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
let focused = true;
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => true,
|
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
|
isTargetWindowFocused: () => focused,
|
|
};
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncWindowsOverlayToMpvZOrder: () => {
|
|
calls.push('sync-windows-z-order');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
isWindowsPlatform: true,
|
|
} as never);
|
|
|
|
calls.length = 0;
|
|
focused = false;
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncWindowsOverlayToMpvZOrder: () => {
|
|
calls.push('sync-windows-z-order');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
isWindowsPlatform: true,
|
|
} as never);
|
|
|
|
assert.ok(!calls.includes('always-on-top:false'));
|
|
assert.ok(!calls.includes('move-top'));
|
|
assert.ok(calls.includes('mouse-ignore:true:forward'));
|
|
assert.ok(calls.includes('sync-windows-z-order'));
|
|
assert.ok(!calls.includes('ensure-level'));
|
|
assert.ok(!calls.includes('enforce-order'));
|
|
assert.ok(!calls.includes('show'));
|
|
});
|
|
|
|
test('tracked Windows overlay stays interactive while the overlay window itself is focused', () => {
|
|
const { window, calls, setFocused } = createMainWindowRecorder();
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => true,
|
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
|
isTargetWindowFocused: () => false,
|
|
};
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncWindowsOverlayToMpvZOrder: () => {
|
|
calls.push('sync-windows-z-order');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
isWindowsPlatform: true,
|
|
} as never);
|
|
|
|
calls.length = 0;
|
|
setFocused(true);
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncWindowsOverlayToMpvZOrder: () => {
|
|
calls.push('sync-windows-z-order');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
isWindowsPlatform: true,
|
|
} as never);
|
|
|
|
assert.ok(calls.includes('mouse-ignore:false:plain'));
|
|
assert.ok(calls.includes('sync-windows-z-order'));
|
|
assert.ok(!calls.includes('move-top'));
|
|
assert.ok(!calls.includes('ensure-level'));
|
|
assert.ok(!calls.includes('enforce-order'));
|
|
});
|
|
|
|
test('tracked Windows overlay reshows click-through even if focus state is stale after a modal closes', () => {
|
|
const { window, calls, setFocused } = createMainWindowRecorder();
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => true,
|
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
|
isTargetWindowFocused: () => false,
|
|
};
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncWindowsOverlayToMpvZOrder: () => {
|
|
calls.push('sync-windows-z-order');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
isWindowsPlatform: true,
|
|
} as never);
|
|
|
|
calls.length = 0;
|
|
window.hide();
|
|
calls.length = 0;
|
|
setFocused(true);
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncWindowsOverlayToMpvZOrder: () => {
|
|
calls.push('sync-windows-z-order');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
isWindowsPlatform: true,
|
|
} as never);
|
|
|
|
assert.ok(calls.includes('mouse-ignore:true:forward'));
|
|
assert.ok(calls.includes('show-inactive'));
|
|
assert.ok(!calls.includes('show'));
|
|
});
|
|
|
|
test('tracked Windows overlay binds above mpv even when tracker focus lags', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => true,
|
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
|
isTargetWindowFocused: () => false,
|
|
};
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncWindowsOverlayToMpvZOrder: () => {
|
|
calls.push('sync-windows-z-order');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
isWindowsPlatform: true,
|
|
} as never);
|
|
|
|
assert.ok(!calls.includes('always-on-top:false'));
|
|
assert.ok(!calls.includes('move-top'));
|
|
assert.ok(calls.includes('mouse-ignore:true:forward'));
|
|
assert.ok(calls.includes('sync-windows-z-order'));
|
|
assert.ok(!calls.includes('ensure-level'));
|
|
});
|
|
|
|
test('visible overlay stays hidden while a modal window is active', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => true,
|
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
|
};
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
modalActive: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: true,
|
|
isWindowsPlatform: false,
|
|
} as never);
|
|
|
|
assert.ok(calls.includes('hide'));
|
|
assert.ok(!calls.includes('show'));
|
|
assert.ok(!calls.includes('update-bounds'));
|
|
});
|
|
|
|
test('macOS tracked visible overlay stays interactive without passively stealing focus', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => true,
|
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
|
};
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: true,
|
|
isWindowsPlatform: false,
|
|
} as never);
|
|
|
|
assert.ok(calls.includes('mouse-ignore:false:plain'));
|
|
assert.ok(calls.includes('show'));
|
|
assert.ok(!calls.includes('focus'));
|
|
});
|
|
|
|
test('macOS keeps active mpv overlay visible and interactive during tracker refresh', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
const osdMessages: string[] = [];
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => true,
|
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
|
isTargetWindowFocused: () => true,
|
|
};
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {
|
|
calls.push('tracker-warning');
|
|
},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: true,
|
|
isWindowsPlatform: false,
|
|
showOverlayLoadingOsd: (message: string) => {
|
|
osdMessages.push(message);
|
|
},
|
|
} as never);
|
|
|
|
assert.ok(calls.includes('update-bounds'));
|
|
assert.ok(calls.includes('sync-layer'));
|
|
assert.ok(calls.includes('mouse-ignore:false:plain'));
|
|
assert.ok(calls.includes('ensure-level'));
|
|
assert.ok(calls.includes('enforce-order'));
|
|
assert.ok(calls.includes('sync-shortcuts'));
|
|
assert.ok(!calls.includes('hide'));
|
|
assert.deepEqual(osdMessages, []);
|
|
});
|
|
|
|
test('macOS tracked overlay releases topmost level when mpv loses foreground', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => true,
|
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
|
isTargetWindowFocused: () => false,
|
|
};
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: true,
|
|
isWindowsPlatform: false,
|
|
} as never);
|
|
|
|
assert.ok(calls.includes('update-bounds'));
|
|
assert.ok(calls.includes('sync-layer'));
|
|
assert.ok(calls.includes('mouse-ignore:true:forward'));
|
|
assert.ok(calls.includes('always-on-top:false'));
|
|
assert.ok(calls.includes('show'));
|
|
assert.ok(calls.includes('sync-shortcuts'));
|
|
assert.ok(!calls.includes('ensure-level'));
|
|
assert.ok(!calls.includes('enforce-order'));
|
|
assert.ok(!calls.includes('focus'));
|
|
assert.ok(!calls.includes('hide'));
|
|
});
|
|
|
|
test('macOS preserves an already visible active mpv overlay while tracker is temporarily not ready', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
const osdMessages: string[] = [];
|
|
let trackerWarning = false;
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => false,
|
|
getGeometry: () => null,
|
|
isTargetWindowFocused: () => true,
|
|
};
|
|
|
|
window.show();
|
|
calls.length = 0;
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: trackerWarning,
|
|
setTrackerNotReadyWarningShown: (shown: boolean) => {
|
|
trackerWarning = shown;
|
|
calls.push(`tracker-warning:${shown}`);
|
|
},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: true,
|
|
isWindowsPlatform: false,
|
|
showOverlayLoadingOsd: (message: string) => {
|
|
osdMessages.push(message);
|
|
},
|
|
} as never);
|
|
|
|
assert.equal(trackerWarning, false);
|
|
assert.ok(calls.includes('sync-layer'));
|
|
assert.ok(calls.includes('mouse-ignore:false:plain'));
|
|
assert.ok(calls.includes('ensure-level'));
|
|
assert.ok(calls.includes('sync-shortcuts'));
|
|
assert.ok(!calls.includes('hide'));
|
|
assert.deepEqual(osdMessages, []);
|
|
});
|
|
|
|
test('forced mouse passthrough keeps macOS tracked overlay passive while visible', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => true,
|
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
|
};
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: true,
|
|
isWindowsPlatform: false,
|
|
forceMousePassthrough: true,
|
|
} as never);
|
|
|
|
assert.ok(calls.includes('mouse-ignore:true:forward'));
|
|
assert.ok(calls.includes('show'));
|
|
assert.ok(!calls.includes('focus'));
|
|
});
|
|
|
|
test('Windows keeps visible overlay hidden while tracker is not ready', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
let trackerWarning = false;
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => false,
|
|
getGeometry: () => null,
|
|
};
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: trackerWarning,
|
|
setTrackerNotReadyWarningShown: (shown: boolean) => {
|
|
trackerWarning = shown;
|
|
},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
isWindowsPlatform: true,
|
|
resolveFallbackBounds: () => ({ x: 12, y: 24, width: 640, height: 360 }),
|
|
} as never);
|
|
|
|
assert.equal(trackerWarning, true);
|
|
assert.ok(calls.includes('hide'));
|
|
assert.ok(!calls.includes('show'));
|
|
assert.ok(!calls.includes('update-bounds'));
|
|
});
|
|
|
|
test('Windows preserves visible overlay and rebinds to mpv while tracker transiently loses a non-minimized window', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
let tracking = true;
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => tracking,
|
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
|
isTargetWindowFocused: () => false,
|
|
isTargetWindowMinimized: () => false,
|
|
};
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncWindowsOverlayToMpvZOrder: () => {
|
|
calls.push('sync-windows-z-order');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
isWindowsPlatform: true,
|
|
} as never);
|
|
|
|
calls.length = 0;
|
|
tracking = false;
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncWindowsOverlayToMpvZOrder: () => {
|
|
calls.push('sync-windows-z-order');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
isWindowsPlatform: true,
|
|
} as never);
|
|
|
|
assert.ok(!calls.includes('hide'));
|
|
assert.ok(!calls.includes('show'));
|
|
assert.ok(!calls.includes('always-on-top:false'));
|
|
assert.ok(!calls.includes('move-top'));
|
|
assert.ok(calls.includes('mouse-ignore:true:forward'));
|
|
assert.ok(calls.includes('sync-windows-z-order'));
|
|
assert.ok(!calls.includes('ensure-level'));
|
|
assert.ok(calls.includes('sync-shortcuts'));
|
|
});
|
|
|
|
test('Windows hides the visible overlay when the tracked window is minimized', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
let tracking = true;
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => tracking,
|
|
getGeometry: () => (tracking ? { x: 0, y: 0, width: 1280, height: 720 } : null),
|
|
isTargetWindowMinimized: () => !tracking,
|
|
};
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncWindowsOverlayToMpvZOrder: () => {
|
|
calls.push('sync-windows-z-order');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
isWindowsPlatform: true,
|
|
} as never);
|
|
|
|
calls.length = 0;
|
|
tracking = false;
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: false,
|
|
setTrackerNotReadyWarningShown: () => {},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncWindowsOverlayToMpvZOrder: () => {
|
|
calls.push('sync-windows-z-order');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: false,
|
|
isWindowsPlatform: true,
|
|
} as never);
|
|
|
|
assert.ok(calls.includes('hide'));
|
|
assert.ok(!calls.includes('sync-windows-z-order'));
|
|
});
|
|
|
|
test('macOS keeps visible overlay hidden while tracker is not initialized yet', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
let trackerWarning = false;
|
|
const osdMessages: string[] = [];
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: null,
|
|
trackerNotReadyWarningShown: trackerWarning,
|
|
setTrackerNotReadyWarningShown: (shown: boolean) => {
|
|
trackerWarning = shown;
|
|
},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: true,
|
|
showOverlayLoadingOsd: (message: string) => {
|
|
osdMessages.push(message);
|
|
},
|
|
} as never);
|
|
|
|
assert.equal(trackerWarning, true);
|
|
assert.deepEqual(osdMessages, ['Overlay loading...']);
|
|
assert.ok(calls.includes('hide'));
|
|
assert.ok(!calls.includes('show'));
|
|
assert.ok(!calls.includes('update-bounds'));
|
|
});
|
|
|
|
test('macOS preserves visible overlay during transient tracker loss with retained geometry', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
const osdMessages: string[] = [];
|
|
let trackerWarning = false;
|
|
let tracking = true;
|
|
const tracker: WindowTrackerStub = {
|
|
isTracking: () => tracking,
|
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
|
isTargetWindowFocused: () => true,
|
|
};
|
|
|
|
const run = () =>
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: tracker as never,
|
|
trackerNotReadyWarningShown: trackerWarning,
|
|
setTrackerNotReadyWarningShown: (shown: boolean) => {
|
|
trackerWarning = shown;
|
|
},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: true,
|
|
showOverlayLoadingOsd: (message: string) => {
|
|
osdMessages.push(message);
|
|
},
|
|
} as never);
|
|
|
|
run();
|
|
calls.length = 0;
|
|
tracking = false;
|
|
|
|
run();
|
|
|
|
assert.equal(trackerWarning, false);
|
|
assert.deepEqual(osdMessages, []);
|
|
assert.ok(calls.includes('update-bounds'));
|
|
assert.ok(calls.includes('sync-layer'));
|
|
assert.ok(calls.includes('mouse-ignore:false:plain'));
|
|
assert.ok(calls.includes('ensure-level'));
|
|
assert.ok(calls.includes('enforce-order'));
|
|
assert.ok(calls.includes('sync-shortcuts'));
|
|
assert.ok(!calls.includes('hide'));
|
|
assert.ok(!calls.includes('show'));
|
|
});
|
|
|
|
test('macOS suppresses immediate repeat loading OSD after tracker recovery until cooldown expires', () => {
|
|
const { window } = createMainWindowRecorder();
|
|
const osdMessages: string[] = [];
|
|
let trackerWarning = false;
|
|
let lastLoadingOsdAtMs: number | null = null;
|
|
let nowMs = 1_000;
|
|
const hiddenTracker: WindowTrackerStub = {
|
|
isTracking: () => false,
|
|
getGeometry: () => null,
|
|
};
|
|
const trackedTracker: WindowTrackerStub = {
|
|
isTracking: () => true,
|
|
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
|
};
|
|
|
|
const run = (windowTracker: WindowTrackerStub) =>
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: windowTracker as never,
|
|
trackerNotReadyWarningShown: trackerWarning,
|
|
setTrackerNotReadyWarningShown: (shown: boolean) => {
|
|
trackerWarning = shown;
|
|
},
|
|
updateVisibleOverlayBounds: () => {},
|
|
ensureOverlayWindowLevel: () => {},
|
|
syncPrimaryOverlayWindowLayer: () => {},
|
|
enforceOverlayLayerOrder: () => {},
|
|
syncOverlayShortcuts: () => {},
|
|
isMacOSPlatform: true,
|
|
showOverlayLoadingOsd: (message: string) => {
|
|
osdMessages.push(message);
|
|
},
|
|
shouldShowOverlayLoadingOsd: () =>
|
|
lastLoadingOsdAtMs === null || nowMs - lastLoadingOsdAtMs >= 5_000,
|
|
markOverlayLoadingOsdShown: () => {
|
|
lastLoadingOsdAtMs = nowMs;
|
|
},
|
|
} as never);
|
|
|
|
run(hiddenTracker);
|
|
run(trackedTracker);
|
|
|
|
nowMs = 2_000;
|
|
run(hiddenTracker);
|
|
run(trackedTracker);
|
|
|
|
nowMs = 6_500;
|
|
run(hiddenTracker);
|
|
|
|
assert.deepEqual(osdMessages, ['Overlay loading...', 'Overlay loading...']);
|
|
});
|
|
|
|
test('setVisibleOverlayVisible does not mutate mpv subtitle visibility directly', () => {
|
|
const calls: string[] = [];
|
|
setVisibleOverlayVisible({
|
|
visible: true,
|
|
setVisibleOverlayVisibleState: (visible) => {
|
|
calls.push(`state:${visible}`);
|
|
},
|
|
updateVisibleOverlayVisibility: () => {
|
|
calls.push('update');
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(calls, ['state:true', 'update']);
|
|
});
|
|
|
|
test('macOS explicit hide resets loading OSD suppression before retry', () => {
|
|
const { window, calls } = createMainWindowRecorder();
|
|
const osdMessages: string[] = [];
|
|
let trackerWarning = false;
|
|
let lastLoadingOsdAtMs: number | null = null;
|
|
let nowMs = 1_000;
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: null,
|
|
trackerNotReadyWarningShown: trackerWarning,
|
|
setTrackerNotReadyWarningShown: (shown: boolean) => {
|
|
trackerWarning = shown;
|
|
calls.push(`warn:${shown ? 'yes' : 'no'}`);
|
|
},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: true,
|
|
showOverlayLoadingOsd: (message: string) => {
|
|
osdMessages.push(message);
|
|
},
|
|
shouldShowOverlayLoadingOsd: () =>
|
|
lastLoadingOsdAtMs === null || nowMs - lastLoadingOsdAtMs >= 5_000,
|
|
markOverlayLoadingOsdShown: () => {
|
|
lastLoadingOsdAtMs = nowMs;
|
|
},
|
|
resetOverlayLoadingOsdSuppression: () => {
|
|
lastLoadingOsdAtMs = null;
|
|
},
|
|
} as never);
|
|
|
|
nowMs = 1_500;
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: false,
|
|
mainWindow: window as never,
|
|
windowTracker: null,
|
|
trackerNotReadyWarningShown: trackerWarning,
|
|
setTrackerNotReadyWarningShown: (shown: boolean) => {
|
|
trackerWarning = shown;
|
|
calls.push(`warn:${shown ? 'yes' : 'no'}`);
|
|
},
|
|
updateVisibleOverlayBounds: () => {},
|
|
ensureOverlayWindowLevel: () => {},
|
|
syncPrimaryOverlayWindowLayer: () => {},
|
|
enforceOverlayLayerOrder: () => {},
|
|
syncOverlayShortcuts: () => {},
|
|
isMacOSPlatform: true,
|
|
showOverlayLoadingOsd: () => {},
|
|
resetOverlayLoadingOsdSuppression: () => {
|
|
lastLoadingOsdAtMs = null;
|
|
},
|
|
} as never);
|
|
|
|
updateVisibleOverlayVisibility({
|
|
visibleOverlayVisible: true,
|
|
mainWindow: window as never,
|
|
windowTracker: null,
|
|
trackerNotReadyWarningShown: trackerWarning,
|
|
setTrackerNotReadyWarningShown: (shown: boolean) => {
|
|
trackerWarning = shown;
|
|
calls.push(`warn:${shown ? 'yes' : 'no'}`);
|
|
},
|
|
updateVisibleOverlayBounds: () => {
|
|
calls.push('update-bounds');
|
|
},
|
|
ensureOverlayWindowLevel: () => {
|
|
calls.push('ensure-level');
|
|
},
|
|
syncPrimaryOverlayWindowLayer: () => {
|
|
calls.push('sync-layer');
|
|
},
|
|
enforceOverlayLayerOrder: () => {
|
|
calls.push('enforce-order');
|
|
},
|
|
syncOverlayShortcuts: () => {
|
|
calls.push('sync-shortcuts');
|
|
},
|
|
isMacOSPlatform: true,
|
|
showOverlayLoadingOsd: (message: string) => {
|
|
osdMessages.push(message);
|
|
},
|
|
shouldShowOverlayLoadingOsd: () =>
|
|
lastLoadingOsdAtMs === null || nowMs - lastLoadingOsdAtMs >= 5_000,
|
|
markOverlayLoadingOsdShown: () => {
|
|
lastLoadingOsdAtMs = nowMs;
|
|
},
|
|
resetOverlayLoadingOsdSuppression: () => {
|
|
lastLoadingOsdAtMs = null;
|
|
},
|
|
} as never);
|
|
|
|
assert.deepEqual(osdMessages, ['Overlay loading...', 'Overlay loading...']);
|
|
});
|