diff --git a/apps/desktop/src/main/lib/app-environment.ts b/apps/desktop/src/main/lib/app-environment.ts index b332b1f4b04..5881e38d02c 100644 --- a/apps/desktop/src/main/lib/app-environment.ts +++ b/apps/desktop/src/main/lib/app-environment.ts @@ -6,3 +6,6 @@ export const SUPERSET_HOME_DIR = join(homedir(), SUPERSET_DIR_NAME); // For lowdb - use our own path instead of app.getPath("userData") export const APP_STATE_PATH = join(SUPERSET_HOME_DIR, "app-state.json"); + +// Window geometry state (separate from UI state - main process only, sync I/O) +export const WINDOW_STATE_PATH = join(SUPERSET_HOME_DIR, "window-state.json"); diff --git a/apps/desktop/src/main/lib/window-state/bounds-validation.test.ts b/apps/desktop/src/main/lib/window-state/bounds-validation.test.ts new file mode 100644 index 00000000000..8b89325ac49 --- /dev/null +++ b/apps/desktop/src/main/lib/window-state/bounds-validation.test.ts @@ -0,0 +1,377 @@ +import { beforeEach, describe, expect, it, type mock } from "bun:test"; +import { screen } from "electron"; +import { + getInitialWindowBounds, + isVisibleOnAnyDisplay, +} from "./bounds-validation"; + +const MIN_VISIBLE_OVERLAP = 50; +const MIN_WINDOW_SIZE = 400; + +describe("isVisibleOnAnyDisplay", () => { + describe("single display setup", () => { + beforeEach(() => { + (screen.getAllDisplays as ReturnType).mockReturnValue([ + { bounds: { x: 0, y: 0, width: 1920, height: 1080 } }, + ]); + }); + + it("should return true for window fully within display", () => { + expect( + isVisibleOnAnyDisplay({ x: 100, y: 100, width: 800, height: 600 }), + ).toBe(true); + }); + + it("should return true for window covering entire display", () => { + expect( + isVisibleOnAnyDisplay({ x: 0, y: 0, width: 1920, height: 1080 }), + ).toBe(true); + }); + + it("should return true for window with more than MIN_VISIBLE_OVERLAP on right edge", () => { + expect( + isVisibleOnAnyDisplay({ + x: 1920 - MIN_VISIBLE_OVERLAP - 1, + y: 100, + width: 800, + height: 600, + }), + ).toBe(true); + }); + + it("should return true for window with more than MIN_VISIBLE_OVERLAP on bottom edge", () => { + expect( + isVisibleOnAnyDisplay({ + x: 100, + y: 1080 - MIN_VISIBLE_OVERLAP - 1, + width: 800, + height: 600, + }), + ).toBe(true); + }); + + it("should return false for window at exactly MIN_VISIBLE_OVERLAP boundary (strict inequality)", () => { + expect( + isVisibleOnAnyDisplay({ + x: 1920 - MIN_VISIBLE_OVERLAP, + y: 100, + width: 800, + height: 600, + }), + ).toBe(false); + }); + + it("should return false for window completely off-screen (right)", () => { + expect( + isVisibleOnAnyDisplay({ x: 2000, y: 100, width: 800, height: 600 }), + ).toBe(false); + }); + + it("should return false for window completely off-screen (left)", () => { + expect( + isVisibleOnAnyDisplay({ x: -900, y: 100, width: 800, height: 600 }), + ).toBe(false); + }); + + it("should return false for window completely off-screen (bottom)", () => { + expect( + isVisibleOnAnyDisplay({ x: 100, y: 1200, width: 800, height: 600 }), + ).toBe(false); + }); + + it("should return false for window completely off-screen (top)", () => { + expect( + isVisibleOnAnyDisplay({ x: 100, y: -700, width: 800, height: 600 }), + ).toBe(false); + }); + + it("should return false for window with insufficient overlap (49px < 50px threshold)", () => { + expect( + isVisibleOnAnyDisplay({ + x: 1920 - MIN_VISIBLE_OVERLAP + 1, + y: 100, + width: 800, + height: 600, + }), + ).toBe(false); + }); + }); + + describe("multi-display setup", () => { + beforeEach(() => { + (screen.getAllDisplays as ReturnType).mockReturnValue([ + { bounds: { x: 0, y: 0, width: 1920, height: 1080 } }, + { bounds: { x: 1920, y: 0, width: 1920, height: 1080 } }, + ]); + }); + + it("should return true for window on secondary display", () => { + expect( + isVisibleOnAnyDisplay({ x: 2000, y: 100, width: 800, height: 600 }), + ).toBe(true); + }); + + it("should return true for window spanning both displays", () => { + expect( + isVisibleOnAnyDisplay({ x: 1500, y: 100, width: 1000, height: 600 }), + ).toBe(true); + }); + + it("should return false for window off-screen to the right of secondary", () => { + expect( + isVisibleOnAnyDisplay({ x: 4000, y: 100, width: 800, height: 600 }), + ).toBe(false); + }); + }); + + describe("secondary display with offset", () => { + beforeEach(() => { + (screen.getAllDisplays as ReturnType).mockReturnValue([ + { bounds: { x: 0, y: 0, width: 1920, height: 1080 } }, + { bounds: { x: 960, y: 1080, width: 1920, height: 1080 } }, + ]); + }); + + it("should return true for window on offset secondary display", () => { + expect( + isVisibleOnAnyDisplay({ x: 1000, y: 1200, width: 800, height: 600 }), + ).toBe(true); + }); + + it("should return false for window in gap between displays", () => { + expect( + isVisibleOnAnyDisplay({ x: 0, y: 1100, width: 800, height: 600 }), + ).toBe(false); + }); + }); + + describe("display to the left (negative coordinates)", () => { + beforeEach(() => { + (screen.getAllDisplays as ReturnType).mockReturnValue([ + { bounds: { x: 0, y: 0, width: 1920, height: 1080 } }, + { bounds: { x: -1920, y: 0, width: 1920, height: 1080 } }, + ]); + }); + + it("should return true for window on display with negative coordinates", () => { + expect( + isVisibleOnAnyDisplay({ x: -1000, y: 100, width: 800, height: 600 }), + ).toBe(true); + }); + }); + + describe("edge cases", () => { + it("should return false when no displays connected", () => { + (screen.getAllDisplays as ReturnType).mockReturnValue([]); + expect( + isVisibleOnAnyDisplay({ x: 100, y: 100, width: 800, height: 600 }), + ).toBe(false); + }); + + it("should return true for zero-size window if position is valid (size validation is separate)", () => { + (screen.getAllDisplays as ReturnType).mockReturnValue([ + { bounds: { x: 0, y: 0, width: 1920, height: 1080 } }, + ]); + expect( + isVisibleOnAnyDisplay({ x: 100, y: 100, width: 0, height: 0 }), + ).toBe(true); + }); + }); +}); + +describe("getInitialWindowBounds", () => { + beforeEach(() => { + (screen.getPrimaryDisplay as ReturnType).mockReturnValue({ + workAreaSize: { width: 1920, height: 1080 }, + }); + (screen.getAllDisplays as ReturnType).mockReturnValue([ + { bounds: { x: 0, y: 0, width: 1920, height: 1080 } }, + ]); + }); + + describe("no saved state", () => { + it("should return primary display size when no saved state", () => { + const result = getInitialWindowBounds(null); + expect(result).toEqual({ + width: 1920, + height: 1080, + center: true, + isMaximized: false, + }); + }); + + it("should not include x/y when centering", () => { + const result = getInitialWindowBounds(null); + expect(result.x).toBeUndefined(); + expect(result.y).toBeUndefined(); + }); + }); + + describe("saved state on visible display", () => { + it("should restore exact position when visible on display", () => { + const result = getInitialWindowBounds({ + x: 100, + y: 200, + width: 800, + height: 600, + isMaximized: false, + }); + expect(result).toEqual({ + x: 100, + y: 200, + width: 800, + height: 600, + center: false, + isMaximized: false, + }); + }); + + it("should preserve isMaximized when restoring position", () => { + const result = getInitialWindowBounds({ + x: 0, + y: 0, + width: 1920, + height: 1080, + isMaximized: true, + }); + expect(result.isMaximized).toBe(true); + expect(result.center).toBe(false); + }); + }); + + describe("saved state on disconnected display", () => { + it("should center window but keep dimensions when display disconnected", () => { + const result = getInitialWindowBounds({ + x: 2000, + y: 100, + width: 800, + height: 600, + isMaximized: false, + }); + expect(result).toEqual({ + width: 800, + height: 600, + center: true, + isMaximized: false, + }); + expect(result.x).toBeUndefined(); + expect(result.y).toBeUndefined(); + }); + + it("should preserve isMaximized when centering", () => { + const result = getInitialWindowBounds({ + x: 2000, + y: 100, + width: 800, + height: 600, + isMaximized: true, + }); + expect(result.isMaximized).toBe(true); + expect(result.center).toBe(true); + }); + }); + + describe("dimension clamping", () => { + it("should clamp width to work area size", () => { + const result = getInitialWindowBounds({ + x: 0, + y: 0, + width: 3000, + height: 600, + isMaximized: false, + }); + expect(result.width).toBe(1920); + }); + + it("should clamp height to work area size", () => { + const result = getInitialWindowBounds({ + x: 0, + y: 0, + width: 800, + height: 2000, + isMaximized: false, + }); + expect(result.height).toBe(1080); + }); + + it("should enforce minimum window size for width", () => { + const result = getInitialWindowBounds({ + x: 0, + y: 0, + width: 100, + height: 600, + isMaximized: false, + }); + expect(result.width).toBe(MIN_WINDOW_SIZE); + }); + + it("should enforce minimum window size for height", () => { + const result = getInitialWindowBounds({ + x: 0, + y: 0, + width: 800, + height: 100, + isMaximized: false, + }); + expect(result.height).toBe(MIN_WINDOW_SIZE); + }); + }); + + describe("DPI/resolution changes", () => { + it("should handle resolution decrease gracefully", () => { + (screen.getPrimaryDisplay as ReturnType).mockReturnValue({ + workAreaSize: { width: 1280, height: 720 }, + }); + + const result = getInitialWindowBounds({ + x: 0, + y: 0, + width: 1920, + height: 1080, + isMaximized: false, + }); + + expect(result.width).toBe(1280); + expect(result.height).toBe(720); + }); + + it("should clamp to work area even if smaller than MIN_WINDOW_SIZE", () => { + (screen.getPrimaryDisplay as ReturnType).mockReturnValue({ + workAreaSize: { width: 300, height: 200 }, + }); + + const result = getInitialWindowBounds({ + x: 0, + y: 0, + width: 800, + height: 600, + isMaximized: false, + }); + + expect(result.width).toBe(300); + expect(result.height).toBe(200); + }); + }); + + describe("multi-monitor scenarios", () => { + beforeEach(() => { + (screen.getAllDisplays as ReturnType).mockReturnValue([ + { bounds: { x: 0, y: 0, width: 1920, height: 1080 } }, + { bounds: { x: 1920, y: 0, width: 1920, height: 1080 } }, + ]); + }); + + it("should restore position on secondary display", () => { + const result = getInitialWindowBounds({ + x: 2000, + y: 100, + width: 800, + height: 600, + isMaximized: false, + }); + expect(result.x).toBe(2000); + expect(result.y).toBe(100); + expect(result.center).toBe(false); + }); + }); +}); diff --git a/apps/desktop/src/main/lib/window-state/bounds-validation.ts b/apps/desktop/src/main/lib/window-state/bounds-validation.ts new file mode 100644 index 00000000000..03984773147 --- /dev/null +++ b/apps/desktop/src/main/lib/window-state/bounds-validation.ts @@ -0,0 +1,103 @@ +import type { Rectangle } from "electron"; +import { screen } from "electron"; +import type { WindowState } from "./window-state"; + +const MIN_VISIBLE_OVERLAP = 50; +const MIN_WINDOW_SIZE = 400; + +/** + * Checks if bounds overlap at least MIN_VISIBLE_OVERLAP pixels with any display. + * Returns false if window would be completely off-screen (e.g., monitor disconnected). + */ +export function isVisibleOnAnyDisplay(bounds: Rectangle): boolean { + const displays = screen.getAllDisplays(); + + return displays.some((display) => { + const db = display.bounds; + return ( + bounds.x < db.x + db.width - MIN_VISIBLE_OVERLAP && + bounds.x + bounds.width > db.x + MIN_VISIBLE_OVERLAP && + bounds.y < db.y + db.height - MIN_VISIBLE_OVERLAP && + bounds.y + bounds.height > db.y + MIN_VISIBLE_OVERLAP + ); + }); +} + +/** + * Clamps dimensions to not exceed the primary display work area. + * Handles DPI/resolution changes since last save. + */ +function clampToWorkArea( + width: number, + height: number, +): { width: number; height: number } { + const { workAreaSize } = screen.getPrimaryDisplay(); + return { + width: Math.min(Math.max(width, MIN_WINDOW_SIZE), workAreaSize.width), + height: Math.min(Math.max(height, MIN_WINDOW_SIZE), workAreaSize.height), + }; +} + +export interface InitialWindowBounds { + x?: number; + y?: number; + width: number; + height: number; + center: boolean; + isMaximized: boolean; +} + +/** + * Computes initial window bounds from saved state, with fallbacks. + * + * - No saved state → default to primary display size, centered + * - Saved position visible → restore exactly + * - Saved position not visible (monitor disconnected) → use saved size, but center + */ +export function getInitialWindowBounds( + savedState: WindowState | null, +): InitialWindowBounds { + const { workAreaSize } = screen.getPrimaryDisplay(); + + // No saved state → default to primary display size, centered + if (!savedState) { + return { + width: workAreaSize.width, + height: workAreaSize.height, + center: true, + isMaximized: false, + }; + } + + const { width, height } = clampToWorkArea( + savedState.width, + savedState.height, + ); + + const savedBounds: Rectangle = { + x: savedState.x, + y: savedState.y, + width, + height, + }; + + // Saved position visible on a connected display → restore exactly + if (isVisibleOnAnyDisplay(savedBounds)) { + return { + x: savedState.x, + y: savedState.y, + width, + height, + center: false, + isMaximized: savedState.isMaximized, + }; + } + + // Position not visible (monitor disconnected) → use saved size, but center + return { + width, + height, + center: true, + isMaximized: savedState.isMaximized, + }; +} diff --git a/apps/desktop/src/main/lib/window-state/index.ts b/apps/desktop/src/main/lib/window-state/index.ts new file mode 100644 index 00000000000..dabdb477f1b --- /dev/null +++ b/apps/desktop/src/main/lib/window-state/index.ts @@ -0,0 +1,11 @@ +export { + getInitialWindowBounds, + type InitialWindowBounds, + isVisibleOnAnyDisplay, +} from "./bounds-validation"; +export { + isValidWindowState, + loadWindowState, + saveWindowState, + type WindowState, +} from "./window-state"; diff --git a/apps/desktop/src/main/lib/window-state/window-state.test.ts b/apps/desktop/src/main/lib/window-state/window-state.test.ts new file mode 100644 index 00000000000..688464272f5 --- /dev/null +++ b/apps/desktop/src/main/lib/window-state/window-state.test.ts @@ -0,0 +1,288 @@ +import { describe, expect, it } from "bun:test"; +import { isValidWindowState } from "./window-state"; + +describe("isValidWindowState", () => { + describe("valid window states", () => { + it("should accept valid window state with positive coordinates", () => { + expect( + isValidWindowState({ + x: 100, + y: 200, + width: 800, + height: 600, + isMaximized: false, + }), + ).toBe(true); + }); + + it("should accept valid window state at origin", () => { + expect( + isValidWindowState({ + x: 0, + y: 0, + width: 1920, + height: 1080, + isMaximized: false, + }), + ).toBe(true); + }); + + it("should accept valid window state with negative coordinates (multi-monitor)", () => { + expect( + isValidWindowState({ + x: -1920, + y: 0, + width: 1920, + height: 1080, + isMaximized: false, + }), + ).toBe(true); + }); + + it("should accept valid window state when maximized", () => { + expect( + isValidWindowState({ + x: 0, + y: 0, + width: 1920, + height: 1080, + isMaximized: true, + }), + ).toBe(true); + }); + + it("should accept valid window state with decimal coordinates", () => { + expect( + isValidWindowState({ + x: 100.5, + y: 200.75, + width: 800.25, + height: 600.5, + isMaximized: false, + }), + ).toBe(true); + }); + + it("should accept state with extra properties (forward compatibility)", () => { + expect( + isValidWindowState({ + x: 0, + y: 0, + width: 800, + height: 600, + isMaximized: false, + futureProperty: "ignored", + }), + ).toBe(true); + }); + + it("should accept MAX_SAFE_INTEGER dimensions", () => { + expect( + isValidWindowState({ + x: Number.MAX_SAFE_INTEGER, + y: Number.MAX_SAFE_INTEGER, + width: Number.MAX_SAFE_INTEGER, + height: Number.MAX_SAFE_INTEGER, + isMaximized: false, + }), + ).toBe(true); + }); + }); + + describe("invalid dimensions", () => { + it("should reject zero width", () => { + expect( + isValidWindowState({ + x: 0, + y: 0, + width: 0, + height: 600, + isMaximized: false, + }), + ).toBe(false); + }); + + it("should reject zero height", () => { + expect( + isValidWindowState({ + x: 0, + y: 0, + width: 800, + height: 0, + isMaximized: false, + }), + ).toBe(false); + }); + + it("should reject negative width", () => { + expect( + isValidWindowState({ + x: 0, + y: 0, + width: -800, + height: 600, + isMaximized: false, + }), + ).toBe(false); + }); + + it("should reject negative height", () => { + expect( + isValidWindowState({ + x: 0, + y: 0, + width: 800, + height: -600, + isMaximized: false, + }), + ).toBe(false); + }); + }); + + describe("invalid number values", () => { + it("should reject Infinity", () => { + expect( + isValidWindowState({ + x: Number.POSITIVE_INFINITY, + y: 0, + width: 800, + height: 600, + isMaximized: false, + }), + ).toBe(false); + }); + + it("should reject NaN", () => { + expect( + isValidWindowState({ + x: 0, + y: 0, + width: Number.NaN, + height: 600, + isMaximized: false, + }), + ).toBe(false); + }); + }); + + describe("missing properties", () => { + it("should reject missing x", () => { + expect( + isValidWindowState({ + y: 0, + width: 800, + height: 600, + isMaximized: false, + }), + ).toBe(false); + }); + + it("should reject missing y", () => { + expect( + isValidWindowState({ + x: 0, + width: 800, + height: 600, + isMaximized: false, + }), + ).toBe(false); + }); + + it("should reject missing width", () => { + expect( + isValidWindowState({ + x: 0, + y: 0, + height: 600, + isMaximized: false, + }), + ).toBe(false); + }); + + it("should reject missing height", () => { + expect( + isValidWindowState({ + x: 0, + y: 0, + width: 800, + isMaximized: false, + }), + ).toBe(false); + }); + + it("should reject missing isMaximized", () => { + expect( + isValidWindowState({ + x: 0, + y: 0, + width: 800, + height: 600, + }), + ).toBe(false); + }); + }); + + describe("wrong types", () => { + it("should reject string for x", () => { + expect( + isValidWindowState({ + x: "100", + y: 0, + width: 800, + height: 600, + isMaximized: false, + }), + ).toBe(false); + }); + + it("should reject string for isMaximized", () => { + expect( + isValidWindowState({ + x: 0, + y: 0, + width: 800, + height: 600, + isMaximized: "false", + }), + ).toBe(false); + }); + + it("should reject number for isMaximized", () => { + expect( + isValidWindowState({ + x: 0, + y: 0, + width: 800, + height: 600, + isMaximized: 1, + }), + ).toBe(false); + }); + }); + + describe("non-object values", () => { + it("should reject null", () => { + expect(isValidWindowState(null)).toBe(false); + }); + + it("should reject undefined", () => { + expect(isValidWindowState(undefined)).toBe(false); + }); + + it("should reject string", () => { + expect(isValidWindowState("not an object")).toBe(false); + }); + + it("should reject number", () => { + expect(isValidWindowState(123)).toBe(false); + }); + + it("should reject array", () => { + expect(isValidWindowState([0, 0, 800, 600, false])).toBe(false); + }); + + it("should reject empty object", () => { + expect(isValidWindowState({})).toBe(false); + }); + }); +}); diff --git a/apps/desktop/src/main/lib/window-state/window-state.ts b/apps/desktop/src/main/lib/window-state/window-state.ts new file mode 100644 index 00000000000..7c749458f78 --- /dev/null +++ b/apps/desktop/src/main/lib/window-state/window-state.ts @@ -0,0 +1,73 @@ +import { + existsSync, + readFileSync, + renameSync, + unlinkSync, + writeFileSync, +} from "node:fs"; +import { dirname, join } from "node:path"; +import { WINDOW_STATE_PATH } from "../app-environment"; + +export interface WindowState { + x: number; + y: number; + width: number; + height: number; + isMaximized: boolean; +} + +/** + * Loads window state from disk. + * Returns null if file doesn't exist, is corrupted, or has invalid shape. + */ +export function loadWindowState(): WindowState | null { + try { + if (!existsSync(WINDOW_STATE_PATH)) return null; + + const raw = readFileSync(WINDOW_STATE_PATH, "utf-8"); + const parsed = JSON.parse(raw); + + if (!isValidWindowState(parsed)) return null; + + return parsed; + } catch { + // Parse error or read error → treat as no saved state + return null; + } +} + +/** + * Saves window state to disk atomically (temp file + rename). + * Corruption-safe: partial writes won't corrupt existing state. + */ +export function saveWindowState(state: WindowState): void { + const tempPath = join( + dirname(WINDOW_STATE_PATH), + `.window-state.${Date.now()}.tmp`, + ); + + try { + writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf-8"); + renameSync(tempPath, WINDOW_STATE_PATH); // Atomic replace + } catch (error) { + // Clean up temp file if rename failed + try { + unlinkSync(tempPath); + } catch {} + console.error("[window-state] Failed to save:", error); + } +} + +export function isValidWindowState(value: unknown): value is WindowState { + if (!value || typeof value !== "object") return false; + const v = value as Record; + return ( + Number.isFinite(v.x) && + Number.isFinite(v.y) && + Number.isFinite(v.width) && + (v.width as number) > 0 && + Number.isFinite(v.height) && + (v.height as number) > 0 && + typeof v.isMaximized === "boolean" + ); +} diff --git a/apps/desktop/src/main/windows/main.ts b/apps/desktop/src/main/windows/main.ts index c49659d112b..118df43d356 100644 --- a/apps/desktop/src/main/windows/main.ts +++ b/apps/desktop/src/main/windows/main.ts @@ -2,7 +2,7 @@ import { join } from "node:path"; import { workspaces, worktrees } from "@superset/local-db"; import { eq } from "drizzle-orm"; import type { BrowserWindow } from "electron"; -import { Notification, screen } from "electron"; +import { Notification } from "electron"; import { createWindow } from "lib/electron-app/factories/windows/create"; import { createAppRouter } from "lib/trpc/routers"; import { localDb } from "main/lib/local-db"; @@ -18,6 +18,11 @@ import { notificationsEmitter, } from "../lib/notifications/server"; import { terminalManager } from "../lib/terminal"; +import { + getInitialWindowBounds, + loadWindowState, + saveWindowState, +} from "../lib/window-state"; // Singleton IPC handler to prevent duplicate handlers on window reopen (macOS) let ipcHandler: ReturnType | null = null; @@ -29,17 +34,20 @@ let currentWindow: BrowserWindow | null = null; const getWindow = () => currentWindow; export async function MainWindow() { - const { width, height } = screen.getPrimaryDisplay().workAreaSize; + const savedWindowState = loadWindowState(); + const initialBounds = getInitialWindowBounds(savedWindowState); const window = createWindow({ id: "main", title: productName, - width, - height, + width: initialBounds.width, + height: initialBounds.height, + x: initialBounds.x, + y: initialBounds.y, minWidth: 400, minHeight: 400, show: false, - center: true, + center: initialBounds.center, movable: true, resizable: true, alwaysOnTop: false, @@ -156,10 +164,25 @@ export async function MainWindow() { ); window.webContents.on("did-finish-load", async () => { + // Restore maximized state if it was saved + if (initialBounds.isMaximized) { + window.maximize(); + } window.show(); }); window.on("close", () => { + // Save window state first, before any cleanup + const isMaximized = window.isMaximized(); + const bounds = isMaximized ? window.getNormalBounds() : window.getBounds(); + saveWindowState({ + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + isMaximized, + }); + server.close(); notificationsEmitter.removeAllListeners(); // Remove terminal listeners to prevent duplicates when window reopens on macOS diff --git a/apps/desktop/test-setup.ts b/apps/desktop/test-setup.ts index d3f666f4431..c9d9ea6a173 100644 --- a/apps/desktop/test-setup.ts +++ b/apps/desktop/test-setup.ts @@ -99,7 +99,14 @@ mock.module("electron", () => ({ screen: { getPrimaryDisplay: mock(() => ({ workAreaSize: { width: 1920, height: 1080 }, + bounds: { x: 0, y: 0, width: 1920, height: 1080 }, })), + getAllDisplays: mock(() => [ + { + bounds: { x: 0, y: 0, width: 1920, height: 1080 }, + workAreaSize: { width: 1920, height: 1080 }, + }, + ]), }, Notification: mock(() => ({ show: mock(),