@@ -32,8 +32,7 @@ const variantList: Record<BadgeVariant, ThemeColors> = { | |||
counterFailed: 'badgeCounterFailed', | |||
}; | |||
interface BadgeProps { | |||
children: string | number; | |||
interface BadgeProps extends React.PropsWithChildren { | |||
className?: string; | |||
title?: string; | |||
variant?: BadgeVariant; | |||
@@ -41,9 +40,9 @@ interface BadgeProps { | |||
export function Badge({ className, children, title, variant = 'default' }: BadgeProps) { | |||
const commonProps = { | |||
'aria-label': title ?? children.toString(), | |||
'aria-label': title, | |||
className, | |||
role: 'status', | |||
role: title ? 'img' : 'presentation', | |||
title, | |||
}; | |||
@@ -23,10 +23,14 @@ import { Badge } from '../Badge'; | |||
it('renders badge correctly', () => { | |||
render(<Badge>foo</Badge>); | |||
expect(screen.getByRole('status')).toBeInTheDocument(); | |||
expect(screen.getByText('foo')).toBeInTheDocument(); | |||
}); | |||
it('renders counter correctly', () => { | |||
render(<Badge variant="counter">23</Badge>); | |||
expect(screen.getByRole('status')).toHaveAttribute('aria-label', '23'); | |||
render( | |||
<Badge title="This 23" variant="counter"> | |||
23 | |||
</Badge>, | |||
); | |||
expect(screen.getByRole('img')).toHaveAccessibleName('This 23'); | |||
}); |
@@ -70,6 +70,7 @@ export default function OverallCodeMeasuresPanel(props: Readonly<OverallCodeMeas | |||
<SoftwareImpactMeasureCard | |||
branch={branch} | |||
component={component} | |||
conditions={conditions} | |||
softwareQuality={SoftwareQuality.Security} | |||
ratingMetricKey={MetricKey.security_rating} | |||
measures={measures} | |||
@@ -77,6 +78,7 @@ export default function OverallCodeMeasuresPanel(props: Readonly<OverallCodeMeas | |||
<SoftwareImpactMeasureCard | |||
branch={branch} | |||
component={component} | |||
conditions={conditions} | |||
softwareQuality={SoftwareQuality.Reliability} | |||
ratingMetricKey={MetricKey.reliability_rating} | |||
measures={measures} | |||
@@ -84,6 +86,7 @@ export default function OverallCodeMeasuresPanel(props: Readonly<OverallCodeMeas | |||
<SoftwareImpactMeasureCard | |||
branch={branch} | |||
component={component} | |||
conditions={conditions} | |||
softwareQuality={SoftwareQuality.Maintainability} | |||
ratingMetricKey={MetricKey.sqale_rating} | |||
measures={measures} |
@@ -20,9 +20,9 @@ | |||
import styled from '@emotion/styled'; | |||
import { LinkHighlight, LinkStandalone } from '@sonarsource/echoes-react'; | |||
import classNames from 'classnames'; | |||
import { BasicSeparator, LightGreyCard, TextBold, TextSubdued } from 'design-system'; | |||
import { Badge, BasicSeparator, LightGreyCard, TextBold, TextSubdued } from 'design-system'; | |||
import * as React from 'react'; | |||
import { useIntl } from 'react-intl'; | |||
import { FormattedMessage, useIntl } from 'react-intl'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils'; | |||
import { | |||
@@ -39,14 +39,16 @@ import { | |||
SoftwareQuality, | |||
} from '../../../types/clean-code-taxonomy'; | |||
import { MetricKey, MetricType } from '../../../types/metrics'; | |||
import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates'; | |||
import { Component, MeasureEnhanced } from '../../../types/types'; | |||
import { OverviewDisabledLinkTooltip } from '../components/OverviewDisabledLinkTooltip'; | |||
import { softwareQualityToMeasure } from '../utils'; | |||
import { Status, softwareQualityToMeasure } from '../utils'; | |||
import SoftwareImpactMeasureBreakdownCard from './SoftwareImpactMeasureBreakdownCard'; | |||
import SoftwareImpactMeasureRating from './SoftwareImpactMeasureRating'; | |||
export interface SoftwareImpactBreakdownCardProps { | |||
component: Component; | |||
conditions: QualityGateStatusConditionEnhanced[]; | |||
softwareQuality: SoftwareQuality; | |||
ratingMetricKey: MetricKey; | |||
measures: MeasureEnhanced[]; | |||
@@ -54,7 +56,7 @@ export interface SoftwareImpactBreakdownCardProps { | |||
} | |||
export function SoftwareImpactMeasureCard(props: Readonly<SoftwareImpactBreakdownCardProps>) { | |||
const { component, softwareQuality, ratingMetricKey, measures, branch } = props; | |||
const { component, conditions, softwareQuality, ratingMetricKey, measures, branch } = props; | |||
const intl = useIntl(); | |||
@@ -92,12 +94,21 @@ export function SoftwareImpactMeasureCard(props: Readonly<SoftwareImpactBreakdow | |||
intl.formatMessage({ id: 'overview.measures.software_impact.count_tooltip' }) | |||
); | |||
const failed = conditions.some((c) => c.level === Status.ERROR && c.metric === ratingMetricKey); | |||
return ( | |||
<LightGreyCard | |||
data-testid={`overview__software-impact-card-${softwareQuality}`} | |||
className="sw-w-1/3 sw-overflow-hidden sw-rounded-2 sw-p-4 sw-flex-col" | |||
> | |||
<TextBold name={intl.formatMessage({ id: `software_quality.${softwareQuality}` })} /> | |||
<div className="sw-flex sw-justify-between"> | |||
<TextBold name={intl.formatMessage({ id: `software_quality.${softwareQuality}` })} /> | |||
{failed && ( | |||
<Badge className="sw-h-fit" variant="deleted"> | |||
<FormattedMessage id="overview.measures.failed_badge" /> | |||
</Badge> | |||
)} | |||
</div> | |||
<BasicSeparator className="sw--mx-4" /> | |||
<div className="sw-flex sw-flex-col sw-gap-3"> | |||
<div className="sw-flex sw-mt-2"> |
@@ -46,7 +46,7 @@ it('should render correctly', async () => { | |||
expect(await screen.findAllByText('metric.level.ERROR')).toHaveLength(2); | |||
expect(screen.getAllByText('metric.level.OK')).toHaveLength(2); | |||
expect(screen.getByRole('status', { name: 'v1.0' })).toBeInTheDocument(); | |||
expect(screen.getByText('v1.0')).toBeInTheDocument(); | |||
expect(screen.getByText(/event.category.OTHER/)).toBeInTheDocument(); | |||
expect(screen.getByText(/event.category.DEFINITION_CHANGE/)).toBeInTheDocument(); | |||
expect(screen.getByText('event.sqUpgrade10.2')).toBeInTheDocument(); |
@@ -262,6 +262,21 @@ describe('project overview', () => { | |||
// eslint-disable-next-line jest/expect-expect | |||
it('should render software impact measure cards', async () => { | |||
qualityGatesHandler.setQualityGateProjectStatus( | |||
mockQualityGateProjectStatus({ | |||
status: 'ERROR', | |||
conditions: [ | |||
{ | |||
actualValue: '2', | |||
comparator: 'GT', | |||
errorThreshold: '1', | |||
metricKey: MetricKey.reliability_rating, | |||
periodIndex: 1, | |||
status: 'ERROR', | |||
}, | |||
], | |||
}), | |||
); | |||
const { user, ui } = getPageObjects(); | |||
renderBranchOverview(); | |||
@@ -288,6 +303,8 @@ describe('project overview', () => { | |||
[SoftwareImpactSeverity.Low]: 1, | |||
}, | |||
[false, true, false], | |||
undefined, | |||
true, | |||
); | |||
ui.expectSoftwareImpactMeasureCard( | |||
SoftwareQuality.Maintainability, |
@@ -40,7 +40,16 @@ export const getPageObjects = () => { | |||
data?: SoftwareImpactMeasureData, | |||
severitiesActiveState?: boolean[], | |||
branch = 'master', | |||
failed = false, | |||
) => { | |||
if (failed) { | |||
expect( | |||
byTestId(`overview__software-impact-card-${softwareQuality}`) | |||
.byText('overview.measures.failed_badge') | |||
.get(), | |||
).toBeInTheDocument(); | |||
} | |||
if (typeof rating === 'string') { | |||
expect( | |||
byText(rating, { exact: true }).get(ui.softwareImpactMeasureCard(softwareQuality).get()), |
@@ -65,7 +65,7 @@ it('should display tags', async () => { | |||
it('should display private badge', () => { | |||
const project: Project = { ...PROJECT, visibility: Visibility.Private }; | |||
renderProjectCard(project); | |||
expect(screen.getByLabelText('visibility.private')).toBeInTheDocument(); | |||
expect(screen.getByText('visibility.private')).toBeInTheDocument(); | |||
}); | |||
it('should display configure analysis button for logged in user and scan rights', () => { | |||
@@ -81,7 +81,7 @@ it('should not display configure analysis button for logged in user and without | |||
it('should display applications', () => { | |||
renderProjectCard({ ...PROJECT, qualifier: ComponentQualifier.Application }); | |||
expect(screen.getByLabelText('qualifier.APP')).toBeInTheDocument(); | |||
expect(screen.getAllByText('qualifier.APP')).toHaveLength(2); | |||
}); | |||
it('should not display awaiting analysis badge and do not display old measures', () => { | |||
@@ -97,7 +97,7 @@ it('should not display awaiting analysis badge and do not display old measures', | |||
[MetricKey.vulnerabilities]: '6', | |||
}, | |||
}); | |||
expect(screen.queryByRole('status', { name: 'projects.awaiting_scan' })).not.toBeInTheDocument(); | |||
expect(screen.queryByText('projects.awaiting_scan')).not.toBeInTheDocument(); | |||
expect(screen.getByText('1')).toBeInTheDocument(); | |||
expect(screen.getByText('2')).toBeInTheDocument(); | |||
expect(screen.getByText('3')).toBeInTheDocument(); | |||
@@ -116,10 +116,10 @@ it('should display awaiting analysis badge and show the old measures', async () | |||
[MetricKey.vulnerabilities]: '6', | |||
}, | |||
}); | |||
expect(screen.getByRole('status', { name: 'projects.awaiting_scan' })).toBeInTheDocument(); | |||
await expect( | |||
screen.getByRole('status', { name: 'projects.awaiting_scan' }), | |||
).toHaveATooltipWithContent('projects.awaiting_scan.description.TRK'); | |||
expect(screen.getByText('projects.awaiting_scan')).toBeInTheDocument(); | |||
await expect(screen.getByText('projects.awaiting_scan')).toHaveATooltipWithContent( | |||
'projects.awaiting_scan.description.TRK', | |||
); | |||
expect(screen.getByText('4')).toBeInTheDocument(); | |||
expect(screen.getByText('5')).toBeInTheDocument(); | |||
expect(screen.getByText('6')).toBeInTheDocument(); | |||
@@ -136,10 +136,10 @@ it('should display awaiting analysis badge and show the old measures for Applica | |||
[MetricKey.vulnerabilities]: '6', | |||
}, | |||
}); | |||
expect(screen.getByRole('status', { name: 'projects.awaiting_scan' })).toBeInTheDocument(); | |||
await expect( | |||
screen.getByRole('status', { name: 'projects.awaiting_scan' }), | |||
).toHaveATooltipWithContent('projects.awaiting_scan.description.APP'); | |||
expect(screen.getByText('projects.awaiting_scan')).toBeInTheDocument(); | |||
await expect(screen.getByText('projects.awaiting_scan')).toHaveATooltipWithContent( | |||
'projects.awaiting_scan.description.APP', | |||
); | |||
expect(screen.getByText('4')).toBeInTheDocument(); | |||
expect(screen.getByText('5')).toBeInTheDocument(); | |||
expect(screen.getByText('6')).toBeInTheDocument(); | |||
@@ -150,7 +150,7 @@ it('should not display awaiting analysis badge if project is not analyzed', () = | |||
...PROJECT, | |||
analysisDate: undefined, | |||
}); | |||
expect(screen.queryByRole('status', { name: 'projects.awaiting_scan' })).not.toBeInTheDocument(); | |||
expect(screen.queryByText('projects.awaiting_scan')).not.toBeInTheDocument(); | |||
}); | |||
it('should not display awaiting analysis badge if project does not have lines of code', () => { | |||
@@ -160,12 +160,12 @@ it('should not display awaiting analysis badge if project does not have lines of | |||
...(({ [MetricKey.ncloc]: _, ...rest }) => rest)(MEASURES), | |||
}, | |||
}); | |||
expect(screen.queryByRole('status', { name: 'projects.awaiting_scan' })).not.toBeInTheDocument(); | |||
expect(screen.queryByText('projects.awaiting_scan')).not.toBeInTheDocument(); | |||
}); | |||
it('should not display awaiting analysis badge if it is a new code filter', () => { | |||
renderProjectCard(PROJECT, undefined, 'leak'); | |||
expect(screen.queryByRole('status', { name: 'projects.awaiting_scan' })).not.toBeInTheDocument(); | |||
expect(screen.queryByText('projects.awaiting_scan')).not.toBeInTheDocument(); | |||
}); | |||
it('should display 3 aplication', () => { |
@@ -71,7 +71,7 @@ describe('rendering', () => { | |||
it('should render correctly for external rule engines', () => { | |||
renderIssue({ issue: mockIssue(true, { externalRuleEngine: 'ESLINT' }) }); | |||
expect(screen.getByRole('status', { name: 'ESLINT' })).toBeInTheDocument(); | |||
expect(screen.getByText('ESLINT')).toBeInTheDocument(); | |||
}); | |||
it('should render the SonarLint icon correctly', async () => { |
@@ -3930,7 +3930,7 @@ overview.accepted_issues.total=Total accepted issues | |||
overview.high_impact_accepted_issues=High impact accepted issues | |||
overview.measures.empty_explanation=Measures on New Code will appear after the second analysis of this branch. | |||
overview.measures.empty_link={learn_more_link} about the Clean as You Code approach. | |||
overview.measures.same_reference.explanation=This branch is configured to use itself as reference branch. It will never have New Code. | |||
overview.measures.same_reference.explanation=This branch is configured to use itself as a reference branch. It will never have New Code. | |||
overview.measures.bad_reference.explanation=This branch could not be compared to its reference branch. See the SCM or analysis report for more details. | |||
overview.measures.bad_setting.link=This can be fixed in the {setting_link} setting. | |||
overview.measures.security_hotspots_reviewed=Reviewed | |||
@@ -4018,7 +4018,7 @@ overview.sonarlint_ad.details_3=Repair flagged issues in real-time with quick fi | |||
overview.sonarlint_ad.details_4=12 major IDE's supported (including key JetBrains and Microsoft IDE's | |||
overview.sonarlint_ad.details_5=Free forever | |||
overview.sonarlint_ad.learn_more=Learn more about SonarLint | |||
overview.sonarlint_ad.close_promotion=Close SonarLint romotion | |||
overview.sonarlint_ad.close_promotion=Close SonarLint promotion | |||
overview.badges.get_badge=Badges | |||
overview.badges.title=Get project badges |