feat(FN-4939): complete Step 2 — guard window position and force show
Fusion-Task-Id: FN-4939 Fusion-Task-Lineage: b2a00edb-5245-4223-97e4-0a16dd2c3289
This commit is contained in:
committed by
gsxdsm
parent
9a462388b5
commit
1f2b553079
@@ -17,14 +17,21 @@ const rendererMocks = vi.hoisted(() => {
|
||||
vi.mock("../renderer.ts", () => rendererMocks);
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const browserWindowHandlers = new Map<string, (...args: unknown[]) => void>();
|
||||
const browserWindowInstance = {
|
||||
loadURL: vi.fn(),
|
||||
loadFile: vi.fn(),
|
||||
on: vi.fn(),
|
||||
on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
|
||||
browserWindowHandlers.set(event, handler);
|
||||
}),
|
||||
once: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
|
||||
browserWindowHandlers.set(event, handler);
|
||||
}),
|
||||
isVisible: vi.fn(() => true),
|
||||
show: vi.fn(),
|
||||
focus: vi.fn(),
|
||||
hide: vi.fn(),
|
||||
maximize: vi.fn(),
|
||||
};
|
||||
|
||||
const BrowserWindow = vi.fn(() => browserWindowInstance) as unknown as {
|
||||
@@ -73,6 +80,10 @@ const mocks = vi.hoisted(() => {
|
||||
const getRendererFilePath = vi.fn(() => "/path/to/dist/client/index.html");
|
||||
const isUrlRenderer = vi.fn(() => false);
|
||||
|
||||
const screen = {
|
||||
getAllDisplays: vi.fn(() => [{ workArea: { x: 0, y: 0, width: 1920, height: 1080 } }]),
|
||||
};
|
||||
|
||||
return {
|
||||
app,
|
||||
BrowserWindow,
|
||||
@@ -82,7 +93,9 @@ const mocks = vi.hoisted(() => {
|
||||
Menu,
|
||||
nativeImage,
|
||||
shell,
|
||||
screen,
|
||||
browserWindowInstance,
|
||||
browserWindowHandlers,
|
||||
isDevelopmentMode,
|
||||
getRendererUrl,
|
||||
getRendererFilePath,
|
||||
@@ -98,6 +111,7 @@ vi.mock("electron", () => ({
|
||||
Menu: mocks.Menu,
|
||||
nativeImage: mocks.nativeImage,
|
||||
shell: mocks.shell,
|
||||
screen: mocks.screen,
|
||||
}));
|
||||
|
||||
const mainDeps = vi.hoisted(() => {
|
||||
@@ -137,6 +151,25 @@ vi.mock("../native.js", () => ({
|
||||
saveDesktopLaunchMode: mainDeps.saveDesktopLaunchMode,
|
||||
saveWindowState: mainDeps.saveWindowState,
|
||||
setupAutoUpdater: mainDeps.setupAutoUpdater,
|
||||
clampWindowStateToVisibleDisplay: vi.fn((state, displays) => {
|
||||
if (state.x === undefined || state.y === undefined) {
|
||||
return state;
|
||||
}
|
||||
const minVisible = 64;
|
||||
const windowRight = state.x + state.width;
|
||||
const windowBottom = state.y + state.height;
|
||||
const visible = displays.some((display: { workArea: { x: number; y: number; width: number; height: number } }) => {
|
||||
const right = display.workArea.x + display.workArea.width;
|
||||
const bottom = display.workArea.y + display.workArea.height;
|
||||
const overlapWidth = Math.max(0, Math.min(windowRight, right) - Math.max(state.x, display.workArea.x));
|
||||
const overlapHeight = Math.max(0, Math.min(windowBottom, bottom) - Math.max(state.y, display.workArea.y));
|
||||
return overlapWidth >= minVisible && overlapHeight >= minVisible;
|
||||
});
|
||||
if (visible) {
|
||||
return state;
|
||||
}
|
||||
return { width: state.width, height: state.height, isMaximized: state.isMaximized };
|
||||
}),
|
||||
normalizeDesktopRemoteLaunch: vi.fn((settings) => {
|
||||
const active = settings.profiles.find((profile: { id: string }) => profile.id === settings.activeProfileId);
|
||||
return active ? { mode: "remote", profileId: active.id, serverBaseUrl: active.serverUrl.replace(/\/$/, ""), serverLabel: active.name, authToken: active.authToken ?? undefined } : null;
|
||||
@@ -182,6 +215,7 @@ describe("main process", () => {
|
||||
rendererMocks.getRendererUrl.mockReturnValue("file:///path/to/dist/client/index.html");
|
||||
rendererMocks.getRendererFilePath.mockReturnValue("/path/to/dist/client/index.html");
|
||||
rendererMocks.isUrlRenderer.mockReturnValue(false);
|
||||
mocks.screen.getAllDisplays.mockReturnValue([{ workArea: { x: 0, y: 0, width: 1920, height: 1080 } }]);
|
||||
});
|
||||
|
||||
it("DASHBOARD_URL defaults to local file URL in production mode", async () => {
|
||||
@@ -333,6 +367,62 @@ describe("main process", () => {
|
||||
expect(mocks.browserWindowInstance.on).toHaveBeenCalledWith("closed", expect.any(Function));
|
||||
});
|
||||
|
||||
it("createMainWindow shows and focuses on ready-to-show", async () => {
|
||||
const { createMainWindow } = await importMainModule();
|
||||
|
||||
createMainWindow();
|
||||
mocks.browserWindowHandlers.get("ready-to-show")?.();
|
||||
|
||||
expect(mocks.browserWindowInstance.show).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.browserWindowInstance.focus).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("createMainWindow fallback timer shows and focuses when ready-to-show never fires", async () => {
|
||||
vi.useFakeTimers();
|
||||
const { createMainWindow } = await importMainModule();
|
||||
|
||||
createMainWindow();
|
||||
vi.advanceTimersByTime(2000);
|
||||
|
||||
expect(mocks.browserWindowInstance.show).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.browserWindowInstance.focus).toHaveBeenCalledTimes(1);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("initializeApp drops off-screen x/y before BrowserWindow construction", async () => {
|
||||
mainDeps.loadWindowState.mockResolvedValueOnce({
|
||||
x: -9999,
|
||||
y: -9999,
|
||||
width: 1280,
|
||||
height: 900,
|
||||
isMaximized: false,
|
||||
});
|
||||
const { initializeApp } = await importMainModule();
|
||||
|
||||
await initializeApp();
|
||||
|
||||
const [options] = mocks.BrowserWindow.mock.calls[0] as Array<Record<string, unknown>>;
|
||||
expect(options).not.toHaveProperty("x");
|
||||
expect(options).not.toHaveProperty("y");
|
||||
});
|
||||
|
||||
it("initializeApp preserves x/y when state overlaps a visible display", async () => {
|
||||
mainDeps.loadWindowState.mockResolvedValueOnce({
|
||||
x: 100,
|
||||
y: 100,
|
||||
width: 1280,
|
||||
height: 900,
|
||||
isMaximized: false,
|
||||
});
|
||||
const { initializeApp } = await importMainModule();
|
||||
|
||||
await initializeApp();
|
||||
|
||||
const [options] = mocks.BrowserWindow.mock.calls[0] as Array<Record<string, unknown>>;
|
||||
expect(options).toMatchObject({ x: 100, y: 100 });
|
||||
});
|
||||
|
||||
it("importing main does not auto-start", async () => {
|
||||
await importMainModule();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { app, BrowserWindow, nativeImage, Tray } from "electron";
|
||||
import { app, BrowserWindow, nativeImage, screen, Tray } from "electron";
|
||||
import { join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { setupDeepLinkHandler, registerDeepLinkProtocol } from "./deep-link.js";
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
setupAutoUpdater,
|
||||
normalizeDesktopRemoteLaunch,
|
||||
buildRemoteShellHandoffUrl,
|
||||
clampWindowStateToVisibleDisplay,
|
||||
type DesktopLaunchMode,
|
||||
type NormalizedDesktopRemoteLaunch,
|
||||
type WindowState,
|
||||
@@ -92,6 +93,17 @@ export function createMainWindow(state?: WindowState, launchTargetUrl?: string):
|
||||
void window.loadFile(getRendererFilePath());
|
||||
}
|
||||
|
||||
const showFallbackTimer = setTimeout(() => {
|
||||
window.show();
|
||||
window.focus();
|
||||
}, 2000);
|
||||
|
||||
window.once("ready-to-show", () => {
|
||||
clearTimeout(showFallbackTimer);
|
||||
window.show();
|
||||
window.focus();
|
||||
});
|
||||
|
||||
window.on("close", (event) => {
|
||||
saveWindowState(window);
|
||||
|
||||
@@ -148,8 +160,17 @@ export async function initializeApp(): Promise<void> {
|
||||
currentDesktopLaunchMode = "local";
|
||||
}
|
||||
|
||||
const windowState = state
|
||||
? clampWindowStateToVisibleDisplay(
|
||||
state,
|
||||
typeof screen?.getAllDisplays === "function"
|
||||
? screen.getAllDisplays().map((display) => ({ workArea: display.workArea }))
|
||||
: [],
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const createdWindow = createMainWindow(
|
||||
state ?? undefined,
|
||||
windowState,
|
||||
currentDesktopLaunchMode === "remote" && currentRemoteLaunch
|
||||
? buildRemoteShellHandoffUrl(currentRemoteLaunch)
|
||||
: undefined,
|
||||
@@ -208,7 +229,7 @@ export async function initializeApp(): Promise<void> {
|
||||
setupDeepLinkHandler(createdWindow);
|
||||
setupAutoUpdater(createdWindow);
|
||||
|
||||
if (state?.isMaximized === true) {
|
||||
if (windowState?.isMaximized === true) {
|
||||
createdWindow.maximize();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user