From 50ab3cd02b5926e2d1953981752dfd4a42442615 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Sun, 4 Oct 2026 17:15:41 -0400 Subject: [PATCH] fix(sonar): handle app.whenReady rejection on startup (typescript:S9383) Replace the bare 'void' on the app.whenReady().then(...) startup chain with a .catch handler that logs startup failures via electron-log. A rejection in onFirstRunMaybe() (which awaits dialog.showMessageBox) or any register*/initialize handler would otherwise be left unhandled in the main process, silently skipping IPC handler registration or crashing the app depending on Node's unhandled-rejection mode. --- src/main/index.test.ts | 120 +++++++++++++++++++++++++++++++++++++++++ src/main/index.ts | 33 +++++++----- 2 files changed, 140 insertions(+), 13 deletions(-) create mode 100644 src/main/index.test.ts diff --git a/src/main/index.test.ts b/src/main/index.test.ts new file mode 100644 index 000000000..56f7e478e --- /dev/null +++ b/src/main/index.test.ts @@ -0,0 +1,120 @@ +import './index'; + +const { calls, menubarOptions, menubarMock } = vi.hoisted(() => ({ + calls: [] as string[], + menubarOptions: [] as unknown[], + menubarMock: vi.fn((options: unknown) => { + menubarOptions.push(options); + return { window: {} }; + }), +})); + +vi.mock('electron', () => ({ + app: { + isPackaged: true, + whenReady: vi.fn(() => { + calls.push('whenReady'); + return Promise.resolve(); + }), + setAsDefaultProtocolClient: vi.fn(), + on: vi.fn(), + }, +})); + +vi.mock('electron-log', () => ({ + default: { + initialize: vi.fn(), + transports: { file: {} }, + }, +})); + +vi.mock('electron-menubar', () => ({ menubar: menubarMock })); + +vi.mock('./config', () => ({ + Paths: { indexHtml: 'index.html' }, + WindowConfig: { width: 500, height: 400 }, +})); + +vi.mock('./handlers', () => ({ + registerAppHandlers: vi.fn(), + registerGitHubCliHandlers: vi.fn(), + registerStorageHandlers: vi.fn(), + registerSystemHandlers: vi.fn(), + registerTrayHandlers: vi.fn(), + registerUpdaterHandlers: vi.fn(), +})); + +vi.mock('./icons', () => ({ + getIdleTrayIcon: vi.fn(() => 'icon.png'), +})); + +vi.mock('./lifecycle', () => ({ + configureWindowEvents: vi.fn(), + handleProtocolURL: vi.fn(), + initializeAppLifecycle: vi.fn(), + onFirstRunMaybe: vi.fn(), +})); + +vi.mock('./menu', () => ({ + default: class { + buildMenu() { + return {}; + } + }, +})); + +vi.mock('./ozone', () => ({ + applyOzonePlatform: vi.fn(() => calls.push('ozone')), +})); + +vi.mock('./updater', () => ({ + default: class {}, +})); + +describe('main/index.ts', () => { + it('configures electron-menubar with the app window settings', () => { + expect(menubarOptions).toEqual([ + { + icon: 'icon.png', + index: 'index.html', + browserWindow: { width: 500, height: 400 }, + preloadWindow: true, + showDockIcon: false, + hideOnClose: true, + escapeToHide: true, + }, + ]); + }); + + it('applies the Ozone backend before Electron becomes ready', () => { + expect(calls).toEqual(['ozone', 'whenReady']); + }); + + it('logs an error when Electron fails to become ready', async () => { + vi.resetModules(); + vi.doMock('electron', () => ({ + app: { + isPackaged: true, + whenReady: vi.fn(() => Promise.reject(new Error('ready-failed'))), + setAsDefaultProtocolClient: vi.fn(), + on: vi.fn(), + }, + })); + vi.doMock('../shared/logger', () => ({ + logError: vi.fn(), + toError: (err: unknown) => (err instanceof Error ? err : new Error(String(err))), + })); + + const { logError } = await import('../shared/logger'); + + await import('./index'); + + await vi.waitFor(() => + expect(logError).toHaveBeenCalledWith( + 'main', + 'Failed to initialize application', + expect.any(Error), + ), + ); + }); +}); diff --git a/src/main/index.ts b/src/main/index.ts index d7735c725..8b9f58646 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -2,6 +2,8 @@ import { app } from 'electron'; import log from 'electron-log'; import { menubar } from 'electron-menubar'; +import { logError, toError } from '../shared/logger'; + import { Paths, WindowConfig } from './config'; import { registerAppHandlers, @@ -52,22 +54,27 @@ app.setAsDefaultProtocolClient(protocol); const appUpdater = new AppUpdater(mb, menuBuilder); -void app.whenReady().then(async () => { - await onFirstRunMaybe(); +app + .whenReady() + .then(async () => { + await onFirstRunMaybe(); - initializeAppLifecycle(mb, contextMenu, protocol); + initializeAppLifecycle(mb, contextMenu, protocol); - // Configure window event handlers (Escape key, DevTools resize) - configureWindowEvents(mb, menuBuilder); + // Configure window event handlers (Escape key, DevTools resize) + configureWindowEvents(mb, menuBuilder); - // Register IPC handlers for various channels - registerTrayHandlers(mb); - registerSystemHandlers(mb); - registerStorageHandlers(); - registerGitHubCliHandlers(); - registerAppHandlers(mb); - registerUpdaterHandlers(appUpdater); -}); + // Register IPC handlers for various channels + registerTrayHandlers(mb); + registerSystemHandlers(mb); + registerStorageHandlers(); + registerGitHubCliHandlers(); + registerAppHandlers(mb); + registerUpdaterHandlers(appUpdater); + }) + .catch((err) => { + logError('main', 'Failed to initialize application', toError(err)); + }); // Handle gitify:// custom protocol URL events for OAuth 2.0 callback app.on('open-url', (event, url) => {