From 92e7f99e4ca9ff3a505efe34ad4c0a5f348b5541 Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Thu, 7 May 2026 02:29:13 +0200 Subject: [PATCH] test(mocks): pin electron mock shapes via satisfies Pick Wraps each vi.mock('electron', ...) factory with satisfies Pick on every mocked namespace, so any drift between the mock and Electron's real type signatures fails at tsc time. Caught two pre-existing drifts: showMessageBox missing checkboxChecked, and ipcRenderer.on returning void instead of IpcRenderer. --- src/main/config.test.ts | 2 +- src/main/events.test.ts | 2 +- src/main/handlers/app.test.ts | 4 ++-- src/main/handlers/system.test.ts | 8 ++++---- src/main/handlers/tray.test.ts | 4 ++-- src/main/lifecycle/first-run.test.ts | 24 +++++++++++++++++++----- src/main/lifecycle/reset.test.ts | 4 +++- src/main/lifecycle/startup.test.ts | 2 +- src/main/menu.test.ts | 6 ++++-- src/main/updater.test.ts | 12 +++++++++--- src/main/utils.test.ts | 10 +++++++--- src/preload/index.test.ts | 4 ++-- src/preload/utils.test.ts | 28 ++++++++++++++++++---------- 13 files changed, 73 insertions(+), 37 deletions(-) diff --git a/src/main/config.test.ts b/src/main/config.test.ts index 119eae6e7..347f9311c 100644 --- a/src/main/config.test.ts +++ b/src/main/config.test.ts @@ -7,7 +7,7 @@ vi.mock('./utils', () => ({ vi.mock('electron', () => ({ app: { isPackaged: true, - }, + } satisfies Pick, })); describe('main/config.ts', () => { diff --git a/src/main/events.test.ts b/src/main/events.test.ts index 063661f42..bf79d9602 100644 --- a/src/main/events.test.ts +++ b/src/main/events.test.ts @@ -7,7 +7,7 @@ vi.mock('electron', () => ({ ipcMain: { on: (...args: unknown[]) => onMock(...args), handle: (...args: unknown[]) => handleMock(...args), - }, + } satisfies Pick, })); import type { Menubar } from 'menubar'; diff --git a/src/main/handlers/app.test.ts b/src/main/handlers/app.test.ts index 85b6d577e..e23cb9d04 100644 --- a/src/main/handlers/app.test.ts +++ b/src/main/handlers/app.test.ts @@ -11,10 +11,10 @@ vi.mock('electron', () => ({ ipcMain: { handle: (...args: unknown[]) => handleMock(...args), on: (...args: unknown[]) => onMock(...args), - }, + } satisfies Pick, app: { getVersion: vi.fn(() => '1.0.0'), - }, + } satisfies Pick, })); vi.mock('../config', () => ({ diff --git a/src/main/handlers/system.test.ts b/src/main/handlers/system.test.ts index 331efc4dc..e9b5ccba6 100644 --- a/src/main/handlers/system.test.ts +++ b/src/main/handlers/system.test.ts @@ -12,17 +12,17 @@ vi.mock('electron', () => ({ ipcMain: { on: (...args: unknown[]) => onMock(...args), handle: (...args: unknown[]) => handleMock(...args), - }, + } satisfies Pick, globalShortcut: { register: vi.fn(), unregister: vi.fn(), - }, + } satisfies Pick, app: { setLoginItemSettings: vi.fn(), - }, + } satisfies Pick, shell: { openExternal: vi.fn(), - }, + } satisfies Pick, })); describe('main/handlers/system.ts', () => { diff --git a/src/main/handlers/tray.test.ts b/src/main/handlers/tray.test.ts index 4c4584258..57cc0339b 100644 --- a/src/main/handlers/tray.test.ts +++ b/src/main/handlers/tray.test.ts @@ -10,10 +10,10 @@ const onMock = vi.fn(); vi.mock('electron', () => ({ ipcMain: { on: (...args: unknown[]) => onMock(...args), - }, + } satisfies Pick, net: { isOnline: vi.fn().mockReturnValue(true), - }, + } satisfies Pick, })); describe('main/handlers/tray.ts', () => { diff --git a/src/main/lifecycle/first-run.test.ts b/src/main/lifecycle/first-run.test.ts index 4a84f955f..a9eac6548 100644 --- a/src/main/lifecycle/first-run.test.ts +++ b/src/main/lifecycle/first-run.test.ts @@ -20,15 +20,23 @@ const moveToApplicationsFolderMock = vi.fn(); const isInApplicationsFolderMock = vi.fn(() => false); const getPathMock = vi.fn(() => '/User/Data'); -const showMessageBoxMock = vi.fn(async () => ({ response: 0 })); +const showMessageBoxMock = vi.fn(async () => ({ + response: 0, + checkboxChecked: false, +})); vi.mock('electron', () => ({ app: { getPath: () => getPathMock(), isInApplicationsFolder: () => isInApplicationsFolderMock(), moveToApplicationsFolder: () => moveToApplicationsFolderMock(), - }, - dialog: { showMessageBox: () => showMessageBoxMock() }, + } satisfies Pick< + Electron.App, + 'getPath' | 'isInApplicationsFolder' | 'moveToApplicationsFolder' + >, + dialog: { + showMessageBox: () => showMessageBoxMock(), + } satisfies Pick, })); // Ensure the module under test thinks we're not in dev mode @@ -97,7 +105,10 @@ describe('main/lifecycle/first-run', () => { it('prompts and moves app on macOS when user accepts', async () => { existsSyncMock.mockReturnValueOnce(false); // marker existsSyncMock.mockReturnValueOnce(false); // folder - showMessageBoxMock.mockResolvedValueOnce({ response: 0 }); + showMessageBoxMock.mockResolvedValueOnce({ + response: 0, + checkboxChecked: false, + }); await onFirstRunMaybe(); @@ -107,7 +118,10 @@ describe('main/lifecycle/first-run', () => { it('does not move when user declines', async () => { existsSyncMock.mockReturnValueOnce(false); existsSyncMock.mockReturnValueOnce(false); - showMessageBoxMock.mockResolvedValueOnce({ response: 1 }); + showMessageBoxMock.mockResolvedValueOnce({ + response: 1, + checkboxChecked: false, + }); await onFirstRunMaybe(); diff --git a/src/main/lifecycle/reset.test.ts b/src/main/lifecycle/reset.test.ts index 00ed60fe4..381bb0ecc 100644 --- a/src/main/lifecycle/reset.test.ts +++ b/src/main/lifecycle/reset.test.ts @@ -1,7 +1,9 @@ import type { Menubar } from 'menubar'; vi.mock('electron', () => ({ - dialog: { showMessageBoxSync: vi.fn(() => 0) }, + dialog: { + showMessageBoxSync: vi.fn(() => 0), + } satisfies Pick, })); const sendRendererEventMock = vi.fn(); diff --git a/src/main/lifecycle/startup.test.ts b/src/main/lifecycle/startup.test.ts index 8876a8f45..fbf7eff69 100644 --- a/src/main/lifecycle/startup.test.ts +++ b/src/main/lifecycle/startup.test.ts @@ -13,7 +13,7 @@ vi.mock('electron', () => ({ requestSingleInstanceLock: () => requestSingleInstanceLockMock(), on: (...a: unknown[]) => appOnMock(...a), quit: () => appQuitMock(), - }, + } satisfies Pick, })); const sendRendererEventMock = vi.fn(); diff --git a/src/main/menu.test.ts b/src/main/menu.test.ts index 10b7ff958..95a54e7b4 100644 --- a/src/main/menu.test.ts +++ b/src/main/menu.test.ts @@ -29,9 +29,11 @@ vi.mock('electron', () => { return { Menu: { buildFromTemplate: vi.fn(), - }, + } satisfies Pick, MenuItem: MockMenuItem, - shell: { openExternal: vi.fn() }, + shell: { + openExternal: vi.fn(), + } satisfies Pick, }; }); diff --git a/src/main/updater.test.ts b/src/main/updater.test.ts index 237f0f4a2..951b65496 100644 --- a/src/main/updater.test.ts +++ b/src/main/updater.test.ts @@ -45,10 +45,16 @@ vi.mock('electron', () => { } } return { - dialog: { showMessageBox: vi.fn() }, + dialog: { + showMessageBox: vi.fn(), + } satisfies Pick, MenuItem, - Menu: { buildFromTemplate: vi.fn() }, - shell: { openExternal: vi.fn() }, + Menu: { + buildFromTemplate: vi.fn(), + } satisfies Pick, + shell: { + openExternal: vi.fn(), + } satisfies Pick, }; }); diff --git a/src/main/utils.test.ts b/src/main/utils.test.ts index cfd17469c..268847d9a 100644 --- a/src/main/utils.test.ts +++ b/src/main/utils.test.ts @@ -19,9 +19,13 @@ vi.mock('node:os', () => ({ vi.mock('electron', () => ({ app: { isPackaged: true, - }, - shell: { openPath: vi.fn(() => Promise.resolve('')) }, - dialog: { showMessageBoxSync: vi.fn(() => 0) }, + } satisfies Pick, + shell: { + openPath: vi.fn(() => Promise.resolve('')), + } satisfies Pick, + dialog: { + showMessageBoxSync: vi.fn(() => 0), + } satisfies Pick, })); const fileGetFileMock = vi.fn(() => ({ path: '/var/log/app/app.log' })); diff --git a/src/preload/index.test.ts b/src/preload/index.test.ts index 55d508f3f..bb11739ad 100644 --- a/src/preload/index.test.ts +++ b/src/preload/index.test.ts @@ -36,11 +36,11 @@ vi.mock('electron', () => ({ contextBridge: { exposeInMainWorld: (key: string, value: unknown) => exposeInMainWorldMock(key, value), - }, + } satisfies Pick, webFrame: { getZoomLevel: () => getZoomLevelMock(), setZoomLevel: (level: number) => setZoomLevelMock(level), - }, + } satisfies Pick, })); // Simple Notification stub diff --git a/src/preload/utils.test.ts b/src/preload/utils.test.ts index 890b29ca7..3b35ddbde 100644 --- a/src/preload/utils.test.ts +++ b/src/preload/utils.test.ts @@ -3,22 +3,30 @@ import { EVENTS } from '../shared/events'; import { invokeMainEvent, onRendererEvent, sendMainEvent } from './utils'; vi.mock('electron', () => { - type Listener = (event: unknown, ...args: unknown[]) => void; + type Listener = Parameters[1]; const listeners: Record = {}; + const ipcRendererStub = { + send: vi.fn(), + invoke: vi.fn().mockResolvedValue('response'), + on: vi.fn(function ( + this: Electron.IpcRenderer, + channel: string, + listener: Listener, + ) { + if (!listeners[channel]) { + listeners[channel] = []; + } + listeners[channel].push(listener); + return this; + }), + } satisfies Pick; return { ipcRenderer: { - send: vi.fn(), - invoke: vi.fn().mockResolvedValue('response'), - on: vi.fn((channel: string, listener: Listener) => { - if (!listeners[channel]) { - listeners[channel] = []; - } - listeners[channel].push(listener); - }), + ...ipcRendererStub, __emit: (channel: string, ...args: unknown[]) => { const list = listeners[channel] || []; for (const l of list) { - l({}, ...args); + l({} as Electron.IpcRendererEvent, ...args); } }, __listeners: listeners,