From 1d10aef7ff6674b2fbd61beb38d801bf7b0a60e2 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 1 Jul 2024 08:18:23 -0400 Subject: [PATCH] refactor: split settings into smaller components (#1320) * refactor: split settings into smaller components * refactor: split settings into smaller components --- .../settings/AppearanceSettings.test.tsx | 111 +++ .../settings/AppearanceSettings.tsx | 111 +++ .../settings/NotificationSettings.test.tsx | 230 +++++++ .../settings/NotificationSettings.tsx | 99 +++ .../settings/SettingsFooter.test.tsx | 148 ++++ src/components/settings/SettingsFooter.tsx | 57 ++ .../settings/SystemSettings.test.tsx | 141 ++++ src/components/settings/SystemSettings.tsx | 66 ++ .../NotificationSettings.test.tsx.snap | 97 +++ .../SettingsFooter.test.tsx.snap | 19 + src/routes/Settings.test.tsx | 646 +----------------- src/routes/Settings.tsx | 326 +-------- .../__snapshots__/Settings.test.tsx.snap | 116 +--- 13 files changed, 1116 insertions(+), 1051 deletions(-) create mode 100644 src/components/settings/AppearanceSettings.test.tsx create mode 100644 src/components/settings/AppearanceSettings.tsx create mode 100644 src/components/settings/NotificationSettings.test.tsx create mode 100644 src/components/settings/NotificationSettings.tsx create mode 100644 src/components/settings/SettingsFooter.test.tsx create mode 100644 src/components/settings/SettingsFooter.tsx create mode 100644 src/components/settings/SystemSettings.test.tsx create mode 100644 src/components/settings/SystemSettings.tsx create mode 100644 src/components/settings/__snapshots__/NotificationSettings.test.tsx.snap create mode 100644 src/components/settings/__snapshots__/SettingsFooter.test.tsx.snap diff --git a/src/components/settings/AppearanceSettings.test.tsx b/src/components/settings/AppearanceSettings.test.tsx new file mode 100644 index 000000000..96cae84b7 --- /dev/null +++ b/src/components/settings/AppearanceSettings.test.tsx @@ -0,0 +1,111 @@ +import { act, fireEvent, render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { mockAuth, mockSettings } from '../../__mocks__/state-mocks'; +import { AppContext } from '../../context/App'; +import { AppearanceSettings } from './AppearanceSettings'; + +describe('routes/components/AppearanceSettings.tsx', () => { + const updateSetting = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should change the theme radio group', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + fireEvent.click(screen.getByLabelText('Light')); + + expect(updateSetting).toHaveBeenCalledTimes(1); + expect(updateSetting).toHaveBeenCalledWith('theme', 'LIGHT'); + }); + + it('should toggle detailed notifications checkbox', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + await screen.findByLabelText('Detailed notifications'); + + fireEvent.click(screen.getByLabelText('Detailed notifications')); + + expect(updateSetting).toHaveBeenCalledTimes(1); + expect(updateSetting).toHaveBeenCalledWith('detailedNotifications', false); + }); + + it('should toggle metric pills checkbox', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + await screen.findByLabelText('Show notification metric pills'); + + fireEvent.click(screen.getByLabelText('Show notification metric pills')); + + expect(updateSetting).toHaveBeenCalledTimes(1); + expect(updateSetting).toHaveBeenCalledWith('showPills', false); + }); + + it('should toggle account hostname checkbox', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + await screen.findByLabelText('Show account hostname'); + + fireEvent.click(screen.getByLabelText('Show account hostname')); + + expect(updateSetting).toHaveBeenCalledTimes(1); + expect(updateSetting).toHaveBeenCalledWith('showAccountHostname', true); + }); +}); diff --git a/src/components/settings/AppearanceSettings.tsx b/src/components/settings/AppearanceSettings.tsx new file mode 100644 index 000000000..e50a818ae --- /dev/null +++ b/src/components/settings/AppearanceSettings.tsx @@ -0,0 +1,111 @@ +import { + CheckIcon, + CommentIcon, + IssueClosedIcon, + MilestoneIcon, + TagIcon, +} from '@primer/octicons-react'; +import { ipcRenderer } from 'electron'; +import { type FC, useContext, useEffect } from 'react'; +import { AppContext } from '../../context/App'; +import { Size, Theme } from '../../types'; +import { setTheme } from '../../utils/theme'; +import { Checkbox } from '../fields/Checkbox'; +import { RadioGroup } from '../fields/RadioGroup'; + +export const AppearanceSettings: FC = () => { + const { settings, updateSetting } = useContext(AppContext); + + useEffect(() => { + ipcRenderer.on('gitify:update-theme', (_, updatedTheme: Theme) => { + if (settings.theme === Theme.SYSTEM) { + setTheme(updatedTheme); + } + }); + }, [settings.theme]); + + return ( +
+ + Appearance + + { + updateSetting('theme', evt.target.value); + }} + /> + + updateSetting('detailedNotifications', evt.target.checked) + } + tooltip={ +
+
+ Enrich notifications with author or last commenter profile + information, state and GitHub-like colors. +
+
+ ⚠️ Users with a large number of unread notifications may{' '} + experience rate limiting under certain circumstances. Disable this + setting if you experience this. +
+
+ } + /> + updateSetting('showPills', evt.target.checked)} + tooltip={ +
+
Show notification metric pills for:
+
+
    +
  • + + linked issues +
  • +
  • + pr reviews +
  • +
  • + + comments +
  • + +
  • + + labels +
  • +
  • + + milestones +
  • +
+
+
+ } + /> + + updateSetting('showAccountHostname', evt.target.checked) + } + /> +
+ ); +}; diff --git a/src/components/settings/NotificationSettings.test.tsx b/src/components/settings/NotificationSettings.test.tsx new file mode 100644 index 000000000..b6ea137c6 --- /dev/null +++ b/src/components/settings/NotificationSettings.test.tsx @@ -0,0 +1,230 @@ +import { act, fireEvent, render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { mockAuth, mockSettings } from '../../__mocks__/state-mocks'; +import { AppContext } from '../../context/App'; +import * as comms from '../../utils/comms'; +import { NotificationSettings } from './NotificationSettings'; + +describe('routes/components/NotificationSettings.tsx', () => { + const updateSetting = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should change the groupBy radio group', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + fireEvent.click(screen.getByLabelText('Date')); + + expect(updateSetting).toHaveBeenCalledTimes(1); + expect(updateSetting).toHaveBeenCalledWith('groupBy', 'DATE'); + }); + it('should toggle the showOnlyParticipating checkbox', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + fireEvent.click(screen.getByLabelText('Show only participating'), { + target: { checked: true }, + }); + + expect(updateSetting).toHaveBeenCalledTimes(1); + expect(updateSetting).toHaveBeenCalledWith('participating', false); + }); + + it('should open official docs for showOnlyParticipating tooltip', async () => { + const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink'); + + await act(async () => { + render( + + + + + , + ); + }); + + const tooltipElement = screen.getByLabelText( + 'tooltip-showOnlyParticipating', + ); + + fireEvent.mouseEnter(tooltipElement); + + fireEvent.click( + screen.getByTitle( + 'Open GitHub documentation for participating and watching notifications', + ), + ); + + expect(openExternalLinkMock).toHaveBeenCalledTimes(1); + expect(openExternalLinkMock).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( + + + + + , + ); + }); + + 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( + + + + + , + ); + }); + + 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 markAsDoneOnOpen checkbox', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + fireEvent.click(screen.getByLabelText('Mark as done on open'), { + target: { checked: true }, + }); + + expect(updateSetting).toHaveBeenCalledTimes(1); + expect(updateSetting).toHaveBeenCalledWith('markAsDoneOnOpen', false); + }); + + it('should toggle the delayNotificationState checkbox', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + fireEvent.click(screen.getByLabelText('Delay notification state'), { + target: { checked: true }, + }); + + expect(updateSetting).toHaveBeenCalledTimes(1); + expect(updateSetting).toHaveBeenCalledWith('delayNotificationState', false); + }); +}); diff --git a/src/components/settings/NotificationSettings.tsx b/src/components/settings/NotificationSettings.tsx new file mode 100644 index 000000000..de33b5c5f --- /dev/null +++ b/src/components/settings/NotificationSettings.tsx @@ -0,0 +1,99 @@ +import { type FC, type MouseEvent, useContext } from 'react'; +import { AppContext } from '../../context/App'; +import { GroupBy } from '../../types'; +import { openGitHubParticipatingDocs } from '../../utils/links'; +import { Checkbox } from '../fields/Checkbox'; +import { RadioGroup } from '../fields/RadioGroup'; + +export const NotificationSettings: FC = () => { + const { settings, updateSetting } = useContext(AppContext); + + return ( +
+ + Notifications + + { + updateSetting('groupBy', evt.target.value); + }} + /> + updateSetting('participating', evt.target.checked)} + tooltip={ +
+ See + + for more details. +
+ } + /> + + settings.detailedNotifications && + updateSetting('showBots', evt.target.checked) + } + disabled={!settings.detailedNotifications} + tooltip={ +
+
+ Show or hide notifications from Bot accounts, such as @dependabot, + @renovatebot, etc +
+
+ ⚠️ This setting requires Detailed Notifications to + be enabled. +
+
+ } + /> + + updateSetting('markAsDoneOnOpen', evt.target.checked) + } + /> + + updateSetting('delayNotificationState', evt.target.checked) + } + tooltip={ +
+ Keep the notification within Gitify window upon interaction (click, + mark as read, mark as done, etc) until the next refresh window + (scheduled or user initiated). +
+ } + /> +
+ ); +}; diff --git a/src/components/settings/SettingsFooter.test.tsx b/src/components/settings/SettingsFooter.test.tsx new file mode 100644 index 000000000..546c75997 --- /dev/null +++ b/src/components/settings/SettingsFooter.test.tsx @@ -0,0 +1,148 @@ +import { act, fireEvent, render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { mockAuth, mockSettings } from '../../__mocks__/state-mocks'; +import { AppContext } from '../../context/App'; +import * as comms from '../../utils/comms'; +import { SettingsFooter } from './SettingsFooter'; + +const mockNavigate = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, +})); + +describe('routes/components/SettingsFooter.tsx', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('app version', () => { + let originalEnv: NodeJS.ProcessEnv; + + beforeEach(() => { + // Save the original node env state + originalEnv = process.env; + }); + + afterEach(() => { + // Restore the original node env state + process.env = originalEnv; + }); + + it('should show production app version', async () => { + process.env = { + ...originalEnv, + NODE_ENV: 'production', + }; + + await act(async () => { + render( + + + + + , + ); + }); + + expect(screen.getByTitle('app-version')).toMatchSnapshot(); + }); + + it('should show development app version', async () => { + process.env = { + ...originalEnv, + NODE_ENV: 'development', + }; + + await act(async () => { + render( + + + + + , + ); + }); + + expect(screen.getByTitle('app-version')).toMatchSnapshot(); + }); + }); + + it('should open release notes', async () => { + const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink'); + + await act(async () => { + render( + + + + + , + ); + }); + + fireEvent.click(screen.getByTitle('View release notes')); + + expect(openExternalLinkMock).toHaveBeenCalledTimes(1); + expect(openExternalLinkMock).toHaveBeenCalledWith( + 'https://github.com/gitify-app/gitify/releases/tag/v0.0.1', + ); + }); + + it('should open account management', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + fireEvent.click(screen.getByTitle('Accounts')); + expect(mockNavigate).toHaveBeenCalledWith('/accounts'); + }); + + it('should quit the app', async () => { + const quitAppMock = jest.spyOn(comms, 'quitApp'); + + await act(async () => { + render( + + + + + , + ); + }); + + fireEvent.click(screen.getByTitle('Quit Gitify')); + expect(quitAppMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/settings/SettingsFooter.tsx b/src/components/settings/SettingsFooter.tsx new file mode 100644 index 000000000..54d4c4708 --- /dev/null +++ b/src/components/settings/SettingsFooter.tsx @@ -0,0 +1,57 @@ +import { PersonIcon, XCircleIcon } from '@primer/octicons-react'; +import { type FC, useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { BUTTON_CLASS_NAME } from '../../styles/gitify'; +import { Size } from '../../types'; +import { getAppVersion, quitApp } from '../../utils/comms'; +import { openGitifyReleaseNotes } from '../../utils/links'; + +export const SettingsFooter: FC = () => { + const [appVersion, setAppVersion] = useState(null); + const navigate = useNavigate(); + + useEffect(() => { + (async () => { + if (process.env.NODE_ENV === 'development') { + setAppVersion('dev'); + } else { + const result = await getAppVersion(); + setAppVersion(`v${result}`); + } + })(); + }, []); + + return ( +
+ +
+ + + +
+
+ ); +}; diff --git a/src/components/settings/SystemSettings.test.tsx b/src/components/settings/SystemSettings.test.tsx new file mode 100644 index 000000000..3ca7c6df0 --- /dev/null +++ b/src/components/settings/SystemSettings.test.tsx @@ -0,0 +1,141 @@ +import { act, fireEvent, render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { mockAuth, mockSettings } from '../../__mocks__/state-mocks'; +import { AppContext } from '../../context/App'; +import { SystemSettings } from './SystemSettings'; + +describe('routes/components/SystemSettings.tsx', () => { + const updateSetting = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should toggle the keyboardShortcut checkbox', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + fireEvent.click(screen.getByLabelText('Enable keyboard shortcut'), { + target: { checked: true }, + }); + + expect(updateSetting).toHaveBeenCalledTimes(1); + expect(updateSetting).toHaveBeenCalledWith('keyboardShortcut', false); + }); + + it('should toggle the showNotificationsCountInTray checkbox', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + fireEvent.click(screen.getByLabelText('Show notifications count in tray'), { + target: { checked: true }, + }); + + expect(updateSetting).toHaveBeenCalledTimes(1); + expect(updateSetting).toHaveBeenCalledWith( + 'showNotificationsCountInTray', + false, + ); + }); + + it('should toggle the showNotifications checkbox', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + fireEvent.click(screen.getByLabelText('Show system notifications'), { + target: { checked: true }, + }); + + expect(updateSetting).toHaveBeenCalledTimes(1); + expect(updateSetting).toHaveBeenCalledWith('showNotifications', false); + }); + + it('should toggle the playSound checkbox', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + fireEvent.click(screen.getByLabelText('Play sound'), { + target: { checked: true }, + }); + + expect(updateSetting).toHaveBeenCalledTimes(1); + expect(updateSetting).toHaveBeenCalledWith('playSound', false); + }); + + it('should toggle the openAtStartup checkbox', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + fireEvent.click(screen.getByLabelText('Open at startup'), { + target: { checked: true }, + }); + + expect(updateSetting).toHaveBeenCalledTimes(1); + expect(updateSetting).toHaveBeenCalledWith('openAtStartup', false); + }); +}); diff --git a/src/components/settings/SystemSettings.tsx b/src/components/settings/SystemSettings.tsx new file mode 100644 index 000000000..44e7a9363 --- /dev/null +++ b/src/components/settings/SystemSettings.tsx @@ -0,0 +1,66 @@ +import { type FC, useContext } from 'react'; +import { AppContext } from '../../context/App'; +import Constants from '../../utils/constants'; +import { isLinux, isMacOS } from '../../utils/platform'; +import { Checkbox } from '../fields/Checkbox'; + +export const SystemSettings: FC = () => { + const { settings, updateSetting } = useContext(AppContext); + + return ( +
+ + System + + + updateSetting('keyboardShortcut', evt.target.checked) + } + tooltip={ +
+ When enabled you can use the hotkeys{' '} + + {Constants.DEFAULT_KEYBOARD_SHORTCUT} + {' '} + to show or hide Gitify. +
+ } + /> + {isMacOS() && ( + + updateSetting('showNotificationsCountInTray', evt.target.checked) + } + /> + )} + + updateSetting('showNotifications', evt.target.checked) + } + /> + updateSetting('playSound', evt.target.checked)} + /> + {!isLinux() && ( + updateSetting('openAtStartup', evt.target.checked)} + /> + )} +
+ ); +}; diff --git a/src/components/settings/__snapshots__/NotificationSettings.test.tsx.snap b/src/components/settings/__snapshots__/NotificationSettings.test.tsx.snap new file mode 100644 index 000000000..ec5007e7e --- /dev/null +++ b/src/components/settings/__snapshots__/NotificationSettings.test.tsx.snap @@ -0,0 +1,97 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`routes/components/NotificationSettings.tsx should be able to toggle the showBots checkbox when detailedNotifications is enabled 1`] = ` +
+
+ +
+
+ +
+
+`; + +exports[`routes/components/NotificationSettings.tsx should not be able to toggle the showBots checkbox when detailedNotifications is disabled 1`] = ` +
+
+ +
+
+ +
+
+`; diff --git a/src/components/settings/__snapshots__/SettingsFooter.test.tsx.snap b/src/components/settings/__snapshots__/SettingsFooter.test.tsx.snap new file mode 100644 index 000000000..75e06fc2a --- /dev/null +++ b/src/components/settings/__snapshots__/SettingsFooter.test.tsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`routes/components/SettingsFooter.tsx app version should show development app version 1`] = ` + + Gitify + dev + +`; + +exports[`routes/components/SettingsFooter.tsx app version should show production app version 1`] = ` + + Gitify + v0.0.1 + +`; diff --git a/src/routes/Settings.test.tsx b/src/routes/Settings.test.tsx index 333cc184e..962984fa6 100644 --- a/src/routes/Settings.test.tsx +++ b/src/routes/Settings.test.tsx @@ -3,7 +3,6 @@ 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(); @@ -14,7 +13,6 @@ jest.mock('react-router-dom', () => ({ describe('routes/Settings.tsx', () => { let originalPlatform: NodeJS.Platform; - const updateSetting = jest.fn(); const fetchNotifications = jest.fn(); beforeAll(() => { @@ -39,631 +37,39 @@ describe('routes/Settings.tsx', () => { }); }); - describe('General', () => { - it('should render itself & its children', async () => { - await act(async () => { - render( - - - - - , - ); - }); - - expect(screen.getByTestId('settings')).toMatchSnapshot(); - }); - - it('should go back by pressing the icon', async () => { - await act(async () => { - render( - - - - - , - ); - }); - - fireEvent.click(screen.getByLabelText('Go Back')); - expect(fetchNotifications).toHaveBeenCalledTimes(1); - expect(mockNavigate).toHaveBeenNthCalledWith(1, -1); - }); - }); - describe('Appearance section', () => { - it('should change the theme radio group', async () => { - await act(async () => { - render( - - - - - , - ); - }); - - fireEvent.click(screen.getByLabelText('Light')); - - expect(updateSetting).toHaveBeenCalledTimes(1); - expect(updateSetting).toHaveBeenCalledWith('theme', 'LIGHT'); - }); - - it('should toggle detailed notifications checkbox', async () => { - await act(async () => { - render( - - - - - , - ); - }); - - await screen.findByLabelText('Detailed notifications'); - - fireEvent.click(screen.getByLabelText('Detailed notifications')); - - expect(updateSetting).toHaveBeenCalledTimes(1); - expect(updateSetting).toHaveBeenCalledWith( - 'detailedNotifications', - false, + it('should render itself & its children', async () => { + await act(async () => { + render( + + + + + , ); }); - it('should toggle metric pills checkbox', async () => { - await act(async () => { - render( - - - - - , - ); - }); - - await screen.findByLabelText('Show notification metric pills'); - - fireEvent.click(screen.getByLabelText('Show notification metric pills')); - - expect(updateSetting).toHaveBeenCalledTimes(1); - expect(updateSetting).toHaveBeenCalledWith('showPills', false); - }); - - it('should toggle account hostname checkbox', async () => { - await act(async () => { - render( - - - - - , - ); - }); - - await screen.findByLabelText('Show account hostname'); - - fireEvent.click(screen.getByLabelText('Show account hostname')); - - expect(updateSetting).toHaveBeenCalledTimes(1); - expect(updateSetting).toHaveBeenCalledWith('showAccountHostname', true); - }); + expect(screen.getByTestId('settings')).toMatchSnapshot(); }); - describe('Notifications section', () => { - it('should change the groupBy radio group', async () => { - await act(async () => { - render( - - - - - , - ); - }); - - fireEvent.click(screen.getByLabelText('Date')); - - expect(updateSetting).toHaveBeenCalledTimes(1); - expect(updateSetting).toHaveBeenCalledWith('groupBy', 'DATE'); - }); - it('should toggle the showOnlyParticipating checkbox', async () => { - await act(async () => { - render( - - - - - , - ); - }); - - fireEvent.click(screen.getByLabelText('Show only participating'), { - target: { checked: true }, - }); - - expect(updateSetting).toHaveBeenCalledTimes(1); - expect(updateSetting).toHaveBeenCalledWith('participating', false); - }); - - it('should open official docs for showOnlyParticipating tooltip', async () => { - const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink'); - - await act(async () => { - render( - - - - - , - ); - }); - - const tooltipElement = screen.getByLabelText( - 'tooltip-showOnlyParticipating', - ); - - fireEvent.mouseEnter(tooltipElement); - - fireEvent.click( - screen.getByTitle( - 'Open GitHub documentation for participating and watching notifications', - ), - ); - - expect(openExternalLinkMock).toHaveBeenCalledTimes(1); - expect(openExternalLinkMock).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( - - - - - , - ); - }); - - 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'), + it('should go back by pressing the icon', async () => { + await act(async () => { + render( + + + + + , ); - - // 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( - - - - - , - ); - }); - - 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 markAsDoneOnOpen checkbox', async () => { - await act(async () => { - render( - - - - - , - ); - }); - - fireEvent.click(screen.getByLabelText('Mark as done on open'), { - target: { checked: true }, - }); - - expect(updateSetting).toHaveBeenCalledTimes(1); - expect(updateSetting).toHaveBeenCalledWith('markAsDoneOnOpen', false); - }); - - it('should toggle the delayNotificationState checkbox', async () => { - await act(async () => { - render( - - - - - , - ); - }); - - fireEvent.click(screen.getByLabelText('Delay notification state'), { - target: { checked: true }, - }); - - expect(updateSetting).toHaveBeenCalledTimes(1); - expect(updateSetting).toHaveBeenCalledWith( - 'delayNotificationState', - false, - ); - }); - }); - - describe('System section', () => { - it('should toggle the keyboardShortcut checkbox', async () => { - await act(async () => { - render( - - - - - , - ); - }); - - fireEvent.click(screen.getByLabelText('Enable keyboard shortcut'), { - target: { checked: true }, - }); - - expect(updateSetting).toHaveBeenCalledTimes(1); - expect(updateSetting).toHaveBeenCalledWith('keyboardShortcut', false); - }); - - it('should toggle the showNotificationsCountInTray checkbox', async () => { - await act(async () => { - render( - - - - - , - ); - }); - - fireEvent.click( - screen.getByLabelText('Show notifications count in tray'), - { - target: { checked: true }, - }, - ); - - expect(updateSetting).toHaveBeenCalledTimes(1); - expect(updateSetting).toHaveBeenCalledWith( - 'showNotificationsCountInTray', - false, - ); - }); - - it('should toggle the showNotifications checkbox', async () => { - await act(async () => { - render( - - - - - , - ); - }); - - fireEvent.click(screen.getByLabelText('Show system notifications'), { - target: { checked: true }, - }); - - expect(updateSetting).toHaveBeenCalledTimes(1); - expect(updateSetting).toHaveBeenCalledWith('showNotifications', false); - }); - - it('should toggle the playSound checkbox', async () => { - await act(async () => { - render( - - - - - , - ); - }); - - fireEvent.click(screen.getByLabelText('Play sound'), { - target: { checked: true }, - }); - - expect(updateSetting).toHaveBeenCalledTimes(1); - expect(updateSetting).toHaveBeenCalledWith('playSound', false); - }); - - it('should toggle the openAtStartup checkbox', async () => { - await act(async () => { - render( - - - - - , - ); - }); - - fireEvent.click(screen.getByLabelText('Open at startup'), { - target: { checked: true }, - }); - - expect(updateSetting).toHaveBeenCalledTimes(1); - expect(updateSetting).toHaveBeenCalledWith('openAtStartup', false); - }); - }); - - describe('Footer section', () => { - describe('app version', () => { - let originalEnv: NodeJS.ProcessEnv; - - beforeEach(() => { - // Save the original node env state - originalEnv = process.env; - }); - - afterEach(() => { - // Restore the original node env state - process.env = originalEnv; - }); - - it('should show production app version', async () => { - process.env = { - ...originalEnv, - NODE_ENV: 'production', - }; - - await act(async () => { - render( - - - - - , - ); - }); - - expect(screen.getByTitle('app-version')).toMatchSnapshot(); - }); - - it('should show development app version', async () => { - process.env = { - ...originalEnv, - NODE_ENV: 'development', - }; - - await act(async () => { - render( - - - - - , - ); - }); - - expect(screen.getByTitle('app-version')).toMatchSnapshot(); - }); - }); - - it('should open release notes', async () => { - const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink'); - - await act(async () => { - render( - - - - - , - ); - }); - - fireEvent.click(screen.getByTitle('View release notes')); - - expect(openExternalLinkMock).toHaveBeenCalledTimes(1); - expect(openExternalLinkMock).toHaveBeenCalledWith( - 'https://github.com/gitify-app/gitify/releases/tag/v0.0.1', - ); - }); - - it('should open account management', async () => { - await act(async () => { - render( - - - - - , - ); - }); - - fireEvent.click(screen.getByTitle('Accounts')); - expect(mockNavigate).toHaveBeenCalledWith('/accounts'); - }); - - it('should quit the app', async () => { - const quitAppMock = jest.spyOn(comms, 'quitApp'); - - await act(async () => { - render( - - - - - , - ); - }); - - fireEvent.click(screen.getByTitle('Quit Gitify')); - expect(quitAppMock).toHaveBeenCalledTimes(1); - }); + fireEvent.click(screen.getByLabelText('Go Back')); + expect(fetchNotifications).toHaveBeenCalledTimes(1); + expect(mockNavigate).toHaveBeenNthCalledWith(1, -1); }); }); diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx index fada57163..878a6e1d7 100644 --- a/src/routes/Settings.tsx +++ b/src/routes/Settings.tsx @@ -1,328 +1,22 @@ -import { - CheckIcon, - CommentIcon, - IssueClosedIcon, - MilestoneIcon, - PersonIcon, - TagIcon, - XCircleIcon, -} from '@primer/octicons-react'; -import { ipcRenderer } from 'electron'; -import { - type FC, - type MouseEvent, - useContext, - useEffect, - useState, -} from 'react'; -import { useNavigate } from 'react-router-dom'; +import type { FC } from 'react'; import { Header } from '../components/Header'; -import { Checkbox } from '../components/fields/Checkbox'; -import { RadioGroup } from '../components/fields/RadioGroup'; -import { AppContext } from '../context/App'; -import { BUTTON_CLASS_NAME } from '../styles/gitify'; -import { GroupBy, Size, Theme } from '../types'; -import { getAppVersion, quitApp } from '../utils/comms'; -import Constants from '../utils/constants'; -import { - openGitHubParticipatingDocs, - openGitifyReleaseNotes, -} from '../utils/links'; -import { isLinux, isMacOS } from '../utils/platform'; -import { setTheme } from '../utils/theme'; +import { AppearanceSettings } from '../components/settings/AppearanceSettings'; +import { NotificationSettings } from '../components/settings/NotificationSettings'; +import { SettingsFooter } from '../components/settings/SettingsFooter'; +import { SystemSettings } from '../components/settings/SystemSettings'; export const SettingsRoute: FC = () => { - const { settings, updateSetting } = useContext(AppContext); - const navigate = useNavigate(); - - const [appVersion, setAppVersion] = useState(null); - - useEffect(() => { - (async () => { - if (process.env.NODE_ENV === 'development') { - setAppVersion('dev'); - } else { - const result = await getAppVersion(); - setAppVersion(`v${result}`); - } - })(); - - ipcRenderer.on('gitify:update-theme', (_, updatedTheme: Theme) => { - if (settings.theme === Theme.SYSTEM) { - setTheme(updatedTheme); - } - }); - }, [settings.theme]); - return (
Settings
-
-
- - Appearance - - { - updateSetting('theme', evt.target.value); - }} - /> - - updateSetting('detailedNotifications', evt.target.checked) - } - tooltip={ -
-
- Enrich notifications with author or last commenter profile - information, state and GitHub-like colors. -
-
- ⚠️ Users with a large number of unread notifications may{' '} - experience rate limiting under certain circumstances. Disable - this setting if you experience this. -
-
- } - /> - updateSetting('showPills', evt.target.checked)} - tooltip={ -
-
Show notification metric pills for:
-
-
    -
  • - - linked issues -
  • -
  • - pr - reviews -
  • -
  • - - comments -
  • -
  • - - labels -
  • -
  • - - milestones -
  • -
-
-
- } - /> - - updateSetting('showAccountHostname', evt.target.checked) - } - /> -
- -
- - Notifications - - { - updateSetting('groupBy', evt.target.value); - }} - /> - - updateSetting('participating', evt.target.checked) - } - tooltip={ -
- See - - for more details. -
- } - /> - - settings.detailedNotifications && - updateSetting('showBots', evt.target.checked) - } - disabled={!settings.detailedNotifications} - tooltip={ -
-
- Show or hide notifications from Bot accounts, such as - @dependabot, @renovatebot, etc -
-
- ⚠️ This setting requires{' '} - Detailed Notifications to be enabled. -
-
- } - /> - - updateSetting('markAsDoneOnOpen', evt.target.checked) - } - /> - - updateSetting('delayNotificationState', evt.target.checked) - } - tooltip={ -
- Keep the notification within Gitify window upon interaction - (click, mark as read, mark as done, etc) until the next refresh - window (scheduled or user initiated). -
- } - /> -
- -
- - System - - - updateSetting('keyboardShortcut', evt.target.checked) - } - tooltip={ -
- When enabled you can use the hotkeys{' '} - - {Constants.DEFAULT_KEYBOARD_SHORTCUT} - {' '} - to show or hide Gitify. -
- } - /> - {isMacOS() && ( - - updateSetting( - 'showNotificationsCountInTray', - evt.target.checked, - ) - } - /> - )} - - updateSetting('showNotifications', evt.target.checked) - } - /> - updateSetting('playSound', evt.target.checked)} - /> - {!isLinux() && ( - - updateSetting('openAtStartup', evt.target.checked) - } - /> - )} -
+
+ + +
-
- -
- - - -
-
+
); }; diff --git a/src/routes/__snapshots__/Settings.test.tsx.snap b/src/routes/__snapshots__/Settings.test.tsx.snap index 7dc308869..d5d0b6cff 100644 --- a/src/routes/__snapshots__/Settings.test.tsx.snap +++ b/src/routes/__snapshots__/Settings.test.tsx.snap @@ -1,24 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`routes/Settings.tsx Footer section app version should show development app version 1`] = ` - - Gitify - dev - -`; - -exports[`routes/Settings.tsx Footer section app version should show production app version 1`] = ` - - Gitify - v0.0.1 - -`; - -exports[`routes/Settings.tsx General should render itself & its children 1`] = ` +exports[`routes/Settings.tsx should render itself & its children 1`] = `
`; - -exports[`routes/Settings.tsx Notifications section should be able to toggle the showBots checkbox when detailedNotifications is enabled 1`] = ` -
-
- -
-
- -
-
-`; - -exports[`routes/Settings.tsx Notifications section should not be able to toggle the showBots checkbox when detailedNotifications is disabled 1`] = ` -
-
- -
-
- -
-
-`;