diff --git a/src/main/main.ts b/src/main/main.ts index 3c9271386..eb5ba9d8b 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -2,6 +2,7 @@ import { app, globalShortcut, ipcMain as ipc, nativeTheme } from 'electron'; import log from 'electron-log'; import { menubar } from 'menubar'; +import { APPLICATION } from '../shared/constants'; import { onFirstRunMaybe } from './first-run'; import { TrayIcons } from './icons'; import MenuBuilder from './menu'; @@ -39,7 +40,8 @@ const contextMenu = menuBuilder.buildMenu(); * https://github.com/electron/update-electron-app */ if (process.platform === 'darwin' || process.platform === 'win32') { - new Updater(mb, menuBuilder); + const updater = new Updater(mb, menuBuilder); + updater.initialize(); } let shouldUseAlternateIdleIcon = false; @@ -48,7 +50,7 @@ app.whenReady().then(async () => { await onFirstRunMaybe(); mb.on('ready', () => { - mb.app.setAppUserModelId('com.electron.gitify'); + mb.app.setAppUserModelId(APPLICATION.ID); /** * TODO: Remove @electron/remote use - see #650 @@ -58,7 +60,7 @@ app.whenReady().then(async () => { require('@electron/remote/main').enable(mb.window.webContents); // Tray configuration - mb.tray.setToolTip('Gitify'); + mb.tray.setToolTip(APPLICATION.NAME); mb.tray.setIgnoreDoubleClickEvents(true); mb.tray.on('right-click', (_event, bounds) => { mb.tray.popUpContextMenu(contextMenu, { x: bounds.x, y: bounds.y }); @@ -114,7 +116,7 @@ app.whenReady().then(async () => { ipc.on('gitify:icon-active', () => { if (!mb.tray.isDestroyed()) { mb.tray.setImage( - menuBuilder.isUpdateAvailableMenuVisible() + menuBuilder.isUpdateAvailable() ? TrayIcons.activeUpdateIcon : TrayIcons.active, ); @@ -125,13 +127,13 @@ app.whenReady().then(async () => { if (!mb.tray.isDestroyed()) { if (shouldUseAlternateIdleIcon) { mb.tray.setImage( - menuBuilder.isUpdateAvailableMenuVisible() + menuBuilder.isUpdateAvailable() ? TrayIcons.idleAlternateUpdateIcon : TrayIcons.idleAlternate, ); } else { mb.tray.setImage( - menuBuilder.isUpdateAvailableMenuVisible() + menuBuilder.isUpdateAvailable() ? TrayIcons.idleUpdateIcon : TrayIcons.idle, ); diff --git a/src/main/menu.test.ts b/src/main/menu.test.ts new file mode 100644 index 000000000..dfea5a3f3 --- /dev/null +++ b/src/main/menu.test.ts @@ -0,0 +1,99 @@ +import { Menu, MenuItem } from 'electron'; +import type { Menubar } from 'menubar'; +import MenuBuilder from './menu'; + +jest.mock('electron', () => ({ + Menu: { + buildFromTemplate: jest.fn(), + }, + MenuItem: jest.fn(), +})); + +describe('main/menu.ts', () => { + let menubar: Menubar; + let menuBuilder: MenuBuilder; + + beforeEach(() => { + menuBuilder = new MenuBuilder(menubar); + }); + + it('should create menu items correctly', () => { + expect(MenuItem).toHaveBeenCalledWith({ + label: 'Check for updates', + enabled: true, + click: expect.any(Function), + }); + + expect(MenuItem).toHaveBeenCalledWith({ + label: 'No updates available', + enabled: false, + visible: false, + }); + + expect(MenuItem).toHaveBeenCalledWith({ + label: 'An update is available', + enabled: false, + visible: false, + }); + + expect(MenuItem).toHaveBeenCalledWith({ + label: 'Restart to install update', + enabled: true, + visible: false, + click: expect.any(Function), + }); + }); + + it('should build menu correctly', () => { + menuBuilder.buildMenu(); + expect(Menu.buildFromTemplate).toHaveBeenCalledWith(expect.any(Array)); + }); + + it('should enable check for updates menu item', () => { + menuBuilder.setCheckForUpdatesMenuEnabled(true); + // biome-ignore lint/complexity/useLiteralKeys: This is a test + expect(menuBuilder['checkForUpdatesMenuItem'].enabled).toBe(true); + }); + + it('should disable check for updates menu item', () => { + menuBuilder.setCheckForUpdatesMenuEnabled(false); + // biome-ignore lint/complexity/useLiteralKeys: This is a test + expect(menuBuilder['checkForUpdatesMenuItem'].enabled).toBe(false); + }); + + it('should show no update available menu item', () => { + menuBuilder.setNoUpdateAvailableMenuVisibility(true); + // biome-ignore lint/complexity/useLiteralKeys: This is a test + expect(menuBuilder['noUpdateAvailableMenuItem'].visible).toBe(true); + }); + + it('should hide no update available menu item', () => { + menuBuilder.setNoUpdateAvailableMenuVisibility(false); + // biome-ignore lint/complexity/useLiteralKeys: This is a test + expect(menuBuilder['noUpdateAvailableMenuItem'].visible).toBe(false); + }); + + it('should show update available menu item', () => { + menuBuilder.setUpdateAvailableMenuVisibility(true); + // biome-ignore lint/complexity/useLiteralKeys: This is a test + expect(menuBuilder['updateAvailableMenuItem'].visible).toBe(true); + }); + + it('should hide update available menu item', () => { + menuBuilder.setUpdateAvailableMenuVisibility(false); + // biome-ignore lint/complexity/useLiteralKeys: This is a test + expect(menuBuilder['updateAvailableMenuItem'].visible).toBe(false); + }); + + it('should show update ready for install menu item', () => { + menuBuilder.setUpdateReadyForInstallMenuVisibility(true); + // biome-ignore lint/complexity/useLiteralKeys: This is a test + expect(menuBuilder['updateReadyForInstallMenuItem'].visible).toBe(true); + }); + + it('should show update ready for install menu item', () => { + menuBuilder.setUpdateReadyForInstallMenuVisibility(false); + // biome-ignore lint/complexity/useLiteralKeys: This is a test + expect(menuBuilder['updateReadyForInstallMenuItem'].visible).toBe(false); + }); +}); diff --git a/src/main/menu.ts b/src/main/menu.ts index bfaee216a..8e1aeb88b 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -4,9 +4,10 @@ import type { Menubar } from 'menubar'; import { openLogsDirectory, resetApp, takeScreenshot } from './utils'; export default class MenuBuilder { - private checkForUpdatesMenuItem: MenuItem; - private updateAvailableMenuItem: MenuItem; - private updateReadyForInstallMenuItem: MenuItem; + private readonly checkForUpdatesMenuItem: MenuItem; + private readonly noUpdateAvailableMenuItem: MenuItem; + private readonly updateAvailableMenuItem: MenuItem; + private readonly updateReadyForInstallMenuItem: MenuItem; menubar: Menubar; @@ -21,6 +22,12 @@ export default class MenuBuilder { }, }); + this.noUpdateAvailableMenuItem = new MenuItem({ + label: 'No updates available', + enabled: false, + visible: false, + }); + this.updateAvailableMenuItem = new MenuItem({ label: 'An update is available', enabled: false, @@ -28,7 +35,8 @@ export default class MenuBuilder { }); this.updateReadyForInstallMenuItem = new MenuItem({ - label: 'Restart to update', + label: 'Restart to install update', + enabled: true, visible: false, click: () => { autoUpdater.quitAndInstall(); @@ -39,6 +47,7 @@ export default class MenuBuilder { buildMenu(): Menu { const contextMenu = Menu.buildFromTemplate([ this.checkForUpdatesMenuItem, + this.noUpdateAvailableMenuItem, this.updateAvailableMenuItem, this.updateReadyForInstallMenuItem, { type: 'separator' }, @@ -88,15 +97,21 @@ export default class MenuBuilder { this.checkForUpdatesMenuItem.enabled = enabled; } - setUpdateAvailableMenuEnabled(enabled: boolean) { - this.updateAvailableMenuItem.enabled = enabled; + setNoUpdateAvailableMenuVisibility(isVisible: boolean) { + this.noUpdateAvailableMenuItem.visible = isVisible; + } + + setUpdateAvailableMenuVisibility(isVisible: boolean) { + this.updateAvailableMenuItem.visible = isVisible; } - setUpdateReadyForInstallMenuEnabled(enabled: boolean) { - this.updateReadyForInstallMenuItem.enabled = enabled; + setUpdateReadyForInstallMenuVisibility(isVisible: boolean) { + this.updateReadyForInstallMenuItem.visible = isVisible; } - isUpdateAvailableMenuVisible() { - return this.updateAvailableMenuItem.visible; + isUpdateAvailable() { + return ( + this.updateAvailableMenuItem.visible || this.updateReadyForInstallMenuItem + ); } } diff --git a/src/main/updater.ts b/src/main/updater.ts index 7785e0eaa..2c80fb4b5 100644 --- a/src/main/updater.ts +++ b/src/main/updater.ts @@ -3,17 +3,20 @@ import { autoUpdater } from 'electron-updater'; import type { Menubar } from 'menubar'; import { updateElectronApp } from 'update-electron-app'; +import { APPLICATION } from '../shared/constants'; import { logError, logInfo } from '../shared/logger'; import type MenuBuilder from './menu'; export default class Updater { - menubar: Menubar; - menuBuilder: MenuBuilder; + private readonly menubar: Menubar; + private readonly menuBuilder: MenuBuilder; constructor(menubar: Menubar, menuBuilder: MenuBuilder) { this.menubar = menubar; this.menuBuilder = menuBuilder; + } + initialize(): void { updateElectronApp({ updateInterval: '24 hours', logger: log, @@ -23,32 +26,37 @@ export default class Updater { logInfo('auto updater', 'Checking for update'); this.menuBuilder.setCheckForUpdatesMenuEnabled(false); + this.menuBuilder.setNoUpdateAvailableMenuVisibility(false); }); autoUpdater.on('update-available', () => { logInfo('auto updater', 'New update available'); - this.menubar.tray.setToolTip('Gitify\nA new update is available'); - menuBuilder.setUpdateAvailableMenuEnabled(true); + this.setTooltipWithStatus('A new update is available'); + this.menuBuilder.setUpdateAvailableMenuVisibility(true); }); autoUpdater.on('download-progress', (progressObj) => { - this.menubar.tray.setToolTip( - `Gitify\nDownloading update: ${progressObj.percent.toFixed(2)} %`, + this.setTooltipWithStatus( + `Downloading update: ${progressObj.percent.toFixed(2)}%`, ); }); autoUpdater.on('update-downloaded', () => { logInfo('auto updater', 'Update downloaded'); - this.menubar.tray.setToolTip('Gitify\nA new update is ready to install'); - menuBuilder.setUpdateReadyForInstallMenuEnabled(true); + this.setTooltipWithStatus('A new update is ready to install'); + this.menuBuilder.setUpdateAvailableMenuVisibility(false); + this.menuBuilder.setUpdateReadyForInstallMenuVisibility(true); }); autoUpdater.on('update-not-available', () => { logInfo('auto updater', 'Update not available'); - this.resetState(); + this.menuBuilder.setCheckForUpdatesMenuEnabled(true); + this.menuBuilder.setNoUpdateAvailableMenuVisibility(true); + this.menuBuilder.setUpdateAvailableMenuVisibility(false); + this.menuBuilder.setUpdateReadyForInstallMenuVisibility(false); }); autoUpdater.on('update-cancelled', () => { @@ -64,10 +72,15 @@ export default class Updater { }); } + private setTooltipWithStatus(status: string) { + this.menubar.tray.setToolTip(`${APPLICATION.NAME}\n${status}`); + } + private resetState() { - this.menubar.tray.setToolTip('Gitify'); + this.menubar.tray.setToolTip(APPLICATION.NAME); this.menuBuilder.setCheckForUpdatesMenuEnabled(true); - this.menuBuilder.setUpdateAvailableMenuEnabled(false); - this.menuBuilder.setUpdateReadyForInstallMenuEnabled(false); + this.menuBuilder.setNoUpdateAvailableMenuVisibility(false); + this.menuBuilder.setUpdateAvailableMenuVisibility(false); + this.menuBuilder.setUpdateReadyForInstallMenuVisibility(false); } } diff --git a/src/shared/constants.ts b/src/shared/constants.ts new file mode 100644 index 000000000..0e895face --- /dev/null +++ b/src/shared/constants.ts @@ -0,0 +1,5 @@ +export const APPLICATION = { + ID: 'com.electron.gitify', + + NAME: 'Gitify', +};