diff --git a/packages/desktop/src/recovery.test.ts b/packages/desktop/src/recovery.test.ts index 4eaca4cb..d463896b 100644 --- a/packages/desktop/src/recovery.test.ts +++ b/packages/desktop/src/recovery.test.ts @@ -4,6 +4,7 @@ import { RecoveryStateStore, extractErrorCode, buildTrayMenuTemplate, + buildAppMenuTemplate, type RecoveryState, } from './recovery'; @@ -211,3 +212,45 @@ describe('buildTrayMenuTemplate', () => { expect(downloadedItem!.enabled).toBe(true); }); }); + +describe('buildAppMenuTemplate', () => { + it('returns top-level menu with App, Edit, Window submenus', () => { + const template = buildAppMenuTemplate('Backspace', defaultState()); + const [appMenu, editMenu, windowMenu] = template; + expect(template.length).toBeGreaterThanOrEqual(3); + expect(appMenu!.label).toBe('Backspace'); + expect(editMenu!.label).toBe('Edit'); + expect(windowMenu!.label).toBe('Window'); + }); + + it('App submenu includes About and Change Instance', () => { + const template = buildAppMenuTemplate('Backspace', defaultState()); + const [appMenu] = template; + const appSub = appMenu!.submenu as MenuItemConstructorOptions[]; + const labels = appSub.map((i) => i.label).filter(Boolean); + expect(appSub.find((i) => i.role === 'about')).toBeDefined(); + expect(labels).toContain('Change Instance'); + expect(appSub.find((i) => i.role === 'quit')).toBeDefined(); + }); + + it('App submenu includes Check for Updates with state-correct label', () => { + const template = buildAppMenuTemplate('Backspace', defaultState({ updateState: 'idle' })); + const [appMenu] = template; + const appSub = appMenu!.submenu as MenuItemConstructorOptions[]; + const item = appSub.find((i) => i.id === 'check-for-updates'); + expect(item).toBeDefined(); + expect(item!.label).toBe('Check for Updates…'); + }); + + it('App submenu inserts Restart to Install Update only when downloaded', () => { + const [idleAppMenu] = buildAppMenuTemplate('Backspace', defaultState({ updateState: 'idle' })); + const idleSub = idleAppMenu!.submenu as MenuItemConstructorOptions[]; + expect(idleSub.find((i) => i.id === 'restart-to-install')).toBeUndefined(); + + const [dlAppMenu] = buildAppMenuTemplate('Backspace', defaultState({ updateState: 'downloaded' })); + const dlSub = dlAppMenu!.submenu as MenuItemConstructorOptions[]; + const restartItem = dlSub.find((i) => i.id === 'restart-to-install'); + expect(restartItem).toBeDefined(); + expect(restartItem!.label).toBe('Restart to Install Update'); + }); +}); diff --git a/packages/desktop/src/recovery.ts b/packages/desktop/src/recovery.ts index 21b0799b..bcc44744 100644 --- a/packages/desktop/src/recovery.ts +++ b/packages/desktop/src/recovery.ts @@ -136,3 +136,60 @@ export function buildTrayMenuTemplate( return items; } + +export function buildAppMenuTemplate( + appName: string, + state: RecoveryState, + actions?: Partial, +): MenuItemConstructorOptions[] { + const appSubmenu: MenuItemConstructorOptions[] = [ + { role: 'about' }, + { type: 'separator' }, + checkForUpdatesItem(state, () => actions?.onCheckForUpdates?.()), + ]; + + if (state.updateState === 'downloaded') { + appSubmenu.push({ + id: 'restart-to-install', + label: 'Restart to Install Update', + enabled: true, + click: actions?.onRestartToInstall, + }); + } + + appSubmenu.push( + { type: 'separator' }, + { label: 'Change Instance', click: actions?.onChangeInstance }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideOthers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' }, + ); + + return [ + { label: appName, submenu: appSubmenu }, + { + label: 'Edit', + submenu: [ + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + { role: 'selectAll' }, + ], + }, + { + label: 'Window', + submenu: [ + { role: 'minimize' }, + { role: 'zoom' }, + { type: 'separator' }, + { role: 'front' }, + ], + }, + ]; +}