Files
makelore/tests/unit/main-window-reopen-navigation.test.ts
2026-09-29 19:50:25 +08:00

139 lines
5.2 KiB
TypeScript

import { EventEmitter } from 'node:events';
import type { BrowserWindow } from 'electron';
import { beforeEach, describe, expect, it, vi } from 'vitest';
function createWindow(loading = false) {
const contents = Object.assign(new EventEmitter(), {
isDestroyed: vi.fn(() => false),
isLoadingMainFrame: vi.fn(() => loading),
getURL: vi.fn(() => loading ? '' : 'file:///index.html'),
send: vi.fn(),
});
return {
webContents: contents,
isDestroyed: vi.fn(() => false),
isVisible: vi.fn(() => false),
isMinimized: vi.fn(() => false),
restore: vi.fn(),
show: vi.fn(),
hide: vi.fn(),
focus: vi.fn(),
};
}
async function setup() {
const trays: FakeTray[] = [];
const menus: Electron.MenuItemConstructorOptions[][] = [];
class FakeTray extends EventEmitter {
destroyed = false;
setToolTip = vi.fn();
setContextMenu = vi.fn();
isDestroyed = () => this.destroyed;
destroy = () => { this.destroyed = true; };
constructor() {
super();
trays.push(this);
}
}
const getFocusedWindow = vi.fn(() => null);
vi.doMock('electron', () => ({
app: { isPackaged: false, name: 'Makelore', quit: vi.fn() },
Tray: FakeTray,
Menu: {
buildFromTemplate: vi.fn((template: Electron.MenuItemConstructorOptions[]) => {
menus.push(template);
return template;
}),
setApplicationMenu: vi.fn(),
},
BrowserWindow: { getFocusedWindow },
nativeImage: { createFromPath: vi.fn(() => ({ isEmpty: () => false, setTemplateImage: vi.fn() })) },
}));
const { createTray } = await import('../../electron/main/tray');
const { createMenu } = await import('../../electron/main/menu');
const { sendWhenWindowReady } = await import('../../electron/main/window-provider');
return { trays, menus, createTray, createMenu, sendWhenWindowReady };
}
function findItem(items: Electron.MenuItemConstructorOptions[], label: string): Electron.MenuItemConstructorOptions {
for (const item of items) {
if (item.label === label) return item;
if (Array.isArray(item.submenu)) {
try { return findItem(item.submenu, label); } catch { /* Search the next submenu. */ }
}
}
throw new Error(`Missing menu item ${label}`);
}
function click(items: Electron.MenuItemConstructorOptions[], label: string): void {
const handler = findItem(items, label).click;
expect(handler).toBeTypeOf('function');
(handler as () => void)();
}
describe('menu and tray after closing the main window', () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
});
it('reopens a window from New Chat and waits for the renderer before navigating', async () => {
const ctx = await setup();
const window = createWindow(true);
const ensure = vi.fn(() => window as unknown as BrowserWindow);
ctx.createMenu(ensure);
click(ctx.menus[0], 'New Chat');
expect(ensure).toHaveBeenCalledOnce();
expect(window.show).toHaveBeenCalledOnce();
expect(window.webContents.send).not.toHaveBeenCalled();
window.webContents.emit('did-finish-load');
expect(window.webContents.send).toHaveBeenCalledExactlyOnceWith('navigate', '/');
expect(window.webContents.listenerCount('destroyed')).toBe(0);
});
it('reuses the tray while resolving a replacement window and restoring a minimized window', async () => {
const ctx = await setup();
const oldWindow = createWindow();
let current: BrowserWindow | null = oldWindow as unknown as BrowserWindow;
const replacement = createWindow(true);
replacement.isMinimized.mockReturnValue(true);
const ensure = vi.fn(() => {
current = replacement as unknown as BrowserWindow;
return current;
});
const tray = ctx.createTray(() => current, ensure);
oldWindow.isDestroyed.mockReturnValue(true);
expect(ctx.createTray(() => current, ensure)).toBe(tray);
expect(ctx.trays).toHaveLength(1);
click(ctx.menus[0], 'Open Settings');
expect(ensure).toHaveBeenCalledOnce();
expect(replacement.restore).toHaveBeenCalledOnce();
expect(oldWindow.show).not.toHaveBeenCalled();
expect(replacement.webContents.send).not.toHaveBeenCalled();
replacement.webContents.emit('did-finish-load');
expect(replacement.webContents.send).toHaveBeenCalledWith('navigate', '/settings');
});
it('opens a fresh window from the tray click when no window exists', async () => {
const ctx = await setup();
const replacement = createWindow();
const ensure = vi.fn(() => replacement as unknown as BrowserWindow);
ctx.createTray(() => null, ensure);
ctx.trays[0].emit('click');
expect(ensure).toHaveBeenCalledOnce();
expect(replacement.show).toHaveBeenCalledOnce();
expect(replacement.focus).toHaveBeenCalledOnce();
});
it('cancels a pending navigation if the replacement window closes before loading', async () => {
const ctx = await setup();
const window = createWindow(true);
ctx.sendWhenWindowReady(window as unknown as BrowserWindow, 'navigate', '/settings');
window.isDestroyed.mockReturnValue(true);
window.webContents.emit('destroyed');
window.webContents.emit('did-finish-load');
expect(window.webContents.send).not.toHaveBeenCalled();
expect(window.webContents.listenerCount('did-finish-load')).toBe(0);
});
});