feat(desktop): add macOS app menu template builder
Adds buildAppMenuTemplate pure function to recovery.ts that produces the three-submenu macOS app menu (App/Edit/Window), reusing MenuActions and checkForUpdatesItem from T4. Includes Restart to Install Update item conditionally on updateState=downloaded. Tests use destructuring to satisfy noUncheckedIndexedAccess. 26 tests pass, tsc clean.
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
|||||||
RecoveryStateStore,
|
RecoveryStateStore,
|
||||||
extractErrorCode,
|
extractErrorCode,
|
||||||
buildTrayMenuTemplate,
|
buildTrayMenuTemplate,
|
||||||
|
buildAppMenuTemplate,
|
||||||
type RecoveryState,
|
type RecoveryState,
|
||||||
} from './recovery';
|
} from './recovery';
|
||||||
|
|
||||||
@@ -211,3 +212,45 @@ describe('buildTrayMenuTemplate', () => {
|
|||||||
expect(downloadedItem!.enabled).toBe(true);
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -136,3 +136,60 @@ export function buildTrayMenuTemplate(
|
|||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildAppMenuTemplate(
|
||||||
|
appName: string,
|
||||||
|
state: RecoveryState,
|
||||||
|
actions?: Partial<MenuActions>,
|
||||||
|
): 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' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user