</div>
<div>{step.content}</div>
<div className="sw-flex sw-justify-between sw-items-center sw-mt-3">
- <strong>
- {stepXofYLabel
- ? stepXofYLabel(index + 1, size)
- : intl.formatMessage({ id: 'guiding.step_x_of_y' }, { '0': index + 1, '1': size })}
- </strong>
+ {(stepXofYLabel || size > 1) && (
+ <strong>
+ {stepXofYLabel
+ ? stepXofYLabel(index + 1, size)
+ : intl.formatMessage({ id: 'guiding.step_x_of_y' }, { '0': index + 1, '1': size })}
+ </strong>
+ )}
+ <span />
<div>
{index > 0 && (
<ButtonLink className="sw-mr-4" {...backProps}>
expect(await screen.findByRole('alertdialog')).toBeInTheDocument();
expect(screen.getByRole('alertdialog')).toHaveTextContent(
- 'Trust The FooFoo bar is bazstep 1 of 5next'
+ 'Trust The FooFoo bar is bazstep 1 of 5next',
);
+ expect(screen.getByText('step 1 of 5')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'next' }));
expect(screen.getByRole('alertdialog')).toHaveTextContent(
- 'Trust The BazBaz foo is barstep 2 of 5go_backnext'
+ 'Trust The BazBaz foo is barstep 2 of 5go_backnext',
);
expect(callback).toHaveBeenCalled();
await user.click(screen.getByRole('button', { name: 'next' }));
expect(screen.getByRole('alertdialog')).toHaveTextContent(
- 'Trust The BarBar baz is foostep 3 of 5go_backnext'
+ 'Trust The BarBar baz is foostep 3 of 5go_backnext',
);
await user.click(screen.getByRole('button', { name: 'next' }));
expect(screen.getByRole('alertdialog')).toHaveTextContent(
- 'Trust The Foo 2Foo baz is barstep 4 of 5go_backnext'
+ 'Trust The Foo 2Foo baz is barstep 4 of 5go_backnext',
);
await user.click(screen.getByRole('button', { name: 'go_back' }));
expect(screen.getByRole('alertdialog')).toHaveTextContent(
- 'Trust The BarBar baz is foostep 3 of 5go_backnext'
+ 'Trust The BarBar baz is foostep 3 of 5go_backnext',
);
await user.click(screen.getByRole('button', { name: 'next' }));
await user.click(screen.getByRole('button', { name: 'next' }));
expect(screen.getByRole('alertdialog')).toHaveTextContent(
- 'Trust The Baz 2Baz bar is foostep 5 of 5go_backclose'
+ 'Trust The Baz 2Baz bar is foostep 5 of 5go_backclose',
);
expect(screen.queryByRole('button', { name: 'next' })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'close_me' })).toBeInTheDocument();
});
+it('should not display steps counter when there is only one step and no render method', async () => {
+ renderSpotlightTour({
+ steps: [
+ {
+ target: '#step1',
+ content: 'Foo bar is baz',
+ title: 'Trust The Foo',
+ placement: 'top',
+ },
+ ],
+ stepXofYLabel: undefined,
+ });
+
+ expect(await screen.findByRole('alertdialog')).toBeInTheDocument();
+ expect(screen.queryByText('step 1 of 1')).not.toBeInTheDocument();
+});
+
function renderSpotlightTour(props: Partial<SpotlightTourProps> = {}) {
return renderWithContext(
<div>
]}
{...props}
/>
- </div>
+ </div>,
);
}
value: string;
failingConditionMetric: MetricKey;
requireLabel: string;
+ guidingKeyOnError?: string;
}
export default function MeasuresCardNumber(
props: React.PropsWithChildren<Props & React.HTMLAttributes<HTMLDivElement>>,
) {
- const { label, value, failedConditions, url, failingConditionMetric, requireLabel, ...rest } =
- props;
+ const {
+ label,
+ value,
+ failedConditions,
+ url,
+ failingConditionMetric,
+ requireLabel,
+ guidingKeyOnError,
+ ...rest
+ } = props;
const failed = Boolean(
failedConditions.find((condition) => condition.metric === failingConditionMetric),
metric={failingConditionMetric}
label={label}
failed={failed}
+ data-guiding-id={failed ? guidingKeyOnError : undefined}
{...rest}
>
{failed && <TextError className="sw-font-regular sw-mt-2" text={requireLabel} />}
count: newViolations,
},
)}
+ guidingKeyOnError="overviewZeroNewIssuesSimplification"
/>
<MeasuresCardNumber
isLastStatus={qgStatusIdx === failedQgStatuses.length - 1}
key={qgStatus.key}
qgStatus={qgStatus}
+ qualityGate={qualityGate}
/>
))}
</div>
QualityGateStatus,
QualityGateStatusConditionEnhanced,
} from '../../../types/quality-gates';
+import { QualityGate } from '../../../types/types';
import QualityGateConditions from '../components/QualityGateConditions';
+import ZeroNewIssuesSimplificationGuide from '../components/ZeroNewIssuesSimplificationGuide';
export interface QualityGatePanelSectionProps {
branchLike?: BranchLike;
isApplication?: boolean;
isLastStatus?: boolean;
qgStatus: QualityGateStatus;
+ qualityGate?: QualityGate;
}
function splitConditions(
}
export function QualityGatePanelSection(props: QualityGatePanelSectionProps) {
- const { isApplication, isLastStatus, qgStatus } = props;
+ const { isApplication, isLastStatus, qgStatus, qualityGate } = props;
const [collapsed, setCollapsed] = React.useState(false);
const toggle = React.useCallback(() => {
</>
)}
+ {qualityGate?.isBuiltIn && (
+ <ZeroNewIssuesSimplificationGuide qualityGate={qualityGate} />
+ )}
<QualityGateConditions
component={qgStatus}
branchLike={qgStatus.branchLike}
failedConditions={newCodeFailedConditions}
+ isBuiltInQualityGate={qualityGate?.isBuiltIn}
/>
</>
)}
import { screen } from '@testing-library/react';
import * as React from 'react';
+import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider';
+import { mockQualityGate, mockQualityGateStatus } from '../../../../helpers/mocks/quality-gates';
+import { mockLoggedInUser } from '../../../../helpers/testMocks';
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { byRole } from '../../../../helpers/testSelector';
import { MetricKey } from '../../../../types/metrics';
import { CaycStatus, Status } from '../../../../types/types';
+import { CurrentUser, NoticeType } from '../../../../types/users';
import QualityGatePanelSection, { QualityGatePanelSectionProps } from '../QualityGatePanelSection';
const failedConditions = [
measure: {
metric: {
id: 'metricId1',
- key: 'metricKey1',
+ key: MetricKey.new_coverage,
name: 'metricName1',
type: 'metricType1',
},
measure: {
metric: {
id: 'metricId2',
- key: 'metricKey2',
+ key: MetricKey.security_hotspots,
name: 'metricName2',
type: 'metricType2',
},
metric: MetricKey.security_hotspots,
op: 'op2',
},
+ {
+ level: 'ERROR' as Status,
+ measure: {
+ metric: {
+ id: 'metricId2',
+ key: MetricKey.new_violations,
+ name: 'metricName2',
+ type: 'metricType2',
+ },
+ },
+ metric: MetricKey.new_violations,
+ op: 'op2',
+ },
];
-const qgStatus = {
+const qgStatus = mockQualityGateStatus({
caycStatus: CaycStatus.Compliant,
failedConditions,
key: 'qgStatusKey',
name: 'qgStatusName',
status: 'ERROR' as Status,
-};
+});
it('should render correctly for an application with 1 new code condition and 1 overall code condition', async () => {
renderQualityGatePanelSection();
- expect(await screen.findByText('quality_gates.conditions.new_code_1')).toBeInTheDocument();
+ expect(await screen.findByText('quality_gates.conditions.new_code_x.2')).toBeInTheDocument();
expect(await screen.findByText('quality_gates.conditions.overall_code_1')).toBeInTheDocument();
});
expect(screen.queryByText('quality_gates.conditions.overall_code_1')).not.toBeInTheDocument();
});
-function renderQualityGatePanelSection(props: Partial<QualityGatePanelSectionProps> = {}) {
- return renderComponent(<QualityGatePanelSection isApplication qgStatus={qgStatus} {...props} />);
+it('should render correctly 0 New issues onboarding', async () => {
+ renderQualityGatePanelSection({
+ isApplication: false,
+ qgStatus: { ...qgStatus, failedConditions: [failedConditions[2]] },
+ qualityGate: mockQualityGate({ isBuiltIn: true }),
+ });
+
+ expect(screen.queryByText('quality_gates.conditions.new_code_1')).not.toBeInTheDocument();
+ expect(await byRole('alertdialog').find()).toBeInTheDocument();
+});
+
+it('should not render 0 New issues onboarding for user who dismissed it', async () => {
+ renderQualityGatePanelSection(
+ {
+ isApplication: false,
+ qgStatus: { ...qgStatus, failedConditions: [failedConditions[2]] },
+ qualityGate: mockQualityGate({ isBuiltIn: true }),
+ },
+ mockLoggedInUser({
+ dismissedNotices: { [NoticeType.OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION]: true },
+ }),
+ );
+
+ expect(screen.queryByText('quality_gates.conditions.new_code_1')).not.toBeInTheDocument();
+ expect(await byRole('alertdialog').query()).not.toBeInTheDocument();
+});
+
+function renderQualityGatePanelSection(
+ props: Partial<QualityGatePanelSectionProps> = {},
+ currentUser: CurrentUser = mockLoggedInUser(),
+) {
+ return renderComponent(
+ <CurrentUserContextProvider currentUser={currentUser}>
+ <QualityGatePanelSection isApplication qgStatus={qgStatus} {...props} />
+ </CurrentUserContextProvider>,
+ );
}
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
import { BranchLike } from '../../../types/branch-like';
+import { MetricKey } from '../../../types/metrics';
import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
import { Component } from '../../../types/types';
import QualityGateCondition from './QualityGateCondition';
+import QualityGateSimplifiedCondition from './QualityGateSimplifiedCondition';
const LEVEL_ORDER = ['ERROR', 'WARN'];
component: Pick<Component, 'key'>;
collapsible?: boolean;
failedConditions: QualityGateStatusConditionEnhanced[];
+ isBuiltInQualityGate?: boolean;
}
const MAX_CONDITIONS = 5;
export function QualityGateConditions(props: QualityGateConditionsProps) {
- const { branchLike, collapsible, component, failedConditions } = props;
+ const { branchLike, collapsible, component, failedConditions, isBuiltInQualityGate } = props;
const [collapsed, toggleCollapsed] = React.useState(Boolean(collapsible));
const handleToggleCollapsed = React.useCallback(() => toggleCollapsed(!collapsed), [collapsed]);
+ const isSimplifiedCondition = React.useCallback(
+ (condition: QualityGateStatusConditionEnhanced) => {
+ const { metric } = condition.measure;
+ return metric.key === MetricKey.new_violations && isBuiltInQualityGate;
+ },
+ [isBuiltInQualityGate],
+ );
+
const sortedConditions = sortBy(failedConditions, (condition) =>
LEVEL_ORDER.indexOf(condition.level),
);
<ul id="overview-quality-gate-conditions-list" className="sw-mb-2">
{renderConditions.map((condition) => (
<div key={condition.measure.metric.key}>
- <QualityGateCondition
- branchLike={branchLike}
- component={component}
- condition={condition}
- />
+ {isSimplifiedCondition(condition) ? (
+ <QualityGateSimplifiedCondition
+ branchLike={branchLike}
+ component={component}
+ condition={condition}
+ />
+ ) : (
+ <QualityGateCondition
+ branchLike={branchLike}
+ component={component}
+ condition={condition}
+ />
+ )}
<BasicSeparator />
</div>
))}
--- /dev/null
+/*
+ * 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 { Highlight, LinkBox } from 'design-system';
+import * as React from 'react';
+import { propsToIssueParams } from '../../../components/shared/utils';
+import { getBranchLikeQuery } from '../../../helpers/branch-like';
+import { translate } from '../../../helpers/l10n';
+import { formatMeasure, isDiffMetric, localizeMetric } from '../../../helpers/measures';
+import { getComponentIssuesUrl } from '../../../helpers/urls';
+import { BranchLike } from '../../../types/branch-like';
+import { MetricKey, MetricType } from '../../../types/metrics';
+import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
+import { Component } from '../../../types/types';
+
+interface Props {
+ branchLike?: BranchLike;
+ component: Pick<Component, 'key'>;
+ condition: QualityGateStatusConditionEnhanced;
+}
+
+export default function QualityGateSimplifiedCondition({
+ branchLike,
+ component,
+ condition,
+}: Readonly<Props>) {
+ const getPrimaryText = () => {
+ const { measure } = condition;
+ const { metric } = measure;
+ const isDiff = isDiffMetric(metric.key);
+
+ const subText =
+ !isDiff && condition.period != null
+ ? `${localizeMetric(metric.key)} ${translate('quality_gates.conditions.new_code')}`
+ : localizeMetric(metric.key);
+
+ return subText;
+ };
+
+ const { measure } = condition;
+ const { metric } = measure;
+
+ const value = (condition.period ? measure.period?.value : measure.value) as string;
+
+ const formattedValue = formatMeasure(value, MetricType.ShortInteger, {
+ decimals: 0,
+ omitExtraDecimalZeros: metric.type === MetricType.Percent,
+ });
+
+ return (
+ <LinkBox
+ to={getComponentIssuesUrl(component.key, {
+ ...propsToIssueParams(condition.measure.metric.key, condition.period != null),
+ ...getBranchLikeQuery(branchLike),
+ })}
+ >
+ <div className="sw-flex sw-p-2 sw-items-baseline">
+ <Highlight className="sw-mx-4 sw-w-6 sw-my-0 sw-text-right">{formattedValue}</Highlight>
+ <Highlight
+ className="sw-text-ellipsis sw-pr-4"
+ data-guiding-id={
+ metric.key === MetricKey.new_violations
+ ? 'overviewZeroNewIssuesSimplification'
+ : undefined
+ }
+ >
+ {getPrimaryText()}
+ </Highlight>
+ </div>
+ </LinkBox>
+ );
+}
--- /dev/null
+/*
+ * 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 { SpotlightTour, SpotlightTourStep } from 'design-system';
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { dismissNotice } from '../../../api/users';
+import { CurrentUserContext } from '../../../app/components/current-user/CurrentUserContext';
+import Link from '../../../components/common/Link';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { QualityGate } from '../../../types/types';
+import { NoticeType } from '../../../types/users';
+
+interface Props {
+ qualityGate: QualityGate;
+}
+
+export default function ZeroNewIssuesSimplificationGuide({ qualityGate }: Readonly<Props>) {
+ const { currentUser, updateDismissedNotices } = React.useContext(CurrentUserContext);
+ const shouldRun =
+ currentUser.isLoggedIn &&
+ !currentUser.dismissedNotices[NoticeType.OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION];
+
+ const steps: SpotlightTourStep[] = [
+ {
+ target: `[data-guiding-id="overviewZeroNewIssuesSimplification"]`,
+ content: (
+ <>
+ <p className="sw-mb-4">
+ <FormattedMessage
+ id="overview.quality_gates.conditions.condition_simplification_tour.content1"
+ defaultMessage={translate(
+ 'overview.quality_gates.conditions.condition_simplification_tour.content1',
+ )}
+ values={{
+ link: (
+ <Link to={`/quality_gates/show/${qualityGate.name}`}>
+ {translateWithParameters(
+ 'overview.quality_gates.conditions.condition_simplification_tour.content1.link',
+ qualityGate.name,
+ )}
+ </Link>
+ ),
+ }}
+ />
+ </p>
+ <p>
+ {translate('overview.quality_gates.conditions.condition_simplification_tour.content2')}
+ </p>
+ </>
+ ),
+ title: translate('overview.quality_gates.conditions.condition_simplification_tour.title'),
+ placement: 'right',
+ },
+ ];
+
+ const onCallback = async (props: { action: string; type: string }) => {
+ if (props.action === 'close' && props.type === 'tour:end' && shouldRun) {
+ await dismissNotice(NoticeType.OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION);
+ updateDismissedNotices(NoticeType.OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION, true);
+ }
+ };
+
+ return (
+ <SpotlightTour
+ run={shouldRun}
+ closeLabel={translate('dismiss')}
+ callback={onCallback}
+ steps={steps}
+ />
+ );
+}
--- /dev/null
+/*
+ * 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 { screen } from '@testing-library/react';
+import React from 'react';
+import { mockQualityGateStatusConditionEnhanced } from '../../../../helpers/mocks/quality-gates';
+import { mockMetric } from '../../../../helpers/testMocks';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { MetricKey, MetricType } from '../../../../types/metrics';
+import { QualityGateStatusConditionEnhanced } from '../../../../types/quality-gates';
+import QualityGateCondition from '../QualityGateCondition';
+import QualityGateSimplifiedCondition from '../QualityGateSimplifiedCondition';
+
+it('should show simplified condition', async () => {
+ renderQualityGateCondition({
+ condition: quickMock(MetricKey.new_violations, MetricType.Integer),
+ });
+ expect(await screen.findByText('metric.new_violations.name')).toBeInTheDocument();
+});
+
+function renderQualityGateCondition(props: Partial<QualityGateCondition['props']>) {
+ return renderComponent(
+ <QualityGateSimplifiedCondition
+ component={{ key: 'abcd-key' }}
+ condition={mockQualityGateStatusConditionEnhanced()}
+ {...props}
+ />,
+ );
+}
+
+function quickMock(
+ metric: MetricKey,
+ type = MetricType.Rating,
+ addPeriod = false,
+ value = '3',
+): QualityGateStatusConditionEnhanced {
+ return mockQualityGateStatusConditionEnhanced({
+ error: '1',
+ measure: {
+ metric: mockMetric({
+ key: metric,
+ name: metric,
+ type,
+ }),
+ value,
+ ...(addPeriod ? { period: { value, index: 1 } } : {}),
+ },
+ metric,
+ ...(addPeriod ? { period: 1 } : {}),
+ });
+}
import * as React from 'react';
import { useEffect, useState } from 'react';
import { getMeasuresWithMetrics } from '../../../api/measures';
+import { fetchQualityGate, getGateForProject } from '../../../api/quality-gates';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { enhanceConditionWithMeasure, enhanceMeasuresWithMetrics } from '../../../helpers/measures';
import { isDefined } from '../../../helpers/types';
import { useBranchStatusQuery } from '../../../queries/branch';
import { PullRequest } from '../../../types/branch-like';
-import { Component, MeasureEnhanced } from '../../../types/types';
+import { Component, MeasureEnhanced, QualityGate } from '../../../types/types';
import MeasuresCardPanel from '../branches/MeasuresCardPanel';
import BranchQualityGate from '../components/BranchQualityGate';
import IgnoredConditionWarning from '../components/IgnoredConditionWarning';
import MetaTopBar from '../components/MetaTopBar';
+import ZeroNewIssuesSimplificationGuide from '../components/ZeroNewIssuesSimplificationGuide';
import '../styles.css';
import { PR_METRICS, Status } from '../utils';
import SonarLintAd from './SonarLintAd';
export default function PullRequestOverview(props: Props) {
const { component, branchLike } = props;
- const [loadingMeasure, setLoadingMeasure] = useState(false);
+ const [isLoadingMeasures, setIsLoadingMeasures] = useState(false);
const [measures, setMeasures] = useState<MeasureEnhanced[]>([]);
- const { data: { conditions, ignoredConditions, status } = {}, isLoading } =
- useBranchStatusQuery(component);
- const loading = isLoading || loadingMeasure;
+ const {
+ data: { conditions, ignoredConditions, status } = {},
+ isLoading: isLoadingBranchStatusesData,
+ } = useBranchStatusQuery(component);
+ const [isLoadingQualityGate, setIsLoadingQualityGate] = useState(false);
+ const [qualityGate, setQualityGate] = useState<QualityGate>();
+ const isLoading = isLoadingBranchStatusesData || isLoadingMeasures || isLoadingQualityGate;
useEffect(() => {
- setLoadingMeasure(true);
+ setIsLoadingMeasures(true);
const metricKeys =
conditions !== undefined
getMeasuresWithMetrics(component.key, metricKeys, getBranchLikeQuery(branchLike)).then(
({ component, metrics }) => {
if (component.measures) {
- setLoadingMeasure(false);
+ setIsLoadingMeasures(false);
setMeasures(enhanceMeasuresWithMetrics(component.measures || [], metrics));
}
},
() => {
- setLoadingMeasure(false);
+ setIsLoadingMeasures(false);
},
);
}, [branchLike, component.key, conditions]);
- if (loading) {
+ useEffect(() => {
+ async function fetchQualityGateDate() {
+ setIsLoadingQualityGate(true);
+
+ const qualityGate = await getGateForProject({ project: component.key });
+ const qgDetails = await fetchQualityGate({ name: qualityGate.name });
+
+ setQualityGate(qgDetails);
+ setIsLoadingQualityGate(false);
+ }
+
+ fetchQualityGateDate();
+ }, [component.key]);
+
+ if (isLoading) {
return (
<CenteredLayout>
<div className="sw-p-6">
measures={measures}
/>
+ {qualityGate?.isBuiltIn && <ZeroNewIssuesSimplificationGuide qualityGate={qualityGate} />}
+
<SonarLintAd status={status} />
</div>
</div>
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
-import { getQualityGateProjectStatus } from '../../../../api/quality-gates';
+import { fetchQualityGate, getQualityGateProjectStatus } from '../../../../api/quality-gates';
import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider';
import { mockPullRequest } from '../../../../helpers/mocks/branch-like';
import { mockComponent } from '../../../../helpers/mocks/component';
-import { mockQualityGateProjectCondition } from '../../../../helpers/mocks/quality-gates';
+import {
+ mockQualityGate,
+ mockQualityGateProjectCondition,
+} from '../../../../helpers/mocks/quality-gates';
import { mockLoggedInUser, mockMeasure, mockMetric } from '../../../../helpers/testMocks';
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import { byLabelText, byRole } from '../../../../helpers/testSelector';
import { ComponentQualifier } from '../../../../types/component';
import { MetricKey, MetricType } from '../../../../types/metrics';
import { CaycStatus } from '../../../../types/types';
+import { NoticeType } from '../../../../types/users';
import PullRequestOverview from '../PullRequestOverview';
jest.mock('../../../../api/measures', () => {
mockMeasure({
metric: MetricKey.new_lines,
}),
+ mockMeasure({
+ metric: MetricKey.new_violations,
+ }),
],
},
metrics: [
mockMetric({ key: MetricKey.new_coverage }),
- mockMetric({
- key: MetricKey.duplicated_lines,
- }),
+ mockMetric({ key: MetricKey.duplicated_lines }),
mockMetric({ key: MetricKey.new_lines, type: MetricType.ShortInteger }),
- mockMetric({
- key: MetricKey.new_bugs,
- type: MetricType.Integer,
- }),
+ mockMetric({ key: MetricKey.new_bugs, type: MetricType.Integer }),
+ mockMetric({ key: MetricKey.new_violations }),
],
}),
};
});
jest.mock('../../../../api/quality-gates', () => {
- const { mockQualityGateProjectStatus, mockQualityGateApplicationStatus } = jest.requireActual(
- '../../../../helpers/mocks/quality-gates',
- );
+ const { mockQualityGateProjectStatus, mockQualityGateApplicationStatus, mockQualityGate } =
+ jest.requireActual('../../../../helpers/mocks/quality-gates');
const { MetricKey } = jest.requireActual('../../../../types/metrics');
return {
getQualityGateProjectStatus: jest.fn().mockResolvedValue(
}),
),
getApplicationQualityGate: jest.fn().mockResolvedValue(mockQualityGateApplicationStatus()),
+ getGateForProject: jest.fn().mockResolvedValue(mockQualityGate({ isBuiltIn: true })),
+ fetchQualityGate: jest.fn().mockResolvedValue(mockQualityGate({ isBuiltIn: true })),
};
});
).not.toBeInTheDocument();
});
+it('should render correctly 0 New issues onboarding', async () => {
+ jest.mocked(getQualityGateProjectStatus).mockResolvedValueOnce({
+ status: 'ERROR',
+ conditions: [
+ mockQualityGateProjectCondition({
+ status: 'ERROR',
+ errorThreshold: '0',
+ metricKey: MetricKey.new_violations,
+ actualValue: '1',
+ }),
+ ],
+ caycStatus: CaycStatus.Compliant,
+ ignoredConditions: false,
+ });
+ jest.mocked(fetchQualityGate).mockResolvedValueOnce(mockQualityGate({ isBuiltIn: true }));
+
+ renderPullRequestOverview();
+
+ expect(
+ await byLabelText('overview.quality_gate_x.overview.gate.ERROR').find(),
+ ).toBeInTheDocument();
+ expect(await byRole('alertdialog').find()).toBeInTheDocument();
+});
+
+it('should not render 0 New issues onboarding when user dismissed it', async () => {
+ jest.mocked(getQualityGateProjectStatus).mockResolvedValueOnce({
+ status: 'ERROR',
+ conditions: [
+ mockQualityGateProjectCondition({
+ status: 'ERROR',
+ errorThreshold: '0',
+ metricKey: MetricKey.new_violations,
+ actualValue: '1',
+ }),
+ ],
+ caycStatus: CaycStatus.Compliant,
+ ignoredConditions: false,
+ });
+ jest.mocked(fetchQualityGate).mockResolvedValueOnce(mockQualityGate({ isBuiltIn: true }));
+
+ renderPullRequestOverview(
+ {},
+ mockLoggedInUser({
+ dismissedNotices: { [NoticeType.OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION]: true },
+ }),
+ );
+
+ await waitFor(async () =>
+ expect(
+ await byLabelText('overview.quality_gate_x.overview.gate.ERROR').find(),
+ ).toBeInTheDocument(),
+ );
+
+ expect(await byRole('alertdialog').query()).not.toBeInTheDocument();
+});
+
function renderPullRequestOverview(
props: Partial<ComponentPropsType<typeof PullRequestOverview>> = {},
+ currentUser = mockLoggedInUser(),
) {
renderComponent(
- <CurrentUserContextProvider currentUser={mockLoggedInUser()}>
+ <CurrentUserContextProvider currentUser={currentUser}>
<PullRequestOverview
branchLike={mockPullRequest()}
component={mockComponent({
import React from 'react';
import { dismissNotice } from '../../../api/users';
import { CurrentUserContext } from '../../../app/components/current-user/CurrentUserContext';
+import DocumentationLink from '../../../components/common/DocumentationLink';
import { translate } from '../../../helpers/l10n';
import { NoticeType } from '../../../types/users';
!currentUser.dismissedNotices[NoticeType.QG_CAYC_CONDITIONS_SIMPLIFICATION];
const steps: SpotlightTourStep[] = [
+ {
+ target: '[data-guiding-id="caycConditionsSimplification"]',
+ content: (
+ <p>{translate('quality_gates.cayc.condition_simplification_tour.page_1.content1')}</p>
+ ),
+ title: translate('quality_gates.cayc.condition_simplification_tour.page_1.title'),
+ placement: 'right',
+ },
+ {
+ target: '[data-guiding-id="caycConditionsSimplification"]',
+ content: (
+ <>
+ <p className="sw-mb-4">
+ {translate('quality_gates.cayc.condition_simplification_tour.page_2.content1')}
+ </p>
+ <p>{translate('quality_gates.cayc.condition_simplification_tour.page_2.content2')}</p>
+ </>
+ ),
+ title: translate('quality_gates.cayc.condition_simplification_tour.page_2.title'),
+ placement: 'right',
+ },
{
target: '[data-guiding-id="caycConditionsSimplification"]',
content: (
<>
<p className="sw-mb-4">
- {translate('quality_gates.cayc.condition_simplification_tour.content1')}
+ {translate('quality_gates.cayc.condition_simplification_tour.page_3.content1')}
</p>
- <p>{translate('quality_gates.cayc.condition_simplification_tour.content2')}</p>
+ <DocumentationLink to="/user-guide/issues/#resolutions">
+ {translate('quality_gates.cayc.condition_simplification_tour.page_3.content2')}
+ </DocumentationLink>
</>
),
- title: translate('quality_gates.cayc.condition_simplification_tour.title'),
+ title: translate('quality_gates.cayc.condition_simplification_tour.page_3.title'),
placement: 'right',
},
];
- const onCallback = async (props: { action: string }) => {
- if (props.action === 'close' && shouldRun) {
+ const onCallback = async (props: { action: string; type: string }) => {
+ if (props.action === 'close' && props.type === 'tour:end' && shouldRun) {
await dismissNotice(NoticeType.QG_CAYC_CONDITIONS_SIMPLIFICATION);
updateDismissedNotices(NoticeType.QG_CAYC_CONDITIONS_SIMPLIFICATION, true);
}
return (
<SpotlightTour
+ continuous
run={shouldRun}
closeLabel={translate('dismiss')}
callback={onCallback}
expect(await byRole('alertdialog').find()).toBeInTheDocument();
+ expect(
+ byRole('alertdialog')
+ .byText('quality_gates.cayc.condition_simplification_tour.page_1.title')
+ .get(),
+ ).toBeInTheDocument();
+
+ await act(async () => {
+ await user.click(byRole('alertdialog').byRole('button', { name: 'next' }).get());
+ });
+
+ expect(
+ byRole('alertdialog')
+ .byText('quality_gates.cayc.condition_simplification_tour.page_2.title')
+ .get(),
+ ).toBeInTheDocument();
+
+ await act(async () => {
+ await user.click(byRole('alertdialog').byRole('button', { name: 'next' }).get());
+ });
+
+ expect(
+ byRole('alertdialog')
+ .byText('quality_gates.cayc.condition_simplification_tour.page_3.title')
+ .get(),
+ ).toBeInTheDocument();
+
await act(async () => {
await user.click(byRole('alertdialog').byRole('button', { name: 'dismiss' }).get());
});
SONARLINT_AD = 'sonarlintAd',
ISSUE_GUIDE = 'issueCleanCodeGuide',
QG_CAYC_CONDITIONS_SIMPLIFICATION = 'qualityGateCaYCConditionsSimplification',
+ OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION = 'overviewZeroNewIssuesSimplification',
}
export interface LoggedInUser extends CurrentUser, UserActive {
*/
package org.sonar.server.user.ws;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.util.Optional;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.sonar.api.utils.System2;
import org.sonar.db.DbTester;
import org.sonar.db.property.PropertyDto;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.sonar.server.user.ws.DismissNoticeAction.AVAILABLE_NOTICE_KEYS;
+@RunWith(DataProviderRunner.class)
public class DismissNoticeActionIT {
@Rule
private final WsActionTester tester = new WsActionTester(new DismissNoticeAction(userSessionRule, db.getDbClient()));
- @Test
- public void dismiss_educationPrinciples() {
- userSessionRule.logIn();
-
- TestResponse testResponse = tester.newRequest()
- .setParam("notice", "educationPrinciples")
- .execute();
-
- assertThat(testResponse.getStatus()).isEqualTo(204);
-
- Optional<PropertyDto> propertyDto = db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.educationPrinciples");
- assertThat(propertyDto).isPresent();
- }
-
- @Test
- public void dismiss_sonarlintAd() {
- userSessionRule.logIn();
-
- TestResponse testResponse = tester.newRequest()
- .setParam("notice", "sonarlintAd")
- .execute();
-
- assertThat(testResponse.getStatus()).isEqualTo(204);
-
- Optional<PropertyDto> propertyDto = db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.sonarlintAd");
- assertThat(propertyDto).isPresent();
- }
-
- @Test
- public void execute_whenNoticeIsIssueCleanCodeGuide_shouldDismissCorrespondingNotice() {
- userSessionRule.logIn();
-
- TestResponse testResponse = tester.newRequest()
- .setParam("notice", "issueCleanCodeGuide")
- .execute();
-
- assertThat(testResponse.getStatus()).isEqualTo(204);
-
- Optional<PropertyDto> propertyDto = db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.issueCleanCodeGuide");
- assertThat(propertyDto).isPresent();
- }
-
@Test
public void authentication_is_required() {
TestRequest testRequest = tester.newRequest()
assertThatThrownBy(testRequest::execute)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
- "Value of parameter 'notice' (not_supported_value) must be one of: [educationPrinciples, sonarlintAd, issueCleanCodeGuide, qualityGateCaYCConditionsSimplification]");
+ "Value of parameter 'notice' (not_supported_value) must be one of: [educationPrinciples, sonarlintAd, issueCleanCodeGuide, qualityGateCaYCConditionsSimplification, " +
+ "overviewZeroNewIssuesSimplification]");
}
@Test
assertThat(db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.educationPrinciples")).isPresent();
TestResponse testResponse = tester.newRequest()
- .setParam("notice", "sonarlintAd")
+ .setParam("notice", "educationPrinciples")
.execute();
assertThat(testResponse.getStatus()).isEqualTo(204);
assertThat(db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.educationPrinciples")).isPresent();
}
+ @Test
+ @UseDataProvider("noticeKeys")
+ public void dismiss_notice(String noticeKey) {
+ userSessionRule.logIn();
+
+ TestResponse testResponse = tester.newRequest()
+ .setParam("notice", noticeKey)
+ .execute();
+
+ assertThat(testResponse.getStatus()).isEqualTo(204);
+
+ Optional<PropertyDto> propertyDto = db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices." + noticeKey);
+ assertThat(propertyDto).isPresent();
+ }
+
+ @DataProvider
+ public static Object[][] noticeKeys() {
+ return AVAILABLE_NOTICE_KEYS.stream()
+ .map(noticeKey -> new Object[] {noticeKey})
+ .toArray(Object[][]::new);
+ }
}
private static final String SONARLINT_AD = "sonarlintAd";
private static final String ISSUE_CLEAN_CODE_GUIDE = "issueCleanCodeGuide";
private static final String QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION = "qualityGateCaYCConditionsSimplification";
+ private static final String OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION = "overviewZeroNewIssuesSimplification";
- protected static final List<String> AVAILABLE_NOTICE_KEYS = List.of(EDUCATION_PRINCIPLES, SONARLINT_AD, ISSUE_CLEAN_CODE_GUIDE, QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION);
+ protected static final List<String> AVAILABLE_NOTICE_KEYS = List.of(EDUCATION_PRINCIPLES, SONARLINT_AD, ISSUE_CLEAN_CODE_GUIDE, QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION,
+ OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION);
public static final String USER_DISMISS_CONSTANT = "user.dismissedNotices.";
private final UserSession userSession;
.setKey(paramKey)
.build();
- if (!dbClient.propertiesDao().selectByQuery(query, dbSession).isEmpty()) {
- // already dismissed
- response.noContent();
+ if (dbClient.propertiesDao().selectByQuery(query, dbSession).isEmpty()) {
+ PropertyDto property = new PropertyDto().setUserUuid(currentUserUuid).setKey(paramKey);
+ dbClient.propertiesDao().saveProperty(dbSession, property);
+ dbSession.commit();
}
- PropertyDto property = new PropertyDto().setUserUuid(currentUserUuid).setKey(paramKey);
- dbClient.propertiesDao().saveProperty(dbSession, property);
- dbSession.commit();
response.noContent();
}
}
quality_gates.cannot_set_default_no_cayc=You must make this quality gate Clean as You Code compliant to make this the default quality gate.
quality_gates.cannot_copy_no_cayc=You must make this quality gate Clean as You Code compliant to copy.
quality_gates.is_default_no_conditions=This is the default quality gate, but it has no configured conditions. Please configure at least 1 condition for this quality gate.
-quality_gates.is_built_in.description=The only Quality Gate you need to {link}
+quality_gates.is_built_in.description=The only quality gate you need to practice {link}
quality_gates.conditions=Conditions
quality_gates.conditions.help=Your project will fail the Quality Gate if it crosses any metric thresholds set for New Code or Overall Code.
quality_gates.conditions.help.link=See also: Clean as You Code
quality_gates.cayc.review_update_modal.confirm_text=Fix Quality Gate
quality_gates.cayc.review_update_modal.description1=This quality gate will be updated to comply with {cayc_link}. Please review the changes below.
quality_gates.cayc.review_update_modal.description2=All other conditions will be preserved
-quality_gates.cayc.condition_simplification_tour.title=One condition, zero issues
-quality_gates.cayc.condition_simplification_tour.content1=One single condition ensures that new code has 0 issues.
-quality_gates.cayc.condition_simplification_tour.content2=This condition replaces the three conditions on Security rating, Reliability rating and Maintainability rating.
+quality_gates.cayc.condition_simplification_tour.page_1.title='Clean as You Code' ready!
+quality_gates.cayc.condition_simplification_tour.page_1.content1=The conditions in this quality gate have been updated to ensure that any code added or changed is clean.
+quality_gates.cayc.condition_simplification_tour.page_2.title=One condition, zero issues
+quality_gates.cayc.condition_simplification_tour.page_2.content1=One single condition ensures that new code has no issues.
+quality_gates.cayc.condition_simplification_tour.page_2.content2=This condition replaces the three conditions on Security rating, Reliability rating and Maintainability rating.
+quality_gates.cayc.condition_simplification_tour.page_3.title=Resolve pending issues
+quality_gates.cayc.condition_simplification_tour.page_3.content1=Starting now, every issue in new code must be resolved for a project to pass this quality gate.
+quality_gates.cayc.condition_simplification_tour.page_3.content2=Learn more: Issue resolutions
quality_gates.cayc.new_maintainability_rating.A=Technical debt ratio is less than {0}
quality_gates.cayc.new_maintainability_rating=Technical debt ratio is greater than {1}
quality_gates.cayc.new_reliability_rating.A=No bugs
overview.quality_gate.conditions.cayc.details_with_link=Fixing {link} will help you achieve a Clean Code state.
overview.quality_gate.conditions.non_cayc.warning.link=this quality gate
overview.quality_gate.conditions.cayc.link=Learn why
+overview.quality_gates.conditions.condition_simplification_tour.title=One condition, zero issues
+overview.quality_gates.conditions.condition_simplification_tour.content1=A new condition was introduced in {link} to ensure that new code has no issues.
+overview.quality_gates.conditions.condition_simplification_tour.content1.link={0} quality gate
+overview.quality_gates.conditions.condition_simplification_tour.content2=Starting now, every issue in new code must be resolved for a project to pass this quality gate.
overview.quality_gate.application.non_cayc.projects_x={0} project(s) in this application use a Quality Gate that does not comply with Clean as You Code
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}