Files
deepseek-harness/apps/desktop/tests/tray.spec.ts
T
winewill 4745934683 feat(desktop): hide the window on close and confirm interruptible quits
Closing the main window hides it on macOS and Windows; the page and the
Host keep running and the next show resumes the same document. Windows
gets a permanent tray icon (open / quit) and a one-time notification the
first time the window is hidden after installation.

Every ordinary quit entry asks the Host over a new quit-inspection IPC
request for active tasks (same rule as the update restart check) and
armed scheduled reminders (schedule family of workspace/session-activity
for loaded sessions). Nothing to interrupt quits silently; otherwise a
native message box explains what stops, with Quit as the default and
Cancel on Esc. Inspection failure or a missed two-second deadline counts
as running tasks. The installer restart, fatal recovery, the development
restart command, and OS session end skip the confirmation.

Also: defer the downloaded-update confirmation while the window is hidden,
render the tray ICO from the vector icon, and point the installer's
running-application message at the tray.

Closes #4013
2026-09-24 14:24:39 +08:00

73 lines
2.9 KiB
TypeScript

import type { MenuItemConstructorOptions } from 'electron'
import { afterEach, expect, it, vi } from 'vitest'
import { resolveDesktopLocale } from '../src/locale.ts'
import { DesktopTray } from '../src/tray.ts'
const native = await vi.hoisted(async () => {
const { EventEmitter } = await import('node:events')
const trays: FakeTray[] = []
class FakeTray extends EventEmitter {
readonly setToolTip = vi.fn()
readonly setContextMenu = vi.fn()
readonly destroy = vi.fn()
constructor(readonly image: unknown) { super(); trays.push(this) }
}
const menus: MenuItemConstructorOptions[][] = []
return {
trays, FakeTray, menus,
createFromPath: vi.fn((path: string) => ({ path })),
buildFromTemplate: vi.fn((template: MenuItemConstructorOptions[]) => { menus.push(template); return { template } }),
}
})
vi.mock('electron', () => ({
Tray: native.FakeTray,
nativeImage: { createFromPath: native.createFromPath },
Menu: { buildFromTemplate: native.buildFromTemplate },
}))
afterEach(() => { native.trays.length = 0; native.menus.length = 0; vi.clearAllMocks() })
function setup(locale = 'en') {
let current = resolveDesktopLocale(locale)
const open = vi.fn()
const quit = vi.fn()
const tray = new DesktopTray({ iconPath: 'C:/app/resources/tray.ico', locale: () => current, open, quit })
return { tray, open, quit, native: native.trays[0]!, setLocale: (next: string) => { current = resolveDesktopLocale(next) } }
}
function labels(menu: MenuItemConstructorOptions[]): (string | undefined)[] {
return menu.map(item => item.type === 'separator' ? 'separator' : item.label)
}
it('shows the application icon with its name as the tooltip and an Open / Quit menu', () => {
const f = setup()
expect(native.createFromPath).toHaveBeenCalledWith('C:/app/resources/tray.ico')
expect(f.native.image).toEqual({ path: 'C:/app/resources/tray.ico' })
expect(f.native.setToolTip).toHaveBeenCalledWith('DeepSeek Harness')
expect(labels(native.menus[0]!)).toEqual(['Open DeepSeek Harness', 'separator', 'Quit DeepSeek Harness'])
expect(f.native.setContextMenu).toHaveBeenCalledWith({ template: native.menus[0] })
})
it('opens the window on a single click and routes menu entries to the open and quit actions', () => {
const f = setup()
f.native.emit('click')
expect(f.open).toHaveBeenCalledOnce()
const menu = native.menus[0]!
;(menu[0] as { click: () => void }).click()
;(menu[2] as { click: () => void }).click()
expect(f.open).toHaveBeenCalledTimes(2)
expect(f.quit).toHaveBeenCalledOnce()
})
it('relabels the menu in the current locale and ignores relabel after disposal', () => {
const f = setup()
f.setLocale('zh')
f.tray.relabel()
expect(labels(native.menus[1]!)).toEqual(['打开 DeepSeek Harness', 'separator', '退出 DeepSeek Harness'])
f.tray.dispose()
f.tray.dispose()
expect(f.native.destroy).toHaveBeenCalledOnce()
f.tray.relabel()
expect(native.menus).toHaveLength(2)
})