@@ -19,10 +19,19 @@ | |||
*/ | |||
import { cloneDeep, flatten, omit, remove } from 'lodash'; | |||
import { Project } from '../../apps/quality-gates/components/Projects'; | |||
import { mockQualityGate } from '../../helpers/mocks/quality-gates'; | |||
import { | |||
mockQualityGate, | |||
mockQualityGateApplicationStatus, | |||
mockQualityGateProjectStatus, | |||
} from '../../helpers/mocks/quality-gates'; | |||
import { mockUserBase } from '../../helpers/mocks/users'; | |||
import { mockCondition, mockGroup } from '../../helpers/testMocks'; | |||
import { MetricKey } from '../../types/metrics'; | |||
import { | |||
QualityGateApplicationStatus, | |||
QualityGateProjectStatus, | |||
SearchPermissionsParameters, | |||
} from '../../types/quality-gates'; | |||
import { CaycStatus, Condition, QualityGate } from '../../types/types'; | |||
import { | |||
addGroup, | |||
@@ -36,7 +45,9 @@ import { | |||
dissociateGateWithProject, | |||
fetchQualityGate, | |||
fetchQualityGates, | |||
getApplicationQualityGate, | |||
getGateForProject, | |||
getQualityGateProjectStatus, | |||
renameQualityGate, | |||
searchGroups, | |||
searchProjects, | |||
@@ -45,6 +56,8 @@ import { | |||
updateCondition, | |||
} from '../quality-gates'; | |||
jest.mock('../quality-gates'); | |||
export class QualityGatesServiceMock { | |||
isAdmin = false; | |||
readOnlyList: QualityGate[]; | |||
@@ -52,6 +65,8 @@ export class QualityGatesServiceMock { | |||
projects: Project[]; | |||
getGateForProjectGateName: string; | |||
throwOnGetGateForProject: boolean; | |||
qualityGateProjectStatus: QualityGateProjectStatus; | |||
applicationQualityGate: QualityGateApplicationStatus; | |||
constructor(list?: QualityGate[]) { | |||
this.readOnlyList = list || [ | |||
@@ -198,22 +213,27 @@ export class QualityGatesServiceMock { | |||
this.getGateForProjectGateName = 'SonarSource way'; | |||
this.throwOnGetGateForProject = false; | |||
(fetchQualityGate as jest.Mock).mockImplementation(this.showHandler); | |||
(fetchQualityGates as jest.Mock).mockImplementation(this.listHandler); | |||
(createQualityGate as jest.Mock).mockImplementation(this.createHandler); | |||
(deleteQualityGate as jest.Mock).mockImplementation(this.destroyHandler); | |||
(copyQualityGate as jest.Mock).mockImplementation(this.copyHandler); | |||
jest.mocked(fetchQualityGate).mockImplementation(this.showHandler); | |||
jest.mocked(fetchQualityGates).mockImplementation(this.listHandler); | |||
jest.mocked(createQualityGate).mockImplementation(this.createHandler); | |||
jest.mocked(deleteQualityGate).mockImplementation(this.destroyHandler); | |||
jest.mocked(copyQualityGate).mockImplementation(this.copyHandler); | |||
(renameQualityGate as jest.Mock).mockImplementation(this.renameHandler); | |||
(createCondition as jest.Mock).mockImplementation(this.createConditionHandler); | |||
(updateCondition as jest.Mock).mockImplementation(this.updateConditionHandler); | |||
(deleteCondition as jest.Mock).mockImplementation(this.deleteConditionHandler); | |||
(searchProjects as jest.Mock).mockImplementation(this.searchProjectsHandler); | |||
(searchUsers as jest.Mock).mockImplementation(this.searchUsersHandler); | |||
(searchGroups as jest.Mock).mockImplementation(this.searchGroupsHandler); | |||
(associateGateWithProject as jest.Mock).mockImplementation(this.selectHandler); | |||
(dissociateGateWithProject as jest.Mock).mockImplementation(this.deSelectHandler); | |||
(setQualityGateAsDefault as jest.Mock).mockImplementation(this.setDefaultHandler); | |||
jest.mocked(createCondition).mockImplementation(this.createConditionHandler); | |||
jest.mocked(updateCondition).mockImplementation(this.updateConditionHandler); | |||
jest.mocked(deleteCondition).mockImplementation(this.deleteConditionHandler); | |||
jest.mocked(searchProjects).mockImplementation(this.searchProjectsHandler); | |||
jest.mocked(searchUsers).mockImplementation(this.searchUsersHandler); | |||
jest.mocked(searchGroups).mockImplementation(this.searchGroupsHandler); | |||
jest.mocked(associateGateWithProject).mockImplementation(this.selectHandler); | |||
jest.mocked(dissociateGateWithProject).mockImplementation(this.deSelectHandler); | |||
jest.mocked(setQualityGateAsDefault).mockImplementation(this.setDefaultHandler); | |||
(getGateForProject as jest.Mock).mockImplementation(this.projectGateHandler); | |||
jest.mocked(getQualityGateProjectStatus).mockImplementation(this.handleQualityGetProjectStatus); | |||
jest.mocked(getApplicationQualityGate).mockImplementation(this.handleGetApplicationQualityGate); | |||
this.qualityGateProjectStatus = mockQualityGateProjectStatus({}); | |||
this.applicationQualityGate = mockQualityGateApplicationStatus({}); | |||
// To be implemented. | |||
(addUser as jest.Mock).mockResolvedValue({}); | |||
@@ -416,6 +436,7 @@ export class QualityGatesServiceMock { | |||
selected, | |||
query, | |||
}: { | |||
gateName: string; | |||
selected: string; | |||
query: string | undefined; | |||
}) => { | |||
@@ -437,7 +458,7 @@ export class QualityGatesServiceMock { | |||
return this.reply(response); | |||
}; | |||
searchUsersHandler = ({ selected }: { selected: string }) => { | |||
searchUsersHandler = ({ selected }: SearchPermissionsParameters) => { | |||
if (selected === 'selected') { | |||
return this.reply({ users: [] }); | |||
} | |||
@@ -445,7 +466,7 @@ export class QualityGatesServiceMock { | |||
return this.reply({ users: [mockUserBase()] }); | |||
}; | |||
searchGroupsHandler = ({ selected }: { selected: string }) => { | |||
searchGroupsHandler = ({ selected }: SearchPermissionsParameters) => { | |||
if (selected === 'selected') { | |||
return this.reply({ groups: [] }); | |||
} | |||
@@ -477,6 +498,22 @@ export class QualityGatesServiceMock { | |||
return this.reply(this.list.find((qg) => qg.name === this.getGateForProjectGateName)); | |||
}; | |||
handleGetApplicationQualityGate = () => { | |||
return this.reply(this.applicationQualityGate); | |||
}; | |||
setApplicationQualityGateStatus = (status: QualityGateApplicationStatus) => { | |||
this.applicationQualityGate = mockQualityGateApplicationStatus(status); | |||
}; | |||
handleQualityGetProjectStatus = () => { | |||
return this.reply(this.qualityGateProjectStatus); | |||
}; | |||
setQualityGateProjectStatus = (status: QualityGateProjectStatus) => { | |||
this.qualityGateProjectStatus = mockQualityGateProjectStatus(status); | |||
}; | |||
reply<T>(response: T): Promise<T> { | |||
return Promise.resolve(cloneDeep(response)); | |||
} |
@@ -81,13 +81,12 @@ export function deleteCondition(data: { id: string }): Promise<void> { | |||
return post('/api/qualitygates/delete_condition', data); | |||
} | |||
export function getGateForProject(data: { project: string }): Promise<QualityGate | undefined> { | |||
export function getGateForProject(data: { project: string }): Promise<QualityGate> { | |||
return getJSON('/api/qualitygates/get_by_project', data).then( | |||
({ qualityGate }) => | |||
qualityGate && { | |||
...qualityGate, | |||
isDefault: qualityGate.default, | |||
}, | |||
({ qualityGate }) => ({ | |||
...qualityGate, | |||
isDefault: qualityGate.default, | |||
}), | |||
throwGlobalError | |||
); | |||
} |
@@ -22,7 +22,12 @@ import * as React from 'react'; | |||
import { getApplicationDetails, getApplicationLeak } from '../../../api/application'; | |||
import { getMeasuresWithPeriodAndMetrics } from '../../../api/measures'; | |||
import { getProjectActivity } from '../../../api/projectActivity'; | |||
import { getApplicationQualityGate, getQualityGateProjectStatus } from '../../../api/quality-gates'; | |||
import { | |||
fetchQualityGate, | |||
getApplicationQualityGate, | |||
getGateForProject, | |||
getQualityGateProjectStatus, | |||
} from '../../../api/quality-gates'; | |||
import { getAllTimeMachineData } from '../../../api/time-machine'; | |||
import { | |||
getActivityGraph, | |||
@@ -47,7 +52,7 @@ import { ComponentQualifier } from '../../../types/component'; | |||
import { MetricKey } from '../../../types/metrics'; | |||
import { Analysis, GraphType, MeasureHistory } from '../../../types/project-activity'; | |||
import { QualityGateStatus, QualityGateStatusCondition } from '../../../types/quality-gates'; | |||
import { Component, MeasureEnhanced, Metric, Period } from '../../../types/types'; | |||
import { Component, MeasureEnhanced, Metric, Period, QualityGate } from '../../../types/types'; | |||
import '../styles.css'; | |||
import { HISTORY_METRICS_LIST, METRICS } from '../utils'; | |||
import BranchOverviewRenderer from './BranchOverviewRenderer'; | |||
@@ -70,6 +75,7 @@ interface State { | |||
metrics?: Metric[]; | |||
period?: Period; | |||
qgStatuses?: QualityGateStatus[]; | |||
qualityGate?: QualityGate; | |||
} | |||
export const BRANCH_OVERVIEW_ACTIVITY_GRAPH = 'sonar_branch_overview.graph'; | |||
@@ -111,6 +117,7 @@ export default class BranchOverview extends React.PureComponent<Props, State> { | |||
this.loadApplicationStatus(); | |||
} else { | |||
this.loadProjectStatus(); | |||
this.loadProjectQualityGate(); | |||
} | |||
}; | |||
@@ -268,6 +275,13 @@ export default class BranchOverview extends React.PureComponent<Props, State> { | |||
); | |||
}; | |||
loadProjectQualityGate = async () => { | |||
const { component } = this.props; | |||
const qualityGate = await getGateForProject({ project: component.key }); | |||
const qgDetails = await fetchQualityGate({ name: qualityGate.name }); | |||
this.setState({ qualityGate: qgDetails }); | |||
}; | |||
loadMeasuresAndMeta = ( | |||
componentKey: string, | |||
branchLike?: BranchLike, | |||
@@ -409,6 +423,7 @@ export default class BranchOverview extends React.PureComponent<Props, State> { | |||
metrics, | |||
period, | |||
qgStatuses, | |||
qualityGate, | |||
} = this.state; | |||
const projectIsEmpty = | |||
@@ -436,6 +451,7 @@ export default class BranchOverview extends React.PureComponent<Props, State> { | |||
period={period} | |||
projectIsEmpty={projectIsEmpty} | |||
qgStatuses={qgStatuses} | |||
qualityGate={qualityGate} | |||
/> | |||
); | |||
} |
@@ -26,7 +26,7 @@ import { Branch } from '../../../types/branch-like'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { Analysis, GraphType, MeasureHistory } from '../../../types/project-activity'; | |||
import { QualityGateStatus } from '../../../types/quality-gates'; | |||
import { Component, MeasureEnhanced, Metric, Period } from '../../../types/types'; | |||
import { Component, MeasureEnhanced, Metric, Period, QualityGate } from '../../../types/types'; | |||
import ActivityPanel from './ActivityPanel'; | |||
import FirstAnalysisNextStepsNotif from './FirstAnalysisNextStepsNotif'; | |||
import MeasuresPanel from './MeasuresPanel'; | |||
@@ -50,6 +50,7 @@ export interface BranchOverviewRendererProps { | |||
period?: Period; | |||
projectIsEmpty?: boolean; | |||
qgStatuses?: QualityGateStatus[]; | |||
qualityGate?: QualityGate; | |||
} | |||
export default function BranchOverviewRenderer(props: BranchOverviewRendererProps) { | |||
@@ -70,6 +71,7 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp | |||
period, | |||
projectIsEmpty, | |||
qgStatuses, | |||
qualityGate, | |||
} = props; | |||
const leakPeriod = component.qualifier === ComponentQualifier.Application ? appLeak : period; | |||
@@ -95,6 +97,7 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp | |||
component={component} | |||
loading={loadingStatus} | |||
qgStatuses={qgStatuses} | |||
qualityGate={qualityGate} | |||
/> | |||
</div> | |||
@@ -23,7 +23,7 @@ import { flatMap } from 'lodash'; | |||
import * as React from 'react'; | |||
import { ComponentQualifier, isApplication } from '../../../types/component'; | |||
import { QualityGateStatus } from '../../../types/quality-gates'; | |||
import { CaycStatus, Component } from '../../../types/types'; | |||
import { CaycStatus, Component, QualityGate } from '../../../types/types'; | |||
import IgnoredConditionWarning from '../components/IgnoredConditionWarning'; | |||
import QualityGateStatusHeader from '../components/QualityGateStatusHeader'; | |||
import QualityGateStatusPassedView from '../components/QualityGateStatusPassedView'; | |||
@@ -37,10 +37,11 @@ export interface QualityGatePanelProps { | |||
component: Pick<Component, 'key' | 'qualifier' | 'qualityGate'>; | |||
loading?: boolean; | |||
qgStatuses?: QualityGateStatus[]; | |||
qualityGate?: QualityGate; | |||
} | |||
export function QualityGatePanel(props: QualityGatePanelProps) { | |||
const { component, loading, qgStatuses = [] } = props; | |||
const { component, loading, qgStatuses = [], qualityGate } = props; | |||
if (qgStatuses === undefined) { | |||
return null; | |||
@@ -112,6 +113,7 @@ export function QualityGatePanel(props: QualityGatePanelProps) { | |||
{qgStatuses.length === 1 && | |||
qgStatuses[0].caycStatus === CaycStatus.NonCompliant && | |||
qualityGate?.actions?.manageConditions && | |||
!isApp && ( | |||
<Card className="sw-mt-4 sw-body-sm"> | |||
<CleanAsYouCodeWarning component={component} /> |
@@ -23,11 +23,9 @@ import userEvent from '@testing-library/user-event'; | |||
import * as React from 'react'; | |||
import { getMeasuresWithPeriodAndMetrics } from '../../../../api/measures'; | |||
import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock'; | |||
import { QualityGatesServiceMock } from '../../../../api/mocks/QualityGatesServiceMock'; | |||
import { getProjectActivity } from '../../../../api/projectActivity'; | |||
import { | |||
getApplicationQualityGate, | |||
getQualityGateProjectStatus, | |||
} from '../../../../api/quality-gates'; | |||
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'; | |||
@@ -84,47 +82,6 @@ jest.mock('../../../../api/measures', () => { | |||
}; | |||
}); | |||
jest.mock('../../../../api/quality-gates', () => { | |||
const { mockQualityGateProjectStatus, mockQualityGateApplicationStatus } = jest.requireActual( | |||
'../../../../helpers/mocks/quality-gates' | |||
); | |||
const { MetricKey } = jest.requireActual('../../../../types/metrics'); | |||
return { | |||
getQualityGateProjectStatus: jest.fn().mockResolvedValue( | |||
mockQualityGateProjectStatus({ | |||
status: 'ERROR', | |||
conditions: [ | |||
{ | |||
actualValue: '2', | |||
comparator: 'GT', | |||
errorThreshold: '1', | |||
metricKey: MetricKey.new_reliability_rating, | |||
periodIndex: 1, | |||
status: 'ERROR', | |||
}, | |||
{ | |||
actualValue: '5', | |||
comparator: 'GT', | |||
errorThreshold: '2.0', | |||
metricKey: MetricKey.bugs, | |||
periodIndex: 0, | |||
status: 'ERROR', | |||
}, | |||
{ | |||
actualValue: '2', | |||
comparator: 'GT', | |||
errorThreshold: '1.0', | |||
metricKey: 'unknown_metric', | |||
periodIndex: 0, | |||
status: 'ERROR', | |||
}, | |||
], | |||
}) | |||
), | |||
getApplicationQualityGate: jest.fn().mockResolvedValue(mockQualityGateApplicationStatus()), | |||
}; | |||
}); | |||
jest.mock('../../../../api/time-machine', () => { | |||
const { MetricKey } = jest.requireActual('../../../../types/metrics'); | |||
return { | |||
@@ -195,19 +152,58 @@ jest.mock('../../../../components/activity-graph/utils', () => { | |||
}); | |||
const almHandler = new AlmSettingsServiceMock(); | |||
beforeEach(jest.clearAllMocks); | |||
let qualityGatesMock: QualityGatesServiceMock; | |||
beforeAll(() => { | |||
qualityGatesMock = new QualityGatesServiceMock(); | |||
qualityGatesMock.setQualityGateProjectStatus( | |||
mockQualityGateProjectStatus({ | |||
status: 'ERROR', | |||
conditions: [ | |||
{ | |||
actualValue: '2', | |||
comparator: 'GT', | |||
errorThreshold: '1', | |||
metricKey: MetricKey.new_reliability_rating, | |||
periodIndex: 1, | |||
status: 'ERROR', | |||
}, | |||
{ | |||
actualValue: '5', | |||
comparator: 'GT', | |||
errorThreshold: '2.0', | |||
metricKey: MetricKey.bugs, | |||
periodIndex: 0, | |||
status: 'ERROR', | |||
}, | |||
{ | |||
actualValue: '2', | |||
comparator: 'GT', | |||
errorThreshold: '1.0', | |||
metricKey: 'unknown_metric', | |||
periodIndex: 0, | |||
status: 'ERROR', | |||
}, | |||
], | |||
}) | |||
); | |||
qualityGatesMock.setApplicationQualityGateStatus(mockQualityGateApplicationStatus()); | |||
}); | |||
afterEach(() => { | |||
jest.clearAllMocks(); | |||
qualityGatesMock.reset(); | |||
almHandler.reset(); | |||
}); | |||
describe('project overview', () => { | |||
it('should show a successful QG', async () => { | |||
const user = userEvent.setup(); | |||
jest | |||
.mocked(getQualityGateProjectStatus) | |||
.mockResolvedValueOnce(mockQualityGateProjectStatus({ status: 'OK' })); | |||
qualityGatesMock.setQualityGateProjectStatus( | |||
mockQualityGateProjectStatus({ | |||
status: 'OK', | |||
}) | |||
); | |||
renderBranchOverview(); | |||
// QG panel | |||
@@ -236,10 +232,61 @@ describe('project overview', () => { | |||
renderBranchOverview(); | |||
expect(await screen.findByText('metric.level.OK')).toBeInTheDocument(); | |||
expect(screen.getByText('overview.quality_gate.conditions.cayc.warning')).toBeInTheDocument(); | |||
expect( | |||
screen.queryByText('overview.quality_gate.conditions.cayc.warning') | |||
).not.toBeInTheDocument(); | |||
}); | |||
it('should show a successful non-compliant QG as admin', async () => { | |||
jest | |||
.mocked(getQualityGateProjectStatus) | |||
.mockResolvedValueOnce( | |||
mockQualityGateProjectStatus({ status: 'OK', caycStatus: CaycStatus.NonCompliant }) | |||
); | |||
qualityGatesMock.setIsAdmin(true); | |||
qualityGatesMock.setGetGateForProjectName('Non Cayc QG'); | |||
renderBranchOverview(); | |||
await screen.findByText('metric.level.OK'); | |||
expect( | |||
await screen.findByText('overview.quality_gate.conditions.cayc.warning') | |||
).toBeInTheDocument(); | |||
}); | |||
it('should show a failed QG', async () => { | |||
qualityGatesMock.setQualityGateProjectStatus( | |||
mockQualityGateProjectStatus({ | |||
status: 'ERROR', | |||
conditions: [ | |||
{ | |||
actualValue: '2', | |||
comparator: 'GT', | |||
errorThreshold: '1', | |||
metricKey: MetricKey.new_reliability_rating, | |||
periodIndex: 1, | |||
status: 'ERROR', | |||
}, | |||
{ | |||
actualValue: '5', | |||
comparator: 'GT', | |||
errorThreshold: '2.0', | |||
metricKey: MetricKey.bugs, | |||
periodIndex: 0, | |||
status: 'ERROR', | |||
}, | |||
{ | |||
actualValue: '2', | |||
comparator: 'GT', | |||
errorThreshold: '1.0', | |||
metricKey: 'unknown_metric', | |||
periodIndex: 0, | |||
status: 'ERROR', | |||
}, | |||
], | |||
}) | |||
); | |||
renderBranchOverview(); | |||
expect(await screen.findByText('metric.level.ERROR')).toBeInTheDocument(); | |||
@@ -312,7 +359,7 @@ describe('application overview', () => { | |||
}, | |||
], | |||
}); | |||
jest.mocked(getApplicationQualityGate).mockResolvedValueOnce(appStatus); | |||
qualityGatesMock.setApplicationQualityGateStatus(appStatus); | |||
renderBranchOverview({ component }); | |||
expect( | |||
@@ -380,7 +427,7 @@ it('should correctly handle graph type storage', async () => { | |||
}); | |||
function renderBranchOverview(props: Partial<BranchOverview['props']> = {}) { | |||
renderComponent( | |||
return renderComponent( | |||
<CurrentUserContextProvider currentUser={mockLoggedInUser()}> | |||
<BranchOverview | |||
branch={mockMainBranch()} |
@@ -26,8 +26,8 @@ import withAvailableFeatures, { | |||
import withMetricsContext from '../../../app/components/metrics/withMetricsContext'; | |||
import DocLink from '../../../components/common/DocLink'; | |||
import DocumentationTooltip from '../../../components/common/DocumentationTooltip'; | |||
import { Button } from '../../../components/controls/buttons'; | |||
import ModalButton, { ModalProps } from '../../../components/controls/ModalButton'; | |||
import { Button } from '../../../components/controls/buttons'; | |||
import { Alert } from '../../../components/ui/Alert'; | |||
import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; | |||
import { Feature } from '../../../types/features'; | |||
@@ -194,8 +194,8 @@ export class Conditions extends React.PureComponent<Props, State> { | |||
</Alert> | |||
)} | |||
{qualityGate.caycStatus === CaycStatus.NonCompliant && ( | |||
<Alert className="big-spacer-top big-spacer-bottom" variant="warning"> | |||
{qualityGate.caycStatus === CaycStatus.NonCompliant && canEdit && ( | |||
<Alert className="big-spacer-top big-spacer-bottom" variant="warning" role="alert"> | |||
<h4 className="spacer-bottom cayc-warning-header"> | |||
{translate('quality_gates.cayc_missing.banner.title')} | |||
</h4> |
@@ -63,6 +63,7 @@ export default class Details extends React.PureComponent<Props, State> { | |||
fetchDetails = () => { | |||
const { qualityGateName } = this.props; | |||
this.setState({ loading: true }); | |||
return fetchQualityGate({ name: qualityGateName }).then( | |||
(qualityGate) => { |
@@ -19,9 +19,9 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { setQualityGateAsDefault } from '../../../api/quality-gates'; | |||
import { Button } from '../../../components/controls/buttons'; | |||
import ModalButton from '../../../components/controls/ModalButton'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import { Button } from '../../../components/controls/buttons'; | |||
import AlertWarnIcon from '../../../components/icons/AlertWarnIcon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { CaycStatus, QualityGate } from '../../../types/types'; | |||
@@ -64,6 +64,7 @@ export default class DetailsHeader extends React.PureComponent<Props> { | |||
render() { | |||
const { qualityGate } = this.props; | |||
const actions = qualityGate.actions || ({} as any); | |||
const canEdit = Boolean(actions?.manageConditions); | |||
return ( | |||
<div className="layout-page-header-panel layout-page-main-header issues-main-header"> | |||
@@ -72,7 +73,7 @@ export default class DetailsHeader extends React.PureComponent<Props> { | |||
<div className="pull-left display-flex-center"> | |||
<h2>{qualityGate.name}</h2> | |||
{qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="spacer-left" />} | |||
{qualityGate.caycStatus === CaycStatus.NonCompliant && ( | |||
{qualityGate.caycStatus === CaycStatus.NonCompliant && canEdit && ( | |||
<Tooltip overlay={<CaycBadgeTooltip />} mouseLeaveDelay={TOOLTIP_MOUSE_LEAVE_DELAY}> | |||
<AlertWarnIcon className="spacer-left" description={<CaycBadgeTooltip />} /> | |||
</Tooltip> |
@@ -49,14 +49,15 @@ export default function List({ qualityGates, currentQualityGate }: Props) { | |||
)} | |||
{qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="little-spacer-left" />} | |||
{qualityGate.caycStatus === CaycStatus.NonCompliant && ( | |||
<Tooltip overlay={translate('quality_gates.cayc.tooltip.message')}> | |||
<AlertWarnIcon | |||
className="spacer-left" | |||
description={translate('quality_gates.cayc.tooltip.message')} | |||
/> | |||
</Tooltip> | |||
)} | |||
{qualityGate.caycStatus === CaycStatus.NonCompliant && | |||
qualityGate.actions?.manageConditions && ( | |||
<Tooltip overlay={translate('quality_gates.cayc.tooltip.message')}> | |||
<AlertWarnIcon | |||
className="spacer-left" | |||
description={translate('quality_gates.cayc.tooltip.message')} | |||
/> | |||
</Tooltip> | |||
)} | |||
</NavLink> | |||
))} | |||
</div> |
@@ -22,12 +22,10 @@ import userEvent from '@testing-library/user-event'; | |||
import selectEvent from 'react-select-event'; | |||
import { QualityGatesServiceMock } from '../../../../api/mocks/QualityGatesServiceMock'; | |||
import { searchProjects, searchUsers } from '../../../../api/quality-gates'; | |||
import { renderAppRoutes, RenderContext } from '../../../../helpers/testReactTestingUtils'; | |||
import { RenderContext, renderAppRoutes } from '../../../../helpers/testReactTestingUtils'; | |||
import { Feature } from '../../../../types/features'; | |||
import routes from '../../routes'; | |||
jest.mock('../../../../api/quality-gates'); | |||
let handler: QualityGatesServiceMock; | |||
beforeAll(() => { | |||
@@ -346,6 +344,33 @@ it('should show warning banner when CAYC condition is not properly set and shoul | |||
expect(overallConditionsWrapper.getByText('Complexity / Function')).toBeInTheDocument(); | |||
}); | |||
it('should not warn user when quality gate is not CAYC compliant and user has no permission to edit it', async () => { | |||
const user = userEvent.setup(); | |||
renderQualityGateApp(); | |||
const nonCompliantQualityGate = await screen.findByRole('link', { name: 'Non Cayc QG' }); | |||
await user.click(nonCompliantQualityGate); | |||
expect(screen.queryByRole('alert')).not.toBeInTheDocument(); | |||
expect(screen.queryByText('quality_gates.cayc.tooltip.message')).not.toBeInTheDocument(); | |||
}); | |||
it('should warn user when quality gate is not CAYC compliant and user has permission to edit it', async () => { | |||
const user = userEvent.setup(); | |||
handler.setIsAdmin(true); | |||
renderQualityGateApp(); | |||
const nonCompliantQualityGate = await screen.findByRole('link', { name: /Non Cayc QG/ }); | |||
await user.click(nonCompliantQualityGate); | |||
expect(await screen.findByRole('alert')).toHaveTextContent( | |||
/quality_gates.cayc_missing.banner.title/ | |||
); | |||
expect(screen.getAllByText('quality_gates.cayc.tooltip.message').length).toBeGreaterThan(0); | |||
}); | |||
it('should show success banner when quality gate is CAYC compliant', async () => { | |||
const user = userEvent.setup(); | |||
handler.setIsAdmin(true); | |||
@@ -626,5 +651,5 @@ describe('The Permissions section', () => { | |||
}); | |||
function renderQualityGateApp(context?: RenderContext) { | |||
renderAppRoutes('quality_gates', routes, context); | |||
return renderAppRoutes('quality_gates', routes, context); | |||
} |