Repository: manusa/electronim
Pull request #260 (2023-01-08 08:48:57)

✨ settings cannot be cancelled when initial setup
Relates to #209

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=260)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=260&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=260&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=260&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=260&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=260&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=260&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=260&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=260&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=260&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=260&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=260&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=260&resolved=false&types=CODE_SMELL)

[![83.3%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '83.3%')](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=260&metric=new_coverage&view=list) [83.3% Coverage](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=260&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=260&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=260&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/src/components/top-app-bar.mjs b/src/components/top-app-bar.mjs
index 298ed7d..5a9859a 100644
--- a/src/components/top-app-bar.mjs
+++ b/src/components/top-app-bar.mjs
@@ -27,13 +27,16 @@ import {html, IconButton} from './index.mjs';
 export const TopAppBar = ({
   icon,
   iconClick = () => {},
+  iconDisabled = false,
   headline,
   trailingIcon
 }) => {
   document.body.classList.add('has-top-app-bar');
   return html`
     <div class='material3 top-app-bar small elevation-0 surface'>
-      <${IconButton} className='leading-navigation-icon title-large' icon=${icon} onClick=${iconClick}/>
+      <${IconButton}
+          className='leading-navigation-icon title-large' icon=${icon} onClick=${iconClick} disabled=${iconDisabled}
+      />
       <div class='top-app-bar__headline title-large'>${headline}</div>
       <div class='trailing-icon title-large'>${trailingIcon}</div>
     </div>
diff --git a/src/main/index.js b/src/main/index.js
index 435b8e7..8798265 100644
--- a/src/main/index.js
+++ b/src/main/index.js
@@ -142,7 +142,7 @@ const initTabListener = () => {
       const ipcSender = event.sender;
       tabManager.addTabs(ipcSender)(tabs);
     } else {
-      openSettingsDialog(mainWindow)();
+      eventBus.emit(APP_EVENTS.settingsOpenDialog);
     }
   });
   eventBus.on(APP_EVENTS.activateTab, (_event, data) => activateTab(data.id));
diff --git a/src/settings/settings.browser.mjs b/src/settings/settings.browser.mjs
index cb15d12..70f812c 100644
--- a/src/settings/settings.browser.mjs
+++ b/src/settings/settings.browser.mjs
@@ -19,7 +19,7 @@ import {
   APP_EVENTS, html, render, useReducer, Icon, IconButton, NavigationRail, TopAppBar
 } from '../components/index.mjs';
 import {
-  reducer, activatePane, dictionariesEnabled, isPaneActive
+  reducer, activatePane, canCancel, canSave, dictionariesEnabled, isPaneActive
 } from './settings.reducer.browser.mjs';
 import {
   OtherPane
@@ -48,10 +48,10 @@ const Settings = ({initialState}) => {
   });
   const cancel = () => ipcRenderer.send(APP_EVENTS.closeDialog);
   return html`
-    <${TopAppBar} headline='Settings' icon=${Icon.arrowBack} iconClick=${cancel}
+    <${TopAppBar} headline='Settings' icon=${Icon.arrowBack} iconClick=${cancel} iconDisabled=${!canCancel(state)}
       trailingIcon=${html`<${IconButton}
           className='settings__submit' icon=${Icon.save} onClick=${save}
-          disabled=${!state.canSave || state.invalidTabs.size !== 0}/>`}
+          disabled=${!canSave(state) || state.invalidTabs.size !== 0}/>`}
     />
     <${NavigationRail}>
       <${NavigationRail.Button} label='Services' icon=${Icon.apps}
@@ -81,6 +81,7 @@ Promise.all([
     const initialState = {
       activePane: ServicesPane.id,
       invalidTabs: new Set(),
+      canCancel: currentSettings.tabs.length > 0,
       canSave: currentSettings.tabs.length > 0,
       dictionaries: {
         available: availableDictionaries,
diff --git a/src/settings/settings.reducer.browser.mjs b/src/settings/settings.reducer.browser.mjs
index 3875e1e..6e1488c 100644
--- a/src/settings/settings.reducer.browser.mjs
+++ b/src/settings/settings.reducer.browser.mjs
@@ -31,6 +31,8 @@ export const ACTIONS = {
 };
 
 // Selectors
+export const canCancel = state => state.canCancel ?? false;
+export const canSave = state => state.canSave ?? false;
 export const isPaneActive = state => paneId => state.activePane === paneId;
 export const closeButtonBehavior = state => state.closeButtonBehavior;
 export const theme = state => state.theme;


Test patch:
diff --git a/src/settings/__tests__/settings.browser.test.mjs b/src/settings/__tests__/settings.browser.test.mjs
index b427375..a2c3514 100644
--- a/src/settings/__tests__/settings.browser.test.mjs
+++ b/src/settings/__tests__/settings.browser.test.mjs
@@ -28,6 +28,57 @@ describe('Settings in Browser test suite', () => {
     window.ipcRenderer = mockIpcRenderer;
     await loadDOM({meta: import.meta, path: ['..', 'index.html']});
   });
+  describe('TopApp Bar', () => {
+    describe('Main Button states', () => {
+      describe('With pre-existing settings', () => {
+        test('Cancel/Back button is enabled', () => {
+          const button = document.querySelector('.top-app-bar .leading-navigation-icon');
+          expect(button.hasAttribute('disabled')).toBe(false);
+        });
+      });
+      describe('With no pre-existing settings', () => {
+        beforeEach(async () => {
+          jest.resetModules();
+          mockIpcRenderer.mockCurrentSettings.tabs = [];
+          await loadDOM({meta: import.meta, path: ['..', 'index.html']});
+        });
+        test('Cancel/Back button is disabled', () => {
+          const button = document.querySelector('.top-app-bar .leading-navigation-icon');
+          expect(button.getAttribute('disabled')).toBe('true');
+        });
+        test('Save button is disabled', () => {
+          const button = document.querySelector('.settings__submit');
+          expect(button.getAttribute('disabled')).toBe('true');
+        });
+      });
+    });
+    describe('Main Button events', () => {
+      test('Submit should send form data', () => {
+        // When
+        fireEvent.click(document.querySelector('.settings__submit'));
+        // Then
+        expect(mockIpcRenderer.send).toHaveBeenCalledTimes(1);
+        expect(mockIpcRenderer.send).toHaveBeenCalledWith('settingsSave',
+          {
+            tabs: [
+              {id: '1', url: 'https://initial-tab.com', sandboxed: true},
+              {id: '2', url: 'https://initial-tab-2.com', disabled: true, disableNotifications: true}
+            ],
+            enabledDictionaries: ['en'],
+            disableNotificationsGlobally: false,
+            theme: 'dark',
+            trayEnabled: true
+          });
+      });
+      test('Cancel should send close dialog event', () => {
+        // When
+        fireEvent.click(document.querySelector('.top-app-bar .leading-navigation-icon'));
+        // Then
+        expect(mockIpcRenderer.send).toHaveBeenCalledTimes(1);
+        expect(mockIpcRenderer.send).toHaveBeenCalledWith('closeDialog');
+      });
+    });
+  });
   describe('Navigation Rail', () => {
     test('Shows navigation rail', () => {
       expect(document.querySelector('.material3.navigation-rail')).not.toBeNull();
@@ -45,32 +96,6 @@ describe('Settings in Browser test suite', () => {
       expect(items).toContainEqual({label, icon});
     });
   });
-  describe('Main Button events', () => {
-    test('Submit should send form data', () => {
-      // When
-      fireEvent.click(document.querySelector('.settings__submit'));
-      // Then
-      expect(mockIpcRenderer.send).toHaveBeenCalledTimes(1);
-      expect(mockIpcRenderer.send).toHaveBeenCalledWith('settingsSave',
-        {
-          tabs: [
-            {id: '1', url: 'https://initial-tab.com', sandboxed: true},
-            {id: '2', url: 'https://initial-tab-2.com', disabled: true, disableNotifications: true}
-          ],
-          enabledDictionaries: ['en'],
-          disableNotificationsGlobally: false,
-          theme: 'dark',
-          trayEnabled: true
-        });
-    });
-    test('Cancel should send close dialog event', () => {
-      // When
-      fireEvent.click(document.querySelector('.top-app-bar .leading-navigation-icon'));
-      // Then
-      expect(mockIpcRenderer.send).toHaveBeenCalledTimes(1);
-      expect(mockIpcRenderer.send).toHaveBeenCalledWith('closeDialog');
-    });
-  });
   describe('New tab Input field', () => {
     let $tabContainer;
     let $input;