Repository: gitify-app/gitify
Pull request #1009 (2024-04-15 20:18:38)

fix: move `update-native-theme` into ipcRenderer `useEffect`
I've been seeing the following warning/error in the console when running locally

```
(node:7668) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 update-native-theme listeners added to [IpcRenderer]. Use emitter.setMaxListeners() to increase limit
```

Moving this into the useEffect along with the other ipcRenderer calls seems to alleviate this.  Tested by toggling theme within the Gitify app, as well as via the macOS system settings and all seemed to behave as anticipated.


Patch:
diff --git a/src/context/App.tsx b/src/context/App.tsx
index 4e2e6b77..013a586b 100644
--- a/src/context/App.tsx
+++ b/src/context/App.tsx
@@ -100,19 +100,20 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
     setTheme(settings.theme as Theme);
   }, [settings.theme]);
 
-  // biome-ignore lint/correctness/useExhaustiveDependencies: We only want fetchNotifications to be called for a subset of settings changes.
+  // biome-ignore lint/correctness/useExhaustiveDependencies: We only want fetchNotifications to be called for certain account or setting changes.
   useEffect(() => {
     fetchNotifications(accounts, settings);
   }, [
     settings.participating,
     settings.showBots,
     settings.detailedNotifications,
+    accounts.token,
+    accounts.enterpriseAccounts.length,
   ]);
 
-  // biome-ignore lint/correctness/useExhaustiveDependencies: We only want fetchNotifications to be called for certain account changes.
-  useEffect(() => {
+  useInterval(() => {
     fetchNotifications(accounts, settings);
-  }, [accounts.token, accounts.enterpriseAccounts.length]);
+  }, 60000);
 
   // biome-ignore lint/correctness/useExhaustiveDependencies: We need to update tray title when settings or notifications changes.
   useEffect(() => {
@@ -125,10 +126,6 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
     }
   }, [settings.showNotificationsCountInTray, notifications]);
 
-  useInterval(() => {
-    fetchNotifications(accounts, settings);
-  }, 60000);
-
   const updateSetting = useCallback(
     (name: keyof SettingsState, value: boolean | Theme) => {
       if (name === 'openAtStartup') {
diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts
index 5565ba13..6fd6f708 100644
--- a/src/hooks/useNotifications.ts
+++ b/src/hooks/useNotifications.ts
@@ -96,12 +96,12 @@ export const useNotifications = (): NotificationsState => {
       }
 
       setIsFetching(true);
-      setRequestFailed(false);
 
       return axios
         .all([getGitHubNotifications(), ...getEnterpriseNotifications()])
         .then(
           axios.spread((gitHubNotifications, ...entAccNotifications) => {
+            setRequestFailed(false);
             const enterpriseNotifications = entAccNotifications.map(
               (accountNotifications) => {
                 const { hostname } = new URL(accountNotifications.config.url);
diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx
index 7c6ab6c7..0e0de767 100644
--- a/src/routes/Settings.tsx
+++ b/src/routes/Settings.tsx
@@ -62,6 +62,12 @@ export const SettingsRoute: FC = () => {
     ipcRenderer.invoke('get-app-version').then((result: string) => {
       setAppVersion(result);
     });
+
+    ipcRenderer.on('update-native-theme', (_, updatedTheme: Theme) => {
+      if (settings.theme === Theme.SYSTEM) {
+        setTheme(updatedTheme);
+      }
+    });
   }, []);
 
   useMemo(() => {
@@ -77,12 +83,6 @@ export const SettingsRoute: FC = () => {
     })();
   }, [accounts.token]);
 
-  ipcRenderer.on('update-native-theme', (_, updatedTheme: Theme) => {
-    if (settings.theme === Theme.SYSTEM) {
-      setTheme(updatedTheme);
-    }
-  });
-
   const logoutUser = useCallback(() => {
     logout();
     navigate(-1);


Test patch:
diff --git a/src/context/App.test.tsx b/src/context/App.test.tsx
index 92f38756..0850df98 100644
--- a/src/context/App.test.tsx
+++ b/src/context/App.test.tsx
@@ -64,7 +64,7 @@ describe('context/App.tsx', () => {
       // Wait for the useEffects, for settings.participating and accounts, to run.
       // Those aren't what we're testing
       await waitFor(() =>
-        expect(fetchNotificationsMock).toHaveBeenCalledTimes(2),
+        expect(fetchNotificationsMock).toHaveBeenCalledTimes(1),
       );
 
       fetchNotificationsMock.mockReset();
@@ -245,7 +245,7 @@ describe('context/App.tsx', () => {
       fireEvent.click(getByText('Test Case'));
 
       await waitFor(() =>
-        expect(fetchNotificationsMock).toHaveBeenCalledTimes(2),
+        expect(fetchNotificationsMock).toHaveBeenCalledTimes(1),
       );
 
       expect(apiRequestAuthMock).toHaveBeenCalledTimes(2);
diff --git a/src/routes/Settings.test.tsx b/src/routes/Settings.test.tsx
index 60107a2d..f19b2955 100644
--- a/src/routes/Settings.test.tsx
+++ b/src/routes/Settings.test.tsx
@@ -30,500 +30,512 @@ describe('routes/Settings.tsx', () => {
     jest.clearAllMocks();
   });
 
-  it('should render itself & its children', async () => {
-    await act(async () => {
-      render(
-        <AppContext.Provider
-          value={{ settings: mockSettings, accounts: mockAccounts }}
-        >
-          <MemoryRouter>
-            <SettingsRoute />
-          </MemoryRouter>
-        </AppContext.Provider>,
-      );
+  describe('General', () => {
+    it('should render itself & its children', async () => {
+      await act(async () => {
+        render(
+          <AppContext.Provider
+            value={{ settings: mockSettings, accounts: mockAccounts }}
+          >
+            <MemoryRouter>
+              <SettingsRoute />
+            </MemoryRouter>
+          </AppContext.Provider>,
+        );
+      });
+
+      expect(screen.getByTestId('settings')).toMatchSnapshot();
     });
 
-    expect(screen.getByTestId('settings')).toMatchSnapshot();
-  });
-
-  it('should press the logout', async () => {
-    const logoutMock = jest.fn();
-    await act(async () => {
-      render(
-        <AppContext.Provider
-          value={{
-            settings: mockSettings,
-            accounts: mockAccounts,
-            logout: logoutMock,
-          }}
-        >
-          <MemoryRouter>
-            <SettingsRoute />
-          </MemoryRouter>
-        </AppContext.Provider>,
-      );
+    it('should go back by pressing the icon', async () => {
+      await act(async () => {
+        render(
+          <AppContext.Provider
+            value={{
+              settings: mockSettings,
+              accounts: mockAccounts,
+            }}
+          >
+            <MemoryRouter>
+              <SettingsRoute />
+            </MemoryRouter>
+          </AppContext.Provider>,
+        );
+      });
+
+      fireEvent.click(screen.getByLabelText('Go Back'));
+      expect(mockNavigate).toHaveBeenNthCalledWith(1, -1);
     });
-
-    fireEvent.click(screen.getByTitle('Logout from octocat'));
-
-    expect(logoutMock).toHaveBeenCalledTimes(1);
-
-    expect(ipcRenderer.send).toHaveBeenCalledTimes(2);
-    expect(ipcRenderer.send).toHaveBeenCalledWith('update-icon');
-    expect(ipcRenderer.send).toHaveBeenCalledWith('update-title', '');
-    expect(mockNavigate).toHaveBeenNthCalledWith(1, -1);
   });
-
-  it('should go back by pressing the icon', async () => {
-    await act(async () => {
-      render(
-        <AppContext.Provider
-          value={{
-            settings: mockSettings,
-            accounts: mockAccounts,
-          }}
-        >
-          <MemoryRouter>
-            <SettingsRoute />
-          </MemoryRouter>
-        </AppContext.Provider>,
-      );
+  describe('Appearance section', () => {
+    it('should change the theme radio group', async () => {
+      await act(async () => {
+        render(
+          <AppContext.Provider
+            value={{
+              settings: mockSettings,
+              accounts: mockAccounts,
+              updateSetting,
+            }}
+          >
+            <MemoryRouter>
+              <SettingsRoute />
+            </MemoryRouter>
+          </AppContext.Provider>,
+        );
+      });
+
+      fireEvent.click(screen.getByLabelText('Light'));
+
+      expect(updateSetting).toHaveBeenCalledTimes(1);
+      expect(updateSetting).toHaveBeenCalledWith('theme', 'LIGHT');
     });
 
-    fireEvent.click(screen.getByLabelText('Go Back'));
-    expect(mockNavigate).toHaveBeenNthCalledWith(1, -1);
-  });
-
-  it('should toggle the showOnlyParticipating checkbox', async () => {
-    await act(async () => {
-      render(
-        <AppContext.Provider
-          value={{
-            settings: mockSettings,
-            accounts: mockAccounts,
-            updateSetting,
-          }}
-        >
-          <MemoryRouter>
-            <SettingsRoute />
-          </MemoryRouter>
-        </AppContext.Provider>,
-      );
-    });
-
-    fireEvent.click(screen.getByLabelText('Show only participating'), {
-      target: { checked: true },
+    it('should be able to enable detailed notifications', async () => {
+      jest.spyOn(apiRequests, 'apiRequestAuth').mockResolvedValue({
+        headers: {
+          'x-oauth-scopes': Constants.AUTH_SCOPE.join(', '),
+        },
+      } as unknown as AxiosResponse);
+
+      await act(async () => {
+        render(
+          <AppContext.Provider
+            value={{
+              settings: mockSettings,
+              accounts: mockAccounts,
+              updateSetting,
+            }}
+          >
+            <MemoryRouter>
+              <SettingsRoute />
+            </MemoryRouter>
+          </AppContext.Provider>,
+        );
+      });
+
+      await screen.findByLabelText('Detailed notifications');
+
+      fireEvent.click(screen.getByLabelText('Detailed notifications'));
+
+      expect(updateSetting).toHaveBeenCalledTimes(1);
+      expect(updateSetting).toHaveBeenCalledWith('detailedNotifications', true);
     });
 
-    expect(updateSetting).toHaveBeenCalledTimes(1);
-    expect(updateSetting).toHaveBeenCalledWith('participating', false);
-  });
-
-  it('should open official docs for showOnlyParticipating tooltip', async () => {
-    await act(async () => {
-      render(
-        <AppContext.Provider
-          value={{
-            settings: mockSettings,
-            accounts: mockAccounts,
-            updateSetting,
-          }}
-        >
-          <MemoryRouter>
-            <SettingsRoute />
-          </MemoryRouter>
-        </AppContext.Provider>,
+    it('should not be able to enable detailed notifications due to missing scope', async () => {
+      jest.spyOn(apiRequests, 'apiRequestAuth').mockResolvedValue({
+        headers: {
+          'x-oauth-scopes': 'read:user, notifications',
+        },
+      } as unknown as AxiosResponse);
+
+      await act(async () => {
+        render(
+          <AppContext.Provider
+            value={{
+              settings: mockSettings,
+              accounts: mockAccounts,
+              updateSetting,
+            }}
+          >
+            <MemoryRouter>
+              <SettingsRoute />
+            </MemoryRouter>
+          </AppContext.Provider>,
+        );
+      });
+
+      expect(
+        screen
+          .getByLabelText('Detailed notifications (requires repo scope)')
+          .closest('input'),
+      ).toHaveProperty('disabled', true);
+
+      // click the checkbox
+      fireEvent.click(
+        screen.getByLabelText('Detailed notifications (requires repo scope)'),
       );
-    });
 
-    const tooltipElement = screen.getByLabelText(
-      'tooltip-showOnlyParticipating',
-    );
+      // check if the checkbox is still unchecked
+      expect(updateSetting).not.toHaveBeenCalled();
+      expect(
+        screen.getByLabelText('Detailed notifications (requires repo scope)'),
+      ).not.toBe('checked');
 
-    fireEvent.mouseEnter(tooltipElement);
-
-    fireEvent.click(
-      screen.getByTitle(
-        'Open GitHub documentation for participating and watching notifications',
-      ),
-    );
-
-    expect(shell.openExternal).toHaveBeenCalledTimes(1);
-    expect(shell.openExternal).toHaveBeenCalledWith(
-      'https://docs.github.com/en/account-and-profile/managing-subscriptions-and-notifications-on-github/setting-up-notifications/configuring-notifications#about-participating-and-watching-notifications',
-    );
-  });
-
-  it('should not be able to toggle the showBots checkbox when detailedNotifications is disabled', async () => {
-    await act(async () => {
-      render(
-        <AppContext.Provider
-          value={{
-            settings: {
-              ...mockSettings,
-              detailedNotifications: false,
-              showBots: true,
-            },
-            accounts: mockAccounts,
-            updateSetting,
-          }}
-        >
-          <MemoryRouter>
-            <SettingsRoute />
-          </MemoryRouter>
-        </AppContext.Provider>,
-      );
+      expect(
+        screen.getByLabelText('Detailed notifications (requires repo scope)')
+          .parentNode.parentNode,
+      ).toMatchSnapshot();
     });
-
-    expect(
-      screen
-        .getByLabelText('Show notifications from Bot accounts')
-        .closest('input'),
-    ).toHaveProperty('disabled', true);
-
-    // click the checkbox
-    fireEvent.click(
-      screen.getByLabelText('Show notifications from Bot accounts'),
-    );
-
-    // check if the checkbox is still unchecked
-    expect(updateSetting).not.toHaveBeenCalled();
-
-    expect(
-      screen.getByLabelText('Show notifications from Bot accounts').parentNode
-        .parentNode,
-    ).toMatchSnapshot();
   });
 
-  it('should be able to toggle the showBots checkbox when detailedNotifications is enabled', async () => {
-    await act(async () => {
-      render(
-        <AppContext.Provider
-          value={{
-            settings: {
-              ...mockSettings,
-              detailedNotifications: true,
-              showBots: true,
-            },
-            accounts: mockAccounts,
-            updateSetting,
-          }}
-        >
-          <MemoryRouter>
-            <SettingsRoute />
-          </MemoryRouter>
-        </AppContext.Provider>,
-      );
+  describe('Notifications section', () => {
+    it('should toggle the showOnlyParticipating checkbox', async () => {
+      await act(async () => {
+        render(
+          <AppContext.Provider
+            value={{
+              settings: mockSettings,
+              accounts: mockAccounts,
+              updateSetting,
+            }}
+          >
+            <MemoryRouter>
+              <SettingsRoute />
+            </MemoryRouter>
+          </AppContext.Provider>,
+        );
+      });
+
+      fireEvent.click(screen.getByLabelText('Show only participating'), {
+        target: { checked: true },
+      });
+
+      expect(updateSetting).toHaveBeenCalledTimes(1);
+      expect(updateSetting).toHaveBeenCalledWith('participating', false);
     });
 
-    expect(
-      screen
-        .getByLabelText('Show notifications from Bot accounts')
-        .closest('input'),
-    ).toHaveProperty('disabled', false);
-
-    // click the checkbox
-    fireEvent.click(
-      screen.getByLabelText('Show notifications from Bot accounts'),
-    );
-
-    // check if the checkbox is still unchecked
-    expect(updateSetting).toHaveBeenCalledWith('showBots', false);
-
-    expect(
-      screen.getByLabelText('Show notifications from Bot accounts').parentNode
-        .parentNode,
-    ).toMatchSnapshot();
-  });
-
-  it('should toggle the showNotificationsCountInTray checkbox', async () => {
-    await act(async () => {
-      render(
-        <AppContext.Provider
-          value={{
-            settings: mockSettings,
-            accounts: mockAccounts,
-            updateSetting,
-          }}
-        >
-          <MemoryRouter>
-            <SettingsRoute />
-          </MemoryRouter>
-        </AppContext.Provider>,
+    it('should open official docs for showOnlyParticipating tooltip', async () => {
+      await act(async () => {
+        render(
+          <AppContext.Provider
+            value={{
+              settings: mockSettings,
+              accounts: mockAccounts,
+              updateSetting,
+            }}
+          >
+            <MemoryRouter>
+              <SettingsRoute />
+            </MemoryRouter>
+          </AppContext.Provider>,
+        );
+      });
+
+      const tooltipElement = screen.getByLabelText(
+        'tooltip-showOnlyParticipating',
       );
-    });
 
-    fireEvent.click(screen.getByLabelText('Show notifications count in tray'), {
-      target: { checked: true },
-    });
+      fireEvent.mouseEnter(tooltipElement);
 
-    expect(updateSetting).toHaveBeenCalledTimes(1);
-    expect(updateSetting).toHaveBeenCalledWith(
-      'showNotificationsCountInTray',
-      false,
-    );
-  });
-
-  it('should toggle the playSound checkbox', async () => {
-    await act(async () => {
-      render(
-        <AppContext.Provider
-          value={{
-            settings: mockSettings,
-            accounts: mockAccounts,
-            updateSetting,
-          }}
-        >
-          <MemoryRouter>
-            <SettingsRoute />
-          </MemoryRouter>
-        </AppContext.Provider>,
+      fireEvent.click(
+        screen.getByTitle(
+          'Open GitHub documentation for participating and watching notifications',
+        ),
       );
-    });
 
-    fireEvent.click(screen.getByLabelText('Play sound'), {
-      target: { checked: true },
+      expect(shell.openExternal).toHaveBeenCalledTimes(1);
+      expect(shell.openExternal).toHaveBeenCalledWith(
+        'https://docs.github.com/en/account-and-profile/managing-subscriptions-and-notifications-on-github/setting-up-notifications/configuring-notifications#about-participating-and-watching-notifications',
+      );
     });
 
-    expect(updateSetting).toHaveBeenCalledTimes(1);
-    expect(updateSetting).toHaveBeenCalledWith('playSound', false);
-  });
-
-  it('should toggle the showNotifications checkbox', async () => {
-    await act(async () => {
-      render(
-        <AppContext.Provider
-          value={{
-            settings: mockSettings,
-            accounts: mockAccounts,
-            updateSetting,
-          }}
-        >
-          <MemoryRouter>
-            <SettingsRoute />
-          </MemoryRouter>
-        </AppContext.Provider>,
+    it('should not be able to toggle the showBots checkbox when detailedNotifications is disabled', async () => {
+      await act(async () => {
+        render(
+          <AppContext.Provider
+            value={{
+              settings: {
+                ...mockSettings,
+                detailedNotifications: false,
+                showBots: true,
+              },
+              accounts: mockAccounts,
+              updateSetting,
+            }}
+          >
+            <MemoryRouter>
+              <SettingsRoute />
+            </MemoryRouter>
+          </AppContext.Provider>,
+        );
+      });
+
+      expect(
+        screen
+          .getByLabelText('Show notifications from Bot accounts')
+          .closest('input'),
+      ).toHaveProperty('disabled', true);
+
+      // click the checkbox
+      fireEvent.click(
+        screen.getByLabelText('Show notifications from Bot accounts'),
       );
-    });
 
-    fireEvent.click(screen.getByLabelText('Show system notifications'), {
-      target: { checked: true },
-    });
+      // check if the checkbox is still unchecked
+      expect(updateSetting).not.toHaveBeenCalled();
 
-    expect(updateSetting).toHaveBeenCalledTimes(1);
-    expect(updateSetting).toHaveBeenCalledWith('showNotifications', false);
-  });
-
-  it('should toggle the markAsDoneOnOpen checkbox', async () => {
-    await act(async () => {
-      render(
-        <AppContext.Provider
-          value={{
-            settings: mockSettings,
-            accounts: mockAccounts,
-            updateSetting,
-          }}
-        >
-          <MemoryRouter>
-            <SettingsRoute />
-          </MemoryRouter>
-        </AppContext.Provider>,
-      );
+      expect(
+        screen.getByLabelText('Show notifications from Bot accounts').parentNode
+          .parentNode,
+      ).toMatchSnapshot();
     });
 
-    fireEvent.click(screen.getByLabelText('Mark as done on open'), {
-      target: { checked: true },
-    });
+    it('should be able to toggle the showBots checkbox when detailedNotifications is enabled', async () => {
+      await act(async () => {
+        render(
+          <AppContext.Provider
+            value={{
+              settings: {
+                ...mockSettings,
+                detailedNotifications: true,
+                showBots: true,
+              },
+              accounts: mockAccounts,
+              updateSetting,
+            }}
+          >
+            <MemoryRouter>
+              <SettingsRoute />
+            </MemoryRouter>
+          </AppContext.Provider>,
+        );
+      });
+
+      expect(
+        screen
+          .getByLabelText('Show notifications from Bot accounts')
+          .closest('input'),
+      ).toHaveProperty('disabled', false);
+
+      // click the checkbox
+      fireEvent.click(
+        screen.getByLabelText('Show notifications from Bot accounts'),
+      );
 
-    expect(updateSetting).toHaveBeenCalledTimes(1);
-    expect(updateSetting).toHaveBeenCalledWith('markAsDoneOnOpen', false);
-  });
+      // check if the checkbox is still unchecked
+      expect(updateSetting).toHaveBeenCalledWith('showBots', false);
 
-  it('should toggle the openAtStartup checkbox', async () => {
-    await act(async () => {
-      render(
-        <AppContext.Provider
-          value={{
-            settings: mockSettings,
-            accounts: mockAccounts,
-            updateSetting,
-          }}
-        >
-          <MemoryRouter>
-            <SettingsRoute />
-          </MemoryRouter>
-        </AppContext.Provider>,
-      );
+      expect(
+        screen.getByLabelText('Show notifications from Bot accounts').parentNode
+          .parentNode,
+      ).toMatchSnapshot();
     });
 
-    fireEvent.click(screen.getByLabelText('Open at startup'), {
-      target: { checked: true },
+    it('should toggle the markAsDoneOnOpen checkbox', async () => {
+      await act(async () => {
+        render(
+          <AppContext.Provider
+            value={{
+              settings: mockSettings,
+              accounts: mockAccounts,
+              updateSetting,
+            }}
+          >
+            <MemoryRouter>
+              <SettingsRoute />
+            </MemoryRouter>
+          </AppContext.Provider>,
+        );
+      });
+
+      fireEvent.click(screen.getByLabelText('Mark as done on open'), {
+        target: { checked: true },
+      });
+
+      expect(updateSetting).toHaveBeenCalledTimes(1);
+      expect(updateSetting).toHaveBeenCalledWith('markAsDoneOnOpen', false);
     });
-
-    expect(updateSetting).toHaveBeenCalledTimes(1);
-    expect(updateSetting).toHaveBeenCalledWith('openAtStartup', false);
   });
 
-  it('should change the theme radio group', async () => {
-    await act(async () => {
-      render(
-        <AppContext.Provider
-          value={{
-            settings: mockSettings,
-            accounts: mockAccounts,
-            updateSetting,
-          }}
-        >
-          <MemoryRouter>
-            <SettingsRoute />
-          </MemoryRouter>
-        </AppContext.Provider>,
+  describe('System section', () => {
+    it('should toggle the showNotificationsCountInTray checkbox', async () => {
+      await act(async () => {
+        render(
+          <AppContext.Provider
+            value={{
+              settings: mockSettings,
+              accounts: mockAccounts,
+              updateSetting,
+            }}
+          >
+            <MemoryRouter>
+              <SettingsRoute />
+            </MemoryRouter>
+          </AppContext.Provider>,
+        );
+      });
+
+      fireEvent.click(
+        screen.getByLabelText('Show notifications count in tray'),
+        {
+          target: { checked: true },
+        },
       );
-    });
 
-    fireEvent.click(screen.getByLabelText('Light'));
-
-    expect(updateSetting).toHaveBeenCalledTimes(1);
-    expect(updateSetting).toHaveBeenCalledWith('theme', 'LIGHT');
-  });
-
-  it('should go to the enterprise login route', async () => {
-    await act(async () => {
-      render(
-        <AppContext.Provider
-          value={{
-            settings: mockSettings,
-            accounts: mockAccounts,
-          }}
-        >
-          <MemoryRouter>
-            <SettingsRoute />
-          </MemoryRouter>
-        </AppContext.Provider>,
+      expect(updateSetting).toHaveBeenCalledTimes(1);
+      expect(updateSetting).toHaveBeenCalledWith(
+        'showNotificationsCountInTray',
+        false,
       );
     });
 
-    fireEvent.click(screen.getByTitle('Login with GitHub Enterprise'));
-    expect(mockNavigate).toHaveBeenNthCalledWith(1, '/login-enterprise', {
-      replace: true,
+    it('should toggle the showNotifications checkbox', async () => {
+      await act(async () => {
+        render(
+          <AppContext.Provider
+            value={{
+              settings: mockSettings,
+              accounts: mockAccounts,
+              updateSetting,
+            }}
+          >
+            <MemoryRouter>
+              <SettingsRoute />
+            </MemoryRouter>
+          </AppContext.Provider>,
+        );
+      });
+
+      fireEvent.click(screen.getByLabelText('Show system notifications'), {
+        target: { checked: true },
+      });
+
+      expect(updateSetting).toHaveBeenCalledTimes(1);
+      expect(updateSetting).toHaveBeenCalledWith('showNotifications', false);
     });
-  });
 
-  it('should quit the app', async () => {
-    await act(async () => {
-      render(
-        <AppContext.Provider
-          value={{ settings: mockSettings, accounts: mockAccounts }}
-        >
-          <MemoryRouter>
-            <SettingsRoute />
-          </MemoryRouter>
-        </AppContext.Provider>,
-      );
+    it('should toggle the playSound checkbox', async () => {
+      await act(async () => {
+        render(
+          <AppContext.Provider
+            value={{
+              settings: mockSettings,
+              accounts: mockAccounts,
+              updateSetting,
+            }}
+          >
+            <MemoryRouter>
+              <SettingsRoute />
+            </MemoryRouter>
+          </AppContext.Provider>,
+        );
+      });
+
+      fireEvent.click(screen.getByLabelText('Play sound'), {
+        target: { checked: true },
+      });
+
+      expect(updateSetting).toHaveBeenCalledTimes(1);
+      expect(updateSetting).toHaveBeenCalledWith('playSound', false);
     });
 
-    fireEvent.click(screen.getByTitle('Quit Gitify'));
-    expect(ipcRenderer.send).toHaveBeenCalledWith('app-quit');
-  });
-
-  it('should be able to enable detailed notifications', async () => {
-    jest.spyOn(apiRequests, 'apiRequestAuth').mockResolvedValue({
-      headers: {
-        'x-oauth-scopes': Constants.AUTH_SCOPE.join(', '),
-      },
-    } as unknown as AxiosResponse);
-
-    await act(async () => {
-      render(
-        <AppContext.Provider
-          value={{
-            settings: mockSettings,
-            accounts: mockAccounts,
-            updateSetting,
-          }}
-        >
-          <MemoryRouter>
-            <SettingsRoute />
-          </MemoryRouter>
-        </AppContext.Provider>,
-      );
+    it('should toggle the openAtStartup checkbox', async () => {
+      await act(async () => {
+        render(
+          <AppContext.Provider
+            value={{
+              settings: mockSettings,
+              accounts: mockAccounts,
+              updateSetting,
+            }}
+          >
+            <MemoryRouter>
+              <SettingsRoute />
+            </MemoryRouter>
+          </AppContext.Provider>,
+        );
+      });
+
+      fireEvent.click(screen.getByLabelText('Open at startup'), {
+        target: { checked: true },
+      });
+
+      expect(updateSetting).toHaveBeenCalledTimes(1);
+      expect(updateSetting).toHaveBeenCalledWith('openAtStartup', false);
     });
-
-    await screen.findByLabelText('Detailed notifications');
-
-    fireEvent.click(screen.getByLabelText('Detailed notifications'));
-
-    expect(updateSetting).toHaveBeenCalledTimes(1);
-    expect(updateSetting).toHaveBeenCalledWith('detailedNotifications', true);
   });
 
-  it('should not be able to enable detailed notifications due to missing scope', async () => {
-    jest.spyOn(apiRequests, 'apiRequestAuth').mockResolvedValue({
-      headers: {
-        'x-oauth-scopes': 'read:user, notifications',
-      },
-    } as unknown as AxiosResponse);
-
-    await act(async () => {
-      render(
-        <AppContext.Provider
-          value={{
-            settings: mockSettings,
-            accounts: mockAccounts,
-            updateSetting,
-          }}
-        >
-          <MemoryRouter>
-            <SettingsRoute />
-          </MemoryRouter>
-        </AppContext.Provider>,
+  describe('Footer section', () => {
+    it('should open release notes', async () => {
+      await act(async () => {
+        render(
+          <AppContext.Provider
+            value={{
+              settings: mockSettings,
+              accounts: mockAccounts,
+            }}
+          >
+            <MemoryRouter>
+              <SettingsRoute />
+            </MemoryRouter>
+          </AppContext.Provider>,
+        );
+      });
+
+      fireEvent.click(screen.getByTitle('View release notes'));
+
+      expect(shell.openExternal).toHaveBeenCalledTimes(1);
+      expect(shell.openExternal).toHaveBeenCalledWith(
+        'https://github.com/gitify-app/gitify/releases/tag/v0.0.1',
       );
     });
 
-    expect(
-      screen
-        .getByLabelText('Detailed notifications (requires repo scope)')
-        .closest('input'),
-    ).toHaveProperty('disabled', true);
-
-    // click the checkbox
-    fireEvent.click(
-      screen.getByLabelText('Detailed notifications (requires repo scope)'),
-    );
-
-    // check if the checkbox is still unchecked
-    expect(updateSetting).not.toHaveBeenCalled();
-    expect(
-      screen.getByLabelText('Detailed notifications (requires repo scope)'),
-    ).not.toBe('checked');
-
-    expect(
-      screen.getByLabelText('Detailed notifications (requires repo scope)')
-        .parentNode.parentNode,
-    ).toMatchSnapshot();
-  });
-
-  it('should open release notes', async () => {
-    await act(async () => {
-      render(
-        <AppContext.Provider
-          value={{
-            settings: mockSettings,
-            accounts: mockAccounts,
-          }}
-        >
-          <MemoryRouter>
-            <SettingsRoute />
-          </MemoryRouter>
-        </AppContext.Provider>,
-      );
+    it('should go to the enterprise login route', async () => {
+      await act(async () => {
+        render(
+          <AppContext.Provider
+            value={{
+              settings: mockSettings,
+              accounts: mockAccounts,
+            }}
+          >
+            <MemoryRouter>
+              <SettingsRoute />
+            </MemoryRouter>
+          </AppContext.Provider>,
+        );
+      });
+
+      fireEvent.click(screen.getByTitle('Login with GitHub Enterprise'));
+      expect(mockNavigate).toHaveBeenNthCalledWith(1, '/login-enterprise', {
+        replace: true,
+      });
     });
 
-    fireEvent.click(screen.getByTitle('View release notes'));
+    it('should press the logout', async () => {
+      const logoutMock = jest.fn();
+      await act(async () => {
+        render(
+          <AppContext.Provider
+            value={{
+              settings: mockSettings,
+              accounts: mockAccounts,
+              logout: logoutMock,
+            }}
+          >
+            <MemoryRouter>
+              <SettingsRoute />
+            </MemoryRouter>
+          </AppContext.Provider>,
+        );
+      });
+
+      fireEvent.click(screen.getByTitle('Logout from octocat'));
+
+      expect(logoutMock).toHaveBeenCalledTimes(1);
+
+      expect(ipcRenderer.send).toHaveBeenCalledTimes(2);
+      expect(ipcRenderer.send).toHaveBeenCalledWith('update-icon');
+      expect(ipcRenderer.send).toHaveBeenCalledWith('update-title', '');
+      expect(mockNavigate).toHaveBeenNthCalledWith(1, -1);
+    });
 
-    expect(shell.openExternal).toHaveBeenCalledTimes(1);
-    expect(shell.openExternal).toHaveBeenCalledWith(
-      'https://github.com/gitify-app/gitify/releases/tag/v0.0.1',
-    );
+    it('should quit the app', async () => {
+      await act(async () => {
+        render(
+          <AppContext.Provider
+            value={{ settings: mockSettings, accounts: mockAccounts }}
+          >
+            <MemoryRouter>
+              <SettingsRoute />
+            </MemoryRouter>
+          </AppContext.Provider>,
+        );
+      });
+
+      fireEvent.click(screen.getByTitle('Quit Gitify'));
+      expect(ipcRenderer.send).toHaveBeenCalledWith('app-quit');
+    });
   });
 });
diff --git a/src/routes/__snapshots__/Settings.test.tsx.snap b/src/routes/__snapshots__/Settings.test.tsx.snap
index 0e599972..fd658a36 100644
--- a/src/routes/__snapshots__/Settings.test.tsx.snap
+++ b/src/routes/__snapshots__/Settings.test.tsx.snap
@@ -1,53 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`routes/Settings.tsx should be able to toggle the showBots checkbox when detailedNotifications is enabled 1`] = `
-<div
-  class="flex items-start"
->
-  <div
-    class="flex items-center h-5"
-  >
-    <input
-      checked=""
-      class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
-      id="showBots"
-      type="checkbox"
-    />
-  </div>
-  <div
-    class="ml-3 "
-  >
-    <label
-      class="font-medium text-gray-700 dark:text-gray-200"
-      for="showBots"
-    >
-      Show notifications from Bot accounts
-      <span
-        aria-label="tooltip-showBots"
-        class="relative"
-        id="tooltip-showBots"
-      >
-        <svg
-          aria-hidden="true"
-          class="text-blue-500 ml-1"
-          fill="currentColor"
-          focusable="false"
-          height="16"
-          style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
-          viewBox="0 0 16 16"
-          width="16"
-        >
-          <path
-            d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.92 6.085h.001a.749.749 0 1 1-1.342-.67c.169-.339.436-.701.849-.977C6.845 4.16 7.369 4 8 4a2.756 2.756 0 0 1 1.637.525c.503.377.863.965.863 1.725 0 .448-.115.83-.329 1.15-.205.307-.47.513-.692.662-.109.072-.22.138-.313.195l-.006.004a6.24 6.24 0 0 0-.26.16.952.952 0 0 0-.276.245.75.75 0 0 1-1.248-.832c.184-.264.42-.489.692-.661.103-.067.207-.132.313-.195l.007-.004c.1-.061.182-.11.258-.161a.969.969 0 0 0 .277-.245C8.96 6.514 9 6.427 9 6.25a.612.612 0 0 0-.262-.525A1.27 1.27 0 0 0 8 5.5c-.369 0-.595.09-.74.187a1.01 1.01 0 0 0-.34.398ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
-          />
-        </svg>
-      </span>
-    </label>
-  </div>
-</div>
-`;
-
-exports[`routes/Settings.tsx should not be able to enable detailed notifications due to missing scope 1`] = `
+exports[`routes/Settings.tsx Appearance section should not be able to enable detailed notifications due to missing scope 1`] = `
 <div
   class="flex items-start"
 >
@@ -95,56 +48,7 @@ exports[`routes/Settings.tsx should not be able to enable detailed notifications
 </div>
 `;
 
-exports[`routes/Settings.tsx should not be able to toggle the showBots checkbox when detailedNotifications is disabled 1`] = `
-<div
-  class="flex items-start"
->
-  <div
-    class="flex items-center h-5"
-  >
-    <input
-      checked=""
-      class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
-      disabled=""
-      id="showBots"
-      type="checkbox"
-    />
-  </div>
-  <div
-    class="ml-3 "
-  >
-    <label
-      class="font-medium text-gray-700 dark:text-gray-200"
-      for="showBots"
-      style="text-decoration: line-through;"
-    >
-      Show notifications from Bot accounts
-      <span
-        aria-label="tooltip-showBots"
-        class="relative"
-        id="tooltip-showBots"
-      >
-        <svg
-          aria-hidden="true"
-          class="text-blue-500 ml-1"
-          fill="currentColor"
-          focusable="false"
-          height="16"
-          style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
-          viewBox="0 0 16 16"
-          width="16"
-        >
-          <path
-            d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.92 6.085h.001a.749.749 0 1 1-1.342-.67c.169-.339.436-.701.849-.977C6.845 4.16 7.369 4 8 4a2.756 2.756 0 0 1 1.637.525c.503.377.863.965.863 1.725 0 .448-.115.83-.329 1.15-.205.307-.47.513-.692.662-.109.072-.22.138-.313.195l-.006.004a6.24 6.24 0 0 0-.26.16.952.952 0 0 0-.276.245.75.75 0 0 1-1.248-.832c.184-.264.42-.489.692-.661.103-.067.207-.132.313-.195l.007-.004c.1-.061.182-.11.258-.161a.969.969 0 0 0 .277-.245C8.96 6.514 9 6.427 9 6.25a.612.612 0 0 0-.262-.525A1.27 1.27 0 0 0 8 5.5c-.369 0-.595.09-.74.187a1.01 1.01 0 0 0-.34.398ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
-          />
-        </svg>
-      </span>
-    </label>
-  </div>
-</div>
-`;
-
-exports[`routes/Settings.tsx should render itself & its children 1`] = `
+exports[`routes/Settings.tsx General should render itself & its children 1`] = `
 <div
   class="flex flex-1 flex-col h-screen dark:bg-gray-dark dark:text-white"
   data-testid="settings"
@@ -678,3 +582,99 @@ exports[`routes/Settings.tsx should render itself & its children 1`] = `
   </div>
 </div>
 `;
+
+exports[`routes/Settings.tsx Notifications section should be able to toggle the showBots checkbox when detailedNotifications is enabled 1`] = `
+<div
+  class="flex items-start"
+>
+  <div
+    class="flex items-center h-5"
+  >
+    <input
+      checked=""
+      class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
+      id="showBots"
+      type="checkbox"
+    />
+  </div>
+  <div
+    class="ml-3 "
+  >
+    <label
+      class="font-medium text-gray-700 dark:text-gray-200"
+      for="showBots"
+    >
+      Show notifications from Bot accounts
+      <span
+        aria-label="tooltip-showBots"
+        class="relative"
+        id="tooltip-showBots"
+      >
+        <svg
+          aria-hidden="true"
+          class="text-blue-500 ml-1"
+          fill="currentColor"
+          focusable="false"
+          height="16"
+          style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+          viewBox="0 0 16 16"
+          width="16"
+        >
+          <path
+            d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.92 6.085h.001a.749.749 0 1 1-1.342-.67c.169-.339.436-.701.849-.977C6.845 4.16 7.369 4 8 4a2.756 2.756 0 0 1 1.637.525c.503.377.863.965.863 1.725 0 .448-.115.83-.329 1.15-.205.307-.47.513-.692.662-.109.072-.22.138-.313.195l-.006.004a6.24 6.24 0 0 0-.26.16.952.952 0 0 0-.276.245.75.75 0 0 1-1.248-.832c.184-.264.42-.489.692-.661.103-.067.207-.132.313-.195l.007-.004c.1-.061.182-.11.258-.161a.969.969 0 0 0 .277-.245C8.96 6.514 9 6.427 9 6.25a.612.612 0 0 0-.262-.525A1.27 1.27 0 0 0 8 5.5c-.369 0-.595.09-.74.187a1.01 1.01 0 0 0-.34.398ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
+          />
+        </svg>
+      </span>
+    </label>
+  </div>
+</div>
+`;
+
+exports[`routes/Settings.tsx Notifications section should not be able to toggle the showBots checkbox when detailedNotifications is disabled 1`] = `
+<div
+  class="flex items-start"
+>
+  <div
+    class="flex items-center h-5"
+  >
+    <input
+      checked=""
+      class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
+      disabled=""
+      id="showBots"
+      type="checkbox"
+    />
+  </div>
+  <div
+    class="ml-3 "
+  >
+    <label
+      class="font-medium text-gray-700 dark:text-gray-200"
+      for="showBots"
+      style="text-decoration: line-through;"
+    >
+      Show notifications from Bot accounts
+      <span
+        aria-label="tooltip-showBots"
+        class="relative"
+        id="tooltip-showBots"
+      >
+        <svg
+          aria-hidden="true"
+          class="text-blue-500 ml-1"
+          fill="currentColor"
+          focusable="false"
+          height="16"
+          style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+          viewBox="0 0 16 16"
+          width="16"
+        >
+          <path
+            d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.92 6.085h.001a.749.749 0 1 1-1.342-.67c.169-.339.436-.701.849-.977C6.845 4.16 7.369 4 8 4a2.756 2.756 0 0 1 1.637.525c.503.377.863.965.863 1.725 0 .448-.115.83-.329 1.15-.205.307-.47.513-.692.662-.109.072-.22.138-.313.195l-.006.004a6.24 6.24 0 0 0-.26.16.952.952 0 0 0-.276.245.75.75 0 0 1-1.248-.832c.184-.264.42-.489.692-.661.103-.067.207-.132.313-.195l.007-.004c.1-.061.182-.11.258-.161a.969.969 0 0 0 .277-.245C8.96 6.514 9 6.427 9 6.25a.612.612 0 0 0-.262-.525A1.27 1.27 0 0 0 8 5.5c-.369 0-.595.09-.74.187a1.01 1.01 0 0 0-.34.398ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
+          />
+        </svg>
+      </span>
+    </label>
+  </div>
+</div>
+`;