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); }); });