test: add src/native/window.test.ts
This commit is contained in:
parent
f9c616ba6e
commit
347944463d
|
|
@ -0,0 +1 @@
|
|||
module.exports = "test-file-stub";
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue