Repository: manusa/electronim
Pull request #231 (2022-11-30 04:42:05)

✨ app-menu contains a Quit entry to exit the app

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=manusa_electronim&pullRequest=231)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=231&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=231&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=231&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=231&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=231&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=231&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=231&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=231&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=231&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=231&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=231&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=231&resolved=false&types=CODE_SMELL)

[![100.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/100-16px.png '100.0%')](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=231&metric=new_coverage&view=list) [100.0% Coverage](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=231&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=231&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=231&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/src/app-menu/app-menu.browser.mjs b/src/app-menu/app-menu.browser.mjs
index 26da6a0..699b85b 100644
--- a/src/app-menu/app-menu.browser.mjs
+++ b/src/app-menu/app-menu.browser.mjs
@@ -15,7 +15,7 @@
  */
 import {html, render, Icon, Menu} from '../components/index.mjs';
 
-const {aboutOpenDialog, close, helpOpenDialog, settingsOpenDialog} = window.electron;
+const {aboutOpenDialog, close, helpOpenDialog, quit, settingsOpenDialog} = window.electron;
 
 const getAppMenu = () => document.querySelector('.app-menu');
 
@@ -35,6 +35,8 @@ const AppMenu = () => {
             onClick=${noBubbling(settingsOpenDialog)} />
           <${Menu.Item} icon=${Icon.info} label='About' data-testid='about-menu-entry'
             onClick=${noBubbling(aboutOpenDialog)} />
+          <${Menu.Item} icon=${Icon.close} label='Quit' data-testid='quit-menu-entry'
+            onClick=${noBubbling(quit)} />
         </${Menu}>
       </div>
     </div>
diff --git a/src/app-menu/preload.js b/src/app-menu/preload.js
index f7f2467..47f78ad 100644
--- a/src/app-menu/preload.js
+++ b/src/app-menu/preload.js
@@ -20,5 +20,6 @@ contextBridge.exposeInMainWorld('electron', {
   close: () => ipcRenderer.send(APP_EVENTS.appMenuClose),
   aboutOpenDialog: () => ipcRenderer.send(APP_EVENTS.aboutOpenDialog),
   helpOpenDialog: () => ipcRenderer.send(APP_EVENTS.helpOpenDialog),
+  quit: () => ipcRenderer.send(APP_EVENTS.quit),
   settingsOpenDialog: () => ipcRenderer.send(APP_EVENTS.settingsOpenDialog)
 });
diff --git a/src/components/icon.mjs b/src/components/icon.mjs
index ffa0f2e..24c1a4d 100644
--- a/src/components/icon.mjs
+++ b/src/components/icon.mjs
@@ -38,6 +38,7 @@ Icon.arrowDropDown = '\ue5c5';
 Icon.check = '\ue5ca';
 Icon.checkBox = '\ue834';
 Icon.checkBoxOutlineBlank = '\ue835';
+Icon.close = '\ue5cd';
 Icon.delete = '\ue872';
 Icon.expandMore = '\ue5cf';
 Icon.help = '\ue887';
diff --git a/src/constants/index.js b/src/constants/index.js
index 4e0ee22..a9f874e 100644
--- a/src/constants/index.js
+++ b/src/constants/index.js
@@ -31,6 +31,7 @@ const APP_EVENTS = {
   addTabs: 'addTabs',
   appMenuOpen: 'appMenuOpen',
   appMenuClose: 'appMenuClose',
+  canNotify: 'canNotify',
   closeDialog: 'closeDialog',
   desktopCapturerGetSources: 'desktopCapturerGetSources',
   dictionaryGetAvailable: 'dictionaryGetAvailable',
@@ -41,13 +42,13 @@ const APP_EVENTS = {
   fullscreenToggle: 'fullscreenToggle',
   helpOpenDialog: 'helpOpenDialog',
   notificationClick: 'notificationClick',
+  quit: 'quit',
   reload: 'reload',
   settingsLoad: 'settingsLoad',
   settingsOpenDialog: 'settingsOpenDialog',
   settingsSave: 'settingsSave',
   setTabFavicon: 'setTabFavicon',
   setTabTitle: 'setTabTitle',
-  canNotify: 'canNotify',
   tabsReady: 'tabsReady',
   tabReorder: 'tabReorder',
   tabSwitchToPosition: 'tabSwitchToPosition',
diff --git a/src/main/index.js b/src/main/index.js
index 641b8b8..5b2a507 100644
--- a/src/main/index.js
+++ b/src/main/index.js
@@ -216,6 +216,7 @@ const initGlobalListeners = () => {
   eventBus.handle(APP_EVENTS.dictionaryGetEnabled, getEnabledDictionaries);
   eventBus.on(APP_EVENTS.fullscreenToggle, fullscreenToggle);
   eventBus.on(APP_EVENTS.helpOpenDialog, openHelpDialog);
+  eventBus.on(APP_EVENTS.quit, () => app.quit());
   eventBus.handle(APP_EVENTS.settingsLoad, loadSettings);
   eventBus.on(APP_EVENTS.settingsOpenDialog, openSettingsDialog(mainWindow));
   eventBus.on(APP_EVENTS.settingsSave, saveSettings);


Test patch:
diff --git a/src/__tests__/electron.js b/src/__tests__/electron.js
index 0ec96df..dc07faf 100644
--- a/src/__tests__/electron.js
+++ b/src/__tests__/electron.js
@@ -71,6 +71,7 @@ const mockElectronInstance = ({...overriddenProps} = {}) => {
     app: {
       getPath: jest.fn(),
       on: jest.fn(),
+      quit: jest.fn(),
       setPath: jest.fn()
     },
     contextBridge: {
diff --git a/src/app-menu/__tests__/app-menu.browser.test.mjs b/src/app-menu/__tests__/app-menu.browser.test.mjs
index 4ee01cf..befae3b 100644
--- a/src/app-menu/__tests__/app-menu.browser.test.mjs
+++ b/src/app-menu/__tests__/app-menu.browser.test.mjs
@@ -24,6 +24,7 @@ describe('App Menu in Browser test suite', () => {
       aboutOpenDialog: jest.fn(),
       close: jest.fn(),
       helpOpenDialog: jest.fn(),
+      quit: jest.fn(),
       settingsOpenDialog: jest.fn()
     };
     await loadDOM({meta: import.meta, path: ['..', 'index.html']});
@@ -37,6 +38,7 @@ describe('App Menu in Browser test suite', () => {
   describe.each([
     {testId: 'about', icon: '\ue88e', label: 'About', expectedFunction: 'aboutOpenDialog'},
     {testId: 'help', icon: '\ue887', label: 'Help', expectedFunction: 'helpOpenDialog'},
+    {testId: 'quit', icon: '\ue5cd', label: 'Quit', expectedFunction: 'quit'},
     {testId: 'settings', icon: '\ue8b8', label: 'Settings', expectedFunction: 'settingsOpenDialog'}
   ])('$label entry', ({testId, icon, label, expectedFunction}) => {
     let entry;
diff --git a/src/app-menu/__tests__/preload.test.js b/src/app-menu/__tests__/preload.test.js
index 9722081..da10874 100644
--- a/src/app-menu/__tests__/preload.test.js
+++ b/src/app-menu/__tests__/preload.test.js
@@ -30,6 +30,7 @@ describe('App Menu Module preload test suite', () => {
         aboutOpenDialog: expect.toBeFunction(),
         close: expect.toBeFunction(),
         helpOpenDialog: expect.toBeFunction(),
+        quit: expect.toBeFunction(),
         settingsOpenDialog: expect.toBeFunction()
       });
     });
@@ -42,6 +43,7 @@ describe('App Menu Module preload test suite', () => {
         ['aboutOpenDialog', 'aboutOpenDialog'],
         ['close', 'appMenuClose'],
         ['helpOpenDialog', 'helpOpenDialog'],
+        ['quit', 'quit'],
         ['settingsOpenDialog', 'settingsOpenDialog']
       ])('%s invokes %s', (apiMethod, event) => {
         api[apiMethod]();
@@ -58,6 +60,7 @@ describe('App Menu Module preload test suite', () => {
         aboutOpenDialog: expect.toBeFunction(),
         close: expect.toBeFunction(),
         helpOpenDialog: expect.toBeFunction(),
+        quit: expect.toBeFunction(),
         settingsOpenDialog: expect.toBeFunction()
       });
     });
diff --git a/src/main/__tests__/global-listeners.test.js b/src/main/__tests__/global-listeners.test.js
index 9f085b7..5e6429b 100644
--- a/src/main/__tests__/global-listeners.test.js
+++ b/src/main/__tests__/global-listeners.test.js
@@ -143,6 +143,12 @@ describe('Main :: Global listeners test suite', () => {
     expect(browserView.webContents.loadURL)
       .toHaveBeenCalledWith(expect.stringMatching(/help\/index.html$/));
   });
+  test('quit, should exit the application', () => {
+    // When
+    eventBus.listeners.quit();
+    // Then
+    expect(electron.app.quit).toHaveBeenCalledTimes(1);
+  });
   test('settingsOpenDialog, should open settings dialog', () => {
     // When
     eventBus.listeners.settingsOpenDialog();