Repository: gitify-app/gitify
Pull request #1203 (2024-06-08 20:45:46)

refactor: prefix all gitify custom ipc events. consolidate under comms
Rename all custom ipc events to be prefixed with `gitify`.

Consolidate ipcRenderer.send / ipcRenderer.invoke within comms.ts

Update tests to mock comms instead of mocking ipcRenderer

setchy: Still yet to conquer the context bridge replacement... In time...

Patch:
diff --git a/main.js b/main.js
index d0d76f0a..33e7dda1 100644
--- a/main.js
+++ b/main.js
@@ -73,37 +73,39 @@ mb.on('ready', () => {
 
   nativeTheme.on('updated', () => {
     if (nativeTheme.shouldUseDarkColors) {
-      mb.window.webContents.send('update-native-theme', 'DARK');
+      mb.window.webContents.send('gitify:update-theme', 'DARK');
     } else {
-      mb.window.webContents.send('update-native-theme', 'LIGHT');
+      mb.window.webContents.send('gitify:update-theme', 'LIGHT');
     }
   });
+});
 
-  ipc.handle('get-app-version', () => app.getVersion());
+ipc.handle('gitify:version', () => app.getVersion());
 
-  ipc.on('reopen-window', () => mb.showWindow());
+ipc.on('gitify:window-show', () => mb.showWindow());
 
-  ipc.on('hide-window', () => mb.hideWindow());
+ipc.on('gitify:window-hide', () => mb.hideWindow());
 
-  ipc.on('app-quit', () => mb.app.quit());
+ipc.on('gitify:quit', () => mb.app.quit());
 
-  ipc.on('update-icon', (_, arg) => {
-    if (!mb.tray.isDestroyed()) {
-      if (arg === 'TrayActive') {
-        mb.tray.setImage(iconActive);
-      } else {
-        mb.tray.setImage(iconIdle);
-      }
-    }
-  });
+ipc.on('gitify:icon-active', () => {
+  if (!mb.tray.isDestroyed()) {
+    mb.tray.setImage(iconActive);
+  }
+});
 
-  ipc.on('update-title', (_, title) => {
-    if (!mb.tray.isDestroyed()) {
-      mb.tray.setTitle(title);
-    }
-  });
+ipc.on('gitify:icon-idle', () => {
+  if (!mb.tray.isDestroyed()) {
+    mb.tray.setImage(iconIdle);
+  }
+});
 
-  ipc.on('set-login-item-settings', (_, settings) => {
-    app.setLoginItemSettings(settings);
-  });
+ipc.on('gitify:update-title', (_, title) => {
+  if (!mb.tray.isDestroyed()) {
+    mb.tray.setTitle(title);
+  }
+});
+
+ipc.on('gitify:update-auto-launch', (_, settings) => {
+  app.setLoginItemSettings(settings);
 });
diff --git a/src/__mocks__/electron.js b/src/__mocks__/electron.js
index 25a226c1..89c50b41 100644
--- a/src/__mocks__/electron.js
+++ b/src/__mocks__/electron.js
@@ -36,7 +36,7 @@ module.exports = {
       switch (channel) {
         case 'get-platform':
           return Promise.resolve('darwin');
-        case 'get-app-version':
+        case 'gitify:version':
           return Promise.resolve('0.0.1');
         default:
           return Promise.reject(new Error(`Unknown channel: ${channel}`));
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index 927d59f0..a6f10faa 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -4,12 +4,11 @@ import {
   SyncIcon,
   XCircleIcon,
 } from '@primer/octicons-react';
-import { ipcRenderer } from 'electron';
 import { type FC, useCallback, useContext, useMemo } from 'react';
 import { useLocation, useNavigate } from 'react-router-dom';
 import { Logo } from '../components/Logo';
 import { AppContext } from '../context/App';
-import { openExternalLink } from '../utils/comms';
+import { openExternalLink, quitApp } from '../utils/comms';
 import { Constants } from '../utils/constants';
 import { getNotificationCount } from '../utils/notifications';
 
@@ -28,10 +27,6 @@ export const Sidebar: FC = () => {
     openExternalLink('https://github.com/notifications');
   }, []);
 
-  const quitApp = useCallback(() => {
-    ipcRenderer.send('app-quit');
-  }, []);
-
   const toggleSettings = () => {
     if (location.pathname.startsWith('/settings')) {
       navigate('/', { replace: true });
diff --git a/src/routes/Login.tsx b/src/routes/Login.tsx
index 8e70f9f3..f33acab4 100644
--- a/src/routes/Login.tsx
+++ b/src/routes/Login.tsx
@@ -1,10 +1,10 @@
 import { KeyIcon, PersonIcon } from '@primer/octicons-react';
-import { ipcRenderer } from 'electron';
 import { type FC, useContext, useEffect } from 'react';
 import { useNavigate } from 'react-router-dom';
 import { Logo } from '../components/Logo';
 import { Button } from '../components/fields/Button';
 import { AppContext } from '../context/App';
+import { showWindow } from '../utils/comms';
 
 export const LoginRoute: FC = () => {
   const navigate = useNavigate();
@@ -12,7 +12,7 @@ export const LoginRoute: FC = () => {
 
   useEffect(() => {
     if (isLoggedIn) {
-      ipcRenderer.send('reopen-window');
+      showWindow();
       navigate('/', { replace: true });
     }
   }, [isLoggedIn]);
diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx
index d0ef2e18..31c75b65 100644
--- a/src/routes/Settings.tsx
+++ b/src/routes/Settings.tsx
@@ -22,7 +22,7 @@ import { Checkbox } from '../components/fields/Checkbox';
 import { RadioGroup } from '../components/fields/RadioGroup';
 import { AppContext } from '../context/App';
 import { Theme } from '../types';
-import { openExternalLink } from '../utils/comms';
+import { getAppVersion, openExternalLink, quitApp } from '../utils/comms';
 import Constants from '../utils/constants';
 import { isLinux, isMacOS } from '../utils/platform';
 import { setTheme } from '../utils/theme';
@@ -50,21 +50,17 @@ export const SettingsRoute: FC = () => {
 
   useEffect(() => {
     (async () => {
-      const result = await ipcRenderer.invoke('get-app-version');
+      const result = await getAppVersion();
       setAppVersion(result);
     })();
 
-    ipcRenderer.on('update-native-theme', (_, updatedTheme: Theme) => {
+    ipcRenderer.on('gitify:update-theme', (_, updatedTheme: Theme) => {
       if (settings.theme === Theme.SYSTEM) {
         setTheme(updatedTheme);
       }
     });
   }, [settings.theme]);
 
-  const quitApp = useCallback(() => {
-    ipcRenderer.send('app-quit');
-  }, []);
-
   const footerButtonClass =
     'hover:text-gray-500 py-1 px-2 my-1 mx-2 focus:outline-none';
 
diff --git a/src/utils/comms.ts b/src/utils/comms.ts
index 99506975..0e67746a 100644
--- a/src/utils/comms.ts
+++ b/src/utils/comms.ts
@@ -6,8 +6,24 @@ export function openExternalLink(url: string): void {
   }
 }
 
+export async function getAppVersion(): Promise<string> {
+  return await ipcRenderer.invoke('gitify:version');
+}
+
+export function quitApp(): void {
+  ipcRenderer.send('gitify:quit');
+}
+
+export function showWindow(): void {
+  ipcRenderer.send('gitify:window-show');
+}
+
+export function hideWindow(): void {
+  ipcRenderer.send('gitify:window-hide');
+}
+
 export function setAutoLaunch(value: boolean): void {
-  ipcRenderer.send('set-login-item-settings', {
+  ipcRenderer.send('gitify:update-auto-launch', {
     openAtLogin: value,
     openAsHidden: value,
   });
@@ -15,12 +31,12 @@ export function setAutoLaunch(value: boolean): void {
 
 export function updateTrayIcon(notificationsLength = 0): void {
   if (notificationsLength > 0) {
-    ipcRenderer.send('update-icon', 'TrayActive');
+    ipcRenderer.send('gitify:icon-active');
   } else {
-    ipcRenderer.send('update-icon');
+    ipcRenderer.send('gitify:icon-idle');
   }
 }
 
 export function updateTrayTitle(title = ''): void {
-  ipcRenderer.send('update-title', title);
+  ipcRenderer.send('gitify:update-title', title);
 }
diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts
index f2e3bbc8..1c7ac742 100644
--- a/src/utils/notifications.ts
+++ b/src/utils/notifications.ts
@@ -1,4 +1,3 @@
-import { ipcRenderer } from 'electron';
 import type {
   AccountNotifications,
   AuthState,
@@ -8,7 +7,7 @@ import type {
 import { Notification } from '../typesGitHub';
 import { openInBrowser } from '../utils/helpers';
 import { listNotificationsForAuthenticatedUser } from './api/client';
-import { updateTrayIcon } from './comms';
+import { hideWindow, showWindow, updateTrayIcon } from './comms';
 import { isWindows } from './platform';
 import { getGitifySubjectDetails } from './subject';
 
@@ -95,10 +94,10 @@ export const raiseNativeNotification = (
 
   nativeNotification.onclick = () => {
     if (notifications.length === 1) {
-      ipcRenderer.send('hide-window');
+      hideWindow();
       openInBrowser(notifications[0]);
     } else {
-      ipcRenderer.send('reopen-window');
+      showWindow();
     }
   };
 


Test patch:
diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx
index a100c21c..216114ce 100644
--- a/src/components/Sidebar.test.tsx
+++ b/src/components/Sidebar.test.tsx
@@ -1,9 +1,10 @@
 import { fireEvent, render, screen } from '@testing-library/react';
-import { ipcRenderer, shell } from 'electron';
+import { shell } from 'electron';
 import { MemoryRouter } from 'react-router-dom';
 import { mockAccountNotifications } from '../__mocks__/notifications-mocks';
 import { mockSettings } from '../__mocks__/state-mocks';
 import { AppContext } from '../context/App';
+import * as comms from '../utils/comms';
 import { Sidebar } from './Sidebar';
 
 const mockNavigate = jest.fn();
@@ -18,7 +19,6 @@ describe('components/Sidebar.tsx', () => {
   beforeEach(() => {
     fetchNotifications.mockReset();
 
-    jest.spyOn(ipcRenderer, 'send');
     jest.spyOn(shell, 'openExternal');
     jest.spyOn(window, 'clearInterval');
   });
@@ -147,6 +147,8 @@ describe('components/Sidebar.tsx', () => {
   });
 
   it('should quit the app', () => {
+    const quitAppMock = jest.spyOn(comms, 'quitApp');
+
     render(
       <AppContext.Provider value={{ isLoggedIn: false, notifications: [] }}>
         <MemoryRouter>
@@ -155,8 +157,7 @@ describe('components/Sidebar.tsx', () => {
       </AppContext.Provider>,
     );
     fireEvent.click(screen.getByTitle('Quit Gitify'));
-    expect(ipcRenderer.send).toHaveBeenCalledTimes(1);
-    expect(ipcRenderer.send).toHaveBeenCalledWith('app-quit');
+    expect(quitAppMock).toHaveBeenCalledTimes(1);
   });
 
   it('should open the gitify repository', () => {
diff --git a/src/routes/Accounts.test.tsx b/src/routes/Accounts.test.tsx
index 1693605a..d8d99dc6 100644
--- a/src/routes/Accounts.test.tsx
+++ b/src/routes/Accounts.test.tsx
@@ -1,5 +1,5 @@
 import { act, fireEvent, render, screen } from '@testing-library/react';
-import { ipcRenderer, shell } from 'electron';
+import { shell } from 'electron';
 import { MemoryRouter } from 'react-router-dom';
 import {
   mockAuth,
@@ -9,6 +9,7 @@ import {
   mockSettings,
 } from '../__mocks__/state-mocks';
 import { AppContext } from '../context/App';
+import * as comms from '../utils/comms';
 import { AccountsRoute } from './Accounts';
 
 const mockNavigate = jest.fn();
@@ -148,6 +149,9 @@ describe('routes/Accounts.tsx', () => {
 
     it('should logout', async () => {
       const logoutFromAccountMock = jest.fn();
+      const updateTrayIconMock = jest.spyOn(comms, 'updateTrayIcon');
+      const updateTrayTitleMock = jest.spyOn(comms, 'updateTrayTitle');
+
       await act(async () => {
         render(
           <AppContext.Provider
@@ -170,9 +174,10 @@ describe('routes/Accounts.tsx', () => {
 
       expect(logoutFromAccountMock).toHaveBeenCalledTimes(1);
 
-      expect(ipcRenderer.send).toHaveBeenCalledTimes(2);
-      expect(ipcRenderer.send).toHaveBeenCalledWith('update-icon');
-      expect(ipcRenderer.send).toHaveBeenCalledWith('update-title', '');
+      expect(updateTrayIconMock).toHaveBeenCalledTimes(1);
+      expect(updateTrayIconMock).toHaveBeenCalledWith();
+      expect(updateTrayTitleMock).toHaveBeenCalledTimes(1);
+      expect(updateTrayTitleMock).toHaveBeenCalledWith();
       expect(mockNavigate).toHaveBeenNthCalledWith(1, -1);
     });
   });
diff --git a/src/routes/Login.test.tsx b/src/routes/Login.test.tsx
index d6d06463..8c985552 100644
--- a/src/routes/Login.test.tsx
+++ b/src/routes/Login.test.tsx
@@ -1,7 +1,7 @@
 import { fireEvent, render, screen } from '@testing-library/react';
-import { ipcRenderer } from 'electron';
 import { MemoryRouter } from 'react-router-dom';
 import { AppContext } from '../context/App';
+import * as comms from '../utils/comms';
 import { LoginRoute } from './Login';
 
 const mockNavigate = jest.fn();
@@ -13,7 +13,6 @@ jest.mock('react-router-dom', () => ({
 describe('routes/Login.tsx', () => {
   beforeEach(() => {
     mockNavigate.mockReset();
-    jest.spyOn(ipcRenderer, 'send');
   });
 
   it('should render itself & its children', () => {
@@ -27,6 +26,8 @@ describe('routes/Login.tsx', () => {
   });
 
   it('should redirect to notifications once logged in', () => {
+    const showWindowMock = jest.spyOn(comms, 'showWindow');
+
     const { rerender } = render(
       <AppContext.Provider value={{ isLoggedIn: false }}>
         <MemoryRouter>
@@ -43,8 +44,7 @@ describe('routes/Login.tsx', () => {
       </AppContext.Provider>,
     );
 
-    expect(ipcRenderer.send).toHaveBeenCalledTimes(1);
-    expect(ipcRenderer.send).toHaveBeenCalledWith('reopen-window');
+    expect(showWindowMock).toHaveBeenCalledTimes(1);
     expect(mockNavigate).toHaveBeenNthCalledWith(1, '/', { replace: true });
   });
 
diff --git a/src/routes/Settings.test.tsx b/src/routes/Settings.test.tsx
index a254d8bb..814da7a8 100644
--- a/src/routes/Settings.test.tsx
+++ b/src/routes/Settings.test.tsx
@@ -1,9 +1,10 @@
 import { act, fireEvent, render, screen } from '@testing-library/react';
-import { ipcRenderer, shell } from 'electron';
+import { shell } from 'electron';
 import { MemoryRouter } from 'react-router-dom';
 import { mockAuth, mockSettings } from '../__mocks__/state-mocks';
 import { mockPlatform } from '../__mocks__/utils';
 import { AppContext } from '../context/App';
+import * as comms from '../utils/comms';
 import { SettingsRoute } from './Settings';
 
 const mockNavigate = jest.fn();
@@ -524,6 +525,8 @@ describe('routes/Settings.tsx', () => {
     });
 
     it('should quit the app', async () => {
+      const quitAppMock = jest.spyOn(comms, 'quitApp');
+
       await act(async () => {
         render(
           <AppContext.Provider
@@ -540,7 +543,7 @@ describe('routes/Settings.tsx', () => {
       });
 
       fireEvent.click(screen.getByTitle('Quit Gitify'));
-      expect(ipcRenderer.send).toHaveBeenCalledWith('app-quit');
+      expect(quitAppMock).toHaveBeenCalledTimes(1);
     });
   });
 });
diff --git a/src/utils/comms.test.ts b/src/utils/comms.test.ts
index 478b2af3..2fa6cb88 100644
--- a/src/utils/comms.test.ts
+++ b/src/utils/comms.test.ts
@@ -1,27 +1,60 @@
 import { ipcRenderer, shell } from 'electron';
-import { openExternalLink, setAutoLaunch, updateTrayIcon } from './comms';
+import {
+  getAppVersion,
+  hideWindow,
+  openExternalLink,
+  quitApp,
+  setAutoLaunch,
+  showWindow,
+  updateTrayIcon,
+} from './comms';
 
 describe('utils/comms.ts', () => {
   beforeEach(() => {
     jest.spyOn(ipcRenderer, 'send');
+    jest.spyOn(ipcRenderer, 'invoke');
   });
 
   afterEach(() => {
     jest.clearAllMocks();
   });
 
+  it('should get app version', async () => {
+    await getAppVersion();
+    expect(ipcRenderer.invoke).toHaveBeenCalledTimes(1);
+    expect(ipcRenderer.invoke).toHaveBeenCalledWith('gitify:version');
+  });
+
+  it('should quit the app', () => {
+    quitApp();
+    expect(ipcRenderer.send).toHaveBeenCalledTimes(1);
+    expect(ipcRenderer.send).toHaveBeenCalledWith('gitify:quit');
+  });
+
+  it('should show the window', () => {
+    showWindow();
+    expect(ipcRenderer.send).toHaveBeenCalledTimes(1);
+    expect(ipcRenderer.send).toHaveBeenCalledWith('gitify:window-show');
+  });
+
+  it('should hide the window', () => {
+    hideWindow();
+    expect(ipcRenderer.send).toHaveBeenCalledTimes(1);
+    expect(ipcRenderer.send).toHaveBeenCalledWith('gitify:window-hide');
+  });
+
   it('should send mark the icons as active', () => {
     const notificationsLength = 3;
     updateTrayIcon(notificationsLength);
     expect(ipcRenderer.send).toHaveBeenCalledTimes(1);
-    expect(ipcRenderer.send).toHaveBeenCalledWith('update-icon', 'TrayActive');
+    expect(ipcRenderer.send).toHaveBeenCalledWith('gitify:icon-active');
   });
 
   it('should send mark the icons as idle', () => {
     const notificationsLength = 0;
     updateTrayIcon(notificationsLength);
     expect(ipcRenderer.send).toHaveBeenCalledTimes(1);
-    expect(ipcRenderer.send).toHaveBeenCalledWith('update-icon');
+    expect(ipcRenderer.send).toHaveBeenCalledWith('gitify:icon-idle');
   });
 
   it('should open an external link', () => {
@@ -38,7 +71,7 @@ describe('utils/comms.ts', () => {
   it('should setAutoLaunch (true)', () => {
     setAutoLaunch(true);
 
-    expect(ipcRenderer.send).toHaveBeenCalledWith('set-login-item-settings', {
+    expect(ipcRenderer.send).toHaveBeenCalledWith('gitify:update-auto-launch', {
       openAtLogin: true,
       openAsHidden: true,
     });
@@ -47,7 +80,7 @@ describe('utils/comms.ts', () => {
   it('should setAutoLaunch (false)', () => {
     setAutoLaunch(false);
 
-    expect(ipcRenderer.send).toHaveBeenCalledWith('set-login-item-settings', {
+    expect(ipcRenderer.send).toHaveBeenCalledWith('gitify:update-auto-launch', {
       openAsHidden: false,
       openAtLogin: false,
     });
diff --git a/src/utils/notifications.test.ts b/src/utils/notifications.test.ts
index 128c22c6..79a06bff 100644
--- a/src/utils/notifications.test.ts
+++ b/src/utils/notifications.test.ts
@@ -1,5 +1,3 @@
-import { ipcRenderer } from 'electron';
-
 import {
   mockAccountNotifications,
   mockSingleAccountNotifications,
@@ -9,6 +7,7 @@ import { mockAuth, mockSettings } from '../__mocks__/state-mocks';
 import { defaultSettings } from '../context/App';
 import type { SettingsState } from '../types';
 import { mockGitHubNotifications } from './api/__mocks__/response-mocks';
+import * as comms from './comms';
 import * as helpers from './helpers';
 import * as notificationsHelpers from './notifications';
 import { filterNotifications } from './notifications';
@@ -112,6 +111,7 @@ describe('utils/notifications.ts', () => {
   });
 
   it('should click on a native notification (with 1 notification)', () => {
+    const hideWindowMock = jest.spyOn(comms, 'hideWindow');
     jest.spyOn(helpers, 'openInBrowser');
 
     const nativeNotification: Notification =
@@ -125,17 +125,19 @@ describe('utils/notifications.ts', () => {
     expect(helpers.openInBrowser).toHaveBeenLastCalledWith(
       mockGitHubNotifications[0],
     );
-    expect(ipcRenderer.send).toHaveBeenCalledWith('hide-window');
+    expect(hideWindowMock).toHaveBeenCalledTimes(1);
   });
 
   it('should click on a native notification (with more than 1 notification)', () => {
+    const showWindowMock = jest.spyOn(comms, 'showWindow');
+
     const nativeNotification = notificationsHelpers.raiseNativeNotification(
       mockGitHubNotifications,
       mockAuth,
     );
     nativeNotification.onclick(null);
 
-    expect(ipcRenderer.send).toHaveBeenCalledWith('reopen-window');
+    expect(showWindowMock).toHaveBeenCalledTimes(1);
   });
 
   it('should play a sound', () => {