test: add src/native/window.test.ts

This commit is contained in:
ToTheos-Dev 2026-05-22 14:25:19 +10:00
parent f9c616ba6e
commit 347944463d
2 changed files with 569 additions and 0 deletions

1
__mocks__/fileMock.js Normal file
View File

@ -0,0 +1 @@
module.exports = "test-file-stub";

568
src/native/window.test.ts Normal file
View File

@ -0,0 +1,568 @@
/// <reference types="jest" />
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<string, unknown>).__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<string, unknown>;
describe("window", () => {
let mockMainWindow: Record<string, jest.Mock | unknown>;
let mockWebContents: Record<string, jest.Mock | unknown>;
let eventHandlers: Record<string, Function[]>;
let ipcHandlers: Record<string, Function>;
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<string, unknown>).__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<string, unknown>)
.__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();
});
});
});