diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2019-07-29 17:36:36 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-08-09 20:21:23 +0200 |
commit | 35275d2329bccf0495ccec61afb47af443a9b23e (patch) | |
tree | ffc2cfe45805c8fc0f5190f186a9d9a65af8dfad | |
parent | 2e3b2cd88fe583b10228014a8b62680ebe8b555a (diff) | |
download | sonarqube-35275d2329bccf0495ccec61afb47af443a9b23e.tar.gz sonarqube-35275d2329bccf0495ccec61afb47af443a9b23e.zip |
SONAR-11833 Make it clear that only On New Code conditions apply to PRs
22 files changed, 1522 insertions, 140 deletions
diff --git a/server/sonar-docs/src/tooltips/quality-gates/quality-gate-conditions.md b/server/sonar-docs/src/tooltips/quality-gates/quality-gate-conditions.md index 9be41a8d9a0..66dc9a705cd 100644 --- a/server/sonar-docs/src/tooltips/quality-gates/quality-gate-conditions.md +++ b/server/sonar-docs/src/tooltips/quality-gates/quality-gate-conditions.md @@ -1,4 +1,4 @@ -For each Quality Gate condition you must choose the metric to be tested, the threshold at which to raise an error, and whether or not to apply the condition to all code or only to New Code period (irrelevant for conditions "on New Code"). +Both conditions on New Code and Overall Code have to be met by a project to pass the Quality Gate. --- diff --git a/server/sonar-web/src/main/js/apps/quality-gates/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/quality-gates/__tests__/utils-test.ts new file mode 100644 index 00000000000..bc351f7e7a8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/__tests__/utils-test.ts @@ -0,0 +1,51 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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 { mockMetric } from '../../../helpers/testMocks'; +import { getLocalizedMetricNameNoDiffMetric } from '../utils'; + +jest.mock('../../../store/rootReducer', () => ({ + getMetricByKey: (store: any, key: string) => store[key] +})); + +jest.mock('../../../app/utils/getStore', () => ({ + default: () => ({ + getState: () => ({ + bugs: mockMetric({ key: 'bugs', name: 'Bugs' }), + existing_metric: mockMetric(), + new_maintainability_rating: mockMetric(), + sqale_rating: mockMetric({ key: 'sqale_rating', name: 'Maintainability Rating' }) + }) + }) +})); + +describe('getLocalizedMetricNameNoDiffMetric', () => { + it('should return the correct corresponding metric', () => { + expect(getLocalizedMetricNameNoDiffMetric(mockMetric())).toBe('Coverage'); + expect(getLocalizedMetricNameNoDiffMetric(mockMetric({ key: 'new_bugs' }))).toBe('Bugs'); + expect( + getLocalizedMetricNameNoDiffMetric( + mockMetric({ key: 'new_custom_metric', name: 'Custom Metric on New Code' }) + ) + ).toBe('Custom Metric on New Code'); + expect( + getLocalizedMetricNameNoDiffMetric(mockMetric({ key: 'new_maintainability_rating' })) + ).toBe('Maintainability Rating'); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx index 84a6e8b4e94..fc914abf50d 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx @@ -18,9 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import ActionsDropdown, { - ActionsDropdownItem -} from 'sonar-ui-common/components/controls/ActionsDropdown'; +import { DeleteButton, EditButton } from 'sonar-ui-common/components/controls/buttons'; import ConfirmModal from 'sonar-ui-common/components/controls/ConfirmModal'; import { getLocalizedMetricName, @@ -29,6 +27,7 @@ import { } from 'sonar-ui-common/helpers/l10n'; import { formatMeasure } from 'sonar-ui-common/helpers/measures'; import { deleteCondition } from '../../../api/quality-gates'; +import { getLocalizedMetricNameNoDiffMetric } from '../utils'; import ConditionModal from './ConditionModal'; interface Props { @@ -99,29 +98,30 @@ export default class Condition extends React.PureComponent<Props, State> { return ( <tr> <td className="text-middle"> - {getLocalizedMetricName(metric)} + {getLocalizedMetricNameNoDiffMetric(metric)} {metric.hidden && ( <span className="text-danger little-spacer-left">{translate('deprecated')}</span> )} </td> - <td className="thin text-middle nowrap">{this.renderOperator()}</td> + <td className="text-middle nowrap">{this.renderOperator()}</td> - <td className="thin text-middle nowrap">{formatMeasure(condition.error, metric.type)}</td> + <td className="text-middle nowrap">{formatMeasure(condition.error, metric.type)}</td> {canEdit && ( - <td className="thin text-middle nowrap"> - <ActionsDropdown className="dropdown-menu-right"> - <ActionsDropdownItem className="js-condition-update" onClick={this.handleOpenUpdate}> - {translate('update_details')} - </ActionsDropdownItem> - <ActionsDropdownItem - destructive={true} - id="condition-delete" - onClick={this.handleDeleteClick}> - {translate('delete')} - </ActionsDropdownItem> - </ActionsDropdown> + <> + <td className="text-center thin"> + <EditButton + data-test="quality-gates__condition-update" + onClick={this.handleOpenUpdate} + /> + </td> + <td className="text-center thin"> + <DeleteButton + data-test="quality-gates__condition-delete" + onClick={this.handleDeleteClick} + /> + </td> {this.state.modal && ( <ConditionModal condition={condition} @@ -147,7 +147,7 @@ export default class Condition extends React.PureComponent<Props, State> { )} </ConfirmModal> )} - </td> + </> )} </tr> ); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx index ba87f3434a8..3ee164f96a7 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx @@ -19,8 +19,10 @@ */ import * as React from 'react'; import ConfirmModal from 'sonar-ui-common/components/controls/ConfirmModal'; +import Radio from 'sonar-ui-common/components/controls/Radio'; import { Alert } from 'sonar-ui-common/components/ui/Alert'; import { getLocalizedMetricName, translate } from 'sonar-ui-common/helpers/l10n'; +import { isDiffMetric } from 'sonar-ui-common/helpers/measures'; import { createCondition, updateCondition } from '../../../api/quality-gates'; import { getPossibleOperators } from '../utils'; import ConditionOperator from './ConditionOperator'; @@ -43,28 +45,20 @@ interface State { errorMessage?: string; metric?: T.Metric; op?: string; + scope: 'new' | 'overall'; } export default class ConditionModal extends React.PureComponent<Props, State> { - mounted = false; - constructor(props: Props) { super(props); this.state = { error: props.condition ? props.condition.error : '', + scope: 'new', metric: props.metric ? props.metric : undefined, op: props.condition ? props.condition.op : undefined }; } - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - getSinglePossibleOperator(metric: T.Metric) { const operators = getPossibleOperators(metric); return Array.isArray(operators) ? undefined : operators; @@ -86,6 +80,21 @@ export default class ConditionModal extends React.PureComponent<Props, State> { return Promise.reject(); }; + handleScopeChange = (scope: 'new' | 'overall') => { + this.setState(({ metric }) => { + const { metrics } = this.props; + let correspondingMetric; + + if (metric && metrics) { + const correspondingMetricKey = + scope === 'new' ? `new_${metric.key}` : metric.key.replace(/^new_/, ''); + correspondingMetric = metrics.find(m => m.key === correspondingMetricKey); + } + + return { scope, metric: correspondingMetric }; + }); + }; + handleMetricChange = (metric: T.Metric) => { this.setState({ metric, op: undefined, error: '' }); }; @@ -100,7 +109,7 @@ export default class ConditionModal extends React.PureComponent<Props, State> { render() { const { header, metrics, onClose } = this.props; - const { op, error, metric } = this.state; + const { op, error, scope, metric } = this.state; return ( <ConfirmModal confirmButtonText={header} @@ -110,13 +119,44 @@ export default class ConditionModal extends React.PureComponent<Props, State> { onConfirm={this.handleFormSubmit} size="small"> {this.state.errorMessage && <Alert variant="error">{this.state.errorMessage}</Alert>} + + {this.props.metric === undefined && ( + <div className="modal-field display-flex-center"> + <Radio checked={scope === 'new'} onCheck={this.handleScopeChange} value="new"> + <span data-test="quality-gates__condition-scope-new"> + {translate('quality_gates.conditions.new_code')} + </span> + </Radio> + <Radio + checked={scope === 'overall'} + className="big-spacer-left" + onCheck={this.handleScopeChange} + value="overall"> + <span data-test="quality-gates__condition-scope-overall"> + {translate('quality_gates.conditions.overall_code')} + </span> + </Radio> + </div> + )} + <div className="modal-field"> - <label htmlFor="condition-metric">{translate('quality_gates.conditions.metric')}</label> - {metrics && <MetricSelect metrics={metrics} onMetricChange={this.handleMetricChange} />} + <label htmlFor="condition-metric"> + {translate('quality_gates.conditions.fails_when')} + </label> + {metrics && ( + <MetricSelect + metric={metric} + metrics={metrics.filter(metric => + scope === 'new' ? isDiffMetric(metric.key) : !isDiffMetric(metric.key) + )} + onMetricChange={this.handleMetricChange} + /> + )} {this.props.metric && ( <span className="note">{getLocalizedMetricName(this.props.metric)}</span> )} </div> + {metric && ( <> <div className="modal-field display-inline-block"> @@ -131,7 +171,7 @@ export default class ConditionModal extends React.PureComponent<Props, State> { </div> <div className="modal-field display-inline-block spacer-left"> <label htmlFor="condition-threshold"> - {translate('quality_gates.conditions.error')} + {translate('quality_gates.conditions.value')} </label> <ThresholdInput metric={metric} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx index 5c04b8148a6..bb12fa3b403 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx @@ -23,17 +23,20 @@ import { Button } from 'sonar-ui-common/components/controls/buttons'; import ModalButton from 'sonar-ui-common/components/controls/ModalButton'; import { Alert } from 'sonar-ui-common/components/ui/Alert'; import { getLocalizedMetricName, translate } from 'sonar-ui-common/helpers/l10n'; +import { isDiffMetric } from 'sonar-ui-common/helpers/measures'; import DocTooltip from '../../../components/docs/DocTooltip'; +import { withAppState } from '../../../components/hoc/withAppState'; import Condition from './Condition'; import ConditionModal from './ConditionModal'; interface Props { + appState: Pick<T.AppState, 'branchesEnabled'>; canEdit: boolean; conditions: T.Condition[]; metrics: T.Dict<T.Metric>; onAddCondition: (condition: T.Condition) => void; - onSaveCondition: (newCondition: T.Condition, oldCondition: T.Condition) => void; onRemoveCondition: (Condition: T.Condition) => void; + onSaveCondition: (newCondition: T.Condition, oldCondition: T.Condition) => void; organization?: string; qualityGate: T.QualityGate; } @@ -41,21 +44,67 @@ interface Props { const FORBIDDEN_METRIC_TYPES = ['DATA', 'DISTRIB', 'STRING', 'BOOL']; const FORBIDDEN_METRICS = ['alert_status', 'releasability_rating', 'security_review_rating']; -export default class Conditions extends React.PureComponent<Props> { - getConditionKey = (condition: T.Condition, index: number) => { - return condition.id ? condition.id : `new-${index}`; +export class Conditions extends React.PureComponent<Props> { + renderConditionsTable = (conditions: T.Condition[], scope: 'new' | 'overall') => { + const { + qualityGate, + metrics, + canEdit, + onRemoveCondition, + onSaveCondition, + organization + } = this.props; + return ( + <table className="data zebra" data-test={`quality-gates__conditions-${scope}`}> + <thead> + <tr> + <th className="nowrap" style={{ width: 300 }}> + {translate('quality_gates.conditions.metric')} + </th> + <th className="nowrap">{translate('quality_gates.conditions.operator')}</th> + <th className="nowrap">{translate('quality_gates.conditions.value')}</th> + {canEdit && ( + <> + <th className="thin">{translate('edit')}</th> + <th className="thin">{translate('delete')}</th> + </> + )} + </tr> + </thead> + <tbody> + {conditions.map(condition => ( + <Condition + canEdit={canEdit} + condition={condition} + key={condition.id} + metric={metrics[condition.metric]} + onRemoveCondition={onRemoveCondition} + onSaveCondition={onSaveCondition} + organization={organization} + qualityGate={qualityGate} + /> + ))} + </tbody> + </table> + ); }; render() { - const { qualityGate, conditions, metrics, canEdit, organization } = this.props; + const { appState, conditions, metrics, canEdit } = this.props; const existingConditions = conditions.filter(condition => metrics[condition.metric]); - const sortedConditions = sortBy( existingConditions, condition => metrics[condition.metric] && metrics[condition.metric].name ); + const sortedConditionsOnOverallMetrics = sortedConditions.filter( + condition => !isDiffMetric(condition.metric) + ); + const sortedConditionsOnNewMetrics = sortedConditions.filter(condition => + isDiffMetric(condition.metric) + ); + const duplicates: T.Condition[] = []; const savedConditions = existingConditions.filter(condition => condition.id != null); savedConditions.forEach(condition => { @@ -82,7 +131,7 @@ export default class Conditions extends React.PureComponent<Props> { ); return ( - <div className="quality-gate-section" id="quality-gate-conditions"> + <div className="quality-gate-section"> {canEdit && ( <div className="pull-right"> <ModalButton @@ -97,11 +146,14 @@ export default class Conditions extends React.PureComponent<Props> { /> )}> {({ onClick }) => ( - <Button onClick={onClick}>{translate('quality_gates.add_condition')}</Button> + <Button data-test="quality-gates__add-condition" onClick={onClick}> + {translate('quality_gates.add_condition')} + </Button> )} </ModalButton> </div> )} + <header className="display-flex-center spacer-bottom"> <h3>{translate('quality_gates.conditions')}</h3> <DocTooltip @@ -110,8 +162,6 @@ export default class Conditions extends React.PureComponent<Props> { /> </header> - <div className="big-spacer-bottom">{translate('quality_gates.introduction')}</div> - {uniqDuplicates.length > 0 && ( <Alert variant="warning"> <p>{translate('quality_gates.duplicated_conditions')}</p> @@ -123,43 +173,40 @@ export default class Conditions extends React.PureComponent<Props> { </Alert> )} - {sortedConditions.length ? ( - <table className="data zebra zebra-hover" id="quality-gate-conditions"> - <thead> - <tr> - <th className="nowrap"> - <div className="display-inline-flex-center"> - {translate('quality_gates.conditions.metric')} - <DocTooltip - className="spacer-left" - doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/quality-gates/metric.md')} - /> - </div> - </th> - <th className="thin nowrap">{translate('quality_gates.conditions.operator')}</th> - <th className="thin nowrap">{translate('quality_gates.conditions.error')}</th> - {canEdit && <th />} - </tr> - </thead> - <tbody> - {sortedConditions.map((condition, index) => ( - <Condition - canEdit={canEdit} - condition={condition} - key={this.getConditionKey(condition, index)} - metric={metrics[condition.metric]} - onRemoveCondition={this.props.onRemoveCondition} - onSaveCondition={this.props.onSaveCondition} - organization={organization} - qualityGate={qualityGate} - /> - ))} - </tbody> - </table> - ) : ( + {sortedConditionsOnNewMetrics.length > 0 && ( + <div className="big-spacer-top"> + <h4>{translate('quality_gates.conditions.new_code.long')}</h4> + + {appState.branchesEnabled && ( + <p className="spacer-top spacer-bottom"> + {translate('quality_gates.conditions.new_code.description')} + </p> + )} + + {this.renderConditionsTable(sortedConditionsOnNewMetrics, 'new')} + </div> + )} + + {sortedConditionsOnOverallMetrics.length > 0 && ( + <div className="big-spacer-top"> + <h4>{translate('quality_gates.conditions.overall_code.long')}</h4> + + {appState.branchesEnabled && ( + <p className="spacer-top spacer-bottom"> + {translate('quality_gates.conditions.overall_code.description')} + </p> + )} + + {this.renderConditionsTable(sortedConditionsOnOverallMetrics, 'overall')} + </div> + )} + + {existingConditions.length === 0 && ( <div className="big-spacer-top">{translate('quality_gates.no_conditions')}</div> )} </div> ); } } + +export default withAppState(Conditions); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx index 032692b0b2f..1f2bc2bba7e 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx @@ -44,7 +44,7 @@ export default function ListHeader({ canCreate, refreshQualityGates, organizatio /> )}> {({ onClick }) => ( - <Button id="quality-gate-add" onClick={onClick}> + <Button data-test="quality-gates__add" onClick={onClick}> {translate('create')} </Button> )} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/MetricSelect.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/MetricSelect.tsx index 2a6f9ad4a00..56300e6d6ac 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/MetricSelect.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/MetricSelect.tsx @@ -20,56 +20,51 @@ import { sortBy } from 'lodash'; import * as React from 'react'; import Select from 'sonar-ui-common/components/controls/Select'; -import { - getLocalizedMetricDomain, - getLocalizedMetricName, - translate -} from 'sonar-ui-common/helpers/l10n'; +import { getLocalizedMetricDomain, translate } from 'sonar-ui-common/helpers/l10n'; +import { getLocalizedMetricNameNoDiffMetric } from '../utils'; interface Props { + metric?: T.Metric; metrics: T.Metric[]; onMetricChange: (metric: T.Metric) => void; } -interface State { - value: number; -} - interface Option { disabled?: boolean; - domain?: string; label: string; - value: number; + value: string; } -export default class MetricSelect extends React.PureComponent<Props, State> { - state = { value: -1 }; - +export default class MetricSelect extends React.PureComponent<Props> { handleChange = (option: Option | null) => { - const value = option ? option.value : -1; - this.setState({ value }); - this.props.onMetricChange(this.props.metrics[value]); + if (option) { + const { metrics } = this.props; + const selectedMetric = metrics.find(metric => metric.key === option.value); + if (selectedMetric) { + this.props.onMetricChange(selectedMetric); + } + } }; render() { - const { metrics } = this.props; + const { metric, metrics } = this.props; - const options: Option[] = sortBy( - metrics.map((metric, index) => ({ - value: index, - label: getLocalizedMetricName(metric), + const options: Array<Option & { domain?: string }> = sortBy( + metrics.map(metric => ({ + value: metric.key, + label: getLocalizedMetricNameNoDiffMetric(metric), domain: metric.domain })), 'domain' ); - // use "disabled" property to emulate optgroups + // Use "disabled" property to emulate optgroups. const optionsWithDomains: Option[] = []; options.forEach((option, index, options) => { const previous = index > 0 ? options[index - 1] : null; if (option.domain && (!previous || previous.domain !== option.domain)) { optionsWithDomains.push({ - value: 0, + value: '<domain>', label: getLocalizedMetricDomain(option.domain), disabled: true }); @@ -84,7 +79,7 @@ export default class MetricSelect extends React.PureComponent<Props, State> { onChange={this.handleChange} options={optionsWithDomains} placeholder={translate('search.search_for_metrics')} - value={this.state.value} + value={metric && metric.key} /> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/Condition-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/Condition-test.tsx new file mode 100644 index 00000000000..336c33538a1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/Condition-test.tsx @@ -0,0 +1,59 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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 { mockCondition, mockMetric, mockQualityGate } from '../../../../helpers/testMocks'; +import Condition from '../Condition'; + +it('should render correctly', () => { + const wrapper = shallowRender(); + expect(wrapper).toMatchSnapshot(); +}); + +it('should render correctly with edit rights', () => { + const wrapper = shallowRender({ canEdit: true }); + expect(wrapper).toMatchSnapshot(); +}); + +it('should render the update modal correctly', () => { + const wrapper = shallowRender({ canEdit: true }); + wrapper.instance().handleOpenUpdate(); + expect(wrapper).toMatchSnapshot(); +}); + +it('should render the delete modal correctly', () => { + const wrapper = shallowRender({ canEdit: true }); + wrapper.instance().handleDeleteClick(); + expect(wrapper).toMatchSnapshot(); +}); + +function shallowRender(props: Partial<Condition['props']> = {}) { + return shallow<Condition>( + <Condition + canEdit={false} + condition={mockCondition()} + metric={mockMetric()} + onRemoveCondition={jest.fn()} + onSaveCondition={jest.fn()} + qualityGate={mockQualityGate()} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/ConditionModal-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/ConditionModal-test.tsx index 7b243bf4b42..8b6d1d2fe15 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/ConditionModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/ConditionModal-test.tsx @@ -19,21 +19,58 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { mockQualityGate } from '../../../../helpers/testMocks'; +import { mockMetric, mockQualityGate } from '../../../../helpers/testMocks'; import ConditionModal from '../ConditionModal'; it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot(); + expect(shallowRender({ metric: mockMetric() })).toMatchSnapshot(); +}); + +it('should correctly handle a metric selection', () => { const wrapper = shallowRender(); + const metric = mockMetric(); + + expect(wrapper.find('MetricSelect').prop('metric')).toBeUndefined(); + + wrapper.instance().handleMetricChange(metric); + expect(wrapper.find('MetricSelect').prop('metric')).toEqual(metric); +}); + +it('should correctly switch scope', () => { + const wrapper = shallowRender({ + metrics: [ + mockMetric({ id: 'new_coverage', key: 'new_coverage', name: 'Coverage on New Code' }), + mockMetric({ + id: 'new_duplication', + key: 'new_duplication', + name: 'Duplication on New Code' + }), + mockMetric(), + mockMetric({ id: 'duplication', key: 'duplication', name: 'Duplication' }) + ] + }); + expect(wrapper).toMatchSnapshot(); + + wrapper.instance().handleScopeChange('overall'); expect(wrapper).toMatchSnapshot(); - wrapper.instance().handleMetricChange({ id: '1', key: 'foo', name: 'Foo', type: 'PERCENT' }); + wrapper.instance().handleScopeChange('new'); expect(wrapper).toMatchSnapshot(); }); function shallowRender(props: Partial<ConditionModal['props']> = {}) { return shallow<ConditionModal>( <ConditionModal - header="a" + header="header" + metrics={[ + mockMetric({ id: 'new_coverage', key: 'new_coverage', name: 'Coverage on New Code' }), + mockMetric({ + id: 'new_duplication', + key: 'new_duplication', + name: 'Duplication on New Code' + }) + ]} onAddCondition={jest.fn()} onClose={jest.fn()} qualityGate={mockQualityGate()} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/Conditions-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/Conditions-test.tsx new file mode 100644 index 00000000000..98982f434e1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/Conditions-test.tsx @@ -0,0 +1,78 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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 { mockCondition, mockMetric, mockQualityGate } from '../../../../helpers/testMocks'; +import { Conditions } from '../Conditions'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +it('should render correctly with new code conditions', () => { + const wrapper = shallowRender({ + conditions: [ + mockCondition(), + mockCondition({ id: 2, metric: 'duplication' }), + mockCondition({ id: 3, metric: 'new_coverage' }), + mockCondition({ id: 4, metric: 'new_duplication' }) + ] + }); + expect(wrapper).toMatchSnapshot(); +}); + +it('should render correctly for no conditions', () => { + const wrapper = shallowRender({ conditions: [] }); + expect(wrapper).toMatchSnapshot(); +}); + +it('should render the add conditions button and modal', () => { + const wrapper = shallowRender({ canEdit: true }); + expect(wrapper).toMatchSnapshot(); +}); + +function shallowRender(props: Partial<Conditions['props']> = {}) { + return shallow<Conditions>( + <Conditions + appState={{ branchesEnabled: true }} + canEdit={false} + conditions={[mockCondition(), mockCondition({ id: 2, metric: 'duplication' })]} + metrics={{ + coverage: mockMetric(), + duplication: mockMetric({ id: 'duplication', key: 'duplication', name: 'Duplication' }), + new_coverage: mockMetric({ + id: 'new_coverage', + key: 'new_coverage', + name: 'Coverage on New Code' + }), + new_duplication: mockMetric({ + id: 'new_duplication', + key: 'new_duplication', + name: 'Duplication on New Code' + }) + }} + onAddCondition={jest.fn()} + onRemoveCondition={jest.fn()} + onSaveCondition={jest.fn()} + qualityGate={mockQualityGate()} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/ListHeader-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/ListHeader-test.tsx new file mode 100644 index 00000000000..651b2260124 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/ListHeader-test.tsx @@ -0,0 +1,33 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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 ListHeader from '../ListHeader'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot(); + const wrapper = shallowRender({ canCreate: true }); + expect(wrapper.find('ModalButton').exists()).toBe(true); + expect(wrapper.find('ModalButton').dive()).toMatchSnapshot(); +}); + +function shallowRender(props = {}) { + return shallow(<ListHeader canCreate={false} refreshQualityGates={jest.fn()} {...props} />); +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/MetricSelect-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/MetricSelect-test.tsx index 6210a5adc43..884627e7db1 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/MetricSelect-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/MetricSelect-test.tsx @@ -19,22 +19,24 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockMetric } from '../../../../helpers/testMocks'; import MetricSelect from '../MetricSelect'; it('should render correctly', () => { - expect( - shallow( - <MetricSelect - metrics={[ - { - id: '1', - key: '1', - name: 'metric 1', - type: 'test' - } - ]} - onMetricChange={jest.fn()} - /> - ) - ).toMatchSnapshot(); + expect(shallowRender()).toMatchSnapshot(); }); + +it('should correctly handle change', () => { + const onMetricChange = jest.fn(); + const metric = mockMetric(); + const metrics = [mockMetric({ key: 'duplication' }), metric]; + const wrapper = shallowRender({ metrics, onMetricChange }); + wrapper.instance().handleChange({ label: metric.name, value: metric.key }); + expect(onMetricChange).toBeCalledWith(metric); +}); + +function shallowRender(props: Partial<MetricSelect['props']> = {}) { + return shallow<MetricSelect>( + <MetricSelect metrics={[mockMetric()]} onMetricChange={jest.fn()} {...props} /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/Condition-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/Condition-test.tsx.snap new file mode 100644 index 00000000000..cd7a503cf00 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/Condition-test.tsx.snap @@ -0,0 +1,189 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<tr> + <td + className="text-middle" + > + Coverage + </td> + <td + className="text-middle nowrap" + > + <span + className="note" + > + quality_gates.operator.LT + </span> + </td> + <td + className="text-middle nowrap" + > + 10.0% + </td> +</tr> +`; + +exports[`should render correctly with edit rights 1`] = ` +<tr> + <td + className="text-middle" + > + Coverage + </td> + <td + className="text-middle nowrap" + > + <span + className="note" + > + quality_gates.operator.LT + </span> + </td> + <td + className="text-middle nowrap" + > + 10.0% + </td> + <td + className="text-center thin" + > + <EditButton + data-test="quality-gates__condition-update" + onClick={[Function]} + /> + </td> + <td + className="text-center thin" + > + <DeleteButton + data-test="quality-gates__condition-delete" + onClick={[Function]} + /> + </td> +</tr> +`; + +exports[`should render the delete modal correctly 1`] = ` +<tr> + <td + className="text-middle" + > + Coverage + </td> + <td + className="text-middle nowrap" + > + <span + className="note" + > + quality_gates.operator.LT + </span> + </td> + <td + className="text-middle nowrap" + > + 10.0% + </td> + <td + className="text-center thin" + > + <EditButton + data-test="quality-gates__condition-update" + onClick={[Function]} + /> + </td> + <td + className="text-center thin" + > + <DeleteButton + data-test="quality-gates__condition-delete" + onClick={[Function]} + /> + </td> + <ConfirmModal + confirmButtonText="delete" + confirmData={ + Object { + "error": "10", + "id": 1, + "metric": "coverage", + "op": "LT", + } + } + header="quality_gates.delete_condition" + isDestructive={true} + onClose={[Function]} + onConfirm={[Function]} + > + quality_gates.delete_condition.confirm.message.Coverage + </ConfirmModal> +</tr> +`; + +exports[`should render the update modal correctly 1`] = ` +<tr> + <td + className="text-middle" + > + Coverage + </td> + <td + className="text-middle nowrap" + > + <span + className="note" + > + quality_gates.operator.LT + </span> + </td> + <td + className="text-middle nowrap" + > + 10.0% + </td> + <td + className="text-center thin" + > + <EditButton + data-test="quality-gates__condition-update" + onClick={[Function]} + /> + </td> + <td + className="text-center thin" + > + <DeleteButton + data-test="quality-gates__condition-delete" + onClick={[Function]} + /> + </td> + <ConditionModal + condition={ + Object { + "error": "10", + "id": 1, + "metric": "coverage", + "op": "LT", + } + } + header="quality_gates.update_condition" + metric={ + Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + } + } + onAddCondition={[Function]} + onClose={[Function]} + qualityGate={ + Object { + "id": 1, + "name": "qualitygate", + } + } + /> +</tr> +`; diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/ConditionModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/ConditionModal-test.tsx.snap index ab32f89c02b..711a500cdf5 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/ConditionModal-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/ConditionModal-test.tsx.snap @@ -1,31 +1,278 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`should correctly switch scope 1`] = ` +<ConfirmModal + confirmButtonText="header" + confirmDisable={true} + header="header" + onClose={[MockFunction]} + onConfirm={[Function]} + size="small" +> + <div + className="modal-field display-flex-center" + > + <Radio + checked={true} + onCheck={[Function]} + value="new" + > + <span + data-test="quality-gates__condition-scope-new" + > + quality_gates.conditions.new_code + </span> + </Radio> + <Radio + checked={false} + className="big-spacer-left" + onCheck={[Function]} + value="overall" + > + <span + data-test="quality-gates__condition-scope-overall" + > + quality_gates.conditions.overall_code + </span> + </Radio> + </div> + <div + className="modal-field" + > + <label + htmlFor="condition-metric" + > + quality_gates.conditions.fails_when + </label> + <MetricSelect + metrics={ + Array [ + Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "Coverage on New Code", + "type": "PERCENT", + }, + Object { + "id": "new_duplication", + "key": "new_duplication", + "name": "Duplication on New Code", + "type": "PERCENT", + }, + ] + } + onMetricChange={[Function]} + /> + </div> +</ConfirmModal> +`; + +exports[`should correctly switch scope 2`] = ` +<ConfirmModal + confirmButtonText="header" + confirmDisable={true} + header="header" + onClose={[MockFunction]} + onConfirm={[Function]} + size="small" +> + <div + className="modal-field display-flex-center" + > + <Radio + checked={false} + onCheck={[Function]} + value="new" + > + <span + data-test="quality-gates__condition-scope-new" + > + quality_gates.conditions.new_code + </span> + </Radio> + <Radio + checked={true} + className="big-spacer-left" + onCheck={[Function]} + value="overall" + > + <span + data-test="quality-gates__condition-scope-overall" + > + quality_gates.conditions.overall_code + </span> + </Radio> + </div> + <div + className="modal-field" + > + <label + htmlFor="condition-metric" + > + quality_gates.conditions.fails_when + </label> + <MetricSelect + metrics={ + Array [ + Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + Object { + "id": "duplication", + "key": "duplication", + "name": "Duplication", + "type": "PERCENT", + }, + ] + } + onMetricChange={[Function]} + /> + </div> +</ConfirmModal> +`; + +exports[`should correctly switch scope 3`] = ` +<ConfirmModal + confirmButtonText="header" + confirmDisable={true} + header="header" + onClose={[MockFunction]} + onConfirm={[Function]} + size="small" +> + <div + className="modal-field display-flex-center" + > + <Radio + checked={true} + onCheck={[Function]} + value="new" + > + <span + data-test="quality-gates__condition-scope-new" + > + quality_gates.conditions.new_code + </span> + </Radio> + <Radio + checked={false} + className="big-spacer-left" + onCheck={[Function]} + value="overall" + > + <span + data-test="quality-gates__condition-scope-overall" + > + quality_gates.conditions.overall_code + </span> + </Radio> + </div> + <div + className="modal-field" + > + <label + htmlFor="condition-metric" + > + quality_gates.conditions.fails_when + </label> + <MetricSelect + metrics={ + Array [ + Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "Coverage on New Code", + "type": "PERCENT", + }, + Object { + "id": "new_duplication", + "key": "new_duplication", + "name": "Duplication on New Code", + "type": "PERCENT", + }, + ] + } + onMetricChange={[Function]} + /> + </div> +</ConfirmModal> +`; + exports[`should render correctly 1`] = ` <ConfirmModal - confirmButtonText="a" + confirmButtonText="header" confirmDisable={true} - header="a" + header="header" onClose={[MockFunction]} onConfirm={[Function]} size="small" > <div + className="modal-field display-flex-center" + > + <Radio + checked={true} + onCheck={[Function]} + value="new" + > + <span + data-test="quality-gates__condition-scope-new" + > + quality_gates.conditions.new_code + </span> + </Radio> + <Radio + checked={false} + className="big-spacer-left" + onCheck={[Function]} + value="overall" + > + <span + data-test="quality-gates__condition-scope-overall" + > + quality_gates.conditions.overall_code + </span> + </Radio> + </div> + <div className="modal-field" > <label htmlFor="condition-metric" > - quality_gates.conditions.metric + quality_gates.conditions.fails_when </label> + <MetricSelect + metrics={ + Array [ + Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "Coverage on New Code", + "type": "PERCENT", + }, + Object { + "id": "new_duplication", + "key": "new_duplication", + "name": "Duplication on New Code", + "type": "PERCENT", + }, + ] + } + onMetricChange={[Function]} + /> </div> </ConfirmModal> `; exports[`should render correctly 2`] = ` <ConfirmModal - confirmButtonText="a" + confirmButtonText="header" confirmDisable={false} - header="a" + header="header" onClose={[MockFunction]} onConfirm={[Function]} size="small" @@ -36,8 +283,40 @@ exports[`should render correctly 2`] = ` <label htmlFor="condition-metric" > - quality_gates.conditions.metric + quality_gates.conditions.fails_when </label> + <MetricSelect + metric={ + Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + } + } + metrics={ + Array [ + Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "Coverage on New Code", + "type": "PERCENT", + }, + Object { + "id": "new_duplication", + "key": "new_duplication", + "name": "Duplication on New Code", + "type": "PERCENT", + }, + ] + } + onMetricChange={[Function]} + /> + <span + className="note" + > + Coverage + </span> </div> <div className="modal-field display-inline-block" @@ -50,9 +329,9 @@ exports[`should render correctly 2`] = ` <ConditionOperator metric={ Object { - "id": "1", - "key": "foo", - "name": "Foo", + "id": "coverage", + "key": "coverage", + "name": "Coverage", "type": "PERCENT", } } @@ -65,14 +344,14 @@ exports[`should render correctly 2`] = ` <label htmlFor="condition-threshold" > - quality_gates.conditions.error + quality_gates.conditions.value </label> <ThresholdInput metric={ Object { - "id": "1", - "key": "foo", - "name": "Foo", + "id": "coverage", + "key": "coverage", + "name": "Coverage", "type": "PERCENT", } } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/Conditions-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/Conditions-test.tsx.snap new file mode 100644 index 00000000000..d3db47fc642 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/Conditions-test.tsx.snap @@ -0,0 +1,493 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<div + className="quality-gate-section" +> + <header + className="display-flex-center spacer-bottom" + > + <h3> + quality_gates.conditions + </h3> + <DocTooltip + className="spacer-left" + doc={Promise {}} + /> + </header> + <div + className="big-spacer-top" + > + <h4> + quality_gates.conditions.overall_code.long + </h4> + <p + className="spacer-top spacer-bottom" + > + quality_gates.conditions.overall_code.description + </p> + <table + className="data zebra" + data-test="quality-gates__conditions-overall" + > + <thead> + <tr> + <th + className="nowrap" + style={ + Object { + "width": 300, + } + } + > + quality_gates.conditions.metric + </th> + <th + className="nowrap" + > + quality_gates.conditions.operator + </th> + <th + className="nowrap" + > + quality_gates.conditions.value + </th> + </tr> + </thead> + <tbody> + <Condition + canEdit={false} + condition={ + Object { + "error": "10", + "id": 1, + "metric": "coverage", + "op": "LT", + } + } + key="1" + metric={ + Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + } + } + onRemoveCondition={[MockFunction]} + onSaveCondition={[MockFunction]} + qualityGate={ + Object { + "id": 1, + "name": "qualitygate", + } + } + /> + <Condition + canEdit={false} + condition={ + Object { + "error": "10", + "id": 2, + "metric": "duplication", + "op": "LT", + } + } + key="2" + metric={ + Object { + "id": "duplication", + "key": "duplication", + "name": "Duplication", + "type": "PERCENT", + } + } + onRemoveCondition={[MockFunction]} + onSaveCondition={[MockFunction]} + qualityGate={ + Object { + "id": 1, + "name": "qualitygate", + } + } + /> + </tbody> + </table> + </div> +</div> +`; + +exports[`should render correctly for no conditions 1`] = ` +<div + className="quality-gate-section" +> + <header + className="display-flex-center spacer-bottom" + > + <h3> + quality_gates.conditions + </h3> + <DocTooltip + className="spacer-left" + doc={Promise {}} + /> + </header> + <div + className="big-spacer-top" + > + quality_gates.no_conditions + </div> +</div> +`; + +exports[`should render correctly with new code conditions 1`] = ` +<div + className="quality-gate-section" +> + <header + className="display-flex-center spacer-bottom" + > + <h3> + quality_gates.conditions + </h3> + <DocTooltip + className="spacer-left" + doc={Promise {}} + /> + </header> + <div + className="big-spacer-top" + > + <h4> + quality_gates.conditions.new_code.long + </h4> + <p + className="spacer-top spacer-bottom" + > + quality_gates.conditions.new_code.description + </p> + <table + className="data zebra" + data-test="quality-gates__conditions-new" + > + <thead> + <tr> + <th + className="nowrap" + style={ + Object { + "width": 300, + } + } + > + quality_gates.conditions.metric + </th> + <th + className="nowrap" + > + quality_gates.conditions.operator + </th> + <th + className="nowrap" + > + quality_gates.conditions.value + </th> + </tr> + </thead> + <tbody> + <Condition + canEdit={false} + condition={ + Object { + "error": "10", + "id": 3, + "metric": "new_coverage", + "op": "LT", + } + } + key="3" + metric={ + Object { + "id": "new_coverage", + "key": "new_coverage", + "name": "Coverage on New Code", + "type": "PERCENT", + } + } + onRemoveCondition={[MockFunction]} + onSaveCondition={[MockFunction]} + qualityGate={ + Object { + "id": 1, + "name": "qualitygate", + } + } + /> + <Condition + canEdit={false} + condition={ + Object { + "error": "10", + "id": 4, + "metric": "new_duplication", + "op": "LT", + } + } + key="4" + metric={ + Object { + "id": "new_duplication", + "key": "new_duplication", + "name": "Duplication on New Code", + "type": "PERCENT", + } + } + onRemoveCondition={[MockFunction]} + onSaveCondition={[MockFunction]} + qualityGate={ + Object { + "id": 1, + "name": "qualitygate", + } + } + /> + </tbody> + </table> + </div> + <div + className="big-spacer-top" + > + <h4> + quality_gates.conditions.overall_code.long + </h4> + <p + className="spacer-top spacer-bottom" + > + quality_gates.conditions.overall_code.description + </p> + <table + className="data zebra" + data-test="quality-gates__conditions-overall" + > + <thead> + <tr> + <th + className="nowrap" + style={ + Object { + "width": 300, + } + } + > + quality_gates.conditions.metric + </th> + <th + className="nowrap" + > + quality_gates.conditions.operator + </th> + <th + className="nowrap" + > + quality_gates.conditions.value + </th> + </tr> + </thead> + <tbody> + <Condition + canEdit={false} + condition={ + Object { + "error": "10", + "id": 1, + "metric": "coverage", + "op": "LT", + } + } + key="1" + metric={ + Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + } + } + onRemoveCondition={[MockFunction]} + onSaveCondition={[MockFunction]} + qualityGate={ + Object { + "id": 1, + "name": "qualitygate", + } + } + /> + <Condition + canEdit={false} + condition={ + Object { + "error": "10", + "id": 2, + "metric": "duplication", + "op": "LT", + } + } + key="2" + metric={ + Object { + "id": "duplication", + "key": "duplication", + "name": "Duplication", + "type": "PERCENT", + } + } + onRemoveCondition={[MockFunction]} + onSaveCondition={[MockFunction]} + qualityGate={ + Object { + "id": 1, + "name": "qualitygate", + } + } + /> + </tbody> + </table> + </div> +</div> +`; + +exports[`should render the add conditions button and modal 1`] = ` +<div + className="quality-gate-section" +> + <div + className="pull-right" + > + <ModalButton + modal={[Function]} + > + <Component /> + </ModalButton> + </div> + <header + className="display-flex-center spacer-bottom" + > + <h3> + quality_gates.conditions + </h3> + <DocTooltip + className="spacer-left" + doc={Promise {}} + /> + </header> + <div + className="big-spacer-top" + > + <h4> + quality_gates.conditions.overall_code.long + </h4> + <p + className="spacer-top spacer-bottom" + > + quality_gates.conditions.overall_code.description + </p> + <table + className="data zebra" + data-test="quality-gates__conditions-overall" + > + <thead> + <tr> + <th + className="nowrap" + style={ + Object { + "width": 300, + } + } + > + quality_gates.conditions.metric + </th> + <th + className="nowrap" + > + quality_gates.conditions.operator + </th> + <th + className="nowrap" + > + quality_gates.conditions.value + </th> + <th + className="thin" + > + edit + </th> + <th + className="thin" + > + delete + </th> + </tr> + </thead> + <tbody> + <Condition + canEdit={true} + condition={ + Object { + "error": "10", + "id": 1, + "metric": "coverage", + "op": "LT", + } + } + key="1" + metric={ + Object { + "id": "coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + } + } + onRemoveCondition={[MockFunction]} + onSaveCondition={[MockFunction]} + qualityGate={ + Object { + "id": 1, + "name": "qualitygate", + } + } + /> + <Condition + canEdit={true} + condition={ + Object { + "error": "10", + "id": 2, + "metric": "duplication", + "op": "LT", + } + } + key="2" + metric={ + Object { + "id": "duplication", + "key": "duplication", + "name": "Duplication", + "type": "PERCENT", + } + } + onRemoveCondition={[MockFunction]} + onSaveCondition={[MockFunction]} + qualityGate={ + Object { + "id": 1, + "name": "qualitygate", + } + } + /> + </tbody> + </table> + </div> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/ListHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/ListHeader-test.tsx.snap new file mode 100644 index 00000000000..f12ac9a8ce6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/ListHeader-test.tsx.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<header + className="page-header" +> + <div + className="display-flex-center" + > + <h1 + className="page-title" + > + quality_gates.page + </h1> + <DocTooltip + className="spacer-left" + doc={Promise {}} + /> + </div> +</header> +`; + +exports[`should render correctly 2`] = ` +<Fragment> + <Button + data-test="quality-gates__add" + onClick={[Function]} + > + create + </Button> +</Fragment> +`; diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/MetricSelect-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/MetricSelect-test.tsx.snap index b03dceb54a2..a387d03d4ed 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/MetricSelect-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/MetricSelect-test.tsx.snap @@ -9,12 +9,11 @@ exports[`should render correctly 1`] = ` Array [ Object { "domain": undefined, - "label": "metric 1", - "value": 0, + "label": "Coverage", + "value": "coverage", }, ] } placeholder="search.search_for_metrics" - value={-1} /> `; diff --git a/server/sonar-web/src/main/js/apps/quality-gates/utils.ts b/server/sonar-web/src/main/js/apps/quality-gates/utils.ts index 8c76e632ee8..9b0acc22663 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/utils.ts +++ b/server/sonar-web/src/main/js/apps/quality-gates/utils.ts @@ -17,6 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { getLocalizedMetricName } from 'sonar-ui-common/helpers/l10n'; +import getStore from '../../app/utils/getStore'; +import { isDiffMetric } from '../../helpers/measures'; +import { getMetricByKey } from '../../store/rootReducer'; + export function checkIfDefault(qualityGate: T.QualityGate, list: T.QualityGate[]): boolean { const finding = list.find(candidate => candidate.id === qualityGate.id); return (finding && finding.isDefault) || false; @@ -56,3 +61,23 @@ export function getPossibleOperators(metric: T.Metric) { return ['LT', 'GT']; } } + +export function metricKeyExists(key: string) { + return getMetricByKey(getStore().getState(), key) !== undefined; +} + +function getNoDiffMetric(metric: T.Metric) { + const store = getStore().getState(); + const regularMetricKey = metric.key.replace(/^new_/, ''); + if (isDiffMetric(metric.key) && metricKeyExists(regularMetricKey)) { + return getMetricByKey(store, regularMetricKey); + } else if (metric.key === 'new_maintainability_rating') { + return getMetricByKey(store, 'sqale_rating') || metric; + } else { + return metric; + } +} + +export function getLocalizedMetricNameNoDiffMetric(metric: T.Metric) { + return getLocalizedMetricName(getNoDiffMetric(metric)); +} diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index 852dbadf38e..03b6da9b100 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -306,6 +306,16 @@ export function mockComponentMeasure( }; } +export function mockCondition(overrides: Partial<T.Condition> = {}): T.Condition { + return { + error: '10', + id: 1, + metric: 'coverage', + op: 'LT', + ...overrides + }; +} + export function mockQualityGateStatusCondition( overrides: Partial<T.QualityGateStatusCondition> = {} ): T.QualityGateStatusCondition { diff --git a/server/sonar-web/src/main/js/store/metrics.ts b/server/sonar-web/src/main/js/store/metrics.ts index 95e3e96cd43..78fd57ed24a 100644 --- a/server/sonar-web/src/main/js/store/metrics.ts +++ b/server/sonar-web/src/main/js/store/metrics.ts @@ -53,3 +53,7 @@ export function getMetrics(state: State) { export function getMetricsKey(state: State) { return state.keys; } + +export function getMetricByKey(state: State, key: string) { + return state.byKey[key]; +} diff --git a/server/sonar-web/src/main/js/store/rootReducer.ts b/server/sonar-web/src/main/js/store/rootReducer.ts index 8f846110260..e489bc707e1 100644 --- a/server/sonar-web/src/main/js/store/rootReducer.ts +++ b/server/sonar-web/src/main/js/store/rootReducer.ts @@ -81,6 +81,10 @@ export function getMetricsKey(state: Store) { return fromMetrics.getMetricsKey(state.metrics); } +export function getMetricByKey(state: Store, key: string) { + return fromMetrics.getMetricByKey(state.metrics, key); +} + export function getOrganizationByKey(state: Store, key: string) { return fromOrganizations.getOrganizationByKey(state.organizations, key); } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index b0dbd4c6f49..5263a4596e1 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1235,7 +1235,6 @@ quality_gates.projects=Projects quality_gates.add_condition=Add Condition quality_gates.update_condition=Update Condition quality_gates.no_conditions=No Conditions -quality_gates.introduction=Only project measures are checked against thresholds. Directories and files are ignored. quality_gates.health_icons=Project health icons represent: quality_gates.projects_for_default=Every project not specifically associated to a quality gate will be associated to this one by default. quality_gates.projects.with=With @@ -1260,12 +1259,18 @@ quality_gates.delete.confirm.message=Are you sure you want to delete the "{0}" q quality_gates.delete.confirm.default=Are you sure you want to delete the "{0}" quality gate, which is the default quality gate? quality_gates.delete_condition=Delete 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.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. +quality_gates.conditions.overall_code=On Overall Code +quality_gates.conditions.overall_code.long=Conditions on Overall Code +quality_gates.conditions.overall_code.description=Conditions on Overall Code apply to long living branches only. quality_gates.conditions.operator=Operator quality_gates.conditions.warning=Warning quality_gates.conditions.warning.tooltip=Warning status is deprecated and will disappear with the next update of the Quality Gate. -quality_gates.conditions.error=Error +quality_gates.conditions.value=Value quality_gates.duplicated_conditions=This quality gate has duplicated conditions: quality_gates.intro.1=Quality Gate is the set of conditions the project must meet before it can be released into production. quality_gates.intro.2=It is possible to set a default Quality Gate, which will be applied to all projects not explicitly assigned to some other gate. |