mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Electron was showing the main BrowserWindow before renderer load completed, so macOS could paint a blank window surface behind the menu bar. Persisted y=0 window state could also restore the titlebar under the menu-bar work area on external displays. Delay the initial show until the renderer entry is loaded, clamp restored macOS bounds into the display workArea, skip the macOS status-bar tray, and unhide the app before focusing restored windows. Constraint: macOS external displays expose menu-bar and status-item chrome differently from Windows and Linux. Rejected: Remove the native application menu | it would break expected macOS menu behavior without fixing the pre-load window surface. Confidence: high Scope-risk: narrow Directive: Do not reintroduce pre-load main-window show without validating macOS external-display menu-bar rendering. Tested: cd desktop && bun test electron/services/windows.test.ts electron/services/tray.test.ts electron/services/singleInstance.test.ts electron/services/menu.test.ts Tested: bun run check:desktop Tested: bun run check:native Tested: git diff --check Not-tested: Live dual-external-monitor screenshot smoke.
121 lines
3.9 KiB
TypeScript
121 lines
3.9 KiB
TypeScript
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
import { tmpdir } from 'node:os'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { installTray, resolveTrayIconPath, shouldInstallTray } from './tray'
|
|
|
|
const trayMocksKey = '__electronTrayMocks'
|
|
|
|
vi.mock('electron', () => {
|
|
const mocks = (globalThis as Record<string, unknown>)[trayMocksKey] as ReturnType<typeof createElectronTrayMocks> | undefined
|
|
if (!mocks) {
|
|
throw new Error('Electron tray mocks were not initialized for this test')
|
|
}
|
|
|
|
return {
|
|
Menu: {
|
|
buildFromTemplate: mocks.buildFromTemplate,
|
|
},
|
|
Tray: mocks.Tray.mockImplementation(() => mocks.tray),
|
|
nativeImage: {
|
|
createFromPath: mocks.createFromPath,
|
|
},
|
|
}
|
|
})
|
|
|
|
function createElectronTrayMocks() {
|
|
const handlers = new Map<string, () => void>()
|
|
return {
|
|
handlers,
|
|
buildFromTemplate: vi.fn((template: unknown) => ({ template })),
|
|
createFromPath: vi.fn((iconPath: string) => ({ iconPath })),
|
|
tray: {
|
|
setToolTip: vi.fn(),
|
|
setContextMenu: vi.fn(),
|
|
on: vi.fn((event: string, handler: () => void) => {
|
|
handlers.set(event, handler)
|
|
}),
|
|
destroy: vi.fn(),
|
|
},
|
|
Tray: vi.fn(),
|
|
}
|
|
}
|
|
|
|
describe('Electron tray service', () => {
|
|
afterEach(() => {
|
|
delete (globalThis as Record<string, unknown>)[trayMocksKey]
|
|
})
|
|
|
|
it('uses the existing desktop icon assets for the tray icon', () => {
|
|
const root = mkdtempSync(path.join(tmpdir(), 'electron-tray-'))
|
|
try {
|
|
const iconPath = path.join(root, 'src-tauri', 'icons', 'icon.png')
|
|
mkdirSync(path.dirname(iconPath), { recursive: true })
|
|
writeFileSync(iconPath, 'png')
|
|
|
|
expect(resolveTrayIconPath(root)).toBe(iconPath)
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('fails clearly when no tray icon exists', () => {
|
|
const root = mkdtempSync(path.join(tmpdir(), 'electron-tray-missing-'))
|
|
try {
|
|
expect(() => resolveTrayIconPath(root)).toThrow('Electron tray icon not found')
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('skips the status-bar tray on macOS and keeps it on Windows and Linux', () => {
|
|
expect(shouldInstallTray('darwin')).toBe(false)
|
|
expect(shouldInstallTray('win32')).toBe(true)
|
|
expect(shouldInstallTray('linux')).toBe(true)
|
|
})
|
|
|
|
it('installs tray handlers that show the app, quit explicitly, and dispose cleanly', async () => {
|
|
const root = mkdtempSync(path.join(tmpdir(), 'electron-tray-install-'))
|
|
try {
|
|
const trayMocks = createElectronTrayMocks()
|
|
;(globalThis as Record<string, unknown>)[trayMocksKey] = trayMocks
|
|
const iconPath = path.join(root, 'src-tauri', 'icons', 'icon.png')
|
|
mkdirSync(path.dirname(iconPath), { recursive: true })
|
|
writeFileSync(iconPath, 'png')
|
|
const show = vi.fn()
|
|
const quit = vi.fn()
|
|
|
|
const controller = await installTray({
|
|
app: { name: 'Claude Code Haha' } as never,
|
|
desktopRoot: root,
|
|
show,
|
|
quit,
|
|
})
|
|
|
|
expect(trayMocks.createFromPath).toHaveBeenCalledWith(iconPath)
|
|
expect(trayMocks.Tray).toHaveBeenCalledTimes(1)
|
|
expect(trayMocks.tray.setToolTip).toHaveBeenCalledWith('Claude Code Haha')
|
|
expect(trayMocks.buildFromTemplate).toHaveBeenCalledTimes(1)
|
|
|
|
const template = trayMocks.buildFromTemplate.mock.calls[0]?.[0] as Array<{ label?: string, click?: () => void, type?: string }>
|
|
expect(template.map(item => item.label ?? item.type)).toEqual([
|
|
'Show Claude Code Haha',
|
|
'separator',
|
|
'Quit Claude Code Haha',
|
|
])
|
|
|
|
template[0]?.click?.()
|
|
template[2]?.click?.()
|
|
trayMocks.handlers.get('click')?.()
|
|
|
|
expect(show).toHaveBeenCalledTimes(2)
|
|
expect(quit).toHaveBeenCalledTimes(1)
|
|
|
|
controller.dispose()
|
|
expect(trayMocks.tray.destroy).toHaveBeenCalledTimes(1)
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true })
|
|
}
|
|
})
|
|
})
|