diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2023-06-02 12:52:05 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-06-05 20:02:48 +0000 |
commit | 3722d4b6b7409e77da6016308f434a190fe503c0 (patch) | |
tree | c5411d835839826419eb5552ab984da5f64e7744 /server | |
parent | 81400607613b2a4a2bb4bcee875eb39c805b77c1 (diff) | |
download | sonarqube-3722d4b6b7409e77da6016308f434a190fe503c0.tar.gz sonarqube-3722d4b6b7409e77da6016308f434a190fe503c0.zip |
SONAR-19391 Update ITs and improve a11y
Diffstat (limited to 'server')
8 files changed, 139 insertions, 117 deletions
diff --git a/server/sonar-web/design-system/src/components/TreeMapRect.tsx b/server/sonar-web/design-system/src/components/TreeMapRect.tsx index a52879fd9e5..8fb0d3dcc5c 100644 --- a/server/sonar-web/design-system/src/components/TreeMapRect.tsx +++ b/server/sonar-web/design-system/src/components/TreeMapRect.tsx @@ -22,7 +22,6 @@ import { scaleLinear } from 'd3-scale'; import React from 'react'; import tw from 'twin.macro'; import { themeColor } from '../helpers'; -import { Key } from '../helpers/keyboard'; import { BasePlacement, PopupPlacement } from '../helpers/positioning'; import Tooltip from './Tooltip'; @@ -49,63 +48,60 @@ interface Props { } export function TreeMapRect(props: Props) { - function handleRectClick() { - props.onClick(props.itemKey); - } - - function handleRectKeyDown(event: React.KeyboardEvent<HTMLAnchorElement>) { - if (event.key === Key.Enter) { - props.onClick(props.itemKey); - } - } + const { + placement, + tooltip, + onClick, + itemKey, + x, + y, + width, + height, + fill, + gradient, + label, + icon, + prefix, + } = props; + + const handleRectClick = React.useCallback(() => { + onClick(itemKey); + return false; + }, [onClick, itemKey]); + + const cellStyles = { + left: x, + top: y, + width, + height, + backgroundColor: fill, + backgroundImage: gradient, + fontSize: SIZE_SCALE(width / label.length), + lineHeight: `${height}px`, + }; + const isTextVisible = width >= TEXT_VISIBLE_AT_WIDTH && height >= TEXT_VISIBLE_AT_HEIGHT; + const isIconVisible = width >= ICON_VISIBLE_AT_WIDTH && height >= ICON_VISIBLE_AT_HEIGHT; - function renderCell() { - const cellStyles = { - left: props.x, - top: props.y, - width: props.width, - height: props.height, - backgroundColor: props.fill, - backgroundImage: props.gradient, - fontSize: SIZE_SCALE(props.width / props.label.length), - lineHeight: `${props.height}px`, - }; - const isTextVisible = - props.width >= TEXT_VISIBLE_AT_WIDTH && props.height >= TEXT_VISIBLE_AT_HEIGHT; - const isIconVisible = - props.width >= ICON_VISIBLE_AT_WIDTH && props.height >= ICON_VISIBLE_AT_HEIGHT; - - return ( + return ( + <Tooltip overlay={tooltip} placement={placement ?? PopupPlacement.Left}> <StyledCell style={cellStyles}> - <StyledCellLink - aria-label={props.prefix ? `${props.prefix} ${props.label}` : props.label} - onClick={handleRectClick} - onKeyDown={handleRectKeyDown} - role="link" - tabIndex={0} - > - <StyledCellLabel width={props.width}> - {isIconVisible && <span className="shrink-0">{props.icon}</span>} + <StyledCellLink href="#" onClick={handleRectClick}> + <StyledCellLabel width={width}> + {isIconVisible && <span className="shrink-0">{icon}</span>} {isTextVisible && - (props.prefix ? ( + (prefix ? ( <span className="treemap-text"> - {props.prefix} + {prefix} <br /> - {props.label.substring(props.prefix.length)} + {label.substring(prefix.length)} </span> ) : ( - <span className="treemap-text">{props.label}</span> + <span className="treemap-text">{label}</span> ))} + <StyledA11yHidden>{tooltip}</StyledA11yHidden> </StyledCellLabel> </StyledCellLink> </StyledCell> - ); - } - - const { placement, tooltip } = props; - return ( - <Tooltip overlay={tooltip} placement={placement ?? PopupPlacement.Left}> - {renderCell()} </Tooltip> ); } @@ -148,3 +144,12 @@ const StyledCellLabel = styled.div<{ width: number }>` ${tw`sw-shrink sw-overflow-hidden sw-whitespace-nowrap sw-text-left sw-text-ellipsis`}; } `; + +const StyledA11yHidden = styled.span` + position: absolute !important; + left: -10000px !important; + top: auto !important; + width: 1px !important; + height: 1px !important; + overflow: hidden !important; +`; diff --git a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/TreeMap-test.tsx.snap b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/TreeMap-test.tsx.snap index 7f3703d9986..f9b2a493f78 100644 --- a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/TreeMap-test.tsx.snap +++ b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/TreeMap-test.tsx.snap @@ -78,7 +78,16 @@ exports[`should render correctly and forward click event 1`] = ` text-align: left; } -.emotion-18 { +.emotion-8 { + position: absolute!important; + left: -10000px!important; + top: auto!important; + width: 1px!important; + height: 1px!important; + overflow: hidden!important; +} + +.emotion-22 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -100,7 +109,7 @@ exports[`should render correctly and forward click event 1`] = ` max-width: 17px; } -.emotion-18 .treemap-text { +.emotion-22 .treemap-text { -webkit-flex-shrink: 1; -ms-flex-negative: 1; flex-shrink: 1; @@ -120,10 +129,8 @@ exports[`should render correctly and forward click event 1`] = ` style="left: 0px; top: 0px; width: 83px; height: 60px; background-color: rgb(119, 119, 119); font-size: 12.974358974358974px; line-height: 60px;" > <a - aria-label="SonarQube_Web" class="emotion-4 emotion-5" - role="link" - tabindex="0" + href="#" > <div class="emotion-6 emotion-7" @@ -137,6 +144,9 @@ exports[`should render correctly and forward click event 1`] = ` > SonarQube_Web </span> + <span + class="emotion-8 emotion-9" + /> </div> </a> </li> @@ -145,10 +155,8 @@ exports[`should render correctly and forward click event 1`] = ` style="left: 0px; top: 60px; width: 83px; height: 40px; font-size: 12.276041666666668px; line-height: 40px;" > <a - aria-label="SonarQube_Search" class="emotion-4 emotion-5" - role="link" - tabindex="0" + href="#" > <div class="emotion-6 emotion-7" @@ -157,6 +165,9 @@ exports[`should render correctly and forward click event 1`] = ` <span class="shrink-0" /> + <span + class="emotion-8 emotion-9" + /> </div> </a> </li> @@ -165,15 +176,17 @@ exports[`should render correctly and forward click event 1`] = ` style="left: 83px; top: 0px; width: 17px; height: 100px; background-color: rgb(119, 119, 119); font-size: 11px; line-height: 100px;" > <a - aria-label="SonarQube_Server" class="emotion-4 emotion-5" - role="link" - tabindex="0" + href="#" > <div - class="emotion-18 emotion-7" + class="emotion-22 emotion-7" width="17" - /> + > + <span + class="emotion-8 emotion-9" + /> + </div> </a> </li> </ul> diff --git a/server/sonar-web/design-system/src/components/subnavigation/SubnavigationAccordion.tsx b/server/sonar-web/design-system/src/components/subnavigation/SubnavigationAccordion.tsx index 01e017f100f..747d16a3199 100644 --- a/server/sonar-web/design-system/src/components/subnavigation/SubnavigationAccordion.tsx +++ b/server/sonar-web/design-system/src/components/subnavigation/SubnavigationAccordion.tsx @@ -57,12 +57,7 @@ export function SubnavigationAccordion(props: Props) { }, [finalExpanded, onSetExpanded]); return ( - <SubnavigationGroup - aria-labelledby={`${id}-subnavigation-accordion-button`} - className={className} - id={`${id}-subnavigation-accordion`} - role="region" - > + <SubnavigationGroup className={className}> <SubnavigationAccordionItem aria-controls={`${id}-subnavigation-accordion`} aria-expanded={finalExpanded} @@ -72,7 +67,14 @@ export function SubnavigationAccordion(props: Props) { {header} <OpenCloseIndicator open={finalExpanded} /> </SubnavigationAccordionItem> - {finalExpanded && children} + {finalExpanded && ( + <section + aria-labelledby={`${id}-subnavigation-accordion-button`} + id={`${id}-subnavigation-accordion`} + > + {children} + </section> + )} </SubnavigationGroup> ); } diff --git a/server/sonar-web/design-system/src/components/subnavigation/SubnavigationItem.tsx b/server/sonar-web/design-system/src/components/subnavigation/SubnavigationItem.tsx index 68bbb8593d2..54ff544fdaf 100644 --- a/server/sonar-web/design-system/src/components/subnavigation/SubnavigationItem.tsx +++ b/server/sonar-web/design-system/src/components/subnavigation/SubnavigationItem.tsx @@ -39,7 +39,6 @@ export function SubnavigationItem(props: Props) { }, [onClick, value]); return ( <StyledSubnavigationItem - aria-current={active} className={classNames({ active }, className)} data-testid="js-subnavigation-item" onClick={handleClick} diff --git a/server/sonar-web/design-system/src/components/subnavigation/__tests__/SubnavigationItem-test.tsx b/server/sonar-web/design-system/src/components/subnavigation/__tests__/SubnavigationItem-test.tsx index 66c776e79f7..17fb3e4e51d 100644 --- a/server/sonar-web/design-system/src/components/subnavigation/__tests__/SubnavigationItem-test.tsx +++ b/server/sonar-web/design-system/src/components/subnavigation/__tests__/SubnavigationItem-test.tsx @@ -22,30 +22,18 @@ import { render } from '../../../helpers/testUtils'; import { FCProps } from '../../../types/misc'; import { SubnavigationItem } from '../SubnavigationItem'; -it('should render correctly', () => { - setupWithProps(); - - expect(screen.getByTestId('js-subnavigation-item')).toHaveAttribute('aria-current', 'false'); -}); - -it('should display selected', () => { - setupWithProps({ active: true }); - - expect(screen.getByTestId('js-subnavigation-item')).toHaveAttribute('aria-current', 'true'); -}); - it('should call onClick with value when clicked', async () => { const onClick = jest.fn(); const { user } = setupWithProps({ onClick }); - await user.click(screen.getByTestId('js-subnavigation-item')); + await user.click(screen.getByRole('button')); expect(onClick).toHaveBeenCalledWith('foo'); }); function setupWithProps(props: Partial<FCProps<typeof SubnavigationItem>> = {}) { return render( <SubnavigationItem active={false} onClick={jest.fn()} value="foo" {...props}> - Foo + <button type="button">Foo</button> </SubnavigationItem> ); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx b/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx index e03cf2668b9..88829f455a1 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx @@ -60,7 +60,7 @@ describe('rendering', () => { await ui.appLoaded(); expect(ui.seeDataAsListLink.get()).toBeInTheDocument(); - expect(ui.overviewFacetBtn.get()).toBeChecked(); + expect(ui.overviewFacetBtn.get()).toHaveAttribute('aria-current', 'true'); expect(ui.bubbleChart.get()).toBeInTheDocument(); expect(within(ui.bubbleChart.get()).getAllByRole('link')).toHaveLength(8); expect(ui.newCodePeriodTxt.get()).toBeInTheDocument(); @@ -100,10 +100,10 @@ describe('rendering', () => { renderMeasuresApp('component_measures?id=foo&metric=sqale_rating&view=treemap'); await ui.appLoaded(); - expect(ui.treeMapCells.getAll()).toHaveLength(7); - expect(ui.treeMapCell(/folderA C metric\.sqale_rating\.name/).get()).toBeInTheDocument(); - expect(ui.treeMapCell(/test1\.js B metric\.sqale_rating\.name/).get()).toBeInTheDocument(); - expect(ui.treeMapCell(/index\.tsx A metric\.sqale_rating\.name/).get()).toBeInTheDocument(); + expect(within(ui.treeMap.get()).getAllByRole('link')).toHaveLength(7); + expect(ui.treeMapCell(/folderA .+ Maintainability Rating: C/).get()).toBeInTheDocument(); + expect(ui.treeMapCell(/test1\.js .+ Maintainability Rating: B/).get()).toBeInTheDocument(); + expect(ui.treeMapCell(/index\.tsx .+ Maintainability Rating: A/).get()).toBeInTheDocument(); }); it('should render correctly for an unknown metric', async () => { @@ -202,10 +202,6 @@ describe('rendering', () => { renderMeasuresApp('component_measures?id=foo&metric=sqale_rating&view=list'); await ui.appLoaded(); - await user.click(ui.maintainabilityFacetBtn.get()); - await user.click(ui.metricBtn('Maintainability Rating').get()); - await ui.changeViewToList(); - expect(ui.notShowingAllComponentsTxt.get()).toBeInTheDocument(); await user.click(ui.showAllBtn.get()); expect(ui.notShowingAllComponentsTxt.query()).not.toBeInTheDocument(); @@ -221,7 +217,7 @@ describe('navigation', () => { // Drilldown to the file level. await user.click(ui.maintainabilityFacetBtn.get()); - await user.click(ui.metricBtn('Code Smells').get()); + await user.click(ui.metricBtn('Code Smells 8').get()); expect( within(ui.measuresRow('folderA').get()).getByRole('cell', { name: '3' }) ).toBeInTheDocument(); @@ -252,7 +248,7 @@ describe('navigation', () => { await ui.appLoaded(); await user.click(ui.maintainabilityFacetBtn.get()); - await user.click(ui.metricBtn('Code Smells').get()); + await user.click(ui.metricBtn('Code Smells 8').get()); await ui.changeViewToList(); expect( @@ -272,19 +268,17 @@ describe('navigation', () => { await ui.appLoaded(); await user.click(ui.maintainabilityFacetBtn.get()); - await user.click(ui.metricBtn('Maintainability Rating').get()); + await user.click(ui.metricBtn('Maintainability Rating metric.has_rating_X.E').get()); await ui.changeViewToTreeMap(); expect(ui.treeMapCell(/folderA/).get()).toBeInTheDocument(); expect(ui.treeMapCell(/test1\.js/).get()).toBeInTheDocument(); - // TODO: once the new design is live, change this to target a link rather than clicking on some text. await user.click(ui.treeMapCell(/folderA/).get()); expect(ui.treeMapCell(/out\.tsx/).get()).toBeInTheDocument(); expect(ui.treeMapCell(/in\.tsx/).get()).toBeInTheDocument(); - // TODO: once the new design is live, change this to target a link rather than clicking on some text. await user.click(ui.treeMapCell(/out.tsx/).get()); expect((await ui.sourceCode.findAll()).length).toBeGreaterThan(0); }); @@ -296,7 +290,7 @@ describe('navigation', () => { // Drilldown to the file level. await user.click(ui.maintainabilityFacetBtn.get()); - await user.click(ui.metricBtn('Code Smells').get()); + await user.click(ui.metricBtn('Code Smells 8').get()); // Select "folderA". await ui.arrowDown(); @@ -347,7 +341,7 @@ describe('redirects', () => { const { ui } = getPageObject(); renderMeasuresApp('component_measures/metric/bugs'); await ui.appLoaded(); - expect(ui.metricBtn('Bugs').get()).toBeChecked(); + expect(ui.metricBtn('Bugs 0').get()).toHaveAttribute('aria-current', 'true'); }); it('should redirect old domain route', async () => { @@ -405,27 +399,48 @@ function getPageObject() { ), // Facets - overviewFacetBtn: byRole('checkbox', { - name: 'component_measures.overview.project_overview.facet', + overviewFacetBtn: byRole('button', { + name: 'component_measures.overview.project_overview.subnavigation', + }), + releasabilityFacetBtn: byRole('button', { + name: 'Releasability component_measures.domain_subnavigation.Releasability.help', + }), + reliabilityFacetBtn: byRole('button', { + name: 'Reliability component_measures.domain_subnavigation.Reliability.help', + }), + securityFacetBtn: byRole('button', { + name: 'Security component_measures.domain_subnavigation.Security.help', + }), + securityReviewFacetBtn: byRole('button', { + name: 'SecurityReview component_measures.domain_subnavigation.SecurityReview.help', + }), + maintainabilityFacetBtn: byRole('button', { + name: 'Maintainability component_measures.domain_subnavigation.Maintainability.help', + }), + coverageFacetBtn: byRole('button', { + name: 'Coverage component_measures.domain_subnavigation.Coverage.help', + }), + duplicationsFacetBtn: byRole('button', { + name: 'Duplications component_measures.domain_subnavigation.Duplications.help', + }), + sizeFacetBtn: byRole('button', { + name: 'Size component_measures.domain_subnavigation.Size.help', + }), + complexityFacetBtn: byRole('button', { + name: 'Complexity component_measures.domain_subnavigation.Complexity.help', + }), + issuesFacetBtn: byRole('button', { + name: 'Issues component_measures.domain_subnavigation.Issues.help', }), - releasabilityFacetBtn: byRole('button', { name: 'Releasability' }), - reliabilityFacetBtn: byRole('button', { name: 'Reliability' }), - securityFacetBtn: byRole('button', { name: 'Security' }), - securityReviewFacetBtn: byRole('button', { name: 'SecurityReview' }), - maintainabilityFacetBtn: byRole('button', { name: 'Maintainability' }), - coverageFacetBtn: byRole('button', { name: 'Coverage' }), - duplicationsFacetBtn: byRole('button', { name: 'Duplications' }), - sizeFacetBtn: byRole('button', { name: 'Size' }), - complexityFacetBtn: byRole('button', { name: 'Complexity' }), - issuesFacetBtn: byRole('button', { name: 'Issues' }), - metricBtn: (name: string) => byRole('checkbox', { name }), + metricBtn: (name: string) => byRole('button', { name }), // Measure content measuresTable: byRole('table'), measuresRows: byRole('row'), measuresRow: (name: string) => byRole('row', { name: new RegExp(name) }), - treeMapCells: byRole('treeitem'), - treeMapCell: (name: string | RegExp) => byRole('treeitem', { name }), + treeMap: byTestId('treemap'), + treeMapCells: byRole('link'), + treeMapCell: (name: string | RegExp) => byRole('link', { name }), fileLink: (name: string) => byRole('link', { name }), sourceCode: byText('function Test() {}'), notShowingAllComponentsTxt: byText(/component_measures.hidden_best_score_metrics/), diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx index 07e89c39453..2de67a1381e 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx @@ -205,7 +205,7 @@ export default class TreeMapView extends React.PureComponent<Props, State> { ? components[0].measures.find((measure) => measure.metric.key !== metric.key) : null; return ( - <div className="measure-details-treemap"> + <div className="measure-details-treemap" data-testid="treemap"> <div className="display-flex-start note spacer-bottom"> <span> <strong className="sw-mr-1">{translate('component_measures.legend.color')}</strong> diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx index caa4aa7f631..67075eabf84 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx @@ -102,7 +102,7 @@ export default function Sidebar(props: Props) { /> </FlagMessage> )} - <nav + <section className="sw-flex sw-flex-col sw-gap-4 sw-p-4" aria-label={translate('component_measures.navigation')} > @@ -116,7 +116,7 @@ export default function Sidebar(props: Props) { active={isProjectOverview(selectedMetric)} onClick={handleProjectOverviewClick} > - <BareButton> + <BareButton aria-current={isProjectOverview(selectedMetric)}> {translate('component_measures.overview', PROJECT_OVERVEW, 'subnavigation')} </BareButton> </SubnavigationItem> @@ -132,7 +132,7 @@ export default function Sidebar(props: Props) { showFullMeasures={showFullMeasures} /> ))} - </nav> + </section> </StyledSidebar> ); } |