@@ -13,7 +13,7 @@ exports[`renderer/components/layout/Contents.tsx > should render itself & its ch
exports[`renderer/components/layout/Contents.tsx > should render with scroll fade 1`] = `
Test
diff --git a/src/renderer/components/settings/AlertSettings.test.tsx b/src/renderer/components/settings/AlertSettings.test.tsx
index e75be63fd..760d0ce53 100644
--- a/src/renderer/components/settings/AlertSettings.test.tsx
+++ b/src/renderer/components/settings/AlertSettings.test.tsx
@@ -1,4 +1,4 @@
-import { act, screen } from '@testing-library/react';
+import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithProviders } from '../../__helpers__/test-utils';
@@ -20,79 +20,75 @@ describe('renderer/components/settings/AlertSettings.tsx', () => {
});
it.each([
- ['checkbox-showNotifications', 'showNotifications'],
- ['checkbox-playSound', 'playSound'],
- ['checkbox-showUpdateNotifications', 'showUpdateNotifications'],
- ] as const)('should toggle %s checkbox', async (testId, setting) => {
- await act(async () => {
- renderWithProviders(
);
- });
+ ['Show desktop notifications', 'showNotifications'],
+ ['Play notification sound', 'playSound'],
+ ['Show app update notifications', 'showUpdateNotifications'],
+ ] as const)('should toggle %s switch', async (label, setting) => {
+ renderWithProviders(
);
- await userEvent.click(screen.getByTestId(testId));
+ await userEvent.click(screen.getByRole('button', { name: label }));
expect(toggleSettingSpy).toHaveBeenCalledTimes(1);
expect(toggleSettingSpy).toHaveBeenCalledWith(setting);
});
- describe('playSound', () => {
- it('should explain when the sound plays and what the volume controls do', async () => {
- renderWithProviders(
);
+ it('shows detailed help in tooltips', async () => {
+ renderWithProviders(
);
- const soundHelp =
- 'Play a sound when new unread notifications arrive after filtering. Use the adjacent volume controls to adjust the sound level.';
+ await userEvent.click(screen.getByTestId('tooltip-icon-tooltip-playSound'));
- const tooltipTrigger = screen.getByRole('button', { name: 'tooltip-playSound' });
+ expect(
+ screen.getByText(
+ 'Play a sound when new unread notifications arrive after filtering. Use the notification volume controls to adjust the sound level.',
+ ),
+ ).toBeInTheDocument();
+ });
- expect(screen.queryByText(soundHelp)).not.toBeInTheDocument();
+ it('shows notification volume and its tooltip when sound is enabled', async () => {
+ renderWithProviders(
);
- await userEvent.click(tooltipTrigger);
+ expect(screen.getByText('Notification volume')).toBeInTheDocument();
+ expect(screen.getByTestId('reveal-volume')).toHaveAttribute('data-state', 'open');
- expect(screen.getByText(soundHelp)).toBeInTheDocument();
- });
+ await userEvent.click(screen.getByTestId('tooltip-icon-tooltip-notificationVolume'));
- it('volume controls should not be shown if playSound checkbox is false', async () => {
- renderWithProviders(
, {
- settings: { ...mockSettings, playSound: false },
- });
+ expect(screen.getByText('Adjust the volume of the notification sound.')).toBeInTheDocument();
+ });
- expect(screen.queryByTestId('settings-volume-group')).not.toBeInTheDocument();
+ it('hides volume controls when playSound is false', () => {
+ renderWithProviders(
, {
+ settings: { ...mockSettings, playSound: false },
});
- it('volume controls should be shown if playSound checkbox is true', async () => {
- renderWithProviders(
, {
- settings: { ...mockSettings, playSound: true },
- });
+ expect(screen.getByTestId('reveal-volume')).toHaveAttribute('data-state', 'closed');
+ });
- expect(screen.getByTestId('settings-volume-group')).toBeInTheDocument();
- });
+ it('should increase notification volume', async () => {
+ renderWithProviders(
);
- it('should increase notification volume', async () => {
- renderWithProviders(
);
+ await userEvent.click(screen.getByTestId('settings-volume-up'));
- await userEvent.click(screen.getByTestId('settings-volume-up'));
+ expect(updateSettingSpy).toHaveBeenCalledTimes(1);
+ expect(updateSettingSpy).toHaveBeenCalledWith('notificationVolume', 30);
+ });
- expect(updateSettingSpy).toHaveBeenCalledTimes(1);
- expect(updateSettingSpy).toHaveBeenCalledWith('notificationVolume', 30);
- });
+ it('should decrease notification volume', async () => {
+ renderWithProviders(
);
- it('should decrease notification volume', async () => {
- renderWithProviders(
);
+ await userEvent.click(screen.getByTestId('settings-volume-down'));
- await userEvent.click(screen.getByTestId('settings-volume-down'));
+ expect(updateSettingSpy).toHaveBeenCalledTimes(1);
+ expect(updateSettingSpy).toHaveBeenCalledWith('notificationVolume', 10);
+ });
- expect(updateSettingSpy).toHaveBeenCalledTimes(1);
- expect(updateSettingSpy).toHaveBeenCalledWith('notificationVolume', 10);
+ it('should reset notification volume', async () => {
+ renderWithProviders(
, {
+ settings: { ...mockSettings, notificationVolume: 30 as Percentage },
});
- it('should reset notification volume', async () => {
- renderWithProviders(
, {
- settings: { ...mockSettings, notificationVolume: 30 as Percentage },
- });
+ await userEvent.click(screen.getByTestId('settings-volume-reset'));
- await userEvent.click(screen.getByTestId('settings-volume-reset'));
-
- expect(updateSettingSpy).toHaveBeenCalledTimes(1);
- expect(updateSettingSpy).toHaveBeenCalledWith('notificationVolume', 20);
- });
+ expect(updateSettingSpy).toHaveBeenCalledTimes(1);
+ expect(updateSettingSpy).toHaveBeenCalledWith('notificationVolume', 20);
});
});
diff --git a/src/renderer/components/settings/AlertSettings.tsx b/src/renderer/components/settings/AlertSettings.tsx
index b49381303..4b725b10d 100644
--- a/src/renderer/components/settings/AlertSettings.tsx
+++ b/src/renderer/components/settings/AlertSettings.tsx
@@ -1,11 +1,10 @@
import { BellIcon, SyncIcon } from '@primer/octicons-react';
-import { Button, ButtonGroup, IconButton, Stack, Text } from '@primer/react';
+import { Button, ButtonGroup, IconButton } from '@primer/react';
import { APPLICATION } from '../../../shared/constants';
import { DEFAULT_SETTINGS_STATE, useSettingsStore } from '../../stores';
-import { Checkbox } from '../fields/Checkbox';
import { SectionHeading } from '../primitives/SectionHeading';
import { VolumeDownIcon } from '../../icons/VolumeDownIcon';
@@ -16,6 +15,10 @@ import {
decreaseVolume,
increaseVolume,
} from '../../utils/ui/volume';
+import { SettingsList } from './controls/SettingsList';
+import { SettingsReveal } from './controls/SettingsReveal';
+import { SettingsRow } from './controls/SettingsRow';
+import { SettingsToggle } from './controls/SettingsToggle';
const defaultSettings = DEFAULT_SETTINGS_STATE;
@@ -34,33 +37,30 @@ export function AlertSettings() {
Alerts
-
-
+ toggleSetting('showNotifications')}
- tooltip={
- Display native operating system notifications for new unread notifications.
- }
+ tooltip="Display native operating system notifications for new unread notifications."
/>
-
- toggleSetting('playSound')}
- tooltip={
-
- Play a sound when new unread notifications arrive after filtering. Use the adjacent
- volume controls to adjust the sound level.
-
- }
- />
+ toggleSetting('playSound')}
+ tooltip="Play a sound when new unread notifications arrive after filtering. Use the notification volume controls to adjust the sound level."
+ />
- {playSound && (
-
+
+
+
- )}
-
+
+
- toggleSetting('showUpdateNotifications')}
- tooltip={
-
- Show a notification when a {APPLICATION.NAME} update is ready. Updates will still be
- checked and shown in the menu bar.
-
- }
+ tooltip={`Show a notification when a ${APPLICATION.NAME} update is ready. Updates are still checked and shown in the menu bar.`}
/>
-
+
);
}
diff --git a/src/renderer/components/settings/AppearanceSettings.test.tsx b/src/renderer/components/settings/AppearanceSettings.test.tsx
index be0e6e1e3..15717342f 100644
--- a/src/renderer/components/settings/AppearanceSettings.test.tsx
+++ b/src/renderer/components/settings/AppearanceSettings.test.tsx
@@ -24,7 +24,7 @@ describe('renderer/components/settings/AppearanceSettings.tsx', () => {
renderWithProviders(
);
});
- await userEvent.selectOptions(screen.getByTestId('settings-theme'), 'LIGHT');
+ await userEvent.selectOptions(screen.getByRole('combobox', { name: 'Theme' }), 'LIGHT');
expect(updateSettingSpy).toHaveBeenCalledTimes(1);
expect(updateSettingSpy).toHaveBeenCalledWith('theme', 'LIGHT');
@@ -35,7 +35,7 @@ describe('renderer/components/settings/AppearanceSettings.tsx', () => {
renderWithProviders(
);
});
- await userEvent.selectOptions(screen.getByTestId('settings-design-language'), 'glass');
+ await userEvent.click(screen.getByTestId('choice-designLanguage-glass'));
expect(updateSettingSpy).toHaveBeenCalledTimes(1);
expect(updateSettingSpy).toHaveBeenCalledWith('designLanguage', DesignLanguage.GLASS);
@@ -85,7 +85,7 @@ describe('renderer/components/settings/AppearanceSettings.tsx', () => {
renderWithProviders(
);
});
- await userEvent.click(screen.getByTestId('checkbox-increaseContrast'));
+ await userEvent.click(screen.getByRole('button', { name: 'Increase contrast' }));
expect(toggleSettingSpy).toHaveBeenCalledTimes(1);
expect(toggleSettingSpy).toHaveBeenCalledWith('increaseContrast');
@@ -98,7 +98,7 @@ describe('renderer/components/settings/AppearanceSettings.tsx', () => {
});
});
- expect(screen.queryByTestId('checkbox-increaseContrast')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('switch-increaseContrast')).not.toBeInTheDocument();
});
it('should toggle the status icon colors checkbox for Glass', async () => {
@@ -108,7 +108,7 @@ describe('renderer/components/settings/AppearanceSettings.tsx', () => {
});
});
- await userEvent.click(screen.getByTestId('checkbox-showStatusIconColors'));
+ await userEvent.click(screen.getByRole('button', { name: 'Show status icon colors' }));
expect(toggleSettingSpy).toHaveBeenCalledTimes(1);
expect(toggleSettingSpy).toHaveBeenCalledWith('showStatusIconColors');
@@ -119,29 +119,33 @@ describe('renderer/components/settings/AppearanceSettings.tsx', () => {
renderWithProviders(
);
});
- expect(screen.queryByTestId('checkbox-showStatusIconColors')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('switch-showStatusIconColors')).not.toBeInTheDocument();
});
- it.each([
- [
- 'tooltip-icon-tooltip-design-language',
- 'Choose Classic for the original Gitify layout, or Glass for the translucent, layered design language.',
- ],
- [
- 'tooltip-icon-tooltip-theme',
- 'Choose a light, dark, or system color mode. Available options depend on the selected design language.',
- ],
- [
- 'tooltip-icon-tooltip-zoom',
- 'Change the size of the Gitify interface. The setting applies immediately and is remembered for the next launch.',
- ],
- ])('should show help for %s', async (testId, help) => {
+ it('shows detailed help in the tooltips', async () => {
await act(async () => {
renderWithProviders(
);
});
- await userEvent.click(screen.getByTestId(testId));
-
- expect(screen.getByText(help)).toBeInTheDocument();
+ await userEvent.click(screen.getByTestId('tooltip-icon-tooltip-designLanguage'));
+ expect(
+ screen.getByText(
+ 'Choose Classic for the original Gitify layout, or Glass for the translucent, layered design language.',
+ ),
+ ).toBeInTheDocument();
+
+ await userEvent.click(screen.getByTestId('tooltip-icon-tooltip-theme'));
+ expect(
+ screen.getByText(
+ 'Choose a light, dark, or system color mode. Available options depend on the selected design language.',
+ ),
+ ).toBeInTheDocument();
+
+ await userEvent.click(screen.getByTestId('tooltip-icon-tooltip-zoom'));
+ expect(
+ screen.getByText(
+ 'Change the size of the Gitify interface. The setting applies immediately and is remembered for the next launch.',
+ ),
+ ).toBeInTheDocument();
});
});
diff --git a/src/renderer/components/settings/AppearanceSettings.tsx b/src/renderer/components/settings/AppearanceSettings.tsx
index 9da215fd7..31ac68187 100644
--- a/src/renderer/components/settings/AppearanceSettings.tsx
+++ b/src/renderer/components/settings/AppearanceSettings.tsx
@@ -1,12 +1,10 @@
import { PaintbrushIcon, SyncIcon, ZoomInIcon, ZoomOutIcon } from '@primer/octicons-react';
-import { Button, ButtonGroup, IconButton, Select, Stack, Text } from '@primer/react';
+import { Button, ButtonGroup, IconButton, Select } from '@primer/react';
import { APPLICATION } from '../../../shared/constants';
import { useSettingsStore } from '../../stores';
-import { Checkbox } from '../fields/Checkbox';
-import { FieldLabel } from '../fields/FieldLabel';
import { SectionHeading } from '../primitives/SectionHeading';
import { DesignLanguage, Theme } from '../../types';
@@ -19,6 +17,10 @@ import {
increaseZoom,
resetZoomLevel,
} from '../../utils/ui/zoom';
+import { SettingsChoice } from './controls/SettingsChoice';
+import { SettingsList } from './controls/SettingsList';
+import { SettingsRow } from './controls/SettingsRow';
+import { SettingsToggle } from './controls/SettingsToggle';
export function AppearanceSettings() {
// Setting store actions
@@ -40,112 +42,80 @@ export function AppearanceSettings() {
Appearance
-
-
-
- Choose Classic for the original {APPLICATION.NAME} layout, or Glass for the
- translucent, layered design language.
-
- }
- />
- updateSetting('designLanguage', evt.target.value as DesignLanguage)}
- value={designLanguage}
- >
- Classic
- Glass
-
-
-
-
-
- Choose a light, dark, or system color mode. Available options depend on the selected
- design language.
-
- }
- />
- updateSetting('theme', evt.target.value as Theme)}
- value={resolveColorMode(designLanguage, theme)}
- >
-
- System
-
-
- Light default
- {colorModeSupported(Theme.LIGHT_COLORBLIND) && (
- Light colorblind
- )}
- {colorModeSupported(Theme.LIGHT_TRITANOPIA) && (
- Light Tritanopia
- )}
-
-
- Dark default
- {colorModeSupported(Theme.DARK_COLORBLIND) && (
- Dark colorblind
- )}
- {colorModeSupported(Theme.DARK_TRITANOPIA) && (
- Dark Tritanopia
- )}
- {colorModeSupported(Theme.DARK_DIMMED) && (
- Soft dark
- )}
-
-
-
-
-
+ updateSetting('designLanguage', value as DesignLanguage)}
+ options={[
+ { label: 'Classic', value: DesignLanguage.CLASSIC },
+ { label: 'Glass', value: DesignLanguage.GLASS },
+ ]}
+ tooltip={`Choose Classic for the original ${APPLICATION.NAME} layout, or Glass for the translucent, layered design language.`}
+ value={designLanguage}
+ />
+
+ updateSetting('theme', value as Theme)}
+ presentation="select"
+ selectChildren={
+ <>
+
+ System
+
+
+ Light default
+ {colorModeSupported(Theme.LIGHT_COLORBLIND) && (
+ Light colorblind
+ )}
+ {colorModeSupported(Theme.LIGHT_TRITANOPIA) && (
+ Light Tritanopia
+ )}
+
+
+ Dark default
+ {colorModeSupported(Theme.DARK_COLORBLIND) && (
+ Dark colorblind
+ )}
+ {colorModeSupported(Theme.DARK_TRITANOPIA) && (
+ Dark Tritanopia
+ )}
+ {colorModeSupported(Theme.DARK_DIMMED) && (
+ Soft dark
+ )}
+
+ >
+ }
+ tooltip="Choose a light, dark, or system color mode. Available options depend on the selected design language."
+ value={resolveColorMode(designLanguage, theme)}
+ />
+
+ toggleSetting('increaseContrast')}
- tooltip={
-
- Use GitHub's high-contrast color schemes for improved legibility. Also applied
- automatically when your system's increase-contrast setting is enabled.
-
- }
+ tooltip="Use GitHub's high-contrast color schemes for improved legibility. Also applied automatically when your system's increase-contrast setting is enabled."
visible={designLanguage === DesignLanguage.CLASSIC}
/>
- toggleSetting('showStatusIconColors')}
- tooltip={
-
- Color the notification type icons by their state (open, closed, done, attention).
- Glass renders them monochrome by default.
-
- }
+ tooltip="Color the notification type icons by their state (open, closed, done, attention). Glass renders them monochrome by default."
visible={designLanguage === DesignLanguage.GLASS}
/>
-
-
- Change the size of the Gitify interface. The setting applies immediately and is
- remembered for the next launch.
-
- }
- />
-
-
+
+
-
-
+
+
);
}
diff --git a/src/renderer/components/settings/InboxSettings.test.tsx b/src/renderer/components/settings/InboxSettings.test.tsx
index 9e0145de4..3a9538e0c 100644
--- a/src/renderer/components/settings/InboxSettings.test.tsx
+++ b/src/renderer/components/settings/InboxSettings.test.tsx
@@ -1,7 +1,7 @@
import { act, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import { renderWithProviders } from '../../__helpers__/test-utils';
+import { navigateMock, renderWithProviders } from '../../__helpers__/test-utils';
import {
mockGitHubCloudAccount,
mockGitHubEnterpriseServerAccount,
@@ -23,14 +23,15 @@ describe('renderer/components/settings/InboxSettings.tsx', () => {
beforeEach(() => {
toggleSettingSpy = vi.spyOn(useSettingsStore.getState(), 'toggleSetting');
updateSettingSpy = vi.spyOn(useSettingsStore.getState(), 'updateSetting');
+ navigateMock.mockReset();
});
- it('should change the groupBy radio group', async () => {
+ it('should change the groupBy choice', async () => {
await act(async () => {
renderWithProviders(
);
});
- await userEvent.click(screen.getByTestId('radio-groupBy-date'));
+ await userEvent.click(screen.getByTestId('choice-groupBy-date'));
expect(updateSettingSpy).toHaveBeenCalledTimes(1);
expect(updateSettingSpy).toHaveBeenCalledWith('groupBy', 'DATE');
@@ -42,7 +43,6 @@ describe('renderer/components/settings/InboxSettings.tsx', () => {
renderWithProviders(
);
});
- // Increase fetch interval
await act(async () => {
await userEvent.click(screen.getByTestId('settings-fetch-interval-increase'));
@@ -57,7 +57,6 @@ describe('renderer/components/settings/InboxSettings.tsx', () => {
expect(updateSettingSpy).toHaveBeenNthCalledWith(2, 'fetchInterval', 180000);
});
- // Decrease fetch interval
await act(async () => {
await userEvent.click(screen.getByTestId('settings-fetch-interval-decrease'));
@@ -65,7 +64,6 @@ describe('renderer/components/settings/InboxSettings.tsx', () => {
expect(updateSettingSpy).toHaveBeenNthCalledWith(3, 'fetchInterval', 120000);
});
- // Fetch interval reset
await act(async () => {
await userEvent.click(screen.getByTestId('settings-fetch-interval-reset'));
@@ -93,7 +91,6 @@ describe('renderer/components/settings/InboxSettings.tsx', () => {
expect(updateSettingSpy).toHaveBeenCalledWith('fetchInterval', 60000);
});
- // Attempt to go below the minimum interval, update settings should not be called
await act(async () => {
await userEvent.click(screen.getByTestId('settings-fetch-interval-decrease'));
@@ -120,7 +117,6 @@ describe('renderer/components/settings/InboxSettings.tsx', () => {
expect(updateSettingSpy).toHaveBeenCalledWith('fetchInterval', 3600000);
});
- // Attempt to go above the maximum interval, update settings should not be called
await act(async () => {
await userEvent.click(screen.getByTestId('settings-fetch-interval-increase'));
@@ -130,38 +126,38 @@ describe('renderer/components/settings/InboxSettings.tsx', () => {
});
it.each([
- ['checkbox-fetchAllNotifications', 'fetchAllNotifications'],
- ['checkbox-detailedNotifications', 'detailedNotifications'],
- ['checkbox-showPills', 'showPills'],
- ['checkbox-showNumber', 'showNumber'],
- ['checkbox-showOnlyParticipating', 'participating'],
- ['checkbox-fetchReadNotifications', 'fetchReadNotifications'],
- ['checkbox-markAsDoneOnOpen', 'markAsDoneOnOpen'],
- ['checkbox-markAsDoneOnUnsubscribe', 'markAsDoneOnUnsubscribe'],
- ['checkbox-showAccountHeader', 'showAccountHeader'],
- ['checkbox-wrapNotificationTitle', 'wrapNotificationTitle'],
- ['checkbox-delayNotificationState', 'delayNotificationState'],
- ] as const)('should toggle %s checkbox', async (testId, setting) => {
+ ['Fetch all notifications', 'fetchAllNotifications'],
+ ['Fetch detailed notifications', 'detailedNotifications'],
+ ['Show notification metrics', 'showPills'],
+ ['Show reference', 'showNumber'],
+ ['Fetch only participating', 'participating'],
+ ['Fetch read and done notifications', 'fetchReadNotifications'],
+ ['Mark as done on open', 'markAsDoneOnOpen'],
+ ['Mark as done on unsubscribe', 'markAsDoneOnUnsubscribe'],
+ ['Show account header', 'showAccountHeader'],
+ ['Show full notification title', 'wrapNotificationTitle'],
+ ['Delay notification removal', 'delayNotificationState'],
+ ] as const)('should toggle %s switch', async (label, setting) => {
await act(async () => {
renderWithProviders(
, {
accounts: [mockGitHubCloudAccount],
});
});
- await userEvent.click(screen.getByTestId(testId));
+ await userEvent.click(screen.getByRole('button', { name: label }));
expect(toggleSettingSpy).toHaveBeenCalledTimes(1);
expect(toggleSettingSpy).toHaveBeenCalledWith(setting);
});
- it('reactively hides the account header checkbox when a second account is added', async () => {
+ it('reactively hides the account header switch when a second account is added', async () => {
await act(async () => {
renderWithProviders(
, {
accounts: [mockGitHubCloudAccount],
});
});
- expect(screen.getByTestId('checkbox-showAccountHeader')).toBeInTheDocument();
+ expect(screen.getByTestId('switch-showAccountHeader')).toBeInTheDocument();
await act(async () => {
useAccountsStore.setState({
@@ -169,7 +165,7 @@ describe('renderer/components/settings/InboxSettings.tsx', () => {
});
});
- expect(screen.queryByTestId('checkbox-showAccountHeader')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('switch-showAccountHeader')).not.toBeInTheDocument();
});
describe('GitHub-specific settings', () => {
@@ -180,7 +176,7 @@ describe('renderer/components/settings/InboxSettings.tsx', () => {
});
});
- expect(screen.queryByTestId('checkbox-showOnlyParticipating')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('switch-showOnlyParticipating')).not.toBeInTheDocument();
expect(
screen.getByText('GitHub-specific options require a GitHub account.'),
).toBeInTheDocument();
@@ -193,13 +189,17 @@ describe('renderer/components/settings/InboxSettings.tsx', () => {
});
});
- expect(screen.getByTestId('checkbox-markAsDoneOnOpen')).toBeDisabled();
- expect(screen.getByTestId('checkbox-markAsDoneOnUnsubscribe')).toBeDisabled();
+ expect(screen.getByRole('button', { name: 'Mark as done on open' })).toHaveAttribute(
+ 'aria-disabled',
+ 'true',
+ );
+ expect(screen.getByRole('button', { name: 'Mark as done on unsubscribe' })).toHaveAttribute(
+ 'aria-disabled',
+ 'true',
+ );
expect(
- screen.getByText(
- 'Mark as done requires GitHub Cloud or GitHub Enterprise Server 3.13 or later.',
- ),
- ).toBeInTheDocument();
+ screen.getAllByText('Requires GitHub Cloud or GitHub Enterprise Server 3.13 or later.'),
+ ).toHaveLength(2);
});
it('hides notification metrics when detailed notifications are disabled', async () => {
@@ -210,7 +210,10 @@ describe('renderer/components/settings/InboxSettings.tsx', () => {
});
});
- expect(screen.queryByTestId('checkbox-showPills')).not.toBeInTheDocument();
+ expect(screen.getByTestId('reveal-notificationMetrics')).toHaveAttribute(
+ 'data-state',
+ 'closed',
+ );
});
it('shows notification metrics when detailed notifications are enabled', async () => {
@@ -221,11 +224,33 @@ describe('renderer/components/settings/InboxSettings.tsx', () => {
});
});
- expect(screen.getByTestId('checkbox-showPills')).toBeInTheDocument();
+ expect(screen.getByTestId('reveal-notificationMetrics')).toHaveAttribute(
+ 'data-state',
+ 'open',
+ );
+ });
+
+ it('shows a banner when GitHub accounts are missing scopes', async () => {
+ await act(async () => {
+ renderWithProviders(
, {
+ accounts: [mockGitHubCloudAccount],
+ });
+ });
+
+ expect(screen.getByTestId('settings-scope-warning')).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ 'One or more accounts are missing scopes required for notification enrichment. Notification details may be incomplete.',
+ ),
+ ).toBeInTheDocument();
+
+ await userEvent.click(screen.getByRole('button', { name: 'Review account scopes' }));
+
+ expect(navigateMock).toHaveBeenCalledWith('/accounts');
});
});
- it('should open official docs for showOnlyParticipating tooltip', async () => {
+ it('opens official docs from the participating help link', async () => {
const openExternalLinkSpy = vi.spyOn(comms, 'openExternalLink').mockImplementation(vi.fn());
await act(async () => {
@@ -234,9 +259,8 @@ describe('renderer/components/settings/InboxSettings.tsx', () => {
});
});
- const tooltipElement = screen.getByLabelText('tooltip-showOnlyParticipating');
+ await userEvent.click(screen.getByTestId('tooltip-icon-tooltip-showOnlyParticipating'));
- await userEvent.click(tooltipElement);
await userEvent.click(
screen.getByTitle('Open GitHub documentation for participating and watching notifications'),
);
@@ -247,7 +271,23 @@ describe('renderer/components/settings/InboxSettings.tsx', () => {
);
});
- it('should show help for the fetch interval', async () => {
+ it.each([
+ ['groupBy', 'tooltip-groupBy'],
+ ['showAccountHeader', 'tooltip-showAccountHeader'],
+ ['wrapNotificationTitle', 'tooltip-wrapNotificationTitle'],
+ ['showNumber', 'tooltip-showNumber'],
+ ['delayNotificationState', 'tooltip-delayNotificationState'],
+ ] as const)('renders the %s tooltip', async (_name, tooltipName) => {
+ await act(async () => {
+ renderWithProviders(
, {
+ accounts: [mockGitHubCloudAccount],
+ });
+ });
+
+ expect(screen.getByTestId(`tooltip-icon-${tooltipName}`)).toBeInTheDocument();
+ });
+
+ it('shows the fetch interval details in a tooltip', async () => {
await act(async () => {
renderWithProviders(
);
});
diff --git a/src/renderer/components/settings/InboxSettings.tsx b/src/renderer/components/settings/InboxSettings.tsx
index ae5c79733..d651e07d6 100644
--- a/src/renderer/components/settings/InboxSettings.tsx
+++ b/src/renderer/components/settings/InboxSettings.tsx
@@ -2,7 +2,6 @@ import type { MouseEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import {
- AlertFillIcon,
CheckIcon,
CommentIcon,
DashIcon,
@@ -20,20 +19,15 @@ import {
SyncIcon,
TagIcon,
} from '@primer/octicons-react';
-import { Button, ButtonGroup, IconButton, Stack, Text } from '@primer/react';
+import { Banner, Button, ButtonGroup, IconButton, Stack, Text } from '@primer/react';
import { formatDuration } from 'date-fns/formatDuration';
import { millisecondsToMinutes } from 'date-fns/millisecondsToMinutes';
-import { APPLICATION } from '../../../shared/constants';
-
import { Constants } from '../../constants';
import { useAccountsStore, useSettingsStore } from '../../stores';
-import { Checkbox } from '../fields/Checkbox';
-import { FieldLabel } from '../fields/FieldLabel';
-import { RadioGroup } from '../fields/RadioGroup';
import { SectionHeading } from '../primitives/SectionHeading';
import { GroupBy, Size } from '../../types';
@@ -41,6 +35,11 @@ import { GroupBy, Size } from '../../types';
import { isMarkAsDoneFeatureSupported } from '../../utils/api/features';
import { hasAlternateScopes, hasRecommendedScopes } from '../../utils/auth/scopes';
import { openGitHubParticipatingDocs } from '../../utils/system/links';
+import { SettingsChoice } from './controls/SettingsChoice';
+import { SettingsList } from './controls/SettingsList';
+import { SettingsReveal } from './controls/SettingsReveal';
+import { SettingsRow } from './controls/SettingsRow';
+import { SettingsToggle } from './controls/SettingsToggle';
import { SettingsGroupHeading } from './SettingsGroupHeading';
export function InboxSettings() {
@@ -77,414 +76,355 @@ export function InboxSettings() {
Inbox
-
- Fetching
+
+
Fetch
-
-
+
+
+ {
+ const newInterval = Math.max(
+ fetchInterval - Constants.FETCH_NOTIFICATIONS_INTERVAL_STEP_MS,
+ Constants.MIN_FETCH_NOTIFICATIONS_INTERVAL_MS,
+ );
+
+ if (newInterval !== fetchInterval) {
+ updateSetting('fetchInterval', newInterval);
+ }
+ }}
+ size="small"
+ unsafeDisableTooltip={true}
+ />
+
+
+ {formatDuration({
+ minutes: millisecondsToMinutes(fetchInterval),
+ })}
+
+
+ {
+ const newInterval = Math.min(
+ fetchInterval + Constants.FETCH_NOTIFICATIONS_INTERVAL_STEP_MS,
+ Constants.MAX_FETCH_NOTIFICATIONS_INTERVAL_MS,
+ );
+
+ if (newInterval !== fetchInterval) {
+ updateSetting('fetchInterval', newInterval);
+ }
+ }}
+ size="small"
+ unsafeDisableTooltip={true}
+ />
+
+ {
+ updateSetting('fetchInterval', Constants.DEFAULT_FETCH_NOTIFICATIONS_INTERVAL_MS);
+ }}
+ size="small"
+ unsafeDisableTooltip={true}
+ variant="danger"
+ />
+
+
+
+ toggleSetting('fetchAllNotifications')}
tooltip={
-
- Check for new notifications at this interval. Shorter intervals keep the list
- fresher but spend more API requests.
-
+
+ When checked, fetch all notifications from your inbox.
+ When unchecked, fetch only the first page of notifications.
+
}
/>
-
- {
- const newInterval = Math.max(
- fetchInterval - Constants.FETCH_NOTIFICATIONS_INTERVAL_STEP_MS,
- Constants.MIN_FETCH_NOTIFICATIONS_INTERVAL_MS,
- );
-
- if (newInterval !== fetchInterval) {
- updateSetting('fetchInterval', newInterval);
- }
- }}
- size="small"
- unsafeDisableTooltip={true}
- />
-
-
- {formatDuration({
- minutes: millisecondsToMinutes(fetchInterval),
- })}
-
-
- {
- const newInterval = Math.min(
- fetchInterval + Constants.FETCH_NOTIFICATIONS_INTERVAL_STEP_MS,
- Constants.MAX_FETCH_NOTIFICATIONS_INTERVAL_MS,
- );
-
- if (newInterval !== fetchInterval) {
- updateSetting('fetchInterval', newInterval);
- }
- }}
- size="small"
- unsafeDisableTooltip={true}
- />
-
- {
- updateSetting('fetchInterval', Constants.DEFAULT_FETCH_NOTIFICATIONS_INTERVAL_MS);
- }}
- size="small"
- unsafeDisableTooltip={true}
- variant="danger"
- />
-
-
-
-
toggleSetting('fetchAllNotifications')}
- tooltip={
-
-
- When checked , {APPLICATION.NAME} will fetch{' '}
- all notifications from your inbox.
-
-
- When unchecked , {APPLICATION.NAME} will fetch the first page of
- notifications.
-
-
- }
- />
-
- toggleSetting('fetchReadNotifications')}
- tooltip={
-
- Fetch all notifications including read and done.
-
- ⚠️ GitHub's API does not distinguish between read and done states, so 'Mark as done'
- actions will be unavailable when this setting is enabled.
-
-
- ⚠️ Enabling this setting will increase API usage and may cause rate limiting for
- users with many notifications.
-
-
- }
- />
+ toggleSetting('fetchReadNotifications')}
+ tooltip={
+
+ Fetch all notifications including read and done.
+
+ GitHub's API does not distinguish between read and done states, so Mark as done
+ actions are unavailable while this is enabled.
+
+
+ This increases API usage and may cause rate limiting for users with many
+ notifications.
+
+
+ }
+ />
+
List
- {
- updateSetting('groupBy', evt.target.value as GroupBy);
- }}
- options={[
- { label: 'Repository', value: GroupBy.REPOSITORY },
- { label: 'Date', value: GroupBy.DATE },
- ]}
- tooltip={
-
- Choose how notifications are displayed in the list.
-
- Repository groups notifications by their repository full
- name.
-
-
- Date shows notifications in chronological order.
-
-
- }
- value={groupBy}
- />
-
- toggleSetting('showAccountHeader')}
- tooltip={
- Show your avatar, username, and quick links above the notifications list.
- }
- visible={!hasMultipleAccounts}
- />
-
- toggleSetting('wrapNotificationTitle')}
- tooltip={
-
- Wrap long notification titles onto multiple lines instead of truncating them.
-
- }
- />
-
- toggleSetting('showNumber')}
- tooltip={
-
- Show the notification reference where available:
-
-
-
-
- Discussion
- #123
-
+
+ updateSetting('groupBy', value as GroupBy)}
+ options={[
+ { label: 'Repository', value: GroupBy.REPOSITORY },
+ { label: 'Date', value: GroupBy.DATE },
+ ]}
+ tooltip={
+
+ Choose how notifications are displayed in the list.
+
+ Repository groups notifications by repository name.
+
+
+ Date shows notifications in chronological order.
+
+
+ }
+ value={groupBy}
+ />
-
-
- Issue
- #123
-
+ toggleSetting('showAccountHeader')}
+ tooltip="Show your avatar, username, and quick links above the notifications list."
+ visible={!hasMultipleAccounts}
+ />
-
-
- Pull Request
- #123
-
+ toggleSetting('wrapNotificationTitle')}
+ tooltip="Wrap long notification titles onto multiple lines instead of truncating them."
+ />
-
-
- Commit
- a1b2c3d
+ toggleSetting('showNumber')}
+ tooltip={
+
+ Show the reference where available:
+
+
+
+
+ Discussion
+ #123
+
+
+
+ Issue
+ #123
+
+
+
+ Pull request
+ #123
+
+
+
+ Commit
+ a1b2c3d
+
+
+
+ Release
+ v1.2.3
+
+
+
+ }
+ />
-
-
- Release
- v1.2.3
-
-
-
-
- }
- />
-
- toggleSetting('delayNotificationState')}
- tooltip={
-
- Keep the notification within {APPLICATION.NAME} upon interaction (ie: open
- notification, mark as read, mark as done) until the next refresh window (scheduled or
- user initiated).
-
- }
- />
+ toggleSetting('delayNotificationState')}
+ tooltip="Keep the notification in Gitify after interacting with it until the next refresh window."
+ />
+
{hasGitHubAccount ? (
<>
GitHub
- toggleSetting('participating')}
- tooltip={
-
-
- When checked , {APPLICATION.NAME} will fetch only
- participating notifications.
-
-
- When unchecked , {APPLICATION.NAME} will fetch participating
- and watching notifications.
-
-
- See{' '}
+
+ toggleSetting('participating')}
+ tooltip={
+
+ When checked, fetch only participating notifications.
+ When unchecked, fetch participating and watching notifications.
) => {
- // Don't trigger onClick of parent element.
event.stopPropagation();
openGitHubParticipatingDocs();
}}
title="Open GitHub documentation for participating and watching notifications"
type="button"
>
- official docs
- {' '}
- for more details.
-
-
- }
- />
-
- toggleSetting('detailedNotifications')}
- tooltip={
-
-
- When checked , {APPLICATION.NAME} will enrich notifications
- with detailed user and state information. You may also choose to display{' '}
- notification metrics .
-
-
- When unchecked , {APPLICATION.NAME} will only fetch basic
- notification details.
-
-
- ⚠️ Users with a large number of unread notifications may experience rate
- limiting under certain circumstances. Please disable this setting if you
- experience this.
-
-
- }
- />
-
- {detailedNotifications &&
- accounts.some(
- (account) =>
- account.forge === 'github' &&
- !hasRecommendedScopes(account) &&
- !hasAlternateScopes(account),
- ) && (
-
-
+ Learn more
+
+
+ }
+ />
+
+ toggleSetting('detailedNotifications')}
+ tooltip={
- One or more accounts are missing scopes required for notification enrichment.
- Notification details may be incomplete.
+ Enrich notifications with detailed user and state information. You may also
+ choose to display notification metrics.
-
- Visit{' '}
- navigate('/accounts')}
- type="button"
- >
- Accounts → Scopes
- {' '}
- to review.
+
+ Users with a large number of unread notifications may experience rate
+ limiting.
-
- )}
-
- {detailedNotifications && (
-
-
toggleSetting('showPills')}
- tooltip={
-
- Show notification metrics for:
-
-
-
-
- linked issues
-
-
-
- reactions
-
-
-
- pull request reviews
-
-
-
- comments
-
-
-
- labels
-
-
-
- milestones
+ }
+ />
+
+ {detailedNotifications &&
+ accounts.some(
+ (account) =>
+ account.forge === 'github' &&
+ !hasRecommendedScopes(account) &&
+ !hasAlternateScopes(account),
+ ) && (
+
+
+ One or more accounts are missing scopes required for notification
+ enrichment. Notification details may be incomplete.
+
+ navigate('/accounts')} size="small">
+ Review account scopes
+
+
+ }
+ title="Missing notification scopes"
+ variant="warning"
+ />
+ )}
+
+
+
+
toggleSetting('showPills')}
+ tooltip={
+
+ Show notification metrics for:
+
+
+
+
+ linked issues
+
+
+
+ reactions
+
+
+
+ pull request reviews
+
+
+
+ comments
+
+
+
+ labels
+
+
+
+ milestones
+
-
-
-
- }
- />
-
- )}
-
- toggleSetting('markAsDoneOnOpen')}
- tooltip={
-
- Mark as done feature is supported by GitHub Cloud and
- GitHub Enterprise Server 3.13 or later.
-
- }
- />
-
- toggleSetting('markAsDoneOnUnsubscribe')}
- tooltip={
-
- Mark as done feature is supported by GitHub Cloud and
- GitHub Enterprise Server 3.13 or later.
-
- }
- />
-
- {!hasMarkAsDoneAccount && (
-
- Mark as done requires GitHub Cloud or GitHub Enterprise Server 3.13 or later.
-
- )}
+
+
+ }
+ />
+
+
+
+ toggleSetting('markAsDoneOnOpen')}
+ tooltip="Mark as done is supported by GitHub Cloud and GitHub Enterprise Server 3.13 or later."
+ />
+
+ toggleSetting('markAsDoneOnUnsubscribe')}
+ tooltip="Mark as done is supported by GitHub Cloud and GitHub Enterprise Server 3.13 or later."
+ />
+
>
) : (
GitHub-specific options require a GitHub account.
)}
-
+
);
}
diff --git a/src/renderer/components/settings/SettingsGroupHeading.test.tsx b/src/renderer/components/settings/SettingsGroupHeading.test.tsx
index 0a98eb95c..9174f0f19 100644
--- a/src/renderer/components/settings/SettingsGroupHeading.test.tsx
+++ b/src/renderer/components/settings/SettingsGroupHeading.test.tsx
@@ -7,7 +7,7 @@ import { SettingsGroupHeading } from './SettingsGroupHeading';
describe('renderer/components/settings/SettingsGroupHeading.tsx', () => {
it('should render a group heading', async () => {
const { container } = renderWithProviders(
- Fetching ,
+ Fetch ,
);
expect(container).toMatchSnapshot();
diff --git a/src/renderer/components/settings/SettingsGroupHeading.tsx b/src/renderer/components/settings/SettingsGroupHeading.tsx
index 89f32340d..8687d7875 100644
--- a/src/renderer/components/settings/SettingsGroupHeading.tsx
+++ b/src/renderer/components/settings/SettingsGroupHeading.tsx
@@ -11,7 +11,10 @@ export function SettingsGroupHeading({
children,
}: Readonly) {
return (
-
+ // Primer's Stack zeroes its own padding, so this needs an important value.
+ // 1rem plus the preceding row's 8px padding keeps the same 24px separation
+ // that section headings get, without drawing a divider after the last row.
+
{children}
diff --git a/src/renderer/components/settings/SystemSettings.test.tsx b/src/renderer/components/settings/SystemSettings.test.tsx
index 56f4aa81b..af8a5cc25 100644
--- a/src/renderer/components/settings/SystemSettings.test.tsx
+++ b/src/renderer/components/settings/SystemSettings.test.tsx
@@ -19,33 +19,33 @@ describe('renderer/components/settings/SystemSettings.tsx', () => {
updateSettingSpy = vi.spyOn(useSettingsStore.getState(), 'updateSetting');
});
- it('should change the open links radio group', async () => {
+ it('should change the link focus choice', async () => {
await act(async () => {
renderWithProviders( );
});
- await userEvent.click(screen.getByTestId('radio-openLinks-background'));
+ await userEvent.click(screen.getByTestId('choice-openLinks-background'));
expect(updateSettingSpy).toHaveBeenCalledTimes(1);
expect(updateSettingSpy).toHaveBeenCalledWith('openLinks', 'BACKGROUND');
});
it.each([
- ['checkbox-keyboardShortcut', 'keyboardShortcut'],
- ['checkbox-openAtStartup', 'openAtStartup'],
- ['checkbox-keepWindowOnBlur', 'keepWindowOnBlur'],
- ] as const)('should toggle %s checkbox', async (testId, setting) => {
+ ['Enable global shortcut', 'keyboardShortcut'],
+ ['Launch at startup', 'openAtStartup'],
+ ['Keep window open when unfocused', 'keepWindowOnBlur'],
+ ] as const)('should toggle %s switch', async (label, setting) => {
await act(async () => {
renderWithProviders( );
});
- await userEvent.click(screen.getByTestId(testId));
+ await userEvent.click(screen.getByRole('button', { name: label }));
expect(toggleSettingSpy).toHaveBeenCalledTimes(1);
expect(toggleSettingSpy).toHaveBeenCalledWith(setting);
});
- describe('X11 backend checkbox', () => {
+ describe('X11 backend switch', () => {
// `window.gitify` is rebuilt in a global `beforeEach`, so the mock has to
// be read inside each test rather than captured at describe scope.
const isLinuxMock = () => window.gitify.platform.isLinux as unknown as ReturnType;
@@ -57,7 +57,7 @@ describe('renderer/components/settings/SystemSettings.tsx', () => {
renderWithProviders( );
});
- expect(screen.queryByTestId('checkbox-useX11Backend')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('switch-useX11Backend')).not.toBeInTheDocument();
expect(
screen.queryByRole('button', { name: 'GNOME extension installation' }),
).not.toBeInTheDocument();
@@ -70,7 +70,9 @@ describe('renderer/components/settings/SystemSettings.tsx', () => {
renderWithProviders( );
});
- await userEvent.click(screen.getByTestId('checkbox-useX11Backend'));
+ await userEvent.click(
+ screen.getByRole('button', { name: 'Use X11 backend (restart required)' }),
+ );
expect(toggleSettingSpy).toHaveBeenCalledWith('useX11Backend');
});
@@ -119,7 +121,19 @@ describe('renderer/components/settings/SystemSettings.tsx', () => {
settings: { ...mockSettings, keyboardShortcut: false },
});
- expect(screen.queryByTestId('settings-shortcut-group')).not.toBeInTheDocument();
+ expect(screen.getByTestId('reveal-shortcutRecorder')).toHaveAttribute('data-state', 'closed');
+ });
+
+ it('shows the shortcut rules on the recorder row', async () => {
+ renderWithProviders( , {
+ settings: { ...mockSettings, keyboardShortcut: true },
+ });
+
+ expect(screen.getByText('Global shortcut')).toBeInTheDocument();
+
+ await userEvent.click(screen.getByTestId('tooltip-icon-tooltip-shortcut'));
+
+ expect(screen.getByText('Shortcuts must include:')).toBeInTheDocument();
});
describe('recording global shortcut', () => {
diff --git a/src/renderer/components/settings/SystemSettings.tsx b/src/renderer/components/settings/SystemSettings.tsx
index 984583666..fcf402a23 100644
--- a/src/renderer/components/settings/SystemSettings.tsx
+++ b/src/renderer/components/settings/SystemSettings.tsx
@@ -3,16 +3,12 @@ import { useEffect, useRef, useState } from 'react';
import { DeviceDesktopIcon, PencilIcon, SyncIcon } from '@primer/octicons-react';
import { Banner, Button, ButtonGroup, IconButton, Stack, Text } from '@primer/react';
-import { APPLICATION } from '../../../shared/constants';
-
import {
DEFAULT_SETTINGS_STATE,
useSettingsStore,
useShortcutRegistrationStore,
} from '../../stores';
-import { Checkbox } from '../fields/Checkbox';
-import { RadioGroup } from '../fields/RadioGroup';
import { SectionHeading } from '../primitives/SectionHeading';
import { type KeyboardAcceleratorShortcut, OpenPreference, toLink } from '../../types';
@@ -23,6 +19,11 @@ import {
keyboardEventToAccelerator,
MODIFIER_SEGMENTS,
} from '../../utils/system/keyboardShortcut';
+import { SettingsChoice } from './controls/SettingsChoice';
+import { SettingsList } from './controls/SettingsList';
+import { SettingsReveal } from './controls/SettingsReveal';
+import { SettingsRow } from './controls/SettingsRow';
+import { SettingsToggle } from './controls/SettingsToggle';
const defaultSettings = DEFAULT_SETTINGS_STATE;
@@ -124,13 +125,11 @@ export function SystemSettings() {
System
-
-
+ {
- updateSetting('openLinks', evt.target.value as OpenPreference);
- }}
+ onChange={(value) => updateSetting('openLinks', value as OpenPreference)}
options={[
{ label: 'Foreground', value: OpenPreference.FOREGROUND },
{ label: 'Background', value: OpenPreference.BACKGROUND },
@@ -149,8 +148,8 @@ export function SystemSettings() {
current window.
- This applies to every link {APPLICATION.NAME} opens, including notification links
- opened in the browser.
+ This applies to every link Gitify opens, including notification links opened in the
+ browser.
}
@@ -174,141 +173,143 @@ export function SystemSettings() {
/>
)}
-
- toggleSetting('keyboardShortcut')}
- tooltip={
-
- Global keyboard shortcut to show or hide {APPLICATION.NAME}.
- Shortcuts must include:
-
-
-
- •
-
- Primary modifier:{' '}
-
- {formatAcceleratorForDisplay('CommandOrControl', isMac)}
-
-
-
-
- •
-
- Optional modifiers:{' '}
-
- {formatAcceleratorForDisplay('Shift', isMac)}
+ {
+ if (keyboardShortcut) {
+ setRecordingShortcut(false);
+ setLiveModifierAccelerator('');
+ }
+ toggleSetting('keyboardShortcut');
+ }}
+ tooltip="Show or hide Gitify from anywhere with a keyboard shortcut."
+ />
+
+
+
+
+ Shortcuts must include:
+
+
+
+ •
+
+ Primary modifier:{' '}
+
+ {formatAcceleratorForDisplay('CommandOrControl', isMac)}
+
- ,{' '}
-
- {formatAcceleratorForDisplay('Alt', isMac)}
+
+
+ •
+
+ Optional modifiers:{' '}
+
+ {formatAcceleratorForDisplay('Shift', isMac)}
+
+ ,{' '}
+
+ {formatAcceleratorForDisplay('Alt', isMac)}
+
-
-
-
- •
- A non-modifier key (letter, number, etc.)
+
+
+ •
+ A non-modifier key (letter, number, etc.)
+
-
-
-
- }
- />
-
- {keyboardShortcut && (
-
- {
- clearShortcutRegistrationError();
- setLiveModifierAccelerator('');
- setRecordingShortcut(true);
- }}
- size="small"
- unsafeDisableTooltip={true}
- />
-
-
- {shortcutButtonLabel}
-
-
- {
- clearShortcutRegistrationError();
- updateSetting('openGitifyShortcut', defaultSettings.openGitifyShortcut);
- }}
- size="small"
- unsafeDisableTooltip={true}
- variant="danger"
- />
-
- )}
-
-
-
+
+ }
+ >
+
+ {
+ clearShortcutRegistrationError();
+ setLiveModifierAccelerator('');
+ setRecordingShortcut(true);
+ }}
+ size="small"
+ unsafeDisableTooltip={true}
+ />
+
+
+ {shortcutButtonLabel}
+
+
+ {
+ clearShortcutRegistrationError();
+ updateSetting('openGitifyShortcut', defaultSettings.openGitifyShortcut);
+ }}
+ size="small"
+ unsafeDisableTooltip={true}
+ variant="danger"
+ />
+
+
+
+
+
+ toggleSetting('keepWindowOnBlur')}
- tooltip={Keep the {APPLICATION.NAME} window open when you click outside it. }
+ tooltip="Keep the Gitify window open when you click outside it."
/>
- toggleSetting('openAtStartup')}
- tooltip={Launch {APPLICATION.NAME} automatically at startup. }
+ tooltip="Launch Gitify automatically at startup."
visible={!isLinux}
/>
- toggleSetting('useX11Backend')}
- tooltip={
-
- Run {APPLICATION.NAME} under X11/XWayland so the window can open next to the tray
- icon. Wayland placement still depends on your compositor and its extensions. This
- disables Vulkan and may soften text on fractional-scaling displays. Takes effect after
- restarting {APPLICATION.NAME}.
-
- }
+ tooltip="Run Gitify under X11/XWayland so the window can open next to the tray icon. Takes effect after restarting Gitify."
visible={isLinux}
/>
-
- {isLinux && (
-
-
- On GNOME Wayland, install the {APPLICATION.NAME} extension to open the window next to
- its tray icon. Install and manage it through GNOME's extension tools.
-
-
- openExternalLink(toLink('https://github.com/gitify-app/gnome#install'))
- }
- size="small"
- >
- GNOME extension installation
-
-
- )}
-
+
+
+ {isLinux && (
+
+
+ On GNOME Wayland, install the Gitify extension to open the window next to its tray icon.
+ Install and manage it through GNOME's extension tools.
+
+ openExternalLink(toLink('https://github.com/gitify-app/gnome#install'))}
+ size="small"
+ >
+ GNOME extension installation
+
+
+ )}
);
}
diff --git a/src/renderer/components/settings/TraySettings.test.tsx b/src/renderer/components/settings/TraySettings.test.tsx
index 53fa38055..f5558fcd3 100644
--- a/src/renderer/components/settings/TraySettings.test.tsx
+++ b/src/renderer/components/settings/TraySettings.test.tsx
@@ -19,10 +19,10 @@ describe('renderer/components/settings/TraySettings.tsx', () => {
});
it.each([
- ['checkbox-showNotificationsCountInTray', 'showNotificationsCountInTray'],
- ['checkbox-useUnreadActiveIcon', 'useUnreadActiveIcon'],
- ] as const)('should toggle %s checkbox', async (testId, setting) => {
- await userEvent.click(screen.getByTestId(testId));
+ ['Show notification count', 'showNotificationsCountInTray'],
+ ['Highlight unread notifications', 'useUnreadActiveIcon'],
+ ] as const)('should toggle %s switch', async (label, setting) => {
+ await userEvent.click(screen.getByRole('button', { name: label }));
expect(toggleSettingSpy).toHaveBeenCalledTimes(1);
expect(toggleSettingSpy).toHaveBeenCalledWith(setting);
@@ -30,7 +30,7 @@ describe('renderer/components/settings/TraySettings.tsx', () => {
it.each(['auto', 'light', 'dark'] as const)(
'selects the %s icon appearance',
async (appearance) => {
- await userEvent.click(screen.getByTestId(`radio-trayIconAppearance-${appearance}`));
+ await userEvent.click(screen.getByTestId(`choice-trayIconAppearance-${appearance}`));
expect(useSettingsStore.getState().trayIconAppearance).toBe(appearance);
},
);
diff --git a/src/renderer/components/settings/TraySettings.tsx b/src/renderer/components/settings/TraySettings.tsx
index 2e1028791..11f4ea425 100644
--- a/src/renderer/components/settings/TraySettings.tsx
+++ b/src/renderer/components/settings/TraySettings.tsx
@@ -6,10 +6,12 @@ import { isTrayIconAppearance } from '../../../shared/events';
import { useSettingsStore } from '../../stores';
-import { Checkbox } from '../fields/Checkbox';
-import { RadioGroup } from '../fields/RadioGroup';
import { SectionHeading } from '../primitives/SectionHeading';
+import { SettingsChoice } from './controls/SettingsChoice';
+import { SettingsList } from './controls/SettingsList';
+import { SettingsToggle } from './controls/SettingsToggle';
+
export function TraySettings() {
const updateSetting = useSettingsStore((s) => s.updateSetting);
@@ -27,62 +29,50 @@ export function TraySettings() {
Tray
-
-
+ toggleSetting('showNotificationsCountInTray')}
- tooltip={
-
- Show the unread notification count next to the tray icon. Useful for a quick glance at
- unread activity.
-
- }
+ tooltip="Show the unread notification count next to the tray icon. Useful for a quick glance at unread activity."
visible={window.gitify.platform.isMacOS()}
/>
- toggleSetting('useUnreadActiveIcon')}
- tooltip={
-
-
- Turn the {APPLICATION.NAME} logo green while there are unread notifications.
-
-
- }
+ tooltip={`Turn the ${APPLICATION.NAME} logo green while there are unread notifications.`}
/>
- {
+ if (isTrayIconAppearance(value)) {
+ updateSetting('trayIconAppearance', value);
+ }
+ }}
options={[
{ label: 'Automatic', value: 'auto' },
{ label: 'Light', value: 'light' },
{ label: 'Dark', value: 'dark' },
]}
- onChange={(event) => {
- const value = event.target.value;
- if (isTrayIconAppearance(value)) {
- updateSetting('trayIconAppearance', value);
- }
- }}
tooltip={
Pick a light icon for dark panels or a dark icon for light panels.
- Choose Automatic to follow the tray appearance on macOS and Windows; Linux uses
- light because panel detection is unreliable.
+ Automatic follows the tray appearance on macOS and Windows; Linux uses light because
+ panel detection is unreliable.
This setting is independent of the {APPLICATION.NAME} app theme.
}
+ value={trayIconAppearance}
/>
-
+
);
}
diff --git a/src/renderer/components/settings/__snapshots__/SettingsGroupHeading.test.tsx.snap b/src/renderer/components/settings/__snapshots__/SettingsGroupHeading.test.tsx.snap
index 8d477a5ca..f89c8bb68 100644
--- a/src/renderer/components/settings/__snapshots__/SettingsGroupHeading.test.tsx.snap
+++ b/src/renderer/components/settings/__snapshots__/SettingsGroupHeading.test.tsx.snap
@@ -3,7 +3,7 @@
exports[`renderer/components/settings/SettingsGroupHeading.tsx > should render a group heading 1`] = `
should render a
class="text-sm font-semibold text-gitify-font prc-Text-Text-9mHv3"
data-component="Text"
>
- Fetching
+ Fetch
diff --git a/src/renderer/components/settings/controls/SettingsChoice.test.tsx b/src/renderer/components/settings/controls/SettingsChoice.test.tsx
new file mode 100644
index 000000000..215644725
--- /dev/null
+++ b/src/renderer/components/settings/controls/SettingsChoice.test.tsx
@@ -0,0 +1,129 @@
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import { renderWithProviders } from '../../../__helpers__/test-utils';
+
+import { SettingsChoice } from './SettingsChoice';
+
+describe('renderer/components/settings/controls/SettingsChoice.tsx', () => {
+ it('renders a labelled segmented choice', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('Group by')).toBeInTheDocument();
+ expect(screen.getByText('Choose how notifications are grouped.')).toBeInTheDocument();
+ expect(screen.getByTestId('choice-groupBy-repository')).toHaveAttribute('aria-pressed', 'true');
+ expect(screen.getByTestId('choice-groupBy-date')).toHaveAttribute('aria-pressed', 'false');
+ });
+
+ it('calls onChange when a segment is selected', async () => {
+ const onChange = vi.fn();
+
+ renderWithProviders(
+ ,
+ );
+
+ await userEvent.click(screen.getByTestId('choice-groupBy-date'));
+
+ expect(onChange).toHaveBeenCalledTimes(1);
+ expect(onChange).toHaveBeenCalledWith('date');
+ });
+
+ it('cycles to the next segment when the label is clicked', async () => {
+ const onChange = vi.fn();
+
+ renderWithProviders(
+ ,
+ );
+
+ await userEvent.click(screen.getByText('Group by'));
+
+ expect(onChange).toHaveBeenCalledWith('date');
+ });
+
+ it('renders a select variant', async () => {
+ const onChange = vi.fn();
+
+ renderWithProviders(
+ ,
+ );
+
+ await userEvent.selectOptions(screen.getByRole('combobox', { name: 'Theme' }), 'dark');
+
+ expect(onChange).toHaveBeenCalledWith('dark');
+ });
+
+ it('focuses the select when the label is clicked', async () => {
+ renderWithProviders(
+ ,
+ );
+
+ await userEvent.click(screen.getByText('Theme'));
+
+ expect(screen.getByRole('combobox', { name: 'Theme' })).toHaveFocus();
+ });
+
+ it('renders nothing when hidden', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.queryByText('Group by')).not.toBeInTheDocument();
+ });
+});
diff --git a/src/renderer/components/settings/controls/SettingsChoice.tsx b/src/renderer/components/settings/controls/SettingsChoice.tsx
new file mode 100644
index 000000000..a3f8ed581
--- /dev/null
+++ b/src/renderer/components/settings/controls/SettingsChoice.tsx
@@ -0,0 +1,155 @@
+import { type ReactNode, useId, useRef } from 'react';
+
+import { SegmentedControl, Select } from '@primer/react';
+
+import { cn } from 'cn';
+
+import { Tooltip } from '../../fields/Tooltip';
+
+export interface SettingsChoiceOption {
+ label: string;
+ value: string;
+}
+
+export interface SettingsChoiceProps {
+ name: string;
+ label: string;
+ caption?: ReactNode;
+ tooltip?: ReactNode;
+ value: string;
+ options?: SettingsChoiceOption[];
+ presentation?: 'segmented' | 'select';
+ selectChildren?: ReactNode;
+ disabled?: boolean;
+ visible?: boolean;
+ onChange: (value: string) => void;
+}
+
+export function SettingsChoice({
+ name,
+ label,
+ caption,
+ tooltip,
+ value,
+ options = [],
+ presentation = 'segmented',
+ selectChildren,
+ disabled = false,
+ visible = true,
+ onChange,
+}: Readonly) {
+ const id = useId();
+ const labelId = `${name}-${id}-label`;
+ const captionId = `${name}-${id}-caption`;
+ const selectId = `${name}-${id}-select`;
+ const selectRef = useRef(null);
+
+ if (!visible) {
+ return null;
+ }
+
+ const focusControl = () => {
+ if (presentation === 'select') {
+ const select = selectRef.current;
+
+ if (!select) {
+ return;
+ }
+
+ select.focus();
+
+ if (typeof select.showPicker === 'function') {
+ try {
+ select.showPicker();
+ } catch {
+ // Focus is the fallback when the browser cannot open the picker.
+ }
+ }
+
+ return;
+ }
+
+ const currentIndex = options.findIndex((option) => option.value === value);
+ const nextOption = options[(currentIndex + 1) % options.length];
+
+ if (nextOption) {
+ onChange(nextOption.value);
+ }
+ };
+
+ const control =
+ presentation === 'select' ? (
+ onChange(event.target.value)}
+ ref={selectRef}
+ value={value}
+ >
+ {selectChildren ??
+ options.map((option) => (
+
+ {option.label}
+
+ ))}
+
+ ) : (
+ {
+ const option = options[index];
+ if (option) {
+ onChange(option.value);
+ }
+ }}
+ >
+ {options.map((option) => (
+
+ {option.label}
+
+ ))}
+
+ );
+
+ return (
+
+
+
+ {/* Primer's unlayered `button { font: inherit }` outranks Tailwind's
+ utilities, so the label typography is marked important to match
+ the row labels. */}
+
+ {label}
+
+ {tooltip && }
+
+
{control}
+
+
+ {caption && (
+
+ {caption}
+
+ )}
+
+ );
+}
diff --git a/src/renderer/components/settings/controls/SettingsList.test.tsx b/src/renderer/components/settings/controls/SettingsList.test.tsx
new file mode 100644
index 000000000..e6d21354a
--- /dev/null
+++ b/src/renderer/components/settings/controls/SettingsList.test.tsx
@@ -0,0 +1,19 @@
+import { screen } from '@testing-library/react';
+
+import { renderWithProviders } from '../../../__helpers__/test-utils';
+
+import { SettingsList } from './SettingsList';
+
+describe('renderer/components/settings/controls/SettingsList.tsx', () => {
+ it('renders its children in a divided list', () => {
+ renderWithProviders(
+
+ First setting
+ Second setting
+ ,
+ );
+
+ expect(screen.getByText('First setting')).toBeInTheDocument();
+ expect(screen.getByText('Second setting')).toBeInTheDocument();
+ });
+});
diff --git a/src/renderer/components/settings/controls/SettingsList.tsx b/src/renderer/components/settings/controls/SettingsList.tsx
new file mode 100644
index 000000000..8ff43b4f5
--- /dev/null
+++ b/src/renderer/components/settings/controls/SettingsList.tsx
@@ -0,0 +1,9 @@
+import type { ReactNode } from 'react';
+
+export interface SettingsListProps {
+ children: ReactNode;
+}
+
+export function SettingsList({ children }: Readonly) {
+ return {children}
;
+}
diff --git a/src/renderer/components/settings/controls/SettingsReveal.test.tsx b/src/renderer/components/settings/controls/SettingsReveal.test.tsx
new file mode 100644
index 000000000..54c22983f
--- /dev/null
+++ b/src/renderer/components/settings/controls/SettingsReveal.test.tsx
@@ -0,0 +1,32 @@
+import { screen } from '@testing-library/react';
+
+import { renderWithProviders } from '../../../__helpers__/test-utils';
+
+import { SettingsReveal } from './SettingsReveal';
+
+describe('renderer/components/settings/controls/SettingsReveal.tsx', () => {
+ it('exposes an open state', () => {
+ renderWithProviders(
+
+ Details
+ ,
+ );
+
+ expect(screen.getByTestId('reveal-details')).toHaveAttribute('data-state', 'open');
+ expect(screen.getByText('Details')).toBeInTheDocument();
+ });
+
+ it('hides and inerts collapsed content', () => {
+ renderWithProviders(
+
+ Details
+ ,
+ );
+
+ const reveal = screen.getByTestId('reveal-details');
+
+ expect(reveal).toHaveAttribute('data-state', 'closed');
+ expect(reveal).toHaveAttribute('aria-hidden', 'true');
+ expect(reveal).toHaveAttribute('inert');
+ });
+});
diff --git a/src/renderer/components/settings/controls/SettingsReveal.tsx b/src/renderer/components/settings/controls/SettingsReveal.tsx
new file mode 100644
index 000000000..8c94550b1
--- /dev/null
+++ b/src/renderer/components/settings/controls/SettingsReveal.tsx
@@ -0,0 +1,26 @@
+import type { ReactNode } from 'react';
+
+import { cn } from 'cn';
+
+export interface SettingsRevealProps {
+ name?: string;
+ open: boolean;
+ children: ReactNode;
+}
+
+export function SettingsReveal({ name, open, children }: Readonly) {
+ return (
+
+ );
+}
diff --git a/src/renderer/components/settings/controls/SettingsRow.test.tsx b/src/renderer/components/settings/controls/SettingsRow.test.tsx
new file mode 100644
index 000000000..d423fda41
--- /dev/null
+++ b/src/renderer/components/settings/controls/SettingsRow.test.tsx
@@ -0,0 +1,50 @@
+import { screen } from '@testing-library/react';
+
+import { renderWithProviders } from '../../../__helpers__/test-utils';
+
+import { SettingsRow } from './SettingsRow';
+
+describe('renderer/components/settings/controls/SettingsRow.tsx', () => {
+ it('renders a label, caption, and control', () => {
+ renderWithProviders(
+
+ Control
+ ,
+ );
+
+ expect(screen.getByText('Fetch interval')).toBeInTheDocument();
+ expect(screen.getByText('Check for new notifications at this interval.')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Control' })).toBeInTheDocument();
+ });
+
+ it('renders a visible disabled reason', () => {
+ renderWithProviders(
+
+ Control
+ ,
+ );
+
+ expect(
+ screen.getByText('Requires GitHub Enterprise Server 3.13 or later.'),
+ ).toBeInTheDocument();
+ });
+
+ it('renders nothing when hidden', () => {
+ renderWithProviders(
+
+ Control
+ ,
+ );
+
+ expect(screen.queryByText('Fetch interval')).not.toBeInTheDocument();
+ });
+});
diff --git a/src/renderer/components/settings/controls/SettingsRow.tsx b/src/renderer/components/settings/controls/SettingsRow.tsx
new file mode 100644
index 000000000..af4d02139
--- /dev/null
+++ b/src/renderer/components/settings/controls/SettingsRow.tsx
@@ -0,0 +1,63 @@
+import { type ReactNode, useId } from 'react';
+
+import { cn } from 'cn';
+
+import { Tooltip } from '../../fields/Tooltip';
+
+export interface SettingsRowProps {
+ name: string;
+ label: string;
+ caption?: ReactNode;
+ tooltip?: ReactNode;
+ children: ReactNode;
+ disabled?: boolean;
+ disabledReason?: ReactNode;
+ visible?: boolean;
+}
+
+export function SettingsRow({
+ name,
+ label,
+ caption,
+ tooltip,
+ children,
+ disabled = false,
+ disabledReason,
+ visible = true,
+}: Readonly) {
+ const id = useId();
+ const labelId = `${name}-${id}-label`;
+ const captionId = `${name}-${id}-caption`;
+
+ if (!visible) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ {label}
+
+ {tooltip && }
+
+
{children}
+
+
+ {caption && (
+
+ {caption}
+
+ )}
+
+ {disabled && disabledReason && (
+ {disabledReason}
+ )}
+
+ );
+}
diff --git a/src/renderer/components/settings/controls/SettingsToggle.test.tsx b/src/renderer/components/settings/controls/SettingsToggle.test.tsx
new file mode 100644
index 000000000..b43997d55
--- /dev/null
+++ b/src/renderer/components/settings/controls/SettingsToggle.test.tsx
@@ -0,0 +1,98 @@
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import { renderWithProviders } from '../../../__helpers__/test-utils';
+
+import { SettingsToggle } from './SettingsToggle';
+
+describe('renderer/components/settings/controls/SettingsToggle.tsx', () => {
+ it('renders a labelled switch with a caption', () => {
+ renderWithProviders(
+ ,
+ );
+
+ const toggle = screen.getByRole('button', { name: 'Show notifications' });
+
+ expect(toggle).toHaveAttribute('aria-pressed', 'false');
+ expect(toggle).toHaveAccessibleDescription(/Shows new notifications\./);
+ expect(screen.getByTestId('switch-showNotifications')).toContainElement(toggle);
+ });
+
+ it('calls onChange with the next value', async () => {
+ const onChange = vi.fn();
+
+ renderWithProviders(
+ ,
+ );
+
+ await userEvent.click(screen.getByRole('button', { name: 'Show notifications' }));
+
+ expect(onChange).toHaveBeenCalledTimes(1);
+ expect(onChange).toHaveBeenCalledWith(true);
+ });
+
+ it('toggles from the label', async () => {
+ const onChange = vi.fn();
+
+ renderWithProviders(
+ ,
+ );
+
+ await userEvent.click(screen.getByText('Show notifications'));
+
+ expect(onChange).toHaveBeenCalledTimes(1);
+ expect(onChange).toHaveBeenCalledWith(false);
+ });
+
+ it('does not toggle when disabled', async () => {
+ const onChange = vi.fn();
+
+ renderWithProviders(
+ ,
+ );
+
+ await userEvent.click(screen.getByRole('button', { name: 'Show notifications' }));
+
+ expect(onChange).not.toHaveBeenCalled();
+ expect(screen.getByRole('button', { name: 'Show notifications' })).toHaveAttribute(
+ 'aria-disabled',
+ 'true',
+ );
+ });
+
+ it('renders nothing when hidden', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.queryByTestId('switch-showNotifications')).not.toBeInTheDocument();
+ });
+});
diff --git a/src/renderer/components/settings/controls/SettingsToggle.tsx b/src/renderer/components/settings/controls/SettingsToggle.tsx
new file mode 100644
index 000000000..3f2d54414
--- /dev/null
+++ b/src/renderer/components/settings/controls/SettingsToggle.tsx
@@ -0,0 +1,77 @@
+import { type ReactNode, useId } from 'react';
+
+import { ToggleSwitch } from '@primer/react';
+
+import { cn } from 'cn';
+
+import { Tooltip } from '../../fields/Tooltip';
+
+export interface SettingsToggleProps {
+ name: string;
+ label: string;
+ caption?: ReactNode;
+ tooltip?: ReactNode;
+ checked: boolean;
+ disabled?: boolean;
+ visible?: boolean;
+ onChange: (checked: boolean) => void;
+}
+
+export function SettingsToggle({
+ name,
+ label,
+ caption,
+ tooltip,
+ checked,
+ disabled = false,
+ visible = true,
+ onChange,
+}: Readonly) {
+ const id = useId();
+ const labelId = `${name}-${id}-label`;
+ const captionId = `${name}-${id}-caption`;
+
+ if (!visible) {
+ return null;
+ }
+
+ return (
+
+
+
+ {/* Primer's unlayered `button { font: inherit }` outranks Tailwind's
+ utilities, so the label typography is marked important to match
+ the other row labels. */}
+ onChange(!checked)}
+ tabIndex={-1}
+ type="button"
+ >
+ {label}
+
+ {tooltip && }
+
+
+ {caption && (
+
+ {caption}
+
+ )}
+
+
+
+
+ );
+}
diff --git a/src/renderer/routes/Settings.test.tsx b/src/renderer/routes/Settings.test.tsx
index 7b2096fd5..04d9face6 100644
--- a/src/renderer/routes/Settings.test.tsx
+++ b/src/renderer/routes/Settings.test.tsx
@@ -43,19 +43,17 @@ describe('renderer/routes/Settings.tsx', () => {
const alerts = section('Alerts');
const system = section('System');
- expect(within(inbox).getByTestId('checkbox-showPills')).toBeInTheDocument();
- expect(within(inbox).getByTestId('checkbox-wrapNotificationTitle')).toBeInTheDocument();
- expect(within(inbox).queryByTestId('checkbox-showNotifications')).not.toBeInTheDocument();
+ expect(within(inbox).getByTestId('switch-showPills')).toBeInTheDocument();
+ expect(within(inbox).getByTestId('switch-wrapNotificationTitle')).toBeInTheDocument();
+ expect(within(inbox).queryByTestId('switch-showNotifications')).not.toBeInTheDocument();
expect(
- within(appearance).queryByTestId('checkbox-wrapNotificationTitle'),
- ).not.toBeInTheDocument();
- expect(within(alerts).getByTestId('checkbox-showNotifications')).toBeInTheDocument();
- expect(within(alerts).getByTestId('checkbox-showUpdateNotifications')).toBeInTheDocument();
- expect(within(alerts).queryByTestId('checkbox-showPills')).not.toBeInTheDocument();
- expect(within(system).queryByTestId('checkbox-showNotifications')).not.toBeInTheDocument();
- expect(
- within(system).queryByTestId('checkbox-showUpdateNotifications'),
+ within(appearance).queryByTestId('switch-wrapNotificationTitle'),
).not.toBeInTheDocument();
+ expect(within(alerts).getByTestId('switch-showNotifications')).toBeInTheDocument();
+ expect(within(alerts).getByTestId('switch-showUpdateNotifications')).toBeInTheDocument();
+ expect(within(alerts).queryByTestId('switch-showPills')).not.toBeInTheDocument();
+ expect(within(system).queryByTestId('switch-showNotifications')).not.toBeInTheDocument();
+ expect(within(system).queryByTestId('switch-showUpdateNotifications')).not.toBeInTheDocument();
});
it('should go back by pressing the icon', async () => {
diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark-classic-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark-classic-chromium-linux.png
index 9293aeb4f..6e95d87c6 100644
Binary files a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark-classic-chromium-linux.png and b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark-classic-chromium-linux.png differ
diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark-glass-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark-glass-chromium-linux.png
index 33c8f6f7c..53f7b6655 100644
Binary files a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark-glass-chromium-linux.png and b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark-glass-chromium-linux.png differ
diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_colorblind-classic-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_colorblind-classic-chromium-linux.png
index 66c34f015..4b120d09a 100644
Binary files a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_colorblind-classic-chromium-linux.png and b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_colorblind-classic-chromium-linux.png differ
diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_dimmed-classic-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_dimmed-classic-chromium-linux.png
index 4961d2a84..c95515e22 100644
Binary files a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_dimmed-classic-chromium-linux.png and b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_dimmed-classic-chromium-linux.png differ
diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_tritanopia-classic-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_tritanopia-classic-chromium-linux.png
index 40244cd26..39d566381 100644
Binary files a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_tritanopia-classic-chromium-linux.png and b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_tritanopia-classic-chromium-linux.png differ
diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light-classic-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light-classic-chromium-linux.png
index edab70874..947aad1c4 100644
Binary files a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light-classic-chromium-linux.png and b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light-classic-chromium-linux.png differ
diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light-glass-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light-glass-chromium-linux.png
index 2535b6f04..6390cd7e2 100644
Binary files a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light-glass-chromium-linux.png and b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light-glass-chromium-linux.png differ
diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light_colorblind-classic-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light_colorblind-classic-chromium-linux.png
index 6aa6807eb..bc8d1ae6c 100644
Binary files a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light_colorblind-classic-chromium-linux.png and b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light_colorblind-classic-chromium-linux.png differ
diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light_tritanopia-classic-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light_tritanopia-classic-chromium-linux.png
index c52a971b2..fd60fcafe 100644
Binary files a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light_tritanopia-classic-chromium-linux.png and b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light_tritanopia-classic-chromium-linux.png differ
diff --git a/src/renderer/routes/__snapshots__/Filters.test.tsx.snap b/src/renderer/routes/__snapshots__/Filters.test.tsx.snap
index 493062459..43c6ec0d8 100644
--- a/src/renderer/routes/__snapshots__/Filters.test.tsx.snap
+++ b/src/renderer/routes/__snapshots__/Filters.test.tsx.snap
@@ -88,7 +88,7 @@ exports[`renderer/routes/Filters.tsx > General > should render itself & its chil