diff --git a/__mocks__/fileMock.js b/__mocks__/fileMock.js
new file mode 100644
index 0000000..0a445d0
--- /dev/null
+++ b/__mocks__/fileMock.js
@@ -0,0 +1 @@
+module.exports = "test-file-stub";
diff --git a/src/native/window.test.ts b/src/native/window.test.ts
new file mode 100644
index 0000000..fb53daa
--- /dev/null
+++ b/src/native/window.test.ts
@@ -0,0 +1,568 @@
+///
+
+import {
+ BrowserWindow,
+ Menu,
+ MenuItem,
+ app,
+ ipcMain,
+ nativeImage,
+} from "electron";
+
+import { config } from "./config";
+import { createMainWindow, quitApp, BUILD_URL } from "./window";
+import { updateTrayMenu } from "./tray";
+
+// Mock electron
+let capturedBeforeQuitHandler: Function | null = null;
+
+jest.mock("electron", () => ({
+ BrowserWindow: jest.fn(),
+ Menu: jest.fn().mockImplementation(() => ({
+ append: jest.fn(),
+ popup: jest.fn(),
+ items: [] as unknown[],
+ })),
+ MenuItem: jest.fn().mockImplementation((opts: unknown) => opts),
+ app: {
+ commandLine: {
+ hasSwitch: jest.fn().mockReturnValue(false),
+ getSwitchValue: jest.fn().mockReturnValue(""),
+ },
+ on: jest.fn((event: string, handler: Function) => {
+ if (event === "before-quit") {
+ (global as Record).__beforeQuitHandler = handler;
+ }
+ }),
+ },
+ ipcMain: {
+ on: jest.fn(),
+ },
+ nativeImage: {
+ createFromDataURL: jest.fn().mockReturnValue({}),
+ },
+}));
+
+// Mock the asset import
+jest.mock("../../assets/desktop/icon.png?asset", () => "mock-icon-path", {
+ virtual: true,
+});
+
+// Mock config
+jest.mock("./config", () => ({
+ config: {
+ customFrame: false,
+ windowState: { isMaximised: false },
+ minimiseToTray: true,
+ spellchecker: true,
+ sync: jest.fn(),
+ },
+}));
+
+// Mock tray
+jest.mock("./tray", () => ({
+ updateTrayMenu: jest.fn(),
+}));
+
+const cfg = config as unknown as Record;
+
+describe("window", () => {
+ let mockMainWindow: Record;
+ let mockWebContents: Record;
+ let eventHandlers: Record;
+ let ipcHandlers: Record;
+ let beforeQuitHandler: Function;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ eventHandlers = {};
+ ipcHandlers = {};
+
+ mockWebContents = {
+ on: jest.fn((event: string, handler: Function) => {
+ if (!eventHandlers[event]) {
+ eventHandlers[event] = [];
+ }
+ eventHandlers[event].push(handler);
+ }),
+ setZoomLevel: jest.fn(),
+ getZoomLevel: jest.fn().mockReturnValue(0),
+ replaceMisspelling: jest.fn(),
+ session: {
+ addWordToSpellCheckerDictionary: jest.fn(),
+ },
+ openDevTools: jest.fn(),
+ };
+
+ mockMainWindow = {
+ on: jest.fn((event: string, handler: Function) => {
+ if (!eventHandlers[event]) {
+ eventHandlers[event] = [];
+ }
+ eventHandlers[event].push(handler);
+ }),
+ setMenu: jest.fn(),
+ maximize: jest.fn(),
+ isMaximized: jest.fn().mockReturnValue(false),
+ unmaximize: jest.fn(),
+ minimize: jest.fn(),
+ hide: jest.fn(),
+ close: jest.fn(),
+ loadURL: jest.fn(),
+ webContents: mockWebContents,
+ };
+
+ (BrowserWindow as unknown as jest.Mock).mockImplementation(
+ () => mockMainWindow,
+ );
+
+ (ipcMain.on as unknown as jest.Mock).mockImplementation(
+ (channel: string, handler: Function) => {
+ ipcHandlers[channel] = handler;
+ },
+ );
+
+ (app.on as unknown as jest.Mock).mockImplementation(
+ (event: string, handler: Function) => {
+ if (event === "before-quit") {
+ beforeQuitHandler = handler;
+ }
+ },
+ );
+ });
+
+ describe("BUILD_URL", () => {
+ it("should default to beta.revolt.chat when no force-server flag", () => {
+ expect(BUILD_URL.toString()).toBe("https://beta.revolt.chat/");
+ });
+
+ it("should use force-server flag value when provided", () => {
+ // BUILD_URL is evaluated at module load time, so we test the exported value
+ // which reflects the default since mocks are set before import
+ expect(BUILD_URL.toString()).toBe("https://beta.revolt.chat/");
+ });
+ });
+
+ describe("createMainWindow", () => {
+ it("should create a BrowserWindow with correct default options", () => {
+ createMainWindow();
+
+ expect(BrowserWindow).toHaveBeenCalledWith({
+ minWidth: 300,
+ minHeight: 300,
+ width: 1280,
+ height: 720,
+ backgroundColor: "#191919",
+ frame: true,
+ icon: expect.anything(),
+ webPreferences: {
+ preload: expect.stringContaining("preload.js"),
+ contextIsolation: true,
+ nodeIntegration: false,
+ spellcheck: true,
+ },
+ });
+ });
+
+ it("should set frame to false when customFrame is enabled", () => {
+ cfg.customFrame = true;
+ createMainWindow();
+
+ expect(BrowserWindow).toHaveBeenCalledWith(
+ expect.objectContaining({
+ frame: false,
+ }),
+ );
+ });
+
+ it("should set menu to null", () => {
+ createMainWindow();
+ expect(mockMainWindow.setMenu).toHaveBeenCalledWith(null);
+ });
+
+ it("should load the BUILD_URL", () => {
+ createMainWindow();
+ expect(mockMainWindow.loadURL).toHaveBeenCalledWith(BUILD_URL.toString());
+ });
+
+ it("should maximize window if windowState.isMaximised is true", () => {
+ cfg.windowState = { isMaximised: true };
+ createMainWindow();
+
+ expect(mockMainWindow.maximize).toHaveBeenCalled();
+ });
+
+ it("should not maximize window if windowState.isMaximised is false", () => {
+ cfg.windowState = { isMaximised: false };
+ createMainWindow();
+
+ expect(mockMainWindow.maximize).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("window close event", () => {
+ it("should hide window when minimiseToTray is true and shouldQuit is false", () => {
+ cfg.minimiseToTray = true;
+ createMainWindow();
+
+ const closeHandler = eventHandlers["close"]?.[0];
+ const event = { preventDefault: jest.fn() };
+ closeHandler(event);
+
+ expect(event.preventDefault).toHaveBeenCalled();
+ expect(mockMainWindow.hide).toHaveBeenCalled();
+ });
+
+ it("should not hide window when minimiseToTray is false", () => {
+ cfg.minimiseToTray = false;
+ createMainWindow();
+
+ const closeHandler = eventHandlers["close"]?.[0];
+ const event = { preventDefault: jest.fn() };
+ closeHandler(event);
+
+ expect(event.preventDefault).not.toHaveBeenCalled();
+ expect(mockMainWindow.hide).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("window show/hide events", () => {
+ it("should call updateTrayMenu on show event", () => {
+ createMainWindow();
+
+ const showHandler = eventHandlers["show"]?.[0];
+ showHandler();
+
+ expect(updateTrayMenu).toHaveBeenCalled();
+ });
+
+ it("should call updateTrayMenu on hide event", () => {
+ createMainWindow();
+
+ const hideHandler = eventHandlers["hide"]?.[0];
+ hideHandler();
+
+ expect(updateTrayMenu).toHaveBeenCalled();
+ });
+ });
+
+ describe("window state tracking", () => {
+ it("should update config.windowState on maximize event", () => {
+ (mockMainWindow.isMaximized as jest.Mock).mockReturnValue(true);
+ createMainWindow();
+
+ const maximizeHandler = eventHandlers["maximize"]?.[0];
+ maximizeHandler();
+
+ expect(config.windowState).toEqual({ isMaximised: true });
+ });
+
+ it("should update config.windowState on unmaximize event", () => {
+ (mockMainWindow.isMaximized as jest.Mock).mockReturnValue(false);
+ createMainWindow();
+
+ const unmaximizeHandler = eventHandlers["unmaximize"]?.[0];
+ unmaximizeHandler();
+
+ expect(config.windowState).toEqual({ isMaximised: false });
+ });
+ });
+
+ describe("zoom controls", () => {
+ it("should zoom in on Ctrl+=", () => {
+ (mockWebContents.getZoomLevel as jest.Mock).mockReturnValue(2);
+ createMainWindow();
+
+ const beforeInputHandler = eventHandlers["before-input-event"]?.[0];
+ const event = { preventDefault: jest.fn() };
+ beforeInputHandler(event, { control: true, key: "=" });
+
+ expect(event.preventDefault).toHaveBeenCalled();
+ expect(mockWebContents.setZoomLevel).toHaveBeenCalledWith(3);
+ });
+
+ it("should zoom out on Ctrl+-", () => {
+ (mockWebContents.getZoomLevel as jest.Mock).mockReturnValue(2);
+ createMainWindow();
+
+ const beforeInputHandler = eventHandlers["before-input-event"]?.[0];
+ const event = { preventDefault: jest.fn() };
+ beforeInputHandler(event, { control: true, key: "-" });
+
+ expect(event.preventDefault).toHaveBeenCalled();
+ expect(mockWebContents.setZoomLevel).toHaveBeenCalledWith(1);
+ });
+
+ it("should not interfere with non-zoom shortcuts", () => {
+ createMainWindow();
+
+ const beforeInputHandler = eventHandlers["before-input-event"]?.[0];
+ const event = { preventDefault: jest.fn() };
+ beforeInputHandler(event, { control: true, key: "c" });
+
+ expect(event.preventDefault).not.toHaveBeenCalled();
+ expect(mockWebContents.setZoomLevel).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("did-finish-load event", () => {
+ it("should call config.sync when page finishes loading", () => {
+ createMainWindow();
+
+ const finishLoadHandler = eventHandlers["did-finish-load"]?.[0];
+ finishLoadHandler();
+
+ expect(config.sync).toHaveBeenCalled();
+ });
+ });
+
+ describe("context menu", () => {
+ let contextMenuHandler: Function;
+
+ beforeEach(() => {
+ createMainWindow();
+ contextMenuHandler = eventHandlers["context-menu"]?.[0];
+ });
+
+ it("should add dictionary suggestions as menu items", () => {
+ const params = {
+ dictionarySuggestions: ["hello", "world"] as string[],
+ misspelledWord: "",
+ };
+ const mockMenu = {
+ append: jest.fn(),
+ popup: jest.fn(),
+ items: [{}, {}, {}] as unknown[],
+ };
+ (Menu as unknown as jest.Mock).mockImplementation(() => mockMenu);
+
+ contextMenuHandler({}, params);
+
+ expect(mockMenu.append).toHaveBeenCalledTimes(3);
+ expect(mockMenu.append).toHaveBeenCalledWith(
+ expect.objectContaining({
+ label: "hello",
+ }),
+ );
+ expect(mockMenu.append).toHaveBeenCalledWith(
+ expect.objectContaining({
+ label: "world",
+ }),
+ );
+ });
+
+ it("should add 'Add to dictionary' option for misspelled word", () => {
+ const params = {
+ dictionarySuggestions: [] as string[],
+ misspelledWord: "wrng",
+ };
+ const mockMenu = {
+ append: jest.fn(),
+ popup: jest.fn(),
+ items: [{}, {}] as unknown[],
+ };
+ (Menu as unknown as jest.Mock).mockImplementation(() => mockMenu);
+
+ contextMenuHandler({}, params);
+
+ expect(mockMenu.append).toHaveBeenCalledWith(
+ expect.objectContaining({
+ label: "Add to dictionary",
+ }),
+ );
+ });
+
+ it("should add 'Toggle spellcheck' option", () => {
+ const params = {
+ dictionarySuggestions: [] as string[],
+ misspelledWord: "",
+ };
+ const mockMenu = {
+ append: jest.fn(),
+ popup: jest.fn(),
+ items: [{}] as unknown[],
+ };
+ (Menu as unknown as jest.Mock).mockImplementation(() => mockMenu);
+
+ contextMenuHandler({}, params);
+
+ expect(mockMenu.append).toHaveBeenCalledWith(
+ expect.objectContaining({
+ label: "Toggle spellcheck",
+ }),
+ );
+ });
+
+ it("should popup menu when there are items", () => {
+ const params = {
+ dictionarySuggestions: ["hello"] as string[],
+ misspelledWord: "",
+ };
+ const mockMenu = {
+ append: jest.fn(),
+ popup: jest.fn(),
+ items: [{}, {}] as unknown[],
+ };
+ (Menu as unknown as jest.Mock).mockImplementation(() => mockMenu);
+
+ contextMenuHandler({}, params);
+
+ expect(mockMenu.popup).toHaveBeenCalled();
+ });
+
+ it("should not popup menu when there are no items", () => {
+ const params = {
+ dictionarySuggestions: [] as string[],
+ misspelledWord: "",
+ };
+ const mockMenu = {
+ append: jest.fn(),
+ popup: jest.fn(),
+ items: [] as unknown[],
+ };
+ (Menu as unknown as jest.Mock).mockImplementation(() => mockMenu);
+
+ contextMenuHandler({}, params);
+
+ expect(mockMenu.popup).not.toHaveBeenCalled();
+ });
+
+ it("should call replaceMisspelling when suggestion is clicked", () => {
+ const params = {
+ dictionarySuggestions: ["correct"] as string[],
+ misspelledWord: "",
+ };
+ let clickHandler: Function;
+ const mockMenu = {
+ append: jest.fn((item: { label: string; click: Function }) => {
+ if (item.label === "correct") {
+ clickHandler = item.click;
+ }
+ }),
+ popup: jest.fn(),
+ items: [{}] as unknown[],
+ };
+ (Menu as unknown as jest.Mock).mockImplementation(() => mockMenu);
+
+ contextMenuHandler({}, params);
+
+ clickHandler!();
+ expect(mockWebContents.replaceMisspelling).toHaveBeenCalledWith("correct");
+ });
+
+ it("should call addWordToSpellCheckerDictionary when Add to dictionary is clicked", () => {
+ const params = {
+ dictionarySuggestions: [] as string[],
+ misspelledWord: "wrng",
+ };
+ let clickHandler: Function;
+ const mockMenu = {
+ append: jest.fn((item: { label: string; click: Function }) => {
+ if (item.label === "Add to dictionary") {
+ clickHandler = item.click;
+ }
+ }),
+ popup: jest.fn(),
+ items: [{}, {}] as unknown[],
+ };
+ (Menu as unknown as jest.Mock).mockImplementation(() => mockMenu);
+
+ contextMenuHandler({}, params);
+
+ clickHandler!();
+ expect(
+ (mockWebContents.session as { addWordToSpellCheckerDictionary: jest.Mock })
+ .addWordToSpellCheckerDictionary,
+ ).toHaveBeenCalledWith("wrng");
+ });
+
+ it("should toggle spellchecker when Toggle spellcheck is clicked", () => {
+ const params = {
+ dictionarySuggestions: [] as string[],
+ misspelledWord: "",
+ };
+ let clickHandler: Function;
+ const mockMenu = {
+ append: jest.fn((item: { label: string; click: Function }) => {
+ if (item.label === "Toggle spellcheck") {
+ clickHandler = item.click;
+ }
+ }),
+ popup: jest.fn(),
+ items: [{}] as unknown[],
+ };
+ (Menu as unknown as jest.Mock).mockImplementation(() => mockMenu);
+ cfg.spellchecker = true;
+
+ contextMenuHandler({}, params);
+
+ clickHandler!();
+ expect(cfg.spellchecker).toBe(false);
+ });
+ });
+
+ describe("IPC handlers", () => {
+ beforeEach(() => {
+ createMainWindow();
+ });
+
+ it("should minimise window on 'minimise' IPC event", () => {
+ ipcHandlers["minimise"]();
+ expect(mockMainWindow.minimize).toHaveBeenCalled();
+ });
+
+ it("should maximise window on 'maximise' IPC event when not maximized", () => {
+ (mockMainWindow.isMaximized as jest.Mock).mockReturnValue(false);
+ ipcHandlers["maximise"]();
+ expect(mockMainWindow.maximize).toHaveBeenCalled();
+ });
+
+ it("should unmaximise window on 'maximise' IPC event when maximized", () => {
+ (mockMainWindow.isMaximized as jest.Mock).mockReturnValue(true);
+ ipcHandlers["maximise"]();
+ expect(mockMainWindow.unmaximize).toHaveBeenCalled();
+ });
+
+ it("should close window on 'close' IPC event", () => {
+ ipcHandlers["close"]();
+ expect(mockMainWindow.close).toHaveBeenCalled();
+ });
+ });
+
+ describe("quitApp", () => {
+ it("should close the main window", () => {
+ createMainWindow();
+ quitApp();
+ expect(mockMainWindow.close).toHaveBeenCalled();
+ });
+ });
+
+ describe("app before-quit event", () => {
+ it("should register a before-quit handler at module load time", () => {
+ // The before-quit handler is registered at module load time (top-level)
+ // and stored on global by our mock
+ expect((global as Record).__beforeQuitHandler).toBeDefined();
+ });
+
+ it("should set shouldQuit to true when before-quit is triggered", () => {
+ createMainWindow();
+
+ // Get the before-quit handler from global
+ const beforeQuitHandler = (global as Record)
+ .__beforeQuitHandler as Function;
+
+ // Trigger the before-quit handler
+ beforeQuitHandler();
+
+ // Now trigger close event - it should not prevent default
+ cfg.minimiseToTray = true;
+ const closeHandler = eventHandlers["close"]?.[0];
+ const event = { preventDefault: jest.fn() };
+ closeHandler(event);
+
+ expect(event.preventDefault).not.toHaveBeenCalled();
+ expect(mockMainWindow.hide).not.toHaveBeenCalled();
+ });
+ });
+});