mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
Ensure overlay modal grabs input
This commit is contained in:
218
src/main/overlay-runtime.test.ts
Normal file
218
src/main/overlay-runtime.test.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createOverlayModalRuntimeService } from './overlay-runtime';
|
||||
|
||||
type MockWindow = {
|
||||
destroyed: boolean;
|
||||
visible: boolean;
|
||||
focused: boolean;
|
||||
ignoreMouseEvents: boolean;
|
||||
webContentsFocused: boolean;
|
||||
showCount: number;
|
||||
hideCount: number;
|
||||
sent: unknown[][];
|
||||
loading: boolean;
|
||||
loadCallbacks: Array<() => void>;
|
||||
};
|
||||
|
||||
function createMockWindow(): MockWindow & {
|
||||
isDestroyed: () => boolean;
|
||||
isVisible: () => boolean;
|
||||
isFocused: () => boolean;
|
||||
setIgnoreMouseEvents: (ignore: boolean) => void;
|
||||
getShowCount: () => number;
|
||||
getHideCount: () => number;
|
||||
show: () => void;
|
||||
hide: () => void;
|
||||
focus: () => void;
|
||||
webContents: {
|
||||
focused: boolean;
|
||||
isLoading: () => boolean;
|
||||
send: (channel: string, payload?: unknown) => void;
|
||||
isFocused: () => boolean;
|
||||
once: (event: 'did-finish-load', cb: () => void) => void;
|
||||
focus: () => void;
|
||||
};
|
||||
} {
|
||||
const state: MockWindow = {
|
||||
destroyed: false,
|
||||
visible: false,
|
||||
focused: false,
|
||||
ignoreMouseEvents: false,
|
||||
webContentsFocused: false,
|
||||
showCount: 0,
|
||||
hideCount: 0,
|
||||
sent: [],
|
||||
loading: false,
|
||||
loadCallbacks: [],
|
||||
};
|
||||
return {
|
||||
...state,
|
||||
isDestroyed: () => state.destroyed,
|
||||
isVisible: () => state.visible,
|
||||
isFocused: () => state.focused,
|
||||
setIgnoreMouseEvents: (ignore: boolean) => {
|
||||
state.ignoreMouseEvents = ignore;
|
||||
},
|
||||
getShowCount: () => state.showCount,
|
||||
getHideCount: () => state.hideCount,
|
||||
show: () => {
|
||||
state.visible = true;
|
||||
state.showCount += 1;
|
||||
},
|
||||
hide: () => {
|
||||
state.visible = false;
|
||||
state.hideCount += 1;
|
||||
},
|
||||
focus: () => {
|
||||
state.focused = true;
|
||||
},
|
||||
webContents: {
|
||||
isLoading: () => state.loading,
|
||||
send: (channel, payload) => {
|
||||
if (payload === undefined) {
|
||||
state.sent.push([channel]);
|
||||
return;
|
||||
}
|
||||
state.sent.push([channel, payload]);
|
||||
},
|
||||
focused: false,
|
||||
isFocused: () => state.webContentsFocused,
|
||||
once: (_event, cb) => {
|
||||
state.loadCallbacks.push(cb);
|
||||
},
|
||||
focus: () => {
|
||||
state.webContentsFocused = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('sendToActiveOverlayWindow targets modal window with full geometry and tracks close restore', () => {
|
||||
const window = createMockWindow();
|
||||
const calls: string[] = [];
|
||||
const runtime = createOverlayModalRuntimeService({
|
||||
getMainWindow: () => null,
|
||||
getInvisibleWindow: () => null,
|
||||
getModalWindow: () => window as never,
|
||||
createModalWindow: () => {
|
||||
calls.push('create-modal-window');
|
||||
return window as never;
|
||||
},
|
||||
getModalGeometry: () => ({ x: 10, y: 20, width: 300, height: 200 }),
|
||||
setModalWindowBounds: (geometry) => {
|
||||
calls.push(`bounds:${geometry.x},${geometry.y},${geometry.width},${geometry.height}`);
|
||||
},
|
||||
});
|
||||
|
||||
const sent = runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||
restoreOnModalClose: 'runtime-options',
|
||||
});
|
||||
assert.equal(sent, true);
|
||||
assert.equal(runtime.getRestoreVisibleOverlayOnModalClose().has('runtime-options'), true);
|
||||
assert.deepEqual(calls, ['bounds:10,20,300,200']);
|
||||
assert.equal(window.getShowCount(), 1);
|
||||
assert.equal(window.isFocused(), true);
|
||||
assert.deepEqual(window.sent, [['runtime-options:open']]);
|
||||
});
|
||||
|
||||
test('sendToActiveOverlayWindow creates modal window lazily when absent', () => {
|
||||
const window = createMockWindow();
|
||||
let modalWindow: ReturnType<typeof createMockWindow> | null = null;
|
||||
const runtime = createOverlayModalRuntimeService({
|
||||
getMainWindow: () => null,
|
||||
getInvisibleWindow: () => null,
|
||||
getModalWindow: () => modalWindow as never,
|
||||
createModalWindow: () => {
|
||||
modalWindow = window;
|
||||
return modalWindow as never;
|
||||
},
|
||||
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||
setModalWindowBounds: () => {},
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
runtime.sendToActiveOverlayWindow('jimaku:open', undefined, { restoreOnModalClose: 'jimaku' }),
|
||||
true,
|
||||
);
|
||||
assert.deepEqual(window.sent, [['jimaku:open']]);
|
||||
});
|
||||
|
||||
test('handleOverlayModalClosed hides modal window only after all pending modals close', () => {
|
||||
const window = createMockWindow();
|
||||
const runtime = createOverlayModalRuntimeService({
|
||||
getMainWindow: () => null,
|
||||
getInvisibleWindow: () => null,
|
||||
getModalWindow: () => window as never,
|
||||
createModalWindow: () => window as never,
|
||||
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||
setModalWindowBounds: () => {},
|
||||
});
|
||||
|
||||
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||
restoreOnModalClose: 'runtime-options',
|
||||
});
|
||||
runtime.sendToActiveOverlayWindow('subsync:open-manual', { sourceTracks: [] }, {
|
||||
restoreOnModalClose: 'subsync',
|
||||
});
|
||||
|
||||
runtime.handleOverlayModalClosed('runtime-options');
|
||||
assert.equal(window.getHideCount(), 0);
|
||||
|
||||
runtime.handleOverlayModalClosed('subsync');
|
||||
assert.equal(window.getHideCount(), 1);
|
||||
});
|
||||
|
||||
test('modal runtime notifies callers when modal input state becomes active/inactive', () => {
|
||||
const window = createMockWindow();
|
||||
const state: boolean[] = [];
|
||||
const runtime = createOverlayModalRuntimeService(
|
||||
{
|
||||
getMainWindow: () => null,
|
||||
getInvisibleWindow: () => null,
|
||||
getModalWindow: () => window as never,
|
||||
createModalWindow: () => window as never,
|
||||
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||
setModalWindowBounds: () => {},
|
||||
},
|
||||
{
|
||||
onModalStateChange: (active: boolean): void => {
|
||||
state.push(active);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||
restoreOnModalClose: 'runtime-options',
|
||||
});
|
||||
runtime.sendToActiveOverlayWindow('subsync:open-manual', { sourceTracks: [] }, {
|
||||
restoreOnModalClose: 'subsync',
|
||||
});
|
||||
assert.deepEqual(state, [true]);
|
||||
|
||||
runtime.handleOverlayModalClosed('runtime-options');
|
||||
assert.deepEqual(state, [true]);
|
||||
|
||||
runtime.handleOverlayModalClosed('subsync');
|
||||
assert.deepEqual(state, [true, false]);
|
||||
});
|
||||
|
||||
test('handleOverlayModalClosed hides modal window for single kiku modal', () => {
|
||||
const window = createMockWindow();
|
||||
const runtime = createOverlayModalRuntimeService({
|
||||
getMainWindow: () => null,
|
||||
getInvisibleWindow: () => null,
|
||||
getModalWindow: () => window as never,
|
||||
createModalWindow: () => window as never,
|
||||
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||
setModalWindowBounds: () => {},
|
||||
});
|
||||
|
||||
runtime.sendToActiveOverlayWindow('kiku:field-grouping-open', { test: true }, {
|
||||
restoreOnModalClose: 'kiku',
|
||||
});
|
||||
runtime.handleOverlayModalClosed('kiku');
|
||||
|
||||
assert.equal(window.getHideCount(), 1);
|
||||
assert.equal(runtime.getRestoreVisibleOverlayOnModalClose().size, 0);
|
||||
});
|
||||
@@ -1,11 +1,16 @@
|
||||
import type { BrowserWindow } from 'electron';
|
||||
import type { WindowGeometry } from '../types';
|
||||
|
||||
type OverlayHostedModal = 'runtime-options' | 'subsync' | 'jimaku';
|
||||
type OverlayHostedModal = 'runtime-options' | 'subsync' | 'jimaku' | 'kiku';
|
||||
type OverlayHostLayer = 'visible' | 'invisible';
|
||||
|
||||
export interface OverlayWindowResolver {
|
||||
getMainWindow: () => BrowserWindow | null;
|
||||
getInvisibleWindow: () => BrowserWindow | null;
|
||||
getModalWindow: () => BrowserWindow | null;
|
||||
createModalWindow: () => BrowserWindow | null;
|
||||
getModalGeometry: () => WindowGeometry;
|
||||
setModalWindowBounds: (geometry: WindowGeometry) => void;
|
||||
}
|
||||
|
||||
export interface OverlayModalRuntime {
|
||||
@@ -19,9 +24,34 @@ export interface OverlayModalRuntime {
|
||||
getRestoreVisibleOverlayOnModalClose: () => Set<OverlayHostedModal>;
|
||||
}
|
||||
|
||||
export function createOverlayModalRuntimeService(deps: OverlayWindowResolver): OverlayModalRuntime {
|
||||
export interface OverlayModalRuntimeOptions {
|
||||
onModalStateChange?: (isActive: boolean) => void;
|
||||
}
|
||||
|
||||
export function createOverlayModalRuntimeService(
|
||||
deps: OverlayWindowResolver,
|
||||
options: OverlayModalRuntimeOptions = {},
|
||||
): OverlayModalRuntime {
|
||||
const restoreVisibleOverlayOnModalClose = new Set<OverlayHostedModal>();
|
||||
const overlayModalAutoShownLayer = new Map<OverlayHostedModal, OverlayHostLayer>();
|
||||
let modalActive = false;
|
||||
|
||||
const notifyModalStateChange = (nextState: boolean): void => {
|
||||
if (modalActive === nextState) return;
|
||||
modalActive = nextState;
|
||||
options.onModalStateChange?.(nextState);
|
||||
};
|
||||
|
||||
const resolveModalWindow = (): BrowserWindow | null => {
|
||||
const existingWindow = deps.getModalWindow();
|
||||
if (existingWindow && !existingWindow.isDestroyed()) {
|
||||
return existingWindow;
|
||||
}
|
||||
const createdWindow = deps.createModalWindow();
|
||||
if (!createdWindow || createdWindow.isDestroyed()) {
|
||||
return null;
|
||||
}
|
||||
return createdWindow;
|
||||
};
|
||||
|
||||
const getTargetOverlayWindow = (): {
|
||||
window: BrowserWindow;
|
||||
@@ -41,6 +71,15 @@ export function createOverlayModalRuntimeService(deps: OverlayWindowResolver): O
|
||||
return null;
|
||||
};
|
||||
|
||||
const showModalWindow = (window: BrowserWindow): void => {
|
||||
window.show();
|
||||
window.setIgnoreMouseEvents(false);
|
||||
window.focus();
|
||||
if (!window.webContents.isFocused()) {
|
||||
window.webContents.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const showOverlayWindowForModal = (window: BrowserWindow, layer: OverlayHostLayer): void => {
|
||||
if (layer === 'invisible' && typeof window.showInactive === 'function') {
|
||||
window.showInactive();
|
||||
@@ -57,39 +96,66 @@ export function createOverlayModalRuntimeService(deps: OverlayWindowResolver): O
|
||||
payload?: unknown,
|
||||
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
|
||||
): boolean => {
|
||||
const restoreOnModalClose = runtimeOptions?.restoreOnModalClose;
|
||||
|
||||
const sendNow = (window: BrowserWindow): void => {
|
||||
if (payload === undefined) {
|
||||
window.webContents.send(channel);
|
||||
} else {
|
||||
window.webContents.send(channel, payload);
|
||||
}
|
||||
};
|
||||
|
||||
if (restoreOnModalClose) {
|
||||
const modalWindow = resolveModalWindow();
|
||||
if (!modalWindow) return false;
|
||||
|
||||
deps.setModalWindowBounds(deps.getModalGeometry());
|
||||
const wasVisible = modalWindow.isVisible();
|
||||
const wasModalActive = restoreVisibleOverlayOnModalClose.size > 0;
|
||||
restoreVisibleOverlayOnModalClose.add(restoreOnModalClose);
|
||||
if (!wasModalActive) {
|
||||
notifyModalStateChange(true);
|
||||
}
|
||||
|
||||
if (!wasVisible) {
|
||||
showModalWindow(modalWindow);
|
||||
} else if (!modalWindow.isFocused()) {
|
||||
showModalWindow(modalWindow);
|
||||
}
|
||||
|
||||
if (modalWindow.webContents.isLoading()) {
|
||||
modalWindow.webContents.once('did-finish-load', () => {
|
||||
if (modalWindow && !modalWindow.isDestroyed() && !modalWindow.webContents.isLoading()) {
|
||||
sendNow(modalWindow);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
sendNow(modalWindow);
|
||||
return true;
|
||||
}
|
||||
|
||||
const target = getTargetOverlayWindow();
|
||||
if (!target) return false;
|
||||
|
||||
const { window: targetWindow, layer } = target;
|
||||
const wasVisible = targetWindow.isVisible();
|
||||
const restoreOnModalClose = runtimeOptions?.restoreOnModalClose;
|
||||
|
||||
const sendNow = (): void => {
|
||||
if (payload === undefined) {
|
||||
targetWindow.webContents.send(channel);
|
||||
} else {
|
||||
targetWindow.webContents.send(channel, payload);
|
||||
}
|
||||
};
|
||||
|
||||
if (!wasVisible) {
|
||||
showOverlayWindowForModal(targetWindow, layer);
|
||||
}
|
||||
if (!wasVisible && restoreOnModalClose) {
|
||||
restoreVisibleOverlayOnModalClose.add(restoreOnModalClose);
|
||||
overlayModalAutoShownLayer.set(restoreOnModalClose, layer);
|
||||
}
|
||||
|
||||
if (targetWindow.webContents.isLoading()) {
|
||||
targetWindow.webContents.once('did-finish-load', () => {
|
||||
if (targetWindow && !targetWindow.isDestroyed() && !targetWindow.webContents.isLoading()) {
|
||||
sendNow();
|
||||
sendNow(targetWindow);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
sendNow();
|
||||
sendNow(targetWindow);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -102,24 +168,13 @@ export function createOverlayModalRuntimeService(deps: OverlayWindowResolver): O
|
||||
const handleOverlayModalClosed = (modal: OverlayHostedModal): void => {
|
||||
if (!restoreVisibleOverlayOnModalClose.has(modal)) return;
|
||||
restoreVisibleOverlayOnModalClose.delete(modal);
|
||||
const layer = overlayModalAutoShownLayer.get(modal);
|
||||
overlayModalAutoShownLayer.delete(modal);
|
||||
if (!layer) return;
|
||||
const shouldKeepLayerVisible = [...restoreVisibleOverlayOnModalClose].some(
|
||||
(pendingModal) => overlayModalAutoShownLayer.get(pendingModal) === layer,
|
||||
);
|
||||
if (shouldKeepLayerVisible) return;
|
||||
|
||||
if (layer === 'visible') {
|
||||
const mainWindow = deps.getMainWindow();
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.hide();
|
||||
}
|
||||
return;
|
||||
const modalWindow = deps.getModalWindow();
|
||||
if (!modalWindow || modalWindow.isDestroyed()) return;
|
||||
if (restoreVisibleOverlayOnModalClose.size === 0) {
|
||||
notifyModalStateChange(false);
|
||||
}
|
||||
const invisibleWindow = deps.getInvisibleWindow();
|
||||
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
|
||||
invisibleWindow.hide();
|
||||
if (restoreVisibleOverlayOnModalClose.size === 0) {
|
||||
modalWindow.hide();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -20,11 +20,23 @@ test('overlay content measurement store main deps builder maps callbacks', () =>
|
||||
test('overlay modal runtime main deps builder maps window resolvers', () => {
|
||||
const mainWindow = { id: 'main' };
|
||||
const invisibleWindow = { id: 'invisible' };
|
||||
const modalWindow = { id: 'modal' };
|
||||
const calls: string[] = [];
|
||||
const deps = createBuildOverlayModalRuntimeMainDepsHandler({
|
||||
getMainWindow: () => mainWindow as never,
|
||||
getInvisibleWindow: () => invisibleWindow as never,
|
||||
getModalWindow: () => modalWindow as never,
|
||||
createModalWindow: () => modalWindow as never,
|
||||
getModalGeometry: () => ({ x: 1, y: 2, width: 3, height: 4 }),
|
||||
setModalWindowBounds: (geometry) =>
|
||||
calls.push(`modal-bounds:${geometry.x},${geometry.y},${geometry.width},${geometry.height}`),
|
||||
})();
|
||||
|
||||
assert.equal(deps.getMainWindow(), mainWindow);
|
||||
assert.equal(deps.getInvisibleWindow(), invisibleWindow);
|
||||
assert.equal(deps.getModalWindow(), modalWindow);
|
||||
assert.equal(deps.createModalWindow(), modalWindow);
|
||||
assert.deepEqual(deps.getModalGeometry(), { x: 1, y: 2, width: 3, height: 4 });
|
||||
deps.setModalWindowBounds({ x: 10, y: 20, width: 30, height: 40 });
|
||||
assert.deepEqual(calls, ['modal-bounds:10,20,30,40']);
|
||||
});
|
||||
|
||||
@@ -14,9 +14,15 @@ export function createBuildOverlayContentMeasurementStoreMainDepsHandler(
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildOverlayModalRuntimeMainDepsHandler(deps: OverlayWindowResolver) {
|
||||
export function createBuildOverlayModalRuntimeMainDepsHandler(
|
||||
deps: OverlayWindowResolver,
|
||||
) {
|
||||
return (): OverlayWindowResolver => ({
|
||||
getMainWindow: () => deps.getMainWindow(),
|
||||
getInvisibleWindow: () => deps.getInvisibleWindow(),
|
||||
getModalWindow: () => deps.getModalWindow(),
|
||||
createModalWindow: () => deps.createModalWindow(),
|
||||
getModalGeometry: () => deps.getModalGeometry(),
|
||||
setModalWindowBounds: (geometry) => deps.setModalWindowBounds(geometry),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import test from 'node:test';
|
||||
import {
|
||||
createBuildCreateInvisibleWindowMainDepsHandler,
|
||||
createBuildCreateMainWindowMainDepsHandler,
|
||||
createBuildCreateModalWindowMainDepsHandler,
|
||||
createBuildCreateOverlayWindowMainDepsHandler,
|
||||
createBuildCreateSecondaryWindowMainDepsHandler,
|
||||
} from './overlay-window-factory-main-deps';
|
||||
@@ -47,5 +48,12 @@ test('overlay window factory main deps builders return mapped handlers', () => {
|
||||
const secondaryDeps = buildSecondaryDeps();
|
||||
secondaryDeps.setSecondaryWindow(null);
|
||||
|
||||
assert.deepEqual(calls, ['set-main', 'set-invisible', 'set-secondary']);
|
||||
const buildModalDeps = createBuildCreateModalWindowMainDepsHandler({
|
||||
createOverlayWindow: () => ({ id: 'modal' }),
|
||||
setModalWindow: () => calls.push('set-modal'),
|
||||
});
|
||||
const modalDeps = buildModalDeps();
|
||||
modalDeps.setModalWindow(null);
|
||||
|
||||
assert.deepEqual(calls, ['set-main', 'set-invisible', 'set-secondary', 'set-modal']);
|
||||
});
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
export function createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps: {
|
||||
createOverlayWindowCore: (
|
||||
kind: 'visible' | 'invisible' | 'secondary',
|
||||
kind: 'visible' | 'invisible' | 'secondary' | 'modal',
|
||||
options: {
|
||||
isDev: boolean;
|
||||
overlayDebugVisualizationEnabled: boolean;
|
||||
ensureOverlayWindowLevel: (window: TWindow) => void;
|
||||
onRuntimeOptionsChanged: () => void;
|
||||
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
||||
isOverlayVisible: (windowKind: 'visible' | 'invisible' | 'secondary') => boolean;
|
||||
isOverlayVisible: (windowKind: 'visible' | 'invisible' | 'secondary' | 'modal') => boolean;
|
||||
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
||||
onWindowClosed: (windowKind: 'visible' | 'invisible' | 'secondary') => void;
|
||||
onWindowClosed: (windowKind: 'visible' | 'invisible' | 'secondary' | 'modal') => void;
|
||||
},
|
||||
) => TWindow;
|
||||
isDev: boolean;
|
||||
@@ -17,9 +17,9 @@ export function createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps: {
|
||||
ensureOverlayWindowLevel: (window: TWindow) => void;
|
||||
onRuntimeOptionsChanged: () => void;
|
||||
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
||||
isOverlayVisible: (windowKind: 'visible' | 'invisible' | 'secondary') => boolean;
|
||||
isOverlayVisible: (windowKind: 'visible' | 'invisible' | 'secondary' | 'modal') => boolean;
|
||||
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
||||
onWindowClosed: (windowKind: 'visible' | 'invisible' | 'secondary') => void;
|
||||
onWindowClosed: (windowKind: 'visible' | 'invisible' | 'secondary' | 'modal') => void;
|
||||
}) {
|
||||
return () => ({
|
||||
createOverlayWindowCore: deps.createOverlayWindowCore,
|
||||
@@ -35,7 +35,7 @@ export function createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps: {
|
||||
}
|
||||
|
||||
export function createBuildCreateMainWindowMainDepsHandler<TWindow>(deps: {
|
||||
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary') => TWindow;
|
||||
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary' | 'modal') => TWindow;
|
||||
setMainWindow: (window: TWindow | null) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
@@ -45,7 +45,7 @@ export function createBuildCreateMainWindowMainDepsHandler<TWindow>(deps: {
|
||||
}
|
||||
|
||||
export function createBuildCreateInvisibleWindowMainDepsHandler<TWindow>(deps: {
|
||||
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary') => TWindow;
|
||||
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary' | 'modal') => TWindow;
|
||||
setInvisibleWindow: (window: TWindow | null) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
@@ -55,7 +55,7 @@ export function createBuildCreateInvisibleWindowMainDepsHandler<TWindow>(deps: {
|
||||
}
|
||||
|
||||
export function createBuildCreateSecondaryWindowMainDepsHandler<TWindow>(deps: {
|
||||
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary') => TWindow;
|
||||
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary' | 'modal') => TWindow;
|
||||
setSecondaryWindow: (window: TWindow | null) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
@@ -63,3 +63,13 @@ export function createBuildCreateSecondaryWindowMainDepsHandler<TWindow>(deps: {
|
||||
setSecondaryWindow: deps.setSecondaryWindow,
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildCreateModalWindowMainDepsHandler<TWindow>(deps: {
|
||||
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary' | 'modal') => TWindow;
|
||||
setModalWindow: (window: TWindow | null) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
createOverlayWindow: deps.createOverlayWindow,
|
||||
setModalWindow: deps.setModalWindow,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
|
||||
import {
|
||||
createCreateInvisibleWindowHandler,
|
||||
createCreateMainWindowHandler,
|
||||
createCreateModalWindowHandler,
|
||||
createCreateOverlayWindowHandler,
|
||||
createCreateSecondaryWindowHandler,
|
||||
} from './overlay-window-factory';
|
||||
@@ -80,3 +81,18 @@ test('create secondary window handler stores secondary window', () => {
|
||||
assert.equal(createSecondaryWindow(), secondaryWindow);
|
||||
assert.deepEqual(calls, ['create:secondary', 'set:secondary']);
|
||||
});
|
||||
|
||||
test('create modal window handler stores modal window', () => {
|
||||
const calls: string[] = [];
|
||||
const modalWindow = { id: 'modal' };
|
||||
const createModalWindow = createCreateModalWindowHandler({
|
||||
createOverlayWindow: (kind) => {
|
||||
calls.push(`create:${kind}`);
|
||||
return modalWindow;
|
||||
},
|
||||
setModalWindow: (window) => calls.push(`set:${(window as { id: string }).id}`),
|
||||
});
|
||||
|
||||
assert.equal(createModalWindow(), modalWindow);
|
||||
assert.deepEqual(calls, ['create:modal', 'set:modal']);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
type OverlayWindowKind = 'visible' | 'invisible' | 'secondary';
|
||||
type OverlayWindowKind = 'visible' | 'invisible' | 'secondary' | 'modal';
|
||||
|
||||
export function createCreateOverlayWindowHandler<TWindow>(deps: {
|
||||
createOverlayWindowCore: (
|
||||
@@ -69,3 +69,14 @@ export function createCreateSecondaryWindowHandler<TWindow>(deps: {
|
||||
return window;
|
||||
};
|
||||
}
|
||||
|
||||
export function createCreateModalWindowHandler<TWindow>(deps: {
|
||||
createOverlayWindow: (kind: OverlayWindowKind) => TWindow;
|
||||
setModalWindow: (window: TWindow | null) => void;
|
||||
}) {
|
||||
return (): TWindow => {
|
||||
const window = deps.createOverlayWindow('modal');
|
||||
deps.setModalWindow(window);
|
||||
return window;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ test('overlay window runtime handlers compose create/main/invisible handlers', (
|
||||
let mainWindow: { kind: string } | null = null;
|
||||
let invisibleWindow: { kind: string } | null = null;
|
||||
let secondaryWindow: { kind: string } | null = null;
|
||||
let modalWindow: { kind: string } | null = null;
|
||||
let debugEnabled = false;
|
||||
const calls: string[] = [];
|
||||
|
||||
@@ -32,6 +33,9 @@ test('overlay window runtime handlers compose create/main/invisible handlers', (
|
||||
setSecondaryWindow: (window) => {
|
||||
secondaryWindow = window;
|
||||
},
|
||||
setModalWindow: (window) => {
|
||||
modalWindow = window;
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(runtime.createOverlayWindow('visible'), { kind: 'visible' });
|
||||
@@ -46,6 +50,8 @@ test('overlay window runtime handlers compose create/main/invisible handlers', (
|
||||
|
||||
assert.deepEqual(runtime.createSecondaryWindow(), { kind: 'secondary' });
|
||||
assert.deepEqual(secondaryWindow, { kind: 'secondary' });
|
||||
assert.deepEqual(runtime.createModalWindow(), { kind: 'modal' });
|
||||
assert.deepEqual(modalWindow, { kind: 'modal' });
|
||||
|
||||
assert.equal(debugEnabled, false);
|
||||
assert.deepEqual(calls, []);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import {
|
||||
createCreateInvisibleWindowHandler,
|
||||
createCreateMainWindowHandler,
|
||||
createCreateModalWindowHandler,
|
||||
createCreateOverlayWindowHandler,
|
||||
createCreateSecondaryWindowHandler,
|
||||
} from './overlay-window-factory';
|
||||
import {
|
||||
createBuildCreateInvisibleWindowMainDepsHandler,
|
||||
createBuildCreateMainWindowMainDepsHandler,
|
||||
createBuildCreateModalWindowMainDepsHandler,
|
||||
createBuildCreateOverlayWindowMainDepsHandler,
|
||||
createBuildCreateSecondaryWindowMainDepsHandler,
|
||||
} from './overlay-window-factory-main-deps';
|
||||
@@ -20,6 +22,7 @@ export function createOverlayWindowRuntimeHandlers<TWindow>(deps: {
|
||||
setMainWindow: (window: TWindow | null) => void;
|
||||
setInvisibleWindow: (window: TWindow | null) => void;
|
||||
setSecondaryWindow: (window: TWindow | null) => void;
|
||||
setModalWindow: (window: TWindow | null) => void;
|
||||
}) {
|
||||
const createOverlayWindow = createCreateOverlayWindowHandler<TWindow>(
|
||||
createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps.createOverlayWindowDeps)(),
|
||||
@@ -42,11 +45,18 @@ export function createOverlayWindowRuntimeHandlers<TWindow>(deps: {
|
||||
setSecondaryWindow: (window) => deps.setSecondaryWindow(window),
|
||||
})(),
|
||||
);
|
||||
const createModalWindow = createCreateModalWindowHandler<TWindow>(
|
||||
createBuildCreateModalWindowMainDepsHandler<TWindow>({
|
||||
createOverlayWindow: (kind) => createOverlayWindow(kind),
|
||||
setModalWindow: (window) => deps.setModalWindow(window),
|
||||
})(),
|
||||
);
|
||||
|
||||
return {
|
||||
createOverlayWindow,
|
||||
createMainWindow,
|
||||
createInvisibleWindow,
|
||||
createSecondaryWindow,
|
||||
createModalWindow,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user