]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21808 Add the failed badge on software quality cards
authorMathieu Suen <mathieu.suen@sonarsource.com>
Mon, 18 Mar 2024 11:20:14 +0000 (12:20 +0100)
committersonartech <sonartech@sonarsource.com>
Mon, 18 Mar 2024 20:02:30 +0000 (20:02 +0000)
server/sonar-web/design-system/src/components/Badge.tsx
server/sonar-web/design-system/src/components/__tests__/Badge-test.tsx
server/sonar-web/src/main/js/apps/overview/branches/OverallCodeMeasuresPanel.tsx
server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureCard.tsx
server/sonar-web/src/main/js/apps/overview/branches/__tests__/ActivityPanel-it.tsx
server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx
server/sonar-web/src/main/js/apps/overview/branches/test-utils.ts
server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCard-test.tsx
server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 54ed4d622896fd03331fecc0c42105c69db97890..1746a417d0c4d9f938e0183ed2607a414cd59414 100644 (file)
@@ -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,
   };
 
index 97f901852d8448b4db1646a9f64a7f5512d8c492..5429ccc2a16c70497a3d25c93cb83915aa75c4b6 100644 (file)
@@ -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');
 });
index 5530d595dac33eee9c5383f5fd55b96fa6c3919e..d0ab2c0b2504f00951616daefc127fda956831a0 100644 (file)
@@ -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}
index 9115a05f775a885932bde8f65940ec90f69ad516..b98ec5f9d902a586a8629ee25c7485d7ad4b4eb7 100644 (file)
@@ -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">
index c76a55708f328e356b9e742413cfa1cd15afa13f..45dddc7b81e85d727096ad2399c58b22498b674b 100644 (file)
@@ -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();
index d4ffff4011fb98fce684140ae2e9e9bc9ab01083..8a7067c81d238b7b203df62383d17005396befa7 100644 (file)
@@ -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,
index 21925f6e06673f023bed696c4cec22bd6ae491aa..17914108eeb45b0cdd05346207197bc2420f3583 100644 (file)
@@ -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()),
index 3167be6de6264040c260cdc58282452494c51926..a81aab52c1777fb395b95193439feecb776fd3c7 100644 (file)
@@ -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', () => {
index d116e14daf8d04c92fd74732d9c0a54a03dbf3a9..ce90ff695371b66ef74223714e794c27d8e20f1b 100644 (file)
@@ -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 () => {
index 8286d2f65aad20f8d6f6d0635cfd8e16862ac25b..2e222a4f9852b644feca96f1e43684c1bcdf5f84 100644 (file)
@@ -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 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