Skip to content

Commit 50ab3cd

Browse files
committed
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.
1 parent d04db40 commit 50ab3cd

2 files changed

Lines changed: 140 additions & 13 deletions

File tree

‎src/main/index.test.ts‎

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import './index';
2+
3+
const { calls, menubarOptions, menubarMock } = vi.hoisted(() => ({
4+
calls: [] as string[],
5+
menubarOptions: [] as unknown[],
6+
menubarMock: vi.fn((options: unknown) => {
7+
menubarOptions.push(options);
8+
return { window: {} };
9+
}),
10+
}));
11+
12+
vi.mock('electron', () => ({
13+
app: {
14+
isPackaged: true,
15+
whenReady: vi.fn(() => {
16+
calls.push('whenReady');
17+
return Promise.resolve();
18+
}),
19+
setAsDefaultProtocolClient: vi.fn(),
20+
on: vi.fn(),
21+
},
22+
}));
23+
24+
vi.mock('electron-log', () => ({
25+
default: {
26+
initialize: vi.fn(),
27+
transports: { file: {} },
28+
},
29+
}));
30+
31+
vi.mock('electron-menubar', () => ({ menubar: menubarMock }));
32+
33+
vi.mock('./config', () => ({
34+
Paths: { indexHtml: 'index.html' },
35+
WindowConfig: { width: 500, height: 400 },
36+
}));
37+
38+
vi.mock('./handlers', () => ({
39+
registerAppHandlers: vi.fn(),
40+
registerGitHubCliHandlers: vi.fn(),
41+
registerStorageHandlers: vi.fn(),
42+
registerSystemHandlers: vi.fn(),
43+
registerTrayHandlers: vi.fn(),
44+
registerUpdaterHandlers: vi.fn(),
45+
}));
46+
47+
vi.mock('./icons', () => ({
48+
getIdleTrayIcon: vi.fn(() => 'icon.png'),
49+
}));
50+
51+
vi.mock('./lifecycle', () => ({
52+
configureWindowEvents: vi.fn(),
53+
handleProtocolURL: vi.fn(),
54+
initializeAppLifecycle: vi.fn(),
55+
onFirstRunMaybe: vi.fn(),
56+
}));
57+
58+
vi.mock('./menu', () => ({
59+
default: class {
60+
buildMenu() {
61+
return {};
62+
}
63+
},
64+
}));
65+
66+
vi.mock('./ozone', () => ({
67+
applyOzonePlatform: vi.fn(() => calls.push('ozone')),
68+
}));
69+
70+
vi.mock('./updater', () => ({
71+
default: class {},
72+
}));
73+
74+
describe('main/index.ts', () => {
75+
it('configures electron-menubar with the app window settings', () => {
76+
expect(menubarOptions).toEqual([
77+
{
78+
icon: 'icon.png',
79+
index: 'index.html',
80+
browserWindow: { width: 500, height: 400 },
81+
preloadWindow: true,
82+
showDockIcon: false,
83+
hideOnClose: true,
84+
escapeToHide: true,
85+
},
86+
]);
87+
});
88+
89+
it('applies the Ozone backend before Electron becomes ready', () => {
90+
expect(calls).toEqual(['ozone', 'whenReady']);
91+
});
92+
93+
it('logs an error when Electron fails to become ready', async () => {
94+
vi.resetModules();
95+
vi.doMock('electron', () => ({
96+
app: {
97+
isPackaged: true,
98+
whenReady: vi.fn(() => Promise.reject(new Error('ready-failed'))),
99+
setAsDefaultProtocolClient: vi.fn(),
100+
on: vi.fn(),
101+
},
102+
}));
103+
vi.doMock('../shared/logger', () => ({
104+
logError: vi.fn(),
105+
toError: (err: unknown) => (err instanceof Error ? err : new Error(String(err))),
106+
}));
107+
108+
const { logError } = await import('../shared/logger');
109+
110+
await import('./index');
111+
112+
await vi.waitFor(() =>
113+
expect(logError).toHaveBeenCalledWith(
114+
'main',
115+
'Failed to initialize application',
116+
expect.any(Error),
117+
),
118+
);
119+
});
120+
});

‎src/main/index.ts‎

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { app } from 'electron';
22
import log from 'electron-log';
33
import { menubar } from 'electron-menubar';
44

5+
import { logError, toError } from '../shared/logger';
6+
57
import { Paths, WindowConfig } from './config';
68
import {
79
registerAppHandlers,
@@ -52,22 +54,27 @@ app.setAsDefaultProtocolClient(protocol);
5254

5355
const appUpdater = new AppUpdater(mb, menuBuilder);
5456

55-
void app.whenReady().then(async () => {
56-
await onFirstRunMaybe();
57+
app
58+
.whenReady()
59+
.then(async () => {
60+
await onFirstRunMaybe();
5761

58-
initializeAppLifecycle(mb, contextMenu, protocol);
62+
initializeAppLifecycle(mb, contextMenu, protocol);
5963

60-
// Configure window event handlers (Escape key, DevTools resize)
61-
configureWindowEvents(mb, menuBuilder);
64+
// Configure window event handlers (Escape key, DevTools resize)
65+
configureWindowEvents(mb, menuBuilder);
6266

63-
// Register IPC handlers for various channels
64-
registerTrayHandlers(mb);
65-
registerSystemHandlers(mb);
66-
registerStorageHandlers();
67-
registerGitHubCliHandlers();
68-
registerAppHandlers(mb);
69-
registerUpdaterHandlers(appUpdater);
70-
});
67+
// Register IPC handlers for various channels
68+
registerTrayHandlers(mb);
69+
registerSystemHandlers(mb);
70+
registerStorageHandlers();
71+
registerGitHubCliHandlers();
72+
registerAppHandlers(mb);
73+
registerUpdaterHandlers(appUpdater);
74+
})
75+
.catch((err) => {
76+
logError('main', 'Failed to initialize application', toError(err));
77+
});
7178

7279
// Handle gitify:// custom protocol URL events for OAuth 2.0 callback
7380
app.on('open-url', (event, url) => {

0 commit comments

Comments
 (0)