Browse Source

SONAR-17816 Warn user about non-cayc-compliant QG

tags/9.9.0.65466
Jeremy Davis 1 year ago
parent
commit
d113ac24b7
27 changed files with 505 additions and 5600 deletions
  1. 2
    2
      server/sonar-web/src/main/js/app/components/branch-status/__tests__/BranchStatusContextProvider-test.tsx
  2. 4
    2
      server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx
  3. 1
    3
      server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx
  4. 40
    0
      server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarning.tsx
  5. 5
    4
      server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanel.tsx
  6. 151
    27
      server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx
  7. 119
    169
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx
  8. 0
    47
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverviewRenderer-test.tsx
  9. 32
    6
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/DebtValue-test.tsx
  10. 0
    63
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/MeasuresPanelIssueMeasureRow-test.tsx
  11. 0
    110
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/MeasuresPanelNoNewCode-test.tsx
  12. 0
    46
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/SecurityHotspotsReviewed-test.tsx
  13. 0
    2734
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/BranchOverview-test.tsx.snap
  14. 0
    435
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/BranchOverviewRenderer-test.tsx.snap
  15. 0
    67
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/DebtValue-test.tsx.snap
  16. 0
    1616
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanelIssueMeasureRow-test.tsx.snap
  17. 0
    174
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanelNoNewCode-test.tsx.snap
  18. 11
    15
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanel-test.tsx.snap
  19. 103
    16
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanelSection-test.tsx.snap
  20. 0
    51
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/SecurityHotspotsReviewed-test.tsx.snap
  21. 2
    4
      server/sonar-web/src/main/js/apps/overview/components/QualityGateConditions.tsx
  22. 0
    1
      server/sonar-web/src/main/js/apps/overview/components/__tests__/QualityGateConditions-test.tsx
  23. 9
    8
      server/sonar-web/src/main/js/apps/overview/styles.css
  24. 10
    0
      server/sonar-web/src/main/js/apps/overview/utils.ts
  25. 4
    0
      server/sonar-web/src/main/js/helpers/mocks/quality-gates.ts
  26. 3
    0
      server/sonar-web/src/main/js/types/quality-gates.ts
  27. 9
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 2
- 2
server/sonar-web/src/main/js/app/components/branch-status/__tests__/BranchStatusContextProvider-test.tsx View File

@@ -22,7 +22,7 @@ import * as React from 'react';
import { getQualityGateProjectStatus } from '../../../../api/quality-gates';
import { mockBranch } from '../../../../helpers/mocks/branch-like';
import { waitAndUpdate } from '../../../../helpers/testUtils';
import { QualityGateProjectStatus } from '../../../../types/quality-gates';
import { BranchStatusData } from '../../../../types/branch-like';
import BranchStatusContextProvider from '../BranchStatusContextProvider';

jest.mock('../../../../api/quality-gates', () => ({
@@ -33,7 +33,7 @@ describe('fetchBranchStatus', () => {
it('should get the branch status', async () => {
const projectKey = 'projectKey';
const branchName = 'branch-6.7';
const status: QualityGateProjectStatus = {
const status: BranchStatusData = {
status: 'OK',
conditions: [],
ignoredConditions: false,

+ 4
- 2
server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx View File

@@ -186,12 +186,13 @@ export default class BranchOverview extends React.PureComponent<Props, State> {
(results) => {
if (this.mounted) {
const qgStatuses = results.map(({ measures = [], project, projectBranchLike }) => {
const { key, name, status } = project;
const { key, name, status, isCaycCompliant } = project;
const conditions = extractStatusConditionsFromApplicationStatusChildProject(project);
const failedConditions = this.getFailedConditions(conditions, measures);

return {
failedConditions,
isCaycCompliant,
key,
name,
status,
@@ -238,12 +239,13 @@ export default class BranchOverview extends React.PureComponent<Props, State> {
this.loadMeasuresAndMeta(key, branch, metricKeys).then(
({ measures, metrics, period }) => {
if (this.mounted && measures) {
const { ignoredConditions, status } = projectStatus;
const { ignoredConditions, isCaycCompliant, status } = projectStatus;
const conditions = extractStatusConditionsFromProjectStatus(projectStatus);
const failedConditions = this.getFailedConditions(conditions, measures);

const qgStatus = {
ignoredConditions,
isCaycCompliant,
failedConditions,
key,
name,

+ 1
- 3
server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx View File

@@ -53,7 +53,7 @@ export interface BranchOverviewRendererProps {
qgStatuses?: QualityGateStatus[];
}

export function BranchOverviewRenderer(props: BranchOverviewRendererProps) {
export default function BranchOverviewRenderer(props: BranchOverviewRendererProps) {
const {
analyses,
appLeak,
@@ -131,5 +131,3 @@ export function BranchOverviewRenderer(props: BranchOverviewRendererProps) {
</>
);
}

export default React.memo(BranchOverviewRenderer);

+ 40
- 0
server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarning.tsx View File

@@ -0,0 +1,40 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import Link from '../../../components/common/Link';
import { Alert } from '../../../components/ui/Alert';
import { translate } from '../../../helpers/l10n';

export default function CleanAsYouCodeWarning() {
return (
<>
<Alert variant="warning">{translate('overview.quality_gate.conditions.cayc.warning')}</Alert>
<p className="big-spacer-top big-spacer-bottom">
{translate('overview.quality_gate.conditions.cayc.details')}
</p>
<Link
target="_blank"
to="https://docs.sonarqube.org/latest/user-guide/clean-as-you-code/#quality-gate"
>
{translate('overview.quality_gate.conditions.cayc.link')}
</Link>
</>
);
}

+ 5
- 4
server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanel.tsx View File

@@ -24,6 +24,7 @@ import HelpTooltip from '../../../components/controls/HelpTooltip';
import { Alert } from '../../../components/ui/Alert';
import DeferredSpinner from '../../../components/ui/DeferredSpinner';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { ComponentQualifier } from '../../../types/component';
import { QualityGateStatus } from '../../../types/quality-gates';
import { Component } from '../../../types/types';
import SonarLintPromotion from '../components/SonarLintPromotion';
@@ -51,8 +52,7 @@ export function QualityGatePanel(props: QualityGatePanelProps) {
);

const showIgnoredConditionWarning =
component.qualifier === 'TRK' &&
qgStatuses !== undefined &&
component.qualifier === ComponentQualifier.Project &&
qgStatuses.some((p) => Boolean(p.ignoredConditions));

return (
@@ -82,7 +82,7 @@ export function QualityGatePanel(props: QualityGatePanelProps) {
</Alert>
)}

<div className="overview-panel-content">
<div>
{loading ? (
<div className="overview-panel-big-padded">
<DeferredSpinner loading={loading} />
@@ -107,7 +107,8 @@ export function QualityGatePanel(props: QualityGatePanelProps) {
</span>
</div>

{overallFailedConditionsCount > 0 && (
{(overallFailedConditionsCount > 0 ||
qgStatuses.some(({ isCaycCompliant }) => !isCaycCompliant)) && (
<div data-test="overview__quality-gate-conditions">
{qgStatuses.map((qgStatus) => (
<QualityGatePanelSection

+ 151
- 27
server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx View File

@@ -18,13 +18,22 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
import { ButtonPlain } from '../../../components/controls/buttons';
import ChevronDownIcon from '../../../components/icons/ChevronDownIcon';
import ChevronRightIcon from '../../../components/icons/ChevronRightIcon';
import { Alert } from '../../../components/ui/Alert';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { isDiffMetric } from '../../../helpers/measures';
import { BranchLike } from '../../../types/branch-like';
import { ComponentQualifier } from '../../../types/component';
import { QualityGateStatus } from '../../../types/quality-gates';
import {
QualityGateStatus,
QualityGateStatusConditionEnhanced,
} from '../../../types/quality-gates';
import { Component } from '../../../types/types';
import QualityGateConditions from '../components/QualityGateConditions';
import { CAYC_METRICS } from '../utils';
import CleanAsYouCodeWarning from './CleanAsYouCodeWarning';

export interface QualityGatePanelSectionProps {
branchLike?: BranchLike;
@@ -32,46 +41,161 @@ export interface QualityGatePanelSectionProps {
qgStatus: QualityGateStatus;
}

function splitConditions(conditions: QualityGateStatusConditionEnhanced[]) {
const caycConditions = [];
const newCodeFailedConditions = [];
const overallFailedConditions = [];

for (const condition of conditions) {
if (CAYC_METRICS.includes(condition.metric)) {
caycConditions.push(condition);
} else if (isDiffMetric(condition.metric)) {
newCodeFailedConditions.push(condition);
} else {
overallFailedConditions.push(condition);
}
}

return [caycConditions, newCodeFailedConditions, overallFailedConditions];
}

function displayConditions(conditions: number) {
if (conditions === 0) {
return null;
}

const text =
conditions === 1
? translate('overview.1_condition_failed')
: translateWithParameters('overview.X_conditions_failed', conditions);

return <span className="text-muted big-spacer-left">{text}</span>;
}

export function QualityGatePanelSection(props: QualityGatePanelSectionProps) {
const { component, qgStatus } = props;
const newCodeFailedConditions = qgStatus.failedConditions.filter((c) => isDiffMetric(c.metric));
const overallFailedConditions = qgStatus.failedConditions.filter((c) => !isDiffMetric(c.metric));
const [collapsed, setCollapsed] = React.useState(false);

const toggle = React.useCallback(() => {
setCollapsed(!collapsed);
}, [collapsed]);

if (newCodeFailedConditions.length === 0 && overallFailedConditions.length === 0) {
if (qgStatus.failedConditions.length === 0 && qgStatus.isCaycCompliant) {
return null;
}

const [caycConditions, newCodeFailedConditions, overallFailedConditions] = splitConditions(
qgStatus.failedConditions
);

/*
* Show Clean as You Code if:
* - The QG is not CAYC-compliant
* - There are *any* failing conditions, we either show:
* - the cayc-specific failures
* - that cayc is passing and only other conditions are failing
*/
const showCayc = !qgStatus.isCaycCompliant || qgStatus.failedConditions.length > 0;

const showSuccessfulCayc = caycConditions.length === 0 && qgStatus.isCaycCompliant;

const hasOtherConditions = newCodeFailedConditions.length + overallFailedConditions.length > 0;

const showName = component.qualifier === ComponentQualifier.Application;

const toggleLabel = collapsed
? translateWithParameters('overview.quality_gate.show_project_conditions_x', qgStatus.name)
: translateWithParameters('overview.quality_gate.hide_project_conditions_x', qgStatus.name);

return (
<div className="overview-quality-gate-conditions">
{showName && (
<h3 className="overview-quality-gate-conditions-project-name">{qgStatus.name}</h3>
<ButtonPlain
aria-label={toggleLabel}
aria-expanded={!collapsed}
className="width-100 text-left"
onClick={toggle}
>
<div className="display-flex-center">
<h3
className="overview-quality-gate-conditions-project-name text-ellipsis"
title={qgStatus.name}
>
{collapsed ? <ChevronRightIcon /> : <ChevronDownIcon />}
<span className="spacer-left">{qgStatus.name}</span>
</h3>
{collapsed && displayConditions(qgStatus.failedConditions.length)}
</div>
</ButtonPlain>
)}

{newCodeFailedConditions.length > 0 && (
{!collapsed && (
<>
<h4 className="overview-quality-gate-conditions-section-title">
{translate('quality_gates.conditions.new_code')}
</h4>
<QualityGateConditions
component={qgStatus}
branchLike={qgStatus.branchLike}
failedConditions={newCodeFailedConditions}
/>
</>
)}
{showCayc && (
<>
<div className="display-flex-center overview-quality-gate-conditions-section-title">
<h4 className="padded">{translate('quality_gates.conditions.cayc')}</h4>
{displayConditions(caycConditions.length)}
</div>

{overallFailedConditions.length > 0 && (
<>
<h4 className="overview-quality-gate-conditions-section-title">
{translate('quality_gates.conditions.overall_code')}
</h4>
<QualityGateConditions
component={qgStatus}
branchLike={qgStatus.branchLike}
failedConditions={overallFailedConditions}
/>
{!qgStatus.isCaycCompliant && (
<div className="big-padded bordered-bottom overview-quality-gate-conditions-list">
<CleanAsYouCodeWarning />
</div>
)}

{showSuccessfulCayc && (
<div className="big-padded bordered-bottom overview-quality-gate-conditions-list">
<Alert variant="success" className="no-margin-bottom">
{translate('overview.quality_gate.conditions.cayc.passed')}
</Alert>
</div>
)}

{caycConditions.length > 0 && (
<QualityGateConditions
component={qgStatus}
branchLike={qgStatus.branchLike}
failedConditions={caycConditions}
/>
)}
</>
)}

{hasOtherConditions && (
<>
<div className="display-flex-center overview-quality-gate-conditions-section-title">
<h4 className="padded">{translate('quality_gates.conditions.other_conditions')}</h4>
{displayConditions(newCodeFailedConditions.length + overallFailedConditions.length)}
</div>

{newCodeFailedConditions.length > 0 && (
<>
<h5 className="big-padded overview-quality-gate-conditions-subsection-title">
{translate('quality_gates.conditions.new_code')}
</h5>
<QualityGateConditions
component={qgStatus}
branchLike={qgStatus.branchLike}
failedConditions={newCodeFailedConditions}
/>
</>
)}

{overallFailedConditions.length > 0 && (
<>
<h5 className="big-padded overview-quality-gate-conditions-subsection-title">
{translate('quality_gates.conditions.overall_code')}
</h5>
<QualityGateConditions
component={qgStatus}
branchLike={qgStatus.branchLike}
failedConditions={overallFailedConditions}
/>
</>
)}
</>
)}
</>
)}
</div>

+ 119
- 169
server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx View File

@@ -17,33 +17,27 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { getApplicationDetails, getApplicationLeak } from '../../../../api/application';
import selectEvent from 'react-select-event';
import { getMeasuresWithPeriodAndMetrics } from '../../../../api/measures';
import { getProjectActivity } from '../../../../api/projectActivity';
import {
getApplicationQualityGate,
getQualityGateProjectStatus,
} from '../../../../api/quality-gates';
import { getAllTimeMachineData } from '../../../../api/time-machine';
import { getQualityGateProjectStatus } from '../../../../api/quality-gates';
import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider';
import { getActivityGraph, saveActivityGraph } from '../../../../components/activity-graph/utils';
import { isDiffMetric } from '../../../../helpers/measures';
import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like';
import { mockMainBranch } from '../../../../helpers/mocks/branch-like';
import { mockComponent } from '../../../../helpers/mocks/component';
import { mockAnalysis } from '../../../../helpers/mocks/project-activity';
import { waitAndUpdate } from '../../../../helpers/testUtils';
import { mockQualityGateProjectStatus } from '../../../../helpers/mocks/quality-gates';
import { mockLoggedInUser, mockPeriod } from '../../../../helpers/testMocks';
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import { ComponentQualifier } from '../../../../types/component';
import { MetricKey } from '../../../../types/metrics';
import { GraphType } from '../../../../types/project-activity';
import { Measure, Metric } from '../../../../types/types';
import BranchOverview, { BRANCH_OVERVIEW_ACTIVITY_GRAPH, NO_CI_DETECTED } from '../BranchOverview';
import BranchOverviewRenderer from '../BranchOverviewRenderer';

jest.mock('../../../../helpers/dates', () => ({
parseDate: jest.fn((date) => `PARSED:${date}`),
toNotSoISOString: jest.fn((date) => date),
}));

jest.mock('../../../../api/measures', () => {
const { mockMeasure, mockMetric } = jest.requireActual('../../../../helpers/testMocks');
@@ -96,8 +90,8 @@ jest.mock('../../../../api/quality-gates', () => {
{
actualValue: '2',
comparator: 'GT',
errorThreshold: '1.0',
metricKey: MetricKey.new_bugs,
errorThreshold: '1',
metricKey: MetricKey.new_reliability_rating,
periodIndex: 1,
status: 'ERROR',
},
@@ -184,7 +178,9 @@ jest.mock('../../../../api/application', () => ({
jest.mock('../../../../components/activity-graph/utils', () => {
const { MetricKey } = jest.requireActual('../../../../types/metrics');
const { GraphType } = jest.requireActual('../../../../types/project-activity');
const original = jest.requireActual('../../../../components/activity-graph/utils');
return {
...original,
getActivityGraph: jest.fn(() => ({ graph: GraphType.coverage })),
saveActivityGraph: jest.fn(),
getHistoryMetrics: jest.fn(() => [MetricKey.lines_to_cover, MetricKey.uncovered_lines]),
@@ -194,57 +190,85 @@ jest.mock('../../../../components/activity-graph/utils', () => {
beforeEach(jest.clearAllMocks);

describe('project overview', () => {
it('should render correctly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
it('should show a successful QG', async () => {
const user = userEvent.setup();
jest
.mocked(getQualityGateProjectStatus)
.mockResolvedValueOnce(mockQualityGateProjectStatus({ status: 'OK' }));
renderBranchOverview();

// QG panel
expect(await screen.findByText('metric.level.OK')).toBeInTheDocument();
expect(screen.getByText('overview.quality_gate_all_conditions_passed')).toBeInTheDocument();
expect(
screen.queryByText('overview.quality_gate.conditions.cayc.warning')
).not.toBeInTheDocument();

//Measures panel
expect(screen.getByText('metric.new_vulnerabilities.name')).toBeInTheDocument();

// go to overall
await user.click(screen.getByText('overview.overall_code'));

expect(screen.getByText('metric.vulnerabilities.name')).toBeInTheDocument();
});

it('should show a successful non-compliant QG', async () => {
jest
.mocked(getQualityGateProjectStatus)
.mockResolvedValueOnce(
mockQualityGateProjectStatus({ status: 'OK', isCaycCompliant: false })
);

renderBranchOverview();

expect(await screen.findByText('metric.level.OK')).toBeInTheDocument();
expect(screen.getByText('overview.quality_gate.conditions.cayc.warning')).toBeInTheDocument();
});

it('should show a failed QG', async () => {
renderBranchOverview();

expect(await screen.findByText('metric.level.ERROR')).toBeInTheDocument();
expect(screen.getByText('overview.X_conditions_failed.2')).toBeInTheDocument();

expect(
screen.queryByText('overview.quality_gate.conditions.cayc.passed')
).not.toBeInTheDocument();
});

it("should correctly load a project's status", async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(getQualityGateProjectStatus).toHaveBeenCalled();
expect(getMeasuresWithPeriodAndMetrics).toHaveBeenCalled();

// Check the conditions got correctly enhanced with measure meta data.
const { qgStatuses } = wrapper.state();
expect(qgStatuses).toHaveLength(1);
const [qgStatus] = qgStatuses!;

expect(qgStatus).toEqual(
expect.objectContaining({
name: 'Foo',
key: 'foo',
it('should show a failed QG with passing CAYC conditions', async () => {
jest.mocked(getQualityGateProjectStatus).mockResolvedValueOnce(
mockQualityGateProjectStatus({
status: 'ERROR',
conditions: [
{
actualValue: '12',
comparator: 'GT',
errorThreshold: '10',
metricKey: MetricKey.new_bugs,
periodIndex: 1,
status: 'ERROR',
},
],
})
);
renderBranchOverview();

const { failedConditions } = qgStatus;
expect(failedConditions).toHaveLength(2);
expect(failedConditions[0]).toMatchObject({
actual: '2',
level: 'ERROR',
metric: MetricKey.new_bugs,
measure: expect.objectContaining({
metric: expect.objectContaining({ key: MetricKey.new_bugs }),
}),
});
expect(failedConditions[1]).toMatchObject({
actual: '5',
level: 'ERROR',
metric: MetricKey.bugs,
measure: expect.objectContaining({
metric: expect.objectContaining({ key: MetricKey.bugs }),
}),
});
expect(await screen.findByText('metric.level.ERROR')).toBeInTheDocument();
expect(screen.getByText('overview.quality_gate.conditions.cayc.passed')).toBeInTheDocument();
});

it('should correctly flag a project as empty', async () => {
(getMeasuresWithPeriodAndMetrics as jest.Mock).mockResolvedValueOnce({ component: {} });
it('should correctly show a project as empty', async () => {
jest.mocked(getMeasuresWithPeriodAndMetrics).mockResolvedValueOnce({
component: { key: '', name: '', qualifier: ComponentQualifier.Project, measures: [] },
metrics: [],
period: mockPeriod(),
});

renderBranchOverview();

const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(wrapper.find(BranchOverviewRenderer).props().projectIsEmpty).toBe(true);
expect(await screen.findByText('overview.project.main_branch_empty')).toBeInTheDocument();
});
});

@@ -254,106 +278,27 @@ describe('application overview', () => {
qualifier: ComponentQualifier.Application,
});

it('should render correctly', async () => {
const wrapper = shallowRender({ component });
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
});

it('should fetch correctly other branch', async () => {
const wrapper = shallowRender({ branch: mockBranch(), component });
await waitAndUpdate(wrapper);
expect(getApplicationDetails).toHaveBeenCalled();
expect(wrapper).toMatchSnapshot();
it('should show failed conditions for every project', async () => {
renderBranchOverview({ component });
expect(await screen.findByText('Foo')).toBeInTheDocument();
expect(screen.getByText('Bar')).toBeInTheDocument();
});

it("should correctly load an application's status", async () => {
const wrapper = shallowRender({ component });
await waitAndUpdate(wrapper);
expect(getApplicationQualityGate).toHaveBeenCalled();
expect(getApplicationLeak).toHaveBeenCalled();
expect(getMeasuresWithPeriodAndMetrics).toHaveBeenCalled();

// Check the conditions got correctly enhanced with measure meta data.
const { qgStatuses } = wrapper.state();
expect(qgStatuses).toHaveLength(2);
const [qgStatus1, qgStatus2] = qgStatuses!;

expect(qgStatus1).toEqual(
expect.objectContaining({
name: 'Foo',
key: 'foo',
status: 'ERROR',
})
);

const { failedConditions: failedConditions1 } = qgStatus1;
expect(failedConditions1).toHaveLength(2);
expect(failedConditions1[0]).toMatchObject({
actual: '10',
level: 'ERROR',
metric: MetricKey.coverage,
measure: expect.objectContaining({
metric: expect.objectContaining({ key: MetricKey.coverage }),
}),
});
expect(failedConditions1[1]).toMatchObject({
actual: '5',
level: 'ERROR',
metric: MetricKey.new_bugs,
measure: expect.objectContaining({
metric: expect.objectContaining({ key: MetricKey.new_bugs }),
}),
it('should correctly show an app as empty', async () => {
jest.mocked(getMeasuresWithPeriodAndMetrics).mockResolvedValueOnce({
component: { key: '', name: '', qualifier: ComponentQualifier.Application, measures: [] },
metrics: [],
period: mockPeriod(),
});

expect(qgStatus1).toEqual(
expect.objectContaining({
name: 'Foo',
key: 'foo',
status: 'ERROR',
})
);
renderBranchOverview({ component });

const { failedConditions: failedConditions2 } = qgStatus2;
expect(failedConditions2).toHaveLength(1);
expect(failedConditions2[0]).toMatchObject({
actual: '15',
level: 'ERROR',
metric: MetricKey.new_bugs,
measure: expect.objectContaining({
metric: expect.objectContaining({ key: MetricKey.new_bugs }),
}),
});
});

it('should correctly flag an application as empty', async () => {
(getMeasuresWithPeriodAndMetrics as jest.Mock).mockResolvedValueOnce({ component: {} });

const wrapper = shallowRender({ component });
await waitAndUpdate(wrapper);

expect(wrapper.find(BranchOverviewRenderer).props().projectIsEmpty).toBe(true);
expect(await screen.findByText('portfolio.app.empty')).toBeInTheDocument();
});
});

it("should correctly load a component's history", async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(getProjectActivity).toHaveBeenCalled();
expect(getAllTimeMachineData).toHaveBeenCalled();

const { measuresHistory } = wrapper.state();
expect(measuresHistory).toHaveLength(6);
expect(measuresHistory![0]).toEqual(
expect.objectContaining({
metric: MetricKey.bugs,
history: [{ date: 'PARSED:2019-01-05', value: '2.0' }],
})
);
});

it.each([
['no analysis', [], undefined],
['no analysis', [], true],
['1 analysis, no CI data', [mockAnalysis()], false],
['1 analysis, no CI detected', [mockAnalysis({ detectedCI: NO_CI_DETECTED })], false],
['1 analysis, CI detected', [mockAnalysis({ detectedCI: 'Cirrus CI' })], true],
@@ -362,36 +307,41 @@ it.each([
async (_, analyses, expected) => {
(getProjectActivity as jest.Mock).mockResolvedValueOnce({ analyses });

const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(wrapper.state().detectedCIOnLastAnalysis).toBe(expected);
renderBranchOverview();

// wait for loading
await screen.findByText('overview.quality_gate');

expect(screen.queryByText('overview.project.next_steps.set_up_ci') === null).toBe(expected);
}
);

it('should correctly handle graph type storage', () => {
const wrapper = shallowRender();
it('should correctly handle graph type storage', async () => {
renderBranchOverview();
expect(getActivityGraph).toHaveBeenCalledWith(BRANCH_OVERVIEW_ACTIVITY_GRAPH, 'foo');
expect(wrapper.state().graph).toBe(GraphType.coverage);

wrapper.instance().handleGraphChange(GraphType.issues);
const select = await screen.findByLabelText('project_activity.graphs.choose_type');
await selectEvent.select(select, `project_activity.graphs.${GraphType.issues}`);

expect(saveActivityGraph).toHaveBeenCalledWith(
BRANCH_OVERVIEW_ACTIVITY_GRAPH,
'foo',
GraphType.issues
);
expect(wrapper.state().graph).toBe(GraphType.issues);
});

function shallowRender(props: Partial<BranchOverview['props']> = {}) {
return shallow<BranchOverview>(
<BranchOverview
branch={mockMainBranch()}
component={mockComponent({
breadcrumbs: [mockComponent({ key: 'foo' })],
key: 'foo',
name: 'Foo',
})}
{...props}
/>
function renderBranchOverview(props: Partial<BranchOverview['props']> = {}) {
renderComponent(
<CurrentUserContextProvider currentUser={mockLoggedInUser()}>
<BranchOverview
branch={mockMainBranch()}
component={mockComponent({
breadcrumbs: [mockComponent({ key: 'foo' })],
key: 'foo',
name: 'Foo',
})}
{...props}
/>
</CurrentUserContextProvider>
);
}

+ 0
- 47
server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverviewRenderer-test.tsx View File

@@ -1,47 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockMainBranch } from '../../../../helpers/mocks/branch-like';
import { mockComponent } from '../../../../helpers/mocks/component';
import { mockMeasureEnhanced } from '../../../../helpers/testMocks';
import { GraphType } from '../../../../types/project-activity';
import { BranchOverviewRenderer, BranchOverviewRendererProps } from '../BranchOverviewRenderer';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
expect(shallowRender({ projectIsEmpty: true })).toMatchSnapshot('empty project');
expect(shallowRender({ loadingHistory: true, loadingStatus: true })).toMatchSnapshot('loading');
});

function shallowRender(props: Partial<BranchOverviewRendererProps> = {}) {
return shallow(
<BranchOverviewRenderer
branch={mockMainBranch()}
component={mockComponent()}
graph={GraphType.issues}
loadingHistory={false}
loadingStatus={false}
measures={[mockMeasureEnhanced()]}
onGraphChange={jest.fn()}
{...props}
/>
);
}

+ 32
- 6
server/sonar-web/src/main/js/apps/overview/branches/__tests__/DebtValue-test.tsx View File

@@ -17,22 +17,48 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import { screen } from '@testing-library/react';
import * as React from 'react';
import { mockMainBranch } from '../../../../helpers/mocks/branch-like';
import { mockComponent } from '../../../../helpers/mocks/component';
import { mockMeasureEnhanced, mockMetric } from '../../../../helpers/testMocks';
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import { MetricKey } from '../../../../types/metrics';
import { DebtValue, DebtValueProps } from '../DebtValue';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
expect(shallowRender({ useDiffMetric: true })).toMatchSnapshot();
expect(shallowRender({ measures: [] })).toMatchSnapshot();
renderDebtValue();

expect(
screen.getByLabelText(
'overview.see_more_details_on_x_of_y.work_duration.x_minutes.1.sqale_index'
)
).toBeInTheDocument();

expect(screen.getByText('sqale_index')).toBeInTheDocument();
});

it('should render diff metric correctly', () => {
renderDebtValue({ useDiffMetric: true });

expect(
screen.getByLabelText(
'overview.see_more_details_on_x_of_y.work_duration.x_minutes.1.new_technical_debt'
)
).toBeInTheDocument();

expect(screen.getByText('new_technical_debt')).toBeInTheDocument();
});

it('should handle missing measure', () => {
renderDebtValue({ measures: [] });

expect(screen.getByLabelText('no_data')).toBeInTheDocument();
expect(screen.getByText('metric.sqale_index.name')).toBeInTheDocument();
});

function shallowRender(props: Partial<DebtValueProps> = {}) {
return shallow(
function renderDebtValue(props: Partial<DebtValueProps> = {}) {
return renderComponent(
<DebtValue
branchLike={mockMainBranch()}
component={mockComponent()}

+ 0
- 63
server/sonar-web/src/main/js/apps/overview/branches/__tests__/MeasuresPanelIssueMeasureRow-test.tsx View File

@@ -1,63 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockMainBranch } from '../../../../helpers/mocks/branch-like';
import { mockComponent } from '../../../../helpers/mocks/component';
import { mockMeasureEnhanced, mockMetric } from '../../../../helpers/testMocks';
import { ComponentQualifier } from '../../../../types/component';
import { IssueType } from '../../../../types/issues';
import { MetricKey } from '../../../../types/metrics';
import MeasuresPanelIssueMeasureRow, {
MeasuresPanelIssueMeasureRowProps,
} from '../MeasuresPanelIssueMeasureRow';

it('should render correctly for projects', () => {
expect(shallowRender({ type: IssueType.Bug })).toMatchSnapshot('Bug');
expect(shallowRender({ type: IssueType.CodeSmell })).toMatchSnapshot('Code Smell');
expect(shallowRender({ type: IssueType.SecurityHotspot })).toMatchSnapshot('Hotspot');
expect(shallowRender({ type: IssueType.Vulnerability })).toMatchSnapshot('Vulnerabilty');
expect(shallowRender({ isNewCodeTab: false })).toMatchSnapshot('Overview');
});

it('should render correctly for apps', () => {
const app = mockComponent({ qualifier: ComponentQualifier.Application });

expect(shallowRender({ component: app })).toMatchSnapshot('new code');
expect(shallowRender({ component: app, isNewCodeTab: false })).toMatchSnapshot('overview');
});

function shallowRender(props: Partial<MeasuresPanelIssueMeasureRowProps> = {}) {
return shallow<MeasuresPanelIssueMeasureRowProps>(
<MeasuresPanelIssueMeasureRow
branchLike={mockMainBranch()}
component={mockComponent()}
isNewCodeTab={true}
measures={[
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.coverage }) }),
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.new_coverage }) }),
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.bugs }) }),
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.new_bugs }) }),
]}
type={IssueType.Bug}
{...props}
/>
);
}

+ 0
- 110
server/sonar-web/src/main/js/apps/overview/branches/__tests__/MeasuresPanelNoNewCode-test.tsx View File

@@ -1,110 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockMainBranch } from '../../../../helpers/mocks/branch-like';
import { mockComponent } from '../../../../helpers/mocks/component';
import { mockPeriod } from '../../../../helpers/testMocks';
import { ComponentQualifier } from '../../../../types/component';
import MeasuresPanelNoNewCode, { MeasuresPanelNoNewCodeProps } from '../MeasuresPanelNoNewCode';

it('should render the default message', () => {
const defaultMessage = `
<div
className="display-flex-center display-flex-justify-center"
style={
{
"height": 500,
}
}
>
<img
alt=""
className="spacer-right"
height={52}
src="/images/source-code.svg"
/>
<div
className="big-spacer-left text-muted"
style={
{
"maxWidth": 500,
}
}
>
<p
className="spacer-bottom big-spacer-top big"
>
overview.measures.empty_explanation
</p>
<p>
<FormattedMessage
defaultMessage="overview.measures.empty_link"
id="overview.measures.empty_link"
values={
{
"learn_more_link": <withAppStateContext(DocLink)
to="/user-guide/clean-as-you-code/"
>
learn_more
</withAppStateContext(DocLink)>,
}
}
/>
</p>
</div>
</div>
`;

expect(shallowRender()).toMatchInlineSnapshot(defaultMessage);
expect(
shallowRender({ component: mockComponent({ qualifier: ComponentQualifier.Application }) })
).toMatchInlineSnapshot(defaultMessage);
expect(
shallowRender({ period: mockPeriod({ date: '2018-05-23', mode: 'REFERENCE_BRANCH' }) })
).toMatchInlineSnapshot(defaultMessage);
expect(
shallowRender({ period: mockPeriod({ date: '2018-05-23', mode: 'PREVIOUS_VERSION' }) })
).toMatchInlineSnapshot(defaultMessage);
expect(
shallowRender({
period: mockPeriod({ date: undefined, mode: 'REFERENCE_BRANCH', parameter: 'master' }),
})
).toMatchSnapshot();
expect(
shallowRender({
period: mockPeriod({ date: undefined, mode: 'REFERENCE_BRANCH', parameter: 'notsame' }),
})
).toMatchSnapshot();
});

it('should render "bad code setting" explanation', () => {
const period = mockPeriod({ date: undefined, mode: 'REFERENCE_BRANCH' });
expect(shallowRender({ period })).toMatchSnapshot('no link');
expect(
shallowRender({ component: mockComponent({ configuration: { showSettings: true } }), period })
).toMatchSnapshot('with link');
});

function shallowRender(props: Partial<MeasuresPanelNoNewCodeProps> = {}) {
return shallow<MeasuresPanelNoNewCodeProps>(
<MeasuresPanelNoNewCode branch={mockMainBranch()} component={mockComponent()} {...props} />
);
}

+ 0
- 46
server/sonar-web/src/main/js/apps/overview/branches/__tests__/SecurityHotspotsReviewed-test.tsx View File

@@ -1,46 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockMeasureEnhanced, mockMetric } from '../../../../helpers/testMocks';
import { MetricKey } from '../../../../types/metrics';
import SecurityHotspotsReviewed, {
SecurityHotspotsReviewedProps,
} from '../SecurityHotspotsReviewed';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
expect(shallowRender({ useDiffMetric: true })).toMatchSnapshot('on new code');
expect(shallowRender({ measures: [] })).toMatchSnapshot('no measures');
});

function shallowRender(props: Partial<SecurityHotspotsReviewedProps> = {}) {
return shallow(
<SecurityHotspotsReviewed
measures={[
mockMeasureEnhanced({ metric: mockMetric({ key: MetricKey.security_hotspots_reviewed }) }),
mockMeasureEnhanced({
metric: mockMetric({ key: MetricKey.new_security_hotspots_reviewed }),
}),
]}
{...props}
/>
);
}

+ 0
- 2734
server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/BranchOverview-test.tsx.snap
File diff suppressed because it is too large
View File


+ 0
- 435
server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/BranchOverviewRenderer-test.tsx.snap View File

@@ -1,435 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly: default 1`] = `
<Fragment>
<withCurrentUserContext(FirstAnalysisNextStepsNotif)
component={
{
"breadcrumbs": [],
"key": "my-project",
"name": "MyProject",
"qualifier": "TRK",
"qualityGate": {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": [
{
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": [],
}
}
/>
<div
className="page page-limited"
>
<div
className="overview"
>
<A11ySkipTarget
anchor="overview_main"
/>
<div
className="display-flex-row"
>
<div
className="width-25 big-spacer-right"
>
<Memo(QualityGatePanel)
component={
{
"breadcrumbs": [],
"key": "my-project",
"name": "MyProject",
"qualifier": "TRK",
"qualityGate": {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": [
{
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": [],
}
}
loading={false}
/>
</div>
<div
className="flex-1"
>
<div
className="display-flex-column"
>
<withRouter(Component)
branch={
{
"analysisDate": "2018-01-01",
"excludedFromPurge": true,
"isMain": true,
"name": "master",
}
}
component={
{
"breadcrumbs": [],
"key": "my-project",
"name": "MyProject",
"qualifier": "TRK",
"qualityGate": {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": [
{
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": [],
}
}
loading={false}
measures={
[
{
"bestValue": true,
"leak": "1",
"metric": {
"id": "coverage",
"key": "coverage",
"name": "coverage",
"type": "PERCENT",
},
"period": {
"bestValue": true,
"index": 1,
"value": "1.0",
},
"value": "1.0",
},
]
}
/>
<Memo(ActivityPanel)
branchLike={
{
"analysisDate": "2018-01-01",
"excludedFromPurge": true,
"isMain": true,
"name": "master",
}
}
component={
{
"breadcrumbs": [],
"key": "my-project",
"name": "MyProject",
"qualifier": "TRK",
"qualityGate": {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": [
{
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": [],
}
}
graph="issues"
loading={false}
measuresHistory={[]}
metrics={[]}
onGraphChange={[MockFunction]}
/>
</div>
</div>
</div>
</div>
</div>
</Fragment>
`;

exports[`should render correctly: empty project 1`] = `
<Fragment>
<withCurrentUserContext(FirstAnalysisNextStepsNotif)
component={
{
"breadcrumbs": [],
"key": "my-project",
"name": "MyProject",
"qualifier": "TRK",
"qualityGate": {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": [
{
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": [],
}
}
/>
<div
className="page page-limited"
>
<div
className="overview"
>
<A11ySkipTarget
anchor="overview_main"
/>
<Memo(NoCodeWarning)
branchLike={
{
"analysisDate": "2018-01-01",
"excludedFromPurge": true,
"isMain": true,
"name": "master",
}
}
component={
{
"breadcrumbs": [],
"key": "my-project",
"name": "MyProject",
"qualifier": "TRK",
"qualityGate": {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": [
{
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": [],
}
}
measures={
[
{
"bestValue": true,
"leak": "1",
"metric": {
"id": "coverage",
"key": "coverage",
"name": "coverage",
"type": "PERCENT",
},
"period": {
"bestValue": true,
"index": 1,
"value": "1.0",
},
"value": "1.0",
},
]
}
/>
</div>
</div>
</Fragment>
`;

exports[`should render correctly: loading 1`] = `
<Fragment>
<withCurrentUserContext(FirstAnalysisNextStepsNotif)
component={
{
"breadcrumbs": [],
"key": "my-project",
"name": "MyProject",
"qualifier": "TRK",
"qualityGate": {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": [
{
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": [],
}
}
/>
<div
className="page page-limited"
>
<div
className="overview"
>
<A11ySkipTarget
anchor="overview_main"
/>
<div
className="display-flex-row"
>
<div
className="width-25 big-spacer-right"
>
<Memo(QualityGatePanel)
component={
{
"breadcrumbs": [],
"key": "my-project",
"name": "MyProject",
"qualifier": "TRK",
"qualityGate": {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": [
{
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": [],
}
}
loading={true}
/>
</div>
<div
className="flex-1"
>
<div
className="display-flex-column"
>
<withRouter(Component)
branch={
{
"analysisDate": "2018-01-01",
"excludedFromPurge": true,
"isMain": true,
"name": "master",
}
}
component={
{
"breadcrumbs": [],
"key": "my-project",
"name": "MyProject",
"qualifier": "TRK",
"qualityGate": {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": [
{
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": [],
}
}
loading={true}
measures={
[
{
"bestValue": true,
"leak": "1",
"metric": {
"id": "coverage",
"key": "coverage",
"name": "coverage",
"type": "PERCENT",
},
"period": {
"bestValue": true,
"index": 1,
"value": "1.0",
},
"value": "1.0",
},
]
}
/>
<Memo(ActivityPanel)
branchLike={
{
"analysisDate": "2018-01-01",
"excludedFromPurge": true,
"isMain": true,
"name": "master",
}
}
component={
{
"breadcrumbs": [],
"key": "my-project",
"name": "MyProject",
"qualifier": "TRK",
"qualityGate": {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": [
{
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": [],
}
}
graph="issues"
loading={true}
measuresHistory={[]}
metrics={[]}
onGraphChange={[MockFunction]}
/>
</div>
</div>
</div>
</div>
</div>
</Fragment>
`;

+ 0
- 67
server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/DebtValue-test.tsx.snap View File

@@ -1,67 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<Fragment>
<DrilldownLink
ariaLabel="overview.see_more_details_on_x_of_y.work_duration.x_minutes.1.sqale_index"
branchLike={
{
"analysisDate": "2018-01-01",
"excludedFromPurge": true,
"isMain": true,
"name": "master",
}
}
className="overview-measures-value text-light"
component="my-project"
metric="sqale_index"
>
work_duration.x_minutes.1
</DrilldownLink>
<span
className="big-spacer-left"
>
sqale_index
</span>
</Fragment>
`;

exports[`should render correctly 2`] = `
<Fragment>
<DrilldownLink
ariaLabel="overview.see_more_details_on_x_of_y.work_duration.x_minutes.1.new_technical_debt"
branchLike={
{
"analysisDate": "2018-01-01",
"excludedFromPurge": true,
"isMain": true,
"name": "master",
}
}
className="overview-measures-value text-light"
component="my-project"
metric="new_technical_debt"
>
work_duration.x_minutes.1
</DrilldownLink>
<span
className="big-spacer-left"
>
new_technical_debt
</span>
</Fragment>
`;

exports[`should render correctly 3`] = `
<Fragment>
<span
aria-label="no_data"
className="overview-measures-empty-value"
/>
<span
className="big-spacer-left"
>
metric.sqale_index.name
</span>
</Fragment>
`;

+ 0
- 1616
server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanelIssueMeasureRow-test.tsx.snap
File diff suppressed because it is too large
View File


+ 0
- 174
server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanelNoNewCode-test.tsx.snap View File

@@ -1,174 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render "bad code setting" explanation: no link 1`] = `
<div
className="display-flex-center display-flex-justify-center"
style={
{
"height": 500,
}
}
>
<img
alt=""
className="spacer-right"
height={52}
src="/images/source-code.svg"
/>
<div
className="big-spacer-left text-muted"
style={
{
"maxWidth": 500,
}
}
>
<p
className="spacer-bottom big-spacer-top big"
>
overview.measures.bad_reference.explanation
</p>
<p>
<FormattedMessage
defaultMessage="overview.measures.empty_link"
id="overview.measures.empty_link"
values={
{
"learn_more_link": <withAppStateContext(DocLink)
to="/user-guide/clean-as-you-code/"
>
learn_more
</withAppStateContext(DocLink)>,
}
}
/>
</p>
</div>
</div>
`;

exports[`should render "bad code setting" explanation: with link 1`] = `
<div
className="display-flex-center display-flex-justify-center"
style={
{
"height": 500,
}
}
>
<img
alt=""
className="spacer-right"
height={52}
src="/images/source-code.svg"
/>
<div
className="big-spacer-left text-muted"
style={
{
"maxWidth": 500,
}
}
>
<p
className="spacer-bottom big-spacer-top big"
>
overview.measures.bad_reference.explanation
</p>
<p>
<FormattedMessage
defaultMessage="overview.measures.empty_link"
id="overview.measures.empty_link"
values={
{
"learn_more_link": <withAppStateContext(DocLink)
to="/user-guide/clean-as-you-code/"
>
learn_more
</withAppStateContext(DocLink)>,
}
}
/>
</p>
</div>
</div>
`;

exports[`should render the default message 5`] = `
<div
className="display-flex-center display-flex-justify-center"
style={
{
"height": 500,
}
}
>
<img
alt=""
className="spacer-right"
height={52}
src="/images/source-code.svg"
/>
<div
className="big-spacer-left text-muted"
style={
{
"maxWidth": 500,
}
}
>
<p
className="spacer-bottom big-spacer-top big"
>
overview.measures.same_reference.explanation
</p>
</div>
</div>
`;

exports[`should render the default message 6`] = `
<div
className="display-flex-center display-flex-justify-center"
style={
{
"height": 500,
}
}
>
<img
alt=""
className="spacer-right"
height={52}
src="/images/source-code.svg"
/>
<div
className="big-spacer-left text-muted"
style={
{
"maxWidth": 500,
}
}
>
<p
className="spacer-bottom big-spacer-top big"
>
overview.measures.bad_reference.explanation
</p>
<p>
<FormattedMessage
defaultMessage="overview.measures.empty_link"
id="overview.measures.empty_link"
values={
{
"learn_more_link": <withAppStateContext(DocLink)
to="/user-guide/clean-as-you-code/"
>
learn_more
</withAppStateContext(DocLink)>,
}
}
/>
</p>
</div>
</div>
`;

+ 11
- 15
server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanel-test.tsx.snap View File

@@ -25,9 +25,7 @@ exports[`should render correctly for applications 1`] = `
}
/>
</div>
<div
className="overview-panel-content"
>
<div>
<div
className="overview-quality-gate-badge-large failed"
>
@@ -97,6 +95,7 @@ exports[`should render correctly for applications 1`] = `
},
],
"ignoredConditions": false,
"isCaycCompliant": true,
"key": "foo",
"name": "Foo",
"status": "ERROR",
@@ -178,6 +177,7 @@ exports[`should render correctly for applications 1`] = `
},
],
"ignoredConditions": false,
"isCaycCompliant": true,
"key": "foo",
"name": "Foo",
"status": "ERROR",
@@ -289,9 +289,7 @@ exports[`should render correctly for applications 2`] = `
}
/>
</div>
<div
className="overview-panel-content"
>
<div>
<div
className="overview-quality-gate-badge-large failed"
>
@@ -361,6 +359,7 @@ exports[`should render correctly for applications 2`] = `
},
],
"ignoredConditions": false,
"isCaycCompliant": true,
"key": "foo",
"name": "Foo",
"status": "ERROR",
@@ -395,6 +394,7 @@ exports[`should render correctly for applications 2`] = `
{
"failedConditions": [],
"ignoredConditions": false,
"isCaycCompliant": true,
"key": "foo",
"name": "Foo",
"status": "OK",
@@ -460,9 +460,7 @@ exports[`should render correctly for projects 1`] = `
}
/>
</div>
<div
className="overview-panel-content"
>
<div>
<div
className="overview-quality-gate-badge-large failed"
>
@@ -532,6 +530,7 @@ exports[`should render correctly for projects 1`] = `
},
],
"ignoredConditions": false,
"isCaycCompliant": true,
"key": "foo",
"name": "Foo",
"status": "ERROR",
@@ -597,9 +596,7 @@ exports[`should render correctly for projects 2`] = `
}
/>
</div>
<div
className="overview-panel-content"
>
<div>
<div
className="overview-quality-gate-badge-large success"
>
@@ -661,9 +658,7 @@ exports[`should render correctly for projects 3`] = `
overlay="overview.quality_gate.ignored_conditions.tooltip"
/>
</Alert>
<div
className="overview-panel-content"
>
<div>
<div
className="overview-quality-gate-badge-large failed"
>
@@ -733,6 +728,7 @@ exports[`should render correctly for projects 3`] = `
},
],
"ignoredConditions": true,
"isCaycCompliant": true,
"key": "foo",
"name": "Foo",
"status": "ERROR",

+ 103
- 16
server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanelSection-test.tsx.snap View File

@@ -4,11 +4,44 @@ exports[`should render correctly 1`] = `
<div
className="overview-quality-gate-conditions"
>
<h4
className="overview-quality-gate-conditions-section-title"
<div
className="display-flex-center overview-quality-gate-conditions-section-title"
>
<h4
className="padded"
>
quality_gates.conditions.cayc
</h4>
</div>
<div
className="big-padded bordered-bottom overview-quality-gate-conditions-list"
>
<Alert
className="no-margin-bottom"
variant="success"
>
overview.quality_gate.conditions.cayc.passed
</Alert>
</div>
<div
className="display-flex-center overview-quality-gate-conditions-section-title"
>
<h4
className="padded"
>
quality_gates.conditions.other_conditions
</h4>
<span
className="text-muted big-spacer-left"
>
overview.X_conditions_failed.2
</span>
</div>
<h5
className="big-padded overview-quality-gate-conditions-subsection-title"
>
quality_gates.conditions.new_code
</h4>
</h5>
<Memo(QualityGateConditions)
component={
{
@@ -61,6 +94,7 @@ exports[`should render correctly 1`] = `
},
],
"ignoredConditions": false,
"isCaycCompliant": true,
"key": "foo",
"name": "Foo",
"status": "ERROR",
@@ -94,11 +128,11 @@ exports[`should render correctly 1`] = `
]
}
/>
<h4
className="overview-quality-gate-conditions-section-title"
<h5
className="big-padded overview-quality-gate-conditions-subsection-title"
>
quality_gates.conditions.overall_code
</h4>
</h5>
<Memo(QualityGateConditions)
component={
{
@@ -151,6 +185,7 @@ exports[`should render correctly 1`] = `
},
],
"ignoredConditions": false,
"isCaycCompliant": true,
"key": "foo",
"name": "Foo",
"status": "ERROR",
@@ -191,16 +226,66 @@ exports[`should render correctly 2`] = `
<div
className="overview-quality-gate-conditions"
>
<h3
className="overview-quality-gate-conditions-project-name"
<ButtonPlain
aria-expanded={true}
aria-label="overview.quality_gate.hide_project_conditions_x.Foo"
className="width-100 text-left"
onClick={[Function]}
>
<div
className="display-flex-center"
>
<h3
className="overview-quality-gate-conditions-project-name text-ellipsis"
title="Foo"
>
<ChevronDownIcon />
<span
className="spacer-left"
>
Foo
</span>
</h3>
</div>
</ButtonPlain>
<div
className="display-flex-center overview-quality-gate-conditions-section-title"
>
<h4
className="padded"
>
quality_gates.conditions.cayc
</h4>
</div>
<div
className="big-padded bordered-bottom overview-quality-gate-conditions-list"
>
<Alert
className="no-margin-bottom"
variant="success"
>
overview.quality_gate.conditions.cayc.passed
</Alert>
</div>
<div
className="display-flex-center overview-quality-gate-conditions-section-title"
>
Foo
</h3>
<h4
className="overview-quality-gate-conditions-section-title"
<h4
className="padded"
>
quality_gates.conditions.other_conditions
</h4>
<span
className="text-muted big-spacer-left"
>
overview.X_conditions_failed.2
</span>
</div>
<h5
className="big-padded overview-quality-gate-conditions-subsection-title"
>
quality_gates.conditions.new_code
</h4>
</h5>
<Memo(QualityGateConditions)
component={
{
@@ -253,6 +338,7 @@ exports[`should render correctly 2`] = `
},
],
"ignoredConditions": false,
"isCaycCompliant": true,
"key": "foo",
"name": "Foo",
"status": "ERROR",
@@ -286,11 +372,11 @@ exports[`should render correctly 2`] = `
]
}
/>
<h4
className="overview-quality-gate-conditions-section-title"
<h5
className="big-padded overview-quality-gate-conditions-subsection-title"
>
quality_gates.conditions.overall_code
</h4>
</h5>
<Memo(QualityGateConditions)
component={
{
@@ -343,6 +429,7 @@ exports[`should render correctly 2`] = `
},
],
"ignoredConditions": false,
"isCaycCompliant": true,
"key": "foo",
"name": "Foo",
"status": "ERROR",

+ 0
- 51
server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/SecurityHotspotsReviewed-test.tsx.snap View File

@@ -1,51 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<Fragment>
<CoverageRating
value="1.0"
/>
<span
className="huge spacer-left"
>
1.0%
</span>
<span
className="big-spacer-left"
>
overview.measures.security_hotspots_reviewed
</span>
</Fragment>
`;

exports[`should render correctly: no measures 1`] = `
<Fragment>
<span
aria-label="no_data"
className="overview-measures-empty-value"
/>
<span
className="big-spacer-left"
>
overview.measures.security_hotspots_reviewed
</span>
</Fragment>
`;

exports[`should render correctly: on new code 1`] = `
<Fragment>
<CoverageRating
value="1.0"
/>
<span
className="huge spacer-left"
>
1.0%
</span>
<span
className="big-spacer-left"
>
overview.measures.security_hotspots_reviewed
</span>
</Fragment>
`;

+ 2
- 4
server/sonar-web/src/main/js/apps/overview/components/QualityGateConditions.tsx View File

@@ -42,9 +42,7 @@ export function QualityGateConditions(props: QualityGateConditionsProps) {
const { branchLike, collapsible, component, failedConditions } = props;
const [collapsed, toggleCollapsed] = React.useState(Boolean(collapsible));

if (failedConditions.length === 0) {
return null;
}
const handleToggleCollapsed = React.useCallback(() => toggleCollapsed(!collapsed), [collapsed]);

const sortedConditions = sortBy(failedConditions, (condition) =>
LEVEL_ORDER.indexOf(condition.level)
@@ -77,7 +75,7 @@ export function QualityGateConditions(props: QualityGateConditionsProps) {
<li>
<ButtonLink
className="overview-quality-gate-conditions-list-collapse"
onClick={() => toggleCollapsed(!collapsed)}
onClick={handleToggleCollapsed}
>
{translateWithParameters(
'overview.X_more_failed_conditions',

+ 0
- 1
server/sonar-web/src/main/js/apps/overview/components/__tests__/QualityGateConditions-test.tsx View File

@@ -28,7 +28,6 @@ import { QualityGateConditions, QualityGateConditionsProps } from '../QualityGat
it('should render correctly', () => {
const wrapper = shallowRender();
expect(wrapper.find('QualityGateCondition').length).toBe(10);
expect(shallowRender({ failedConditions: [] }).type()).toBeNull();
});

it('should be collapsible', () => {

+ 9
- 8
server/sonar-web/src/main/js/apps/overview/styles.css View File

@@ -115,26 +115,27 @@
color: white;
}

.overview-quality-gate-conditions {
padding-bottom: calc(2 * var(--gridSize));
.overview-quality-gate-conditions-list {
background-color: white;
}

.overview-quality-gate-conditions-project-name {
padding: var(--gridSize) 0 var(--gridSize) calc(2 * var(--gridSize));
padding: calc(2 * var(--gridSize)) 0 calc(2 * var(--gridSize)) calc(2 * var(--gridSize));
font-size: var(--bigFontSize);
background: var(--barBorderColor);
}

.overview-quality-gate-conditions-section-title {
padding: calc(2 * var(--gridSize)) calc(2 * var(--gridSize)) var(--gridSize)
calc(2 * var(--gridSize));
border-bottom: 1px solid var(--barBorderColor);
margin: 0;
font-size: var(--baseFontSize);
background: var(--barBorderColor);
}

.overview-quality-gate-conditions-list {
margin-bottom: calc(2 * var(--gridSize));
.overview-quality-gate-conditions-subsection-title {
background-color: white;
border-bottom: 1px solid var(--barBorderColor);
margin: 0;
font-size: var(--baseFontSize);
}

.overview-quality-gate-conditions-list-collapse {

+ 10
- 0
server/sonar-web/src/main/js/apps/overview/utils.ts View File

@@ -112,6 +112,16 @@ export enum MeasurementType {
Duplication = 'DUPLICATION',
}

/*
* Metrics part of Clean As You Code
*/
export const CAYC_METRICS: string[] = [
MetricKey.new_maintainability_rating,
MetricKey.new_reliability_rating,
MetricKey.new_security_hotspots_reviewed,
MetricKey.new_security_rating,
];

const MEASUREMENTS_MAP = {
[MeasurementType.Coverage]: {
metric: MetricKey.coverage,

+ 4
- 0
server/sonar-web/src/main/js/helpers/mocks/quality-gates.ts View File

@@ -40,6 +40,7 @@ export function mockQualityGateStatus(
): QualityGateStatus {
return {
ignoredConditions: false,
isCaycCompliant: true,
failedConditions: [mockQualityGateStatusConditionEnhanced()],
key: 'foo',
name: 'Foo',
@@ -90,6 +91,7 @@ export function mockQualityGateProjectStatus(
},
],
ignoredConditions: false,
isCaycCompliant: true,
status: 'OK',
...overrides,
};
@@ -121,6 +123,7 @@ export function mockQualityGateApplicationStatus(
value: '5',
},
],
isCaycCompliant: true,
status: 'ERROR',
},
{
@@ -136,6 +139,7 @@ export function mockQualityGateApplicationStatus(
value: '15',
},
],
isCaycCompliant: true,
status: 'ERROR',
},
],

+ 3
- 0
server/sonar-web/src/main/js/types/quality-gates.ts View File

@@ -25,6 +25,7 @@ export interface QualityGateProjectStatus {
conditions?: QualityGateProjectStatusCondition[];
ignoredConditions: boolean;
status: Status;
isCaycCompliant: boolean;
}

export interface QualityGateProjectStatusCondition {
@@ -58,11 +59,13 @@ export interface QualityGateApplicationStatusChildProject {
key: string;
name: string;
status: Status;
isCaycCompliant: boolean;
}

export interface QualityGateStatus {
failedConditions: QualityGateStatusConditionEnhanced[];
ignoredConditions?: boolean;
isCaycCompliant: boolean;
key: string;
name: string;
status: Status;

+ 9
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -1810,6 +1810,8 @@ quality_gates.condition_deleted=Successfully deleted condition
quality_gates.delete_condition.confirm.message=Are you sure you want to delete the "{0}" condition?
quality_gates.conditions.fails_when=Quality Gate fails when
quality_gates.conditions.metric=Metric
quality_gates.conditions.cayc=Clean as You Code
quality_gates.conditions.other_conditions=Other conditions
quality_gates.conditions.new_code=On New Code
quality_gates.conditions.new_code.long=Conditions on New Code
quality_gates.conditions.new_code.description=Conditions on New Code apply to all branches and to Pull Requests.
@@ -3254,6 +3256,7 @@ system.version_is_availble={version} is available
#------------------------------------------------------------------------------
overview.failed_conditions=Failed conditions
overview.X_more_failed_conditions={0} more failed conditions
overview.1_condition_failed=1 condition failed
overview.X_conditions_failed={0} conditions failed
overview.fix_failed_conditions_with_sonarlint=Fix issues before they fail your Quality Gate with {link} in your IDE. Power up with connected mode!
overview.quality_gate=Quality Gate Status
@@ -3265,6 +3268,12 @@ overview.you_should_define_quality_gate=You should define a quality gate on this
overview.quality_gate.ignored_conditions=Some Quality Gate conditions on New Code were ignored because of the small number of New Lines
overview.quality_gate.ignored_conditions.tooltip=At the start of a new code period, if very few lines have been added or modified, it might be difficult to reach the desired level of code coverage or duplications. To prevent Quality Gate failure when there's little that can be done about it, Quality Gate conditions about duplications in new code and coverage on new code are ignored until the number of new lines is at least 20. An administrator can disable this in the general settings.
overview.quality_gate.conditions_on_new_code=Only conditions on new code that are defined in the Quality Gate are checked. See the {link} associated to the project for details.
overview.quality_gate.conditions.cayc.warning=Some Clean as You Code conditions are missing or are too permissive.
overview.quality_gate.conditions.cayc.details=Clean as You Code conditions ensure that only Clean Code passes the gate.
overview.quality_gate.conditions.cayc.link=What is Clean as You Code
overview.quality_gate.conditions.cayc.passed=All conditions passed
overview.quality_gate.show_project_conditions_x=Show failed conditions for project {0}
overview.quality_gate.hide_project_conditions_x=Hide failed conditions for project {0}
overview.quality_profiles=Quality Profiles used
overview.new_code_period_x=New Code: {0}
overview.max_new_code_period_from_x=Max New Code from: {0}

Loading…
Cancel
Save