Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions src/main/events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,41 +22,41 @@ describe('main/events', () => {
});

it('onMainEvent registers ipcMain.on listener', () => {
const listener = jest.fn();
const listenerMock = jest.fn();
onMainEvent(
EVENTS.WINDOW_SHOW,
listener as unknown as (e: Electron.IpcMainEvent, d: unknown) => void,
listenerMock as unknown as (e: Electron.IpcMainEvent, d: unknown) => void,
);
expect(onMock).toHaveBeenCalledWith(EVENTS.WINDOW_SHOW, listener);
expect(onMock).toHaveBeenCalledWith(EVENTS.WINDOW_SHOW, listenerMock);
});

it('handleMainEvent registers ipcMain.handle listener', () => {
const listener = jest.fn();
const listenerMock = jest.fn();
handleMainEvent(
EVENTS.VERSION,
listener as unknown as (
listenerMock as unknown as (
e: Electron.IpcMainInvokeEvent,
d: unknown,
) => void,
);
expect(handleMock).toHaveBeenCalledWith(EVENTS.VERSION, listener);
expect(handleMock).toHaveBeenCalledWith(EVENTS.VERSION, listenerMock);
});

it('sendRendererEvent forwards event to webContents with data', () => {
const send = jest.fn();
const mb: MockMenubar = { window: { webContents: { send } } };
const sendMock = jest.fn();
const mb: MockMenubar = { window: { webContents: { send: sendMock } } };
sendRendererEvent(
mb as unknown as Menubar,
EVENTS.UPDATE_ICON_TITLE,
'title',
);
expect(send).toHaveBeenCalledWith(EVENTS.UPDATE_ICON_TITLE, 'title');
expect(sendMock).toHaveBeenCalledWith(EVENTS.UPDATE_ICON_TITLE, 'title');
});

it('sendRendererEvent forwards event without data', () => {
const send = jest.fn();
const mb: MockMenubar = { window: { webContents: { send } } };
const sendMock = jest.fn();
const mb: MockMenubar = { window: { webContents: { send: sendMock } } };
sendRendererEvent(mb as unknown as Menubar, EVENTS.RESET_APP);
expect(send).toHaveBeenCalledWith(EVENTS.RESET_APP, undefined);
expect(sendMock).toHaveBeenCalledWith(EVENTS.RESET_APP, undefined);
});
});
84 changes: 42 additions & 42 deletions src/main/first-run.test.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
import path from 'node:path';

// Mocks
const existsSync = jest.fn();
const mkdirSync = jest.fn();
const writeFileSync = jest.fn();
const existsSyncMock = jest.fn();
const mkdirSyncMock = jest.fn();
const writeFileSyncMock = jest.fn();

jest.mock('node:fs', () => ({
__esModule: true,
default: {
existsSync: (...a: unknown[]) => existsSync(...a),
mkdirSync: (...a: unknown[]) => mkdirSync(...a),
writeFileSync: (...a: unknown[]) => writeFileSync(...a),
existsSync: (...a: unknown[]) => existsSyncMock(...a),
mkdirSync: (...a: unknown[]) => mkdirSyncMock(...a),
writeFileSync: (...a: unknown[]) => writeFileSyncMock(...a),
},
existsSync: (...a: unknown[]) => existsSync(...a),
mkdirSync: (...a: unknown[]) => mkdirSync(...a),
writeFileSync: (...a: unknown[]) => writeFileSync(...a),
existsSync: (...a: unknown[]) => existsSyncMock(...a),
mkdirSync: (...a: unknown[]) => mkdirSyncMock(...a),
writeFileSync: (...a: unknown[]) => writeFileSyncMock(...a),
}));

const moveToApplicationsFolder = jest.fn();
const isInApplicationsFolder = jest.fn(() => false);
const getPath = jest.fn(() => '/User/Data');
const moveToApplicationsFolderMock = jest.fn();
const isInApplicationsFolderMock = jest.fn(() => false);
const getPathMock = jest.fn(() => '/User/Data');

const showMessageBox = jest.fn(async () => ({ response: 0 }));
const showMessageBoxMock = jest.fn(async () => ({ response: 0 }));

jest.mock('electron', () => ({
app: {
getPath: () => getPath(),
isInApplicationsFolder: () => isInApplicationsFolder(),
moveToApplicationsFolder: () => moveToApplicationsFolder(),
getPath: () => getPathMock(),
isInApplicationsFolder: () => isInApplicationsFolderMock(),
moveToApplicationsFolder: () => moveToApplicationsFolderMock(),
},
dialog: { showMessageBox: () => showMessageBox() },
dialog: { showMessageBox: () => showMessageBoxMock() },
}));

const logError = jest.fn();
const logErrorMock = jest.fn();
jest.mock('../shared/logger', () => ({
logError: (...a: unknown[]) => logError(...a),
logError: (...a: unknown[]) => logErrorMock(...a),
}));

let mac = true;
Expand All @@ -54,55 +54,55 @@ describe('main/first-run', () => {
});

it('creates first-run marker when not existing and returns true', async () => {
existsSync.mockReturnValueOnce(false); // marker absent
existsSync.mockReturnValueOnce(false); // folder absent
existsSyncMock.mockReturnValueOnce(false); // marker absent
existsSyncMock.mockReturnValueOnce(false); // folder absent
await onFirstRunMaybe();
expect(mkdirSync).toHaveBeenCalledWith(path.dirname(configPath()));
expect(writeFileSync).toHaveBeenCalledWith(configPath(), '');
expect(mkdirSyncMock).toHaveBeenCalledWith(path.dirname(configPath()));
expect(writeFileSyncMock).toHaveBeenCalledWith(configPath(), '');
});

it('skips writing when marker exists', async () => {
existsSync.mockReturnValueOnce(true); // marker present
existsSyncMock.mockReturnValueOnce(true); // marker present
await onFirstRunMaybe();
expect(writeFileSync).not.toHaveBeenCalled();
expect(mkdirSync).not.toHaveBeenCalled();
expect(writeFileSyncMock).not.toHaveBeenCalled();
expect(mkdirSyncMock).not.toHaveBeenCalled();
});

it('handles fs write error gracefully', async () => {
existsSync.mockReturnValueOnce(false); // marker absent
existsSync.mockReturnValueOnce(true); // folder exists
writeFileSync.mockImplementation(() => {
existsSyncMock.mockReturnValueOnce(false); // marker absent
existsSyncMock.mockReturnValueOnce(true); // folder exists
writeFileSyncMock.mockImplementation(() => {
throw new Error('fail');
});
await onFirstRunMaybe();
expect(logError).toHaveBeenCalledWith(
expect(logErrorMock).toHaveBeenCalledWith(
'isFirstRun',
'Unable to write firstRun file',
expect.any(Error),
);
});

it('prompts and moves app on macOS when user accepts', async () => {
existsSync.mockReturnValueOnce(false); // marker
existsSync.mockReturnValueOnce(false); // folder
showMessageBox.mockResolvedValueOnce({ response: 0 });
existsSyncMock.mockReturnValueOnce(false); // marker
existsSyncMock.mockReturnValueOnce(false); // folder
showMessageBoxMock.mockResolvedValueOnce({ response: 0 });
await onFirstRunMaybe();
expect(moveToApplicationsFolder).toHaveBeenCalled();
expect(moveToApplicationsFolderMock).toHaveBeenCalled();
});

it('does not move when user declines', async () => {
existsSync.mockReturnValueOnce(false);
existsSync.mockReturnValueOnce(false);
showMessageBox.mockResolvedValueOnce({ response: 1 });
existsSyncMock.mockReturnValueOnce(false);
existsSyncMock.mockReturnValueOnce(false);
showMessageBoxMock.mockResolvedValueOnce({ response: 1 });
await onFirstRunMaybe();
expect(moveToApplicationsFolder).not.toHaveBeenCalled();
expect(moveToApplicationsFolderMock).not.toHaveBeenCalled();
});

it('skips prompt on non-macOS', async () => {
mac = false;
existsSync.mockReturnValueOnce(false);
existsSync.mockReturnValueOnce(false);
existsSyncMock.mockReturnValueOnce(false);
existsSyncMock.mockReturnValueOnce(false);
await onFirstRunMaybe();
expect(showMessageBox).not.toHaveBeenCalled();
expect(showMessageBoxMock).not.toHaveBeenCalled();
});
});
50 changes: 25 additions & 25 deletions src/preload/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
import { EVENTS } from '../shared/events';

// Mocks shared modules used inside preload
const sendMainEvent = jest.fn();
const invokeMainEvent = jest.fn();
const onRendererEvent = jest.fn();
const logError = jest.fn();
const sendMainEventMock = jest.fn();
const invokeMainEventMock = jest.fn();
const onRendererEventMock = jest.fn();
const logErrorMock = jest.fn();

jest.mock('./utils', () => ({
sendMainEvent: (...args: unknown[]) => sendMainEvent(...args),
invokeMainEvent: (...args: unknown[]) => invokeMainEvent(...args),
onRendererEvent: (...args: unknown[]) => onRendererEvent(...args),
sendMainEvent: (...args: unknown[]) => sendMainEventMock(...args),
invokeMainEvent: (...args: unknown[]) => invokeMainEventMock(...args),
onRendererEvent: (...args: unknown[]) => onRendererEventMock(...args),
}));

jest.mock('../shared/logger', () => ({
logError: (...args: unknown[]) => logError(...args),
logError: (...args: unknown[]) => logErrorMock(...args),
}));

// We'll reconfigure the electron mock per context isolation scenario.
const exposeInMainWorld = jest.fn();
const getZoomLevel = jest.fn(() => 1);
const setZoomLevel = jest.fn((_level: number) => undefined);
const exposeInMainWorldMock = jest.fn();
const getZoomLevelMock = jest.fn(() => 1);
const setZoomLevelMock = jest.fn((_level: number) => undefined);

jest.mock('electron', () => ({
contextBridge: {
exposeInMainWorld: (key: string, value: unknown) =>
exposeInMainWorld(key, value),
exposeInMainWorldMock(key, value),
},
webFrame: {
getZoomLevel: () => getZoomLevel(),
setZoomLevel: (level: number) => setZoomLevel(level),
getZoomLevel: () => getZoomLevelMock(),
setZoomLevel: (level: number) => setZoomLevelMock(level),
},
}));

Expand Down Expand Up @@ -79,17 +79,17 @@ describe('preload/index', () => {
const w = window as unknown as { gitify: Record<string, unknown> };

expect(w.gitify).toBeDefined();
expect(exposeInMainWorld).not.toHaveBeenCalled();
expect(exposeInMainWorldMock).not.toHaveBeenCalled();
});

it('exposes api via contextBridge when context isolation enabled', async () => {
(process as unknown as { contextIsolated?: boolean }).contextIsolated =
true;
await importPreload();

expect(exposeInMainWorld).toHaveBeenCalledTimes(1);
expect(exposeInMainWorldMock).toHaveBeenCalledTimes(1);

const [key, api] = exposeInMainWorld.mock.calls[0];
const [key, api] = exposeInMainWorldMock.mock.calls[0];
expect(key).toBe('gitify');
expect(api).toHaveProperty('openExternalLink');
});
Expand All @@ -100,7 +100,7 @@ describe('preload/index', () => {
const api = (window as unknown as { gitify: TestApi }).gitify; // casting only in test boundary
api.tray.updateColor(-1);

expect(sendMainEvent).toHaveBeenNthCalledWith(
expect(sendMainEventMock).toHaveBeenNthCalledWith(
1,
EVENTS.UPDATE_ICON_COLOR,
-1,
Expand All @@ -113,7 +113,7 @@ describe('preload/index', () => {
const api = (window as unknown as { gitify: TestApi }).gitify;
api.openExternalLink('https://example.com', true);

expect(sendMainEvent).toHaveBeenCalledWith(EVENTS.OPEN_EXTERNAL, {
expect(sendMainEventMock).toHaveBeenCalledWith(EVENTS.OPEN_EXTERNAL, {
url: 'https://example.com',
activate: true,
});
Expand All @@ -135,7 +135,7 @@ describe('preload/index', () => {
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';

invokeMainEvent.mockResolvedValueOnce('1.2.3');
invokeMainEventMock.mockResolvedValueOnce('1.2.3');

await importPreload();

Expand All @@ -149,19 +149,19 @@ describe('preload/index', () => {
await importPreload();

const api = (window as unknown as { gitify: TestApi }).gitify;
const callback = jest.fn();
api.onSystemThemeUpdate(callback);
const callbackMock = jest.fn();
api.onSystemThemeUpdate(callbackMock);

expect(onRendererEvent).toHaveBeenCalledWith(
expect(onRendererEventMock).toHaveBeenCalledWith(
EVENTS.UPDATE_THEME,
expect.any(Function),
);

// Simulate event
const listener = onRendererEvent.mock.calls[0][1];
const listener = onRendererEventMock.mock.calls[0][1];
listener({}, 'dark');

expect(callback).toHaveBeenCalledWith('dark');
expect(callbackMock).toHaveBeenCalledWith('dark');
});

it('raiseNativeNotification without url calls app.show', async () => {
Expand Down
8 changes: 4 additions & 4 deletions src/preload/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ describe('preload/utils', () => {
});

it('onRendererEvent registers listener and receives emitted data', () => {
const handler = jest.fn();
const handlerMock = jest.fn();
onRendererEvent(
EVENTS.UPDATE_ICON_TITLE,
handler as unknown as (
handlerMock as unknown as (
e: Electron.IpcRendererEvent,
args: string,
) => void,
Expand All @@ -65,8 +65,8 @@ describe('preload/utils', () => {
).__emit(EVENTS.UPDATE_ICON_TITLE, 'payload');
expect(ipcRenderer.on).toHaveBeenCalledWith(
EVENTS.UPDATE_ICON_TITLE,
handler,
handlerMock,
);
expect(handler).toHaveBeenCalledWith({}, 'payload');
expect(handlerMock).toHaveBeenCalledWith({}, 'payload');
});
});
1 change: 1 addition & 0 deletions src/renderer/__helpers__/test-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function AppContextProvider({
return {
auth: mockAuth,
settings: mockSettings,
isLoggedIn: true,

notifications: [],

Expand Down
Loading
Loading