mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-10-21 17:01:25 -07:00
# Conflicts: # launcher/main.test.ts # src/cli/help.test.ts # src/core/services/__tests__/stats-server.test.ts # src/core/services/cli-command.test.ts # src/core/services/tokenizer/yomitan-parser-runtime.test.ts # src/main/overlay-runtime.test.ts # src/main/runtime/character-dictionary-auto-sync.test.ts # src/main/runtime/cli-command-context-deps.test.ts # src/main/runtime/cli-command-context-factory.test.ts # src/main/runtime/cli-command-context-main-deps.test.ts # src/main/runtime/composers/cli-startup-composer.test.ts # src/main/runtime/stats-server-runtime.test.ts # src/main/runtime/tray-main-actions.test.ts # src/main/runtime/tray-main-deps.test.ts # src/main/runtime/tray-runtime-handlers.test.ts # src/main/runtime/tray-runtime.test.ts # src/main/runtime/yomitan-anki-server-sync.test.ts # src/preload.ts # src/renderer/handlers/mouse.test.ts # src/renderer/yomitan-popup.test.ts
726 lines
24 KiB
TypeScript
726 lines
24 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import type { HyprlandPlacementStatus } from '../core/services/hyprland-window-placement';
|
|
import {
|
|
FakeOverlayWindow,
|
|
LOADING_BLANK_WINDOW,
|
|
MODAL_GEOMETRY,
|
|
createHarness,
|
|
type FakeOverlayWindowInit,
|
|
} from './overlay-runtime-test-harness';
|
|
import type { OverlayHostedModal } from '../shared/ipc/contracts';
|
|
|
|
const SUBSYNC_PAYLOAD = {
|
|
ffsubsyncAvailable: true,
|
|
videoReferenceAvailable: true,
|
|
subtitleTracks: [],
|
|
defaultReferenceTrackId: null,
|
|
defaultTargetTrackId: null,
|
|
};
|
|
|
|
const PENDING_PLACEMENT: HyprlandPlacementStatus = {
|
|
applicable: true,
|
|
clientFound: false,
|
|
dispatched: false,
|
|
};
|
|
|
|
type FakeTimer = { callback: () => void; active: boolean; unref: () => void };
|
|
|
|
/**
|
|
* Swaps the global setTimeout/clearTimeout for a manual queue while `run` executes. The
|
|
* placement reconcile ladder calls the globals directly, so there is no option to inject.
|
|
*/
|
|
function withFakeTimers(
|
|
run: (timers: { runNext: () => boolean; runAll: () => void; activeCount: () => number }) => void,
|
|
): void {
|
|
const timers: FakeTimer[] = [];
|
|
const originalSetTimeout = globalThis.setTimeout;
|
|
const originalClearTimeout = globalThis.clearTimeout;
|
|
globalThis.setTimeout = ((callback: () => void) => {
|
|
const timer: FakeTimer = { callback, active: true, unref() {} };
|
|
timers.push(timer);
|
|
return timer;
|
|
}) as unknown as typeof globalThis.setTimeout;
|
|
globalThis.clearTimeout = ((timer?: FakeTimer) => {
|
|
if (timer) timer.active = false;
|
|
}) as unknown as typeof globalThis.clearTimeout;
|
|
|
|
const runNext = (): boolean => {
|
|
const timer = timers.find((candidate) => candidate.active);
|
|
if (!timer) return false;
|
|
timer.active = false;
|
|
timer.callback();
|
|
return true;
|
|
};
|
|
try {
|
|
run({
|
|
runNext,
|
|
runAll: () => {
|
|
for (let ran = 0; runNext(); ran += 1) {
|
|
assert.ok(ran < 50, 'timer queue did not drain');
|
|
}
|
|
},
|
|
activeCount: () => timers.filter((timer) => timer.active).length,
|
|
});
|
|
} finally {
|
|
globalThis.setTimeout = originalSetTimeout;
|
|
globalThis.clearTimeout = originalClearTimeout;
|
|
}
|
|
}
|
|
|
|
test('sendToActiveOverlayWindow targets modal window with full geometry and tracks close restore', () => {
|
|
const modal = new FakeOverlayWindow();
|
|
const h = createHarness({ modal });
|
|
|
|
const sent = h.runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
|
restoreOnModalClose: 'runtime-options',
|
|
});
|
|
assert.equal(sent, true);
|
|
assert.equal(h.runtime.getRestoreVisibleOverlayOnModalClose().has('runtime-options'), true);
|
|
assert.deepEqual(h.boundsCalls, [MODAL_GEOMETRY]);
|
|
assert.equal(modal.showCount, 0);
|
|
|
|
h.runtime.notifyOverlayModalOpened('runtime-options');
|
|
assert.equal(h.createCalls, 0);
|
|
assert.equal(modal.showCount, 1);
|
|
assert.equal(modal.isFocused(), true);
|
|
assert.deepEqual(h.boundsCalls, [MODAL_GEOMETRY, MODAL_GEOMETRY]);
|
|
assert.deepEqual(modal.alwaysOnTopCalls, ['top:true:screen-saver:3']);
|
|
assert.deepEqual(modal.sent, [['runtime-options:open']]);
|
|
});
|
|
|
|
test('sendToActiveOverlayWindow creates modal window lazily when absent', () => {
|
|
const modal = new FakeOverlayWindow();
|
|
const h = createHarness({ createModal: () => modal });
|
|
|
|
assert.equal(
|
|
h.runtime.sendToActiveOverlayWindow('jimaku:open', undefined, {
|
|
restoreOnModalClose: 'jimaku',
|
|
}),
|
|
true,
|
|
);
|
|
assert.equal(h.createCalls, 1);
|
|
assert.equal(modal.showCount, 0);
|
|
h.runtime.notifyOverlayModalOpened('jimaku');
|
|
assert.equal(modal.showCount, 1);
|
|
assert.deepEqual(modal.sent, [['jimaku:open']]);
|
|
});
|
|
|
|
for (const platform of ['darwin', 'win32'] as const) {
|
|
test(`primeModalWindow creates and warms a hidden modal on ${platform}`, () => {
|
|
const modal = new FakeOverlayWindow({ ...LOADING_BLANK_WINDOW, documentLoaded: false });
|
|
const h = createHarness({ createModal: () => modal, platform });
|
|
|
|
assert.equal(h.runtime.primeModalWindow(), true);
|
|
assert.equal(h.createCalls, 1);
|
|
assert.equal(modal.isVisible(), false);
|
|
|
|
modal.finishLoad();
|
|
modal.emitReadyToShow();
|
|
|
|
assert.equal(
|
|
h.runtime.sendToActiveOverlayWindow('session-help:open', undefined, {
|
|
restoreOnModalClose: 'session-help',
|
|
preferModalWindow: true,
|
|
}),
|
|
true,
|
|
);
|
|
assert.equal(h.createCalls, 1);
|
|
assert.equal(modal.isVisible(), true);
|
|
assert.deepEqual(modal.sent, [['session-help:open']]);
|
|
});
|
|
}
|
|
|
|
test('sendToActiveOverlayWindow delivers to a modal window created for the send', () => {
|
|
// ready-to-show can fire before the document loads, and Electron still reports
|
|
// isLoading() inside did-finish-load; only did-stop-loading sees the settled state.
|
|
const modal = new FakeOverlayWindow({ ...LOADING_BLANK_WINDOW, documentLoaded: false });
|
|
const h = createHarness({ createModal: () => modal });
|
|
|
|
assert.equal(
|
|
h.runtime.sendToActiveOverlayWindow(
|
|
'media-timing-review:open',
|
|
{ reviewId: 'r1' },
|
|
{ restoreOnModalClose: 'media-timing-review', preferModalWindow: true },
|
|
),
|
|
true,
|
|
);
|
|
modal.emitReadyToShow();
|
|
assert.deepEqual(modal.sent, []);
|
|
modal.finishLoad();
|
|
assert.deepEqual(modal.sent, [['media-timing-review:open', { reviewId: 'r1' }]]);
|
|
});
|
|
|
|
test('primeModalWindow leaves Linux modal creation lazy', () => {
|
|
const h = createHarness({ createModal: () => new FakeOverlayWindow(), platform: 'linux' });
|
|
|
|
assert.equal(h.runtime.primeModalWindow(), false);
|
|
assert.equal(h.createCalls, 0);
|
|
});
|
|
|
|
test('sendToActiveOverlayWindow does not retain restore state when modal creation fails', () => {
|
|
const h = createHarness({ createModal: () => null });
|
|
|
|
assert.equal(
|
|
h.runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
|
restoreOnModalClose: 'runtime-options',
|
|
}),
|
|
false,
|
|
);
|
|
assert.equal(h.runtime.getRestoreVisibleOverlayOnModalClose().has('runtime-options'), false);
|
|
});
|
|
|
|
const readinessCases: Array<{
|
|
name: string;
|
|
window: FakeOverlayWindowInit;
|
|
opens: OverlayHostedModal[];
|
|
sentBeforeLoad: unknown[][];
|
|
}> = [
|
|
{
|
|
name: 'delivers on first modal load without waiting for ready-to-show',
|
|
window: LOADING_BLANK_WINDOW,
|
|
opens: ['runtime-options'],
|
|
sentBeforeLoad: [],
|
|
},
|
|
{
|
|
name: 'delivers when the modal loaded before listeners were registered',
|
|
window: { contentReady: false },
|
|
opens: ['runtime-options'],
|
|
sentBeforeLoad: [['runtime-options:open']],
|
|
},
|
|
{
|
|
name: 'does not infer document readiness from a pending file URL',
|
|
window: { contentReady: false, documentLoaded: false },
|
|
opens: ['runtime-options'],
|
|
sentBeforeLoad: [],
|
|
},
|
|
{
|
|
name: 'rejects stale content readiness during document reload',
|
|
window: { contentReady: true, documentLoaded: false },
|
|
opens: ['session-help'],
|
|
sentBeforeLoad: [],
|
|
},
|
|
{
|
|
name: 'flushes every queued open in order once the modal loads',
|
|
window: LOADING_BLANK_WINDOW,
|
|
opens: ['runtime-options', 'session-help'],
|
|
sentBeforeLoad: [],
|
|
},
|
|
];
|
|
|
|
for (const c of readinessCases) {
|
|
test(`sendToActiveOverlayWindow ${c.name}`, () => {
|
|
const modal = new FakeOverlayWindow(c.window);
|
|
const h = createHarness({ modal });
|
|
|
|
for (const modalId of c.opens) {
|
|
assert.equal(
|
|
h.runtime.sendToActiveOverlayWindow(`${modalId}:open`, undefined, {
|
|
restoreOnModalClose: modalId,
|
|
}),
|
|
true,
|
|
);
|
|
}
|
|
assert.deepEqual(modal.sent, c.sentBeforeLoad);
|
|
|
|
const expected = c.opens.map((modalId) => [`${modalId}:open`]);
|
|
modal.finishLoad();
|
|
assert.deepEqual(modal.sent, expected);
|
|
|
|
// Later ready-to-show and the renderer ack must not resend, and the ack reveals once.
|
|
modal.emitReadyToShow();
|
|
h.runtime.notifyOverlayModalOpened(c.opens[0]!);
|
|
assert.deepEqual(modal.sent, expected);
|
|
assert.equal(modal.showCount, 1);
|
|
assert.equal(h.createCalls, 0);
|
|
});
|
|
}
|
|
|
|
test('handleOverlayModalClosed keeps the modal window warm after all pending modals close', () => {
|
|
const modal = new FakeOverlayWindow();
|
|
const h = createHarness({ modal, platform: 'darwin' });
|
|
|
|
h.runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
|
restoreOnModalClose: 'runtime-options',
|
|
});
|
|
h.runtime.sendToActiveOverlayWindow('subsync:open-manual', SUBSYNC_PAYLOAD, {
|
|
restoreOnModalClose: 'subsync',
|
|
});
|
|
|
|
h.runtime.handleOverlayModalClosed('runtime-options');
|
|
assert.equal(modal.isDestroyed(), false);
|
|
|
|
h.runtime.handleOverlayModalClosed('subsync');
|
|
assert.equal(modal.isDestroyed(), false);
|
|
assert.equal(modal.isVisible(), false);
|
|
assert.equal(modal.ignoreMouseEvents, true);
|
|
assert.equal(h.runtime.getRestoreVisibleOverlayOnModalClose().size, 0);
|
|
});
|
|
|
|
test('sendToActiveOverlayWindow prefers visible main overlay window for modal open', () => {
|
|
const main = new FakeOverlayWindow({ visible: true });
|
|
const h = createHarness({ main, createModal: () => new FakeOverlayWindow() });
|
|
|
|
const sent = h.runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
|
restoreOnModalClose: 'runtime-options',
|
|
});
|
|
|
|
assert.equal(sent, true);
|
|
assert.equal(h.createCalls, 0);
|
|
assert.deepEqual(main.sent, [['runtime-options:open']]);
|
|
});
|
|
|
|
test('sendToActiveOverlayWindow can prefer modal window even when main overlay is visible', () => {
|
|
const main = new FakeOverlayWindow({ visible: true });
|
|
const modal = new FakeOverlayWindow();
|
|
const h = createHarness({ main, modal });
|
|
|
|
const sent = h.runtime.sendToActiveOverlayWindow(
|
|
'youtube:picker-open',
|
|
{ sessionId: 'yt-1' },
|
|
{ restoreOnModalClose: 'youtube-track-picker', preferModalWindow: true },
|
|
);
|
|
|
|
assert.equal(sent, true);
|
|
assert.deepEqual(main.sent, []);
|
|
assert.deepEqual(modal.sent, [['youtube:picker-open', { sessionId: 'yt-1' }]]);
|
|
});
|
|
|
|
test('modal window path makes visible main overlay click-through until modal closes', () => {
|
|
const main = new FakeOverlayWindow({ visible: true });
|
|
const modal = new FakeOverlayWindow();
|
|
const h = createHarness({ main, modal });
|
|
|
|
const sent = h.runtime.sendToActiveOverlayWindow(
|
|
'youtube:picker-open',
|
|
{ sessionId: 'yt-1' },
|
|
{ restoreOnModalClose: 'youtube-track-picker', preferModalWindow: true },
|
|
);
|
|
h.runtime.notifyOverlayModalOpened('youtube-track-picker');
|
|
|
|
assert.equal(sent, true);
|
|
assert.equal(main.ignoreMouseEvents, true);
|
|
assert.equal(main.forwardedIgnoreMouseEvents, true);
|
|
assert.equal(modal.ignoreMouseEvents, false);
|
|
|
|
h.runtime.handleOverlayModalClosed('youtube-track-picker');
|
|
|
|
assert.equal(main.ignoreMouseEvents, true);
|
|
});
|
|
|
|
test('modal window path restores visible main overlay before modal input deactivates', () => {
|
|
const main = new FakeOverlayWindow({ visible: true });
|
|
const events: string[] = [];
|
|
const h = createHarness({
|
|
main,
|
|
modal: new FakeOverlayWindow(),
|
|
options: {
|
|
onModalStateChange: (active) => events.push(`state:${active}:visible:${main.isVisible()}`),
|
|
},
|
|
});
|
|
|
|
h.runtime.sendToActiveOverlayWindow(
|
|
'youtube:picker-open',
|
|
{ sessionId: 'yt-1' },
|
|
{ restoreOnModalClose: 'youtube-track-picker', preferModalWindow: true },
|
|
);
|
|
h.runtime.notifyOverlayModalOpened('youtube-track-picker');
|
|
|
|
assert.equal(main.hideCount, 1);
|
|
assert.equal(main.isVisible(), false);
|
|
|
|
h.runtime.handleOverlayModalClosed('youtube-track-picker');
|
|
|
|
assert.equal(main.showCount, 1);
|
|
assert.equal(main.isVisible(), true);
|
|
assert.deepEqual(events, ['state:true:visible:true', 'state:false:visible:true']);
|
|
});
|
|
|
|
test('macOS maps a new modal panel before focusing SubMiner and hiding the subtitle overlay', () => {
|
|
const log: string[] = [];
|
|
const main = new FakeOverlayWindow({ name: 'main', log, visible: true });
|
|
const modal = new FakeOverlayWindow({ name: 'modal', log });
|
|
const h = createHarness({
|
|
main,
|
|
modal,
|
|
platform: 'darwin',
|
|
options: { focusApplication: () => log.push('focus-application') },
|
|
});
|
|
|
|
h.runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
|
restoreOnModalClose: 'runtime-options',
|
|
preferModalWindow: true,
|
|
});
|
|
h.runtime.notifyOverlayModalOpened('runtime-options');
|
|
|
|
assert.deepEqual(log, ['modal:show-inactive', 'focus-application', 'main:hide']);
|
|
assert.equal(modal.isVisible(), true);
|
|
assert.equal(main.isVisible(), false);
|
|
});
|
|
|
|
test('modal window path runs final close handoff before modal input deactivates', () => {
|
|
const main = new FakeOverlayWindow({ visible: true });
|
|
const events: string[] = [];
|
|
const h = createHarness({
|
|
main,
|
|
modal: new FakeOverlayWindow(),
|
|
options: {
|
|
onFinalModalClosed: () => events.push(`handoff:visible:${main.isVisible()}`),
|
|
onModalStateChange: (active) => events.push(`state:${active}:visible:${main.isVisible()}`),
|
|
},
|
|
});
|
|
|
|
h.runtime.sendToActiveOverlayWindow(
|
|
'youtube:picker-open',
|
|
{ sessionId: 'yt-1' },
|
|
{ restoreOnModalClose: 'youtube-track-picker', preferModalWindow: true },
|
|
);
|
|
h.runtime.notifyOverlayModalOpened('youtube-track-picker');
|
|
h.runtime.handleOverlayModalClosed('youtube-track-picker');
|
|
|
|
assert.deepEqual(events, [
|
|
'state:true:visible:true',
|
|
'handoff:visible:true',
|
|
'state:false:visible:true',
|
|
]);
|
|
});
|
|
|
|
test('modal runtime deactivates modal state when final close handoff throws', () => {
|
|
const events: string[] = [];
|
|
const h = createHarness({
|
|
main: new FakeOverlayWindow({ visible: true }),
|
|
modal: new FakeOverlayWindow(),
|
|
options: {
|
|
onFinalModalClosed: () => {
|
|
events.push('handoff');
|
|
throw new Error('handoff failed');
|
|
},
|
|
onModalStateChange: (active) => events.push(`state:${active}`),
|
|
},
|
|
});
|
|
|
|
h.runtime.sendToActiveOverlayWindow(
|
|
'youtube:picker-open',
|
|
{ sessionId: 'yt-1' },
|
|
{ restoreOnModalClose: 'youtube-track-picker', preferModalWindow: true },
|
|
);
|
|
h.runtime.notifyOverlayModalOpened('youtube-track-picker');
|
|
|
|
assert.doesNotThrow(() => h.runtime.handleOverlayModalClosed('youtube-track-picker'));
|
|
assert.deepEqual(events, ['state:true', 'handoff', 'state:false']);
|
|
});
|
|
|
|
test('modal runtime notifies callers when modal input state becomes active/inactive', () => {
|
|
const state: boolean[] = [];
|
|
const h = createHarness({
|
|
modal: new FakeOverlayWindow(),
|
|
options: { onModalStateChange: (active) => state.push(active) },
|
|
});
|
|
|
|
h.runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
|
restoreOnModalClose: 'runtime-options',
|
|
});
|
|
h.runtime.sendToActiveOverlayWindow('subsync:open-manual', SUBSYNC_PAYLOAD, {
|
|
restoreOnModalClose: 'subsync',
|
|
});
|
|
assert.deepEqual(state, []);
|
|
h.runtime.notifyOverlayModalOpened('runtime-options');
|
|
assert.deepEqual(state, [true]);
|
|
|
|
h.runtime.handleOverlayModalClosed('runtime-options');
|
|
assert.deepEqual(state, [true]);
|
|
|
|
h.runtime.handleOverlayModalClosed('subsync');
|
|
assert.deepEqual(state, [true, false]);
|
|
});
|
|
|
|
test('notifyOverlayModalOpened enables input on visible main overlay window when no modal window exists', () => {
|
|
const main = new FakeOverlayWindow({ visible: true, ignoreMouseEvents: true });
|
|
const state: boolean[] = [];
|
|
const h = createHarness({
|
|
main,
|
|
options: { onModalStateChange: (active) => state.push(active) },
|
|
});
|
|
|
|
const sent = h.runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
|
restoreOnModalClose: 'runtime-options',
|
|
});
|
|
h.runtime.notifyOverlayModalOpened('runtime-options');
|
|
|
|
assert.equal(sent, true);
|
|
assert.equal(h.createCalls, 0);
|
|
assert.deepEqual(state, [true]);
|
|
assert.equal(main.ignoreMouseEvents, false);
|
|
assert.equal(main.isFocused(), true);
|
|
assert.equal(main.webContentsFocused, true);
|
|
});
|
|
|
|
test('handleOverlayModalClosed is a no-op when no modal window can be targeted', () => {
|
|
const state: boolean[] = [];
|
|
const h = createHarness({
|
|
createModal: () => null,
|
|
options: { onModalStateChange: (active) => state.push(active) },
|
|
});
|
|
|
|
const sent = h.runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
|
restoreOnModalClose: 'runtime-options',
|
|
});
|
|
assert.equal(sent, false);
|
|
h.runtime.notifyOverlayModalOpened('runtime-options');
|
|
h.runtime.handleOverlayModalClosed('runtime-options');
|
|
|
|
assert.deepEqual(state, []);
|
|
});
|
|
|
|
test('modal fallback reveal skips showing window when content is not ready', () => {
|
|
const modal = new FakeOverlayWindow(LOADING_BLANK_WINDOW);
|
|
let scheduledReveal: (() => void) | null = null;
|
|
const h = createHarness({
|
|
modal,
|
|
platform: 'darwin',
|
|
options: {
|
|
scheduleRevealFallback: (callback) => {
|
|
scheduledReveal = callback;
|
|
return { scheduled: true } as never;
|
|
},
|
|
clearRevealFallback: () => {
|
|
scheduledReveal = null;
|
|
},
|
|
},
|
|
});
|
|
|
|
const sent = h.runtime.sendToActiveOverlayWindow('jimaku:open', undefined, {
|
|
restoreOnModalClose: 'jimaku',
|
|
});
|
|
|
|
assert.equal(sent, true);
|
|
assert.ok(scheduledReveal, 'expected reveal callback');
|
|
(scheduledReveal as () => void)();
|
|
assert.equal(modal.showCount, 0);
|
|
|
|
h.runtime.notifyOverlayModalOpened('jimaku');
|
|
assert.equal(modal.showCount, 1);
|
|
assert.equal(modal.ignoreMouseEvents, false);
|
|
});
|
|
|
|
test('modal reopen reuses the warm window and shows it immediately on macOS', () => {
|
|
const modal = new FakeOverlayWindow();
|
|
const state: boolean[] = [];
|
|
const h = createHarness({
|
|
modal,
|
|
createModal: () => modal,
|
|
platform: 'darwin',
|
|
options: { onModalStateChange: (active) => state.push(active) },
|
|
});
|
|
|
|
h.runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
|
restoreOnModalClose: 'runtime-options',
|
|
});
|
|
h.runtime.notifyOverlayModalOpened('runtime-options');
|
|
h.runtime.handleOverlayModalClosed('runtime-options');
|
|
|
|
assert.equal(modal.isDestroyed(), false);
|
|
assert.equal(modal.isVisible(), false);
|
|
assert.deepEqual(state, [true, false]);
|
|
|
|
const sent = h.runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
|
restoreOnModalClose: 'runtime-options',
|
|
});
|
|
|
|
assert.equal(sent, true);
|
|
assert.equal(h.createCalls, 0);
|
|
assert.equal(modal.isVisible(), true);
|
|
assert.equal(modal.showCount, 2);
|
|
|
|
h.runtime.notifyOverlayModalOpened('runtime-options');
|
|
assert.deepEqual(state, [true, false, true]);
|
|
});
|
|
|
|
test('modal reopen on Windows uses a fresh prewarmed interactive window', () => {
|
|
const firstWindow = new FakeOverlayWindow();
|
|
const replacementWindow = new FakeOverlayWindow();
|
|
const h = createHarness({
|
|
modal: firstWindow,
|
|
createModal: () => replacementWindow,
|
|
platform: 'win32',
|
|
});
|
|
|
|
h.runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
|
restoreOnModalClose: 'runtime-options',
|
|
});
|
|
h.runtime.notifyOverlayModalOpened('runtime-options');
|
|
h.runtime.handleOverlayModalClosed('runtime-options');
|
|
|
|
assert.equal(firstWindow.isDestroyed(), true);
|
|
assert.equal(h.modal, replacementWindow);
|
|
assert.equal(replacementWindow.isVisible(), false);
|
|
assert.equal(h.createCalls, 1);
|
|
|
|
const sent = h.runtime.sendToActiveOverlayWindow('session-help:open', undefined, {
|
|
restoreOnModalClose: 'session-help',
|
|
});
|
|
|
|
assert.equal(sent, true);
|
|
assert.equal(h.createCalls, 1);
|
|
assert.equal(replacementWindow.isVisible(), true);
|
|
assert.equal(replacementWindow.ignoreMouseEvents, false);
|
|
assert.deepEqual(replacementWindow.sent, [['session-help:open']]);
|
|
});
|
|
|
|
test('visible stale modal window is made interactive again before reopening', () => {
|
|
const modal = new FakeOverlayWindow({
|
|
visible: true,
|
|
focused: true,
|
|
webContentsFocused: false,
|
|
ignoreMouseEvents: true,
|
|
});
|
|
const h = createHarness({ modal });
|
|
|
|
const sent = h.runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
|
restoreOnModalClose: 'runtime-options',
|
|
});
|
|
|
|
assert.equal(sent, true);
|
|
assert.equal(modal.ignoreMouseEvents, false);
|
|
assert.equal(modal.isFocused(), true);
|
|
assert.equal(modal.webContentsFocused, true);
|
|
assert.deepEqual(modal.sent, [['runtime-options:open']]);
|
|
});
|
|
|
|
test('waitForModalOpen resolves true after modal acknowledgement', async () => {
|
|
const h = createHarness({ modal: new FakeOverlayWindow() });
|
|
|
|
h.runtime.sendToActiveOverlayWindow(
|
|
'youtube:picker-open',
|
|
{ sessionId: 'yt-1' },
|
|
{ restoreOnModalClose: 'youtube-track-picker' },
|
|
);
|
|
const pending = h.runtime.waitForModalOpen('youtube-track-picker', 1000);
|
|
h.runtime.notifyOverlayModalOpened('youtube-track-picker');
|
|
|
|
assert.equal(await pending, true);
|
|
});
|
|
|
|
test('waitForModalOpen resolves true when modal acknowledgement arrives before waiter registration', async () => {
|
|
const h = createHarness({ modal: new FakeOverlayWindow() });
|
|
|
|
h.runtime.sendToActiveOverlayWindow(
|
|
'kiku:field-grouping-request',
|
|
{},
|
|
{ restoreOnModalClose: 'kiku' },
|
|
);
|
|
h.runtime.notifyOverlayModalOpened('kiku');
|
|
|
|
assert.equal(await h.runtime.waitForModalOpen('kiku', 5), true);
|
|
});
|
|
|
|
test('waitForModalOpen resolves false on timeout', async () => {
|
|
const h = createHarness();
|
|
|
|
assert.equal(await h.runtime.waitForModalOpen('youtube-track-picker', 5), false);
|
|
});
|
|
|
|
function openKikuInModalWindow(h: ReturnType<typeof createHarness>): void {
|
|
h.runtime.sendToActiveOverlayWindow(
|
|
'kiku:field-grouping-open',
|
|
{ test: true },
|
|
{ restoreOnModalClose: 'kiku', preferModalWindow: true },
|
|
);
|
|
}
|
|
|
|
test('modal placement reconcile retries until the Hyprland client is mapped', () => {
|
|
withFakeTimers((timers) => {
|
|
// The compositor never maps the window, so every reconcile reports pending.
|
|
const h = createHarness({
|
|
modal: new FakeOverlayWindow(),
|
|
setModalWindowBounds: () => PENDING_PLACEMENT,
|
|
});
|
|
openKikuInModalWindow(h);
|
|
h.runtime.notifyOverlayModalOpened('kiku');
|
|
const reconcilesBeforeLadder = h.boundsCalls.length;
|
|
|
|
timers.runAll();
|
|
|
|
// One re-assert per post-show ladder delay.
|
|
assert.equal(h.boundsCalls.length - reconcilesBeforeLadder, 6);
|
|
});
|
|
});
|
|
|
|
test('modal placement reconcile stops retrying once the client is mapped', () => {
|
|
withFakeTimers((timers) => {
|
|
const h = createHarness({
|
|
modal: new FakeOverlayWindow(),
|
|
setModalWindowBounds: () => ({ applicable: true, clientFound: true, dispatched: true }),
|
|
});
|
|
openKikuInModalWindow(h);
|
|
h.runtime.notifyOverlayModalOpened('kiku');
|
|
const reconcilesBeforeLadder = h.boundsCalls.length;
|
|
|
|
timers.runAll();
|
|
|
|
assert.equal(h.boundsCalls.length - reconcilesBeforeLadder, 1);
|
|
});
|
|
});
|
|
|
|
test('modal placement reconcile cancels stale retry ladder after a newer visible modal interaction', () => {
|
|
withFakeTimers((timers) => {
|
|
const h = createHarness({
|
|
modal: new FakeOverlayWindow(),
|
|
setModalWindowBounds: () => PENDING_PLACEMENT,
|
|
});
|
|
openKikuInModalWindow(h);
|
|
h.runtime.notifyOverlayModalOpened('kiku');
|
|
assert.equal(timers.activeCount(), 1);
|
|
|
|
openKikuInModalWindow(h);
|
|
assert.equal(timers.activeCount(), 2);
|
|
|
|
timers.runNext();
|
|
|
|
assert.equal(timers.activeCount(), 1, 'stale retry should not schedule a continuation');
|
|
});
|
|
});
|
|
|
|
test('Linux keeps the dedicated modal window unmapped until the renderer opens the modal, then hides the overlay before revealing it', () => {
|
|
const log: string[] = [];
|
|
const main = new FakeOverlayWindow({ name: 'main', log, visible: true });
|
|
const modal = new FakeOverlayWindow({ name: 'modal', log });
|
|
let revealScheduled = false;
|
|
const h = createHarness({
|
|
main,
|
|
modal,
|
|
platform: 'linux',
|
|
options: {
|
|
scheduleRevealFallback: () => {
|
|
revealScheduled = true;
|
|
return { scheduled: true } as never;
|
|
},
|
|
clearRevealFallback: () => {},
|
|
},
|
|
});
|
|
|
|
const open = () =>
|
|
h.runtime.sendToActiveOverlayWindow(
|
|
'media-timing-review:open',
|
|
{ reviewId: 'review' },
|
|
{ restoreOnModalClose: 'media-timing-review', preferModalWindow: true },
|
|
);
|
|
|
|
assert.equal(open(), true);
|
|
assert.deepEqual(modal.sent, [['media-timing-review:open', { reviewId: 'review' }]]);
|
|
assert.equal(revealScheduled, false);
|
|
assert.equal(modal.showCount, 0);
|
|
assert.equal(main.hideCount, 0);
|
|
|
|
// The open retry must not map the window before the renderer answers either.
|
|
assert.equal(open(), true);
|
|
assert.equal(modal.showCount, 0);
|
|
|
|
h.runtime.notifyOverlayModalOpened('media-timing-review');
|
|
|
|
assert.deepEqual(log, ['main:hide', 'modal:show']);
|
|
assert.equal(main.isVisible(), false);
|
|
assert.equal(modal.isVisible(), true);
|
|
assert.equal(modal.ignoreMouseEvents, false);
|
|
});
|