// closeToTray=false 로 메인 창을 닫으면 창 없는 프로세스로 남지 않고 종료한다. 창이 사라진 뒤 트레이 // '표시'·두 번째 실행은 창을 다시 만든다. import { beforeEach, describe, expect, it, vi } from 'vitest' type Listener = (...args: unknown[]) => void const state = vi.hoisted(() => ({ windows: [] as Array<{ listeners: Map; destroyed: boolean }>, quit: vi.fn(), })) vi.unmock('../../../src/main/windows/WindowManager') vi.mock('electron', () => { const quit = state.quit class FakeBrowserWindow { listeners = new Map() destroyed = false webContents = { on: vi.fn(), setWindowOpenHandler: vi.fn(), openDevTools: vi.fn(), insertCSS: vi.fn(async () => ''), send: vi.fn(), } constructor() { state.windows.push(this) } on(event: string, listener: Listener): this { this.listeners.set(event, listener) return this } once(event: string, listener: Listener): this { return this.on(event, listener) } setSkipTaskbar = vi.fn() setMenu = vi.fn() loadURL = vi.fn(async () => undefined) loadFile = vi.fn(async () => undefined) center = vi.fn() restore = vi.fn() show = vi.fn() focus = vi.fn() hide = vi.fn() setAlwaysOnTop = vi.fn() flashFrame = vi.fn() isMinimized = vi.fn(() => false) isDestroyed(): boolean { return this.destroyed } } return { app: { quit, getPath: vi.fn(() => '.'), on: vi.fn(), getVersion: vi.fn(() => '1.0.0'), isPackaged: false }, BrowserWindow: Object.assign(FakeBrowserWindow, { getAllWindows: vi.fn(() => []) }), screen: { getPrimaryDisplay: vi.fn(() => ({ workAreaSize: { width: 1920, height: 1080 }, workArea: { x: 0, y: 0, width: 1920, height: 1080 } })), getCursorScreenPoint: vi.fn(() => ({ x: 0, y: 0 })), getDisplayNearestPoint: vi.fn(() => ({ workArea: { x: 0, y: 0, width: 1920, height: 1080 } })), }, ipcMain: { on: vi.fn(), handle: vi.fn(), removeHandler: vi.fn() }, Menu: { setApplicationMenu: vi.fn(), buildFromTemplate: vi.fn() }, clipboard: { writeText: vi.fn() }, shell: { openExternal: vi.fn(async () => undefined) }, safeStorage: { isEncryptionAvailable: vi.fn(() => false) }, nativeImage: { createFromPath: vi.fn(), createEmpty: vi.fn() }, Tray: vi.fn(), dialog: { showErrorBox: vi.fn() }, } }) vi.mock('@electron-toolkit/utils', () => ({ is: { dev: false } })) vi.mock('../../../src/main/lifecycle', () => { let quitting = false return { getIsQuitting: () => quitting, setIsQuitting: (value: boolean) => { quitting = value }, } }) const config: Record = { closeToTray: false, theme: 'dark', language: 'ko' } vi.mock('../../../src/main/services/ConfigService', () => ({ configGet: (key: string) => config[key], configSet: (key: string, value: unknown) => { config[key] = value }, })) beforeEach(async () => { state.windows.length = 0 state.quit.mockClear() const lifecycle = await import('../../../src/main/lifecycle') lifecycle.setIsQuitting(false) }) describe('main window close', () => { it('closeToTray=false 이면 창 닫기가 앱 종료로 이어진다', async () => { config.closeToTray = false const { createMainWindow } = await import('../../../src/main/windows/WindowManager') createMainWindow() const win = state.windows[0] const event = { preventDefault: vi.fn() } win.listeners.get('close')?.(event) expect(event.preventDefault).not.toHaveBeenCalled() expect(state.quit).toHaveBeenCalledTimes(1) }) it('closeToTray=true 이면 숨기기만 하고 종료하지 않는다', async () => { vi.resetModules() config.closeToTray = true const { createMainWindow } = await import('../../../src/main/windows/WindowManager') createMainWindow() const win = state.windows[0] const event = { preventDefault: vi.fn() } win.listeners.get('close')?.(event) expect(event.preventDefault).toHaveBeenCalled() expect(state.quit).not.toHaveBeenCalled() }) it('창이 파괴된 뒤 showMainWindow 는 창을 다시 만든다(부트스트랩 전에는 만들지 않는다)', async () => { vi.resetModules() const { createMainWindow, showMainWindow, getMainWindow } = await import('../../../src/main/windows/WindowManager') expect(showMainWindow()).toBeNull() expect(state.windows).toHaveLength(0) createMainWindow() const first = state.windows[0] first.destroyed = true first.listeners.get('closed')?.() expect(getMainWindow()).toBeNull() expect(showMainWindow()).not.toBeNull() expect(state.windows).toHaveLength(2) }) })