From 30ceed7204ffcdac975d15ad8410119caf2dd61f Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Wed, 28 Oct 2026 19:19:38 -0400 Subject: [PATCH] fix(a11y): give help buttons and scope status icons accessible names Help buttons announced their internal id (e.g. "tooltip-search"); they now read "Help: - {tooltip && } + {tooltip && } {counter !== undefined && ( { it('lists the supported search qualifiers including commenter', async () => { renderWithProviders(); - const tooltipIconElement = screen.getByTestId('tooltip-icon-tooltip-search'); + const tooltipIconElement = screen.getByRole('button', { name: 'Help: Search' }); await userEvent.click(tooltipIconElement); expect(screen.getByText('Author (author:handle)')).toBeInTheDocument(); @@ -30,7 +30,7 @@ describe('renderer/components/filters/SearchFilter.tsx', () => { it('marks qualifiers that require detailed notifications as disabled', async () => { renderWithProviders(, { settings: { detailedNotifications: false } }); - const tooltipIconElement = screen.getByTestId('tooltip-icon-tooltip-search'); + const tooltipIconElement = screen.getByRole('button', { name: 'Help: Search' }); await userEvent.click(tooltipIconElement); expect(screen.getByText('Author (author:handle)')).toHaveAttribute('aria-disabled', 'true'); diff --git a/src/renderer/components/primitives/HelpPopover.test.tsx b/src/renderer/components/primitives/HelpPopover.test.tsx index 85954a017..4d57e3e1c 100644 --- a/src/renderer/components/primitives/HelpPopover.test.tsx +++ b/src/renderer/components/primitives/HelpPopover.test.tsx @@ -8,20 +8,21 @@ import { HelpPopover, type HelpPopoverProps } from './HelpPopover'; describe('renderer/components/primitives/HelpPopover.tsx', () => { const props: HelpPopoverProps = { name: 'test', + label: 'Test setting', content: 'This is some help text', }; it('renders', () => { renderWithProviders(); - expect(screen.getByTestId('tooltip-icon-test')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Help: Test setting' })).toBeInTheDocument(); expect(screen.queryByText(props.content as string)).not.toBeInTheDocument(); }); it('toggles (show/hide) help on clicking the icon', async () => { renderWithProviders(); - const icon = screen.getByTestId('tooltip-icon-test'); + const icon = screen.getByRole('button', { name: 'Help: Test setting' }); await userEvent.click(icon); expect(screen.getByText(props.content as string)).toBeInTheDocument(); @@ -43,7 +44,7 @@ describe('renderer/components/primitives/HelpPopover.tsx', () => { Object.defineProperty(scrollContainer, 'clientHeight', { configurable: true, value: 100 }); Object.defineProperty(scrollContainer, 'scrollHeight', { configurable: true, value: 400 }); - await userEvent.click(screen.getByTestId('tooltip-icon-test')); + await userEvent.click(screen.getByRole('button', { name: 'Help: Test setting' })); expect(screen.getByText(props.content as string)).toBeInTheDocument(); fireEvent.scroll(scrollContainer); @@ -56,7 +57,7 @@ describe('renderer/components/primitives/HelpPopover.tsx', () => { it('hides help when clicking outside', async () => { renderWithProviders(); - const icon = screen.getByTestId('tooltip-icon-test'); + const icon = screen.getByRole('button', { name: 'Help: Test setting' }); await userEvent.click(icon); expect(screen.getByText(props.content as string)).toBeInTheDocument(); diff --git a/src/renderer/components/primitives/HelpPopover.tsx b/src/renderer/components/primitives/HelpPopover.tsx index b9abe52ea..7e736d1a5 100644 --- a/src/renderer/components/primitives/HelpPopover.tsx +++ b/src/renderer/components/primitives/HelpPopover.tsx @@ -7,6 +7,8 @@ import { cn } from 'cn'; export interface HelpPopoverProps { name: string; + /** Accessible name for the help button, e.g. the label of the control it explains. */ + label: string; content: ReactNode | string; } @@ -74,7 +76,7 @@ export function HelpPopover(props: Readonly) { renderAnchor={(anchorProps) => ( } + label={props.children} name={`tooltip-${name}`} /> ); diff --git a/src/renderer/components/settings/AlertSettings.test.tsx b/src/renderer/components/settings/AlertSettings.test.tsx index ad347a9cf..babf904c8 100644 --- a/src/renderer/components/settings/AlertSettings.test.tsx +++ b/src/renderer/components/settings/AlertSettings.test.tsx @@ -35,7 +35,7 @@ describe('renderer/components/settings/AlertSettings.tsx', () => { it('shows detailed help in tooltips', async () => { renderWithProviders(); - await userEvent.click(screen.getByTestId('tooltip-icon-tooltip-playSound')); + await userEvent.click(screen.getByRole('button', { name: 'Help: Play notification sound' })); expect( screen.getByText( @@ -50,7 +50,7 @@ describe('renderer/components/settings/AlertSettings.tsx', () => { expect(screen.getByText('Notification volume')).toBeInTheDocument(); expect(screen.getByTestId('reveal-volume')).toHaveAttribute('data-state', 'open'); - await userEvent.click(screen.getByTestId('tooltip-icon-tooltip-notificationVolume')); + await userEvent.click(screen.getByRole('button', { name: 'Help: Notification volume' })); expect(screen.getByText('Adjust the volume of the notification sound.')).toBeInTheDocument(); }); diff --git a/src/renderer/components/settings/AppearanceSettings.test.tsx b/src/renderer/components/settings/AppearanceSettings.test.tsx index bddc6d877..66a07bebc 100644 --- a/src/renderer/components/settings/AppearanceSettings.test.tsx +++ b/src/renderer/components/settings/AppearanceSettings.test.tsx @@ -127,21 +127,21 @@ describe('renderer/components/settings/AppearanceSettings.tsx', () => { renderWithProviders(); }); - await userEvent.click(screen.getByTestId('tooltip-icon-tooltip-designLanguage')); + await userEvent.click(screen.getByRole('button', { name: 'Help: Design' })); 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')); + await userEvent.click(screen.getByRole('button', { name: 'Help: 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')); + await userEvent.click(screen.getByRole('button', { name: 'Help: Zoom' })); expect( screen.getByText( '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 7809cf4b2..4b1868454 100644 --- a/src/renderer/components/settings/InboxSettings.test.tsx +++ b/src/renderer/components/settings/InboxSettings.test.tsx @@ -259,7 +259,7 @@ describe('renderer/components/settings/InboxSettings.tsx', () => { }); }); - await userEvent.click(screen.getByTestId('tooltip-icon-tooltip-showOnlyParticipating')); + await userEvent.click(screen.getByRole('button', { name: 'Help: Fetch only participating' })); await userEvent.click( screen.getByTitle('Open GitHub documentation for participating and watching notifications'), @@ -272,19 +272,19 @@ describe('renderer/components/settings/InboxSettings.tsx', () => { }); 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) => { + ['groupBy', 'Group by'], + ['showAccountHeader', 'Show account header'], + ['wrapNotificationTitle', 'Show full notification title'], + ['showNumber', 'Show reference'], + ['delayNotificationState', 'Delay notification removal'], + ] as const)('renders the %s tooltip', async (_name, label) => { await act(async () => { renderWithProviders(, { accounts: [mockGitHubCloudAccount], }); }); - expect(screen.getByTestId(`tooltip-icon-${tooltipName}`)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: `Help: ${label}` })).toBeInTheDocument(); }); it('shows the fetch interval details in a tooltip', async () => { @@ -292,7 +292,7 @@ describe('renderer/components/settings/InboxSettings.tsx', () => { renderWithProviders(); }); - await userEvent.click(screen.getByTestId('tooltip-icon-tooltip-fetchInterval')); + await userEvent.click(screen.getByRole('button', { name: 'Help: Fetch interval' })); expect( screen.getByText( diff --git a/src/renderer/components/settings/SystemSettings.test.tsx b/src/renderer/components/settings/SystemSettings.test.tsx index 78bafde3e..c71a6d3c5 100644 --- a/src/renderer/components/settings/SystemSettings.test.tsx +++ b/src/renderer/components/settings/SystemSettings.test.tsx @@ -131,7 +131,7 @@ describe('renderer/components/settings/SystemSettings.tsx', () => { expect(screen.getByText('Global shortcut')).toBeInTheDocument(); - await userEvent.click(screen.getByTestId('tooltip-icon-tooltip-shortcut')); + await userEvent.click(screen.getByRole('button', { name: 'Help: Global shortcut' })); expect(screen.getByText('Shortcuts must include:')).toBeInTheDocument(); }); diff --git a/src/renderer/components/settings/controls/SettingsChoice.tsx b/src/renderer/components/settings/controls/SettingsChoice.tsx index 918f9a241..57be7ef4e 100644 --- a/src/renderer/components/settings/controls/SettingsChoice.tsx +++ b/src/renderer/components/settings/controls/SettingsChoice.tsx @@ -144,7 +144,7 @@ export function SettingsChoice({ > {label} - {tooltip && } + {tooltip && }
{control}
diff --git a/src/renderer/components/settings/controls/SettingsRow.tsx b/src/renderer/components/settings/controls/SettingsRow.tsx index 907e0ad60..8b52c18bf 100644 --- a/src/renderer/components/settings/controls/SettingsRow.tsx +++ b/src/renderer/components/settings/controls/SettingsRow.tsx @@ -44,7 +44,7 @@ export function SettingsRow({ {label} - {tooltip && } + {tooltip && }
{children}
diff --git a/src/renderer/components/settings/controls/SettingsToggle.tsx b/src/renderer/components/settings/controls/SettingsToggle.tsx index f72fb35b1..c10f01c28 100644 --- a/src/renderer/components/settings/controls/SettingsToggle.tsx +++ b/src/renderer/components/settings/controls/SettingsToggle.tsx @@ -57,7 +57,7 @@ export function SettingsToggle({ > {label} - {tooltip && } + {tooltip && } {caption && ( diff --git a/src/renderer/icons/ScopeStatusIcon.test.tsx b/src/renderer/icons/ScopeStatusIcon.test.tsx index a924dc4b3..d5674c07f 100644 --- a/src/renderer/icons/ScopeStatusIcon.test.tsx +++ b/src/renderer/icons/ScopeStatusIcon.test.tsx @@ -11,14 +11,9 @@ describe('renderer/icons/ScopeStatusIcon.tsx', () => { expect(tree.container).toMatchSnapshot(); }); - it('renders a success icon with test id when withTestId is true', () => { - renderWithProviders(); - expect(screen.getByTestId('account-scopes-scope-granted')).toBeInTheDocument(); - }); - - it('does not render a test id by default', () => { + it('announces the granted status', () => { renderWithProviders(); - expect(screen.queryByTestId('account-scopes-scope-granted')).not.toBeInTheDocument(); + expect(screen.getByRole('img', { name: 'granted' })).toBeInTheDocument(); }); }); @@ -28,14 +23,9 @@ describe('renderer/icons/ScopeStatusIcon.tsx', () => { expect(tree.container).toMatchSnapshot(); }); - it('renders a danger icon with test id when withTestId is true', () => { - renderWithProviders(); - expect(screen.getByTestId('account-scopes-scope-missing')).toBeInTheDocument(); - }); - - it('does not render a test id by default', () => { + it('announces the missing status', () => { renderWithProviders(); - expect(screen.queryByTestId('account-scopes-scope-missing')).not.toBeInTheDocument(); + expect(screen.getByRole('img', { name: 'missing' })).toBeInTheDocument(); }); }); @@ -45,24 +35,17 @@ describe('renderer/icons/ScopeStatusIcon.tsx', () => { expect(tree.container).toMatchSnapshot(); }); - it('renders a dash icon with test id when withTestId is true', () => { - renderWithProviders( - , - ); - expect(screen.getByTestId('account-scopes-scope-na')).toBeInTheDocument(); - }); - - it('does not render a test id by default', () => { - renderWithProviders(); - expect(screen.queryByTestId('account-scopes-scope-na')).not.toBeInTheDocument(); + it('ignores granted=true when notApplicable is true', () => { + renderWithProviders(); + expect(screen.getByRole('img', { name: 'not applicable' })).toBeInTheDocument(); + expect(screen.queryByRole('img', { name: 'granted' })).not.toBeInTheDocument(); }); + }); - it('ignores granted=true when notApplicable is true', () => { - renderWithProviders( - , - ); - expect(screen.getByTestId('account-scopes-scope-na')).toBeInTheDocument(); - expect(screen.queryByTestId('account-scopes-scope-granted')).not.toBeInTheDocument(); + describe('subject prop', () => { + it('prefixes the subject to the accessible name', () => { + renderWithProviders(); + expect(screen.getByRole('img', { name: 'repo missing' })).toBeInTheDocument(); }); }); diff --git a/src/renderer/icons/ScopeStatusIcon.tsx b/src/renderer/icons/ScopeStatusIcon.tsx index a666fe0c0..f345f938e 100644 --- a/src/renderer/icons/ScopeStatusIcon.tsx +++ b/src/renderer/icons/ScopeStatusIcon.tsx @@ -4,36 +4,29 @@ export interface ScopeStatusIconProps { granted: boolean; notApplicable?: boolean; size?: number; - withTestId?: boolean; + /** What the status describes (e.g. a scope name), prefixed to the accessible name. */ + subject?: string; } export function ScopeStatusIcon({ granted, notApplicable = false, size = 14, - withTestId = false, + subject, }: Readonly) { + let status = granted ? 'granted' : 'missing'; if (notApplicable) { - return ( - - ); + status = 'not applicable'; + } + const ariaLabel = subject ? `${subject} ${status}` : status; + + if (notApplicable) { + return ; } return granted ? ( - + ) : ( - + ); } diff --git a/src/renderer/icons/__snapshots__/ScopeStatusIcon.test.tsx.snap b/src/renderer/icons/__snapshots__/ScopeStatusIcon.test.tsx.snap index 5b40f633f..ac709ad7e 100644 --- a/src/renderer/icons/__snapshots__/ScopeStatusIcon.test.tsx.snap +++ b/src/renderer/icons/__snapshots__/ScopeStatusIcon.test.tsx.snap @@ -3,7 +3,7 @@ exports[`renderer/icons/ScopeStatusIcon.tsx > granted state > renders a success icon when granted 1`] = `