From: Wouter Admiraal Date: Tue, 8 Sep 2020 14:29:17 +0000 (+0200) Subject: SONAR-13856 Add 'Always use the Default' option at project level for QG X-Git-Tag: 8.6.0.39681~187 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=6ecb6c40842e94997ff5aaf729ecc1eb98251c3c;p=sonarqube.git SONAR-13856 Add 'Always use the Default' option at project level for QG --- diff --git a/server/sonar-web/src/main/js/api/quality-gates.ts b/server/sonar-web/src/main/js/api/quality-gates.ts index 4078b2664d7..054e1b63708 100644 --- a/server/sonar-web/src/main/js/api/quality-gates.ts +++ b/server/sonar-web/src/main/js/api/quality-gates.ts @@ -22,7 +22,7 @@ import throwGlobalError from '../app/utils/throwGlobalError'; import { BranchParameters } from '../types/branch-like'; import { QualityGateApplicationStatus, QualityGateProjectStatus } from '../types/quality-gates'; -export function fetchQualityGates(data: { +export function fetchQualityGates(data?: { organization?: string; }): Promise<{ actions: { create: boolean }; diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css index 7b277f0c018..4c6ef70ab93 100644 --- a/server/sonar-web/src/main/js/app/styles/init/misc.css +++ b/server/sonar-web/src/main/js/app/styles/init/misc.css @@ -434,7 +434,7 @@ th.huge-spacer-right { .display-flex-start { display: flex !important; - align-items: flex-start; + align-items: flex-start !important; } .display-flex-end { diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx deleted file mode 100644 index 904280fd446..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/App.tsx +++ /dev/null @@ -1,147 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import { Helmet } from 'react-helmet-async'; -import { translate } from 'sonar-ui-common/helpers/l10n'; -import { - associateGateWithProject, - dissociateGateWithProject, - fetchQualityGates, - getGateForProject -} from '../../api/quality-gates'; -import A11ySkipTarget from '../../app/components/a11y/A11ySkipTarget'; -import Suggestions from '../../app/components/embed-docs-modal/Suggestions'; -import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage'; -import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization'; -import Form from './Form'; -import Header from './Header'; - -interface Props { - component: T.Component; - onComponentChange: (changes: {}) => void; -} - -interface State { - allGates?: T.QualityGate[]; - gate?: T.QualityGate; - loading: boolean; -} - -export default class App extends React.PureComponent { - mounted = false; - state: State = { loading: true }; - - componentDidMount() { - this.mounted = true; - if (this.checkPermissions()) { - this.fetchQualityGates(); - } else { - handleRequiredAuthorization(); - } - } - - componentWillUnmount() { - this.mounted = false; - } - - checkPermissions() { - const { configuration } = this.props.component; - const hasPermission = configuration && configuration.showQualityGates; - return !!hasPermission; - } - - fetchQualityGates() { - const { component } = this.props; - this.setState({ loading: true }); - Promise.all([ - fetchQualityGates({ organization: component.organization }), - getGateForProject({ organization: component.organization, project: component.key }) - ]).then( - ([qualityGateList, gate]) => { - if (this.mounted) { - this.setState({ - allGates: qualityGateList?.qualitygates, - gate, - loading: false - }); - } - }, - () => { - if (this.mounted) { - this.setState({ loading: false }); - } - } - ); - } - - handleChangeGate = (oldId?: string, newId?: string) => { - const { allGates } = this.state; - if ((!oldId && !newId) || !allGates) { - return Promise.resolve(); - } - - const { component } = this.props; - const requestData = { - gateId: newId ? newId : oldId!, - organization: component.organization, - projectKey: component.key - }; - const request = newId - ? associateGateWithProject(requestData) - : dissociateGateWithProject(requestData); - - return request.then(() => { - if (this.mounted) { - addGlobalSuccessMessage(translate('project_quality_gate.successfully_updated')); - if (newId) { - const newGate = allGates.find(gate => gate.id === newId); - if (newGate) { - this.setState({ gate: newGate }); - this.props.onComponentChange({ qualityGate: newGate }); - } - } else { - this.setState({ gate: undefined }); - } - } - }); - }; - - render() { - if (!this.checkPermissions()) { - return null; - } - - const { allGates, gate, loading } = this.state; - - return ( -
- - - -
- {loading ? ( - - ) : ( - allGates &&
- )} -
- ); - } -} diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/Form.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/Form.tsx deleted file mode 100644 index bdd3eb375c2..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/Form.tsx +++ /dev/null @@ -1,117 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import Select from 'sonar-ui-common/components/controls/Select'; -import { translate } from 'sonar-ui-common/helpers/l10n'; - -interface Props { - allGates: T.QualityGate[]; - gate?: T.QualityGate; - onChange: (oldGate?: string, newGate?: string) => Promise; -} - -interface State { - loading: boolean; -} - -interface Option { - isDefault?: boolean; - label: string; - value: string; -} - -export default class Form extends React.PureComponent { - mounted = false; - state: State = { loading: false }; - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - stopLoading = () => { - if (this.mounted) { - this.setState({ loading: false }); - } - }; - - handleChange = (option: { value: string }) => { - const { gate } = this.props; - - const isSet = gate == null && option.value != null; - const isUnset = gate != null && option.value == null; - const isChanged = gate != null && gate.id !== option.value; - const hasChanged = isSet || isUnset || isChanged; - - if (hasChanged) { - this.setState({ loading: true }); - this.props.onChange(gate && gate.id, option.value).then(this.stopLoading, this.stopLoading); - } - }; - - renderGateName = (option: { isDefault?: boolean; label: string }) => { - if (option.isDefault) { - return ( - - {translate('default')} - {': '} - {option.label} - - ); - } - - return {option.label}; - }; - - renderSelect() { - const { gate, allGates } = this.props; - - const options: Option[] = allGates.map(gate => ({ - value: String(gate.id), - label: gate.name, - isDefault: gate.isDefault - })); - - return ( - props.onSelect(value)} + options={options} + optionRenderer={option => {option.label}} + value={selectedQualityGateId} + /> + + + + + {needsReanalysis && ( + + {translate('project_quality_gate.requires_new_analysis')} + + )} + + +
+ {translate('save')} + {submitting && } +
+ + + + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx deleted file mode 100644 index 00f3ee3d174..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/App-test.tsx +++ /dev/null @@ -1,145 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 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. - */ -/* eslint-disable import/first */ -jest.mock('../../../api/quality-gates', () => ({ - associateGateWithProject: jest.fn(() => Promise.resolve()), - dissociateGateWithProject: jest.fn(() => Promise.resolve()), - fetchQualityGates: jest.fn(() => Promise.resolve({})), - getGateForProject: jest.fn(() => Promise.resolve()) -})); - -jest.mock('../../../app/utils/addGlobalSuccessMessage', () => ({ - default: jest.fn() -})); - -jest.mock('../../../app/utils/handleRequiredAuthorization', () => ({ - default: jest.fn() -})); - -import { shallow } from 'enzyme'; -import * as React from 'react'; -import App from '../App'; - -const associateGateWithProject = require('../../../api/quality-gates') - .associateGateWithProject as jest.Mock; - -const dissociateGateWithProject = require('../../../api/quality-gates') - .dissociateGateWithProject as jest.Mock; - -const fetchQualityGates = require('../../../api/quality-gates').fetchQualityGates as jest.Mock; - -const getGateForProject = require('../../../api/quality-gates').getGateForProject as jest.Mock; - -const addGlobalSuccessMessage = require('../../../app/utils/addGlobalSuccessMessage') - .default as jest.Mock; - -const handleRequiredAuthorization = require('../../../app/utils/handleRequiredAuthorization') - .default as jest.Mock; - -const component = { - analysisDate: '', - breadcrumbs: [], - configuration: { showQualityGates: true }, - key: 'component', - name: 'component', - organization: 'org', - qualifier: 'TRK', - version: '0.0.1' -} as T.Component; - -beforeEach(() => { - associateGateWithProject.mockClear(); - dissociateGateWithProject.mockClear(); - addGlobalSuccessMessage.mockClear(); -}); - -it('checks permissions', () => { - handleRequiredAuthorization.mockClear(); - shallow( - - ); - expect(handleRequiredAuthorization).toBeCalled(); -}); - -it('fetches quality gates', () => { - fetchQualityGates.mockClear(); - getGateForProject.mockClear(); - shallow(); - expect(fetchQualityGates).toBeCalledWith({ organization: 'org' }); - expect(getGateForProject).toBeCalledWith({ organization: 'org', project: 'component' }); -}); - -it('changes quality gate from custom to default', () => { - const gate = randomGate('foo'); - const allGates = [gate, randomGate('bar', true), randomGate('baz')]; - const wrapper = mountRender(allGates, gate); - wrapper.find('Form').prop('onChange')('foo', 'bar'); - expect(associateGateWithProject).toBeCalledWith({ - gateId: 'bar', - organization: 'org', - projectKey: 'component' - }); -}); - -it('changes quality gate from custom to custom', () => { - const allGates = [randomGate('foo'), randomGate('bar', true), randomGate('baz')]; - const wrapper = mountRender(allGates, randomGate('foo')); - wrapper.find('Form').prop('onChange')('foo', 'baz'); - expect(associateGateWithProject).toBeCalledWith({ - gateId: 'baz', - organization: 'org', - projectKey: 'component' - }); -}); - -it('changes quality gate from custom to none', () => { - const allGates = [randomGate('foo'), randomGate('bar'), randomGate('baz')]; - const wrapper = mountRender(allGates, randomGate('foo')); - wrapper.find('Form').prop('onChange')('foo', undefined); - expect(dissociateGateWithProject).toBeCalledWith({ - gateId: 'foo', - organization: 'org', - projectKey: 'component' - }); -}); - -it('changes quality gate from none to custom', () => { - const allGates = [randomGate('foo'), randomGate('bar'), randomGate('baz')]; - const wrapper = mountRender(allGates); - wrapper.find('Form').prop('onChange')(undefined, 'baz'); - expect(associateGateWithProject).toBeCalledWith({ - gateId: 'baz', - organization: 'org', - projectKey: 'component' - }); -}); - -function randomGate(id: string, isDefault = false) { - return { id, isDefault, name: id }; -} - -function mountRender(allGates: any[], gate?: any) { - const wrapper = shallow(); - wrapper.setState({ allGates, loading: false, gate }); - return wrapper; -} diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/Form-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/Form-test.tsx deleted file mode 100644 index b69f987d2e9..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/Form-test.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 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 Form from '../Form'; - -it('renders', () => { - const foo = randomGate('1'); - const allGates = [foo, randomGate('2')]; - expect(shallow(
)).toMatchSnapshot(); -}); - -it('changes quality gate', () => { - const allGates = [randomGate('1'), randomGate('2')]; - const onChange = jest.fn(() => Promise.resolve()); - const wrapper = shallow(); - - wrapper.find('Select').prop('onChange')({ value: '2' }); - expect(onChange).lastCalledWith(undefined, '2'); - - wrapper.setProps({ gate: randomGate('1') }); - wrapper.find('Select').prop('onChange')({ value: '2' }); - expect(onChange).lastCalledWith('1', '2'); -}); - -function randomGate(id: string) { - return { - id, - name: `name-${id}` - }; -} diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/Header-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/Header-test.tsx deleted file mode 100644 index 9730d14d916..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/Header-test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 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 Header from '../Header'; - -it('renders', () => { - expect(shallow(
)).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx new file mode 100644 index 00000000000..7f1d4bd5a6e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx @@ -0,0 +1,178 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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 { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; +import { + associateGateWithProject, + dissociateGateWithProject, + fetchQualityGates, + getGateForProject, + searchProjects +} from '../../../api/quality-gates'; +import handleRequiredAuthorization from '../../../app/utils/handleRequiredAuthorization'; +import { mockQualityGate } from '../../../helpers/mocks/quality-gates'; +import { mockComponent } from '../../../helpers/testMocks'; +import { USE_SYSTEM_DEFAULT } from '../constants'; +import ProjectQualityGateApp from '../ProjectQualityGateApp'; + +jest.mock('../../../api/quality-gates', () => { + const { mockQualityGate } = jest.requireActual('../../../helpers/mocks/quality-gates'); + + const gate1 = mockQualityGate(); + const gate2 = mockQualityGate({ id: '2', isBuiltIn: true }); + const gate3 = mockQualityGate({ id: '3', isDefault: true }); + + return { + associateGateWithProject: jest.fn().mockResolvedValue(null), + dissociateGateWithProject: jest.fn().mockResolvedValue(null), + fetchQualityGates: jest.fn().mockResolvedValue({ + qualitygates: [gate1, gate2, gate3] + }), + getGateForProject: jest.fn().mockResolvedValue(gate2), + searchProjects: jest.fn().mockResolvedValue({ results: [] }) + }; +}); + +jest.mock('../../../app/utils/addGlobalSuccessMessage', () => ({ + default: jest.fn() +})); + +jest.mock('../../../app/utils/handleRequiredAuthorization', () => ({ + default: jest.fn() +})); + +beforeEach(jest.clearAllMocks); + +it('renders correctly', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +it('correctly checks user permissions', () => { + shallowRender({ component: mockComponent({ configuration: { showQualityGates: false } }) }); + expect(handleRequiredAuthorization).toBeCalled(); +}); + +it('correctly loads Quality Gate data', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + expect(fetchQualityGates).toBeCalled(); + expect(getGateForProject).toBeCalledWith({ project: 'foo' }); + + expect(wrapper.state().allQualityGates).toHaveLength(3); + expect(wrapper.state().currentQualityGate?.id).toBe('2'); + expect(wrapper.state().selectedQualityGateId).toBe('2'); +}); + +it('correctly fallbacks to the default Quality Gate', async () => { + (getGateForProject as jest.Mock).mockResolvedValueOnce( + mockQualityGate({ id: '3', isDefault: true }) + ); + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + expect(searchProjects).toBeCalled(); + + expect(wrapper.state().currentQualityGate?.id).toBe('3'); + expect(wrapper.state().selectedQualityGateId).toBe(USE_SYSTEM_DEFAULT); +}); + +it('correctly detects if the default Quality Gate was explicitly selected', async () => { + (getGateForProject as jest.Mock).mockResolvedValueOnce( + mockQualityGate({ id: '3', isDefault: true }) + ); + (searchProjects as jest.Mock).mockResolvedValueOnce({ + results: [{ key: 'foo', selected: true }] + }); + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + expect(searchProjects).toBeCalled(); + + expect(wrapper.state().currentQualityGate?.id).toBe('3'); + expect(wrapper.state().selectedQualityGateId).toBe('3'); +}); + +it('correctly associates a selected Quality Gate', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().handleSelect('3'); + wrapper.instance().handleSubmit(); + + expect(associateGateWithProject).toHaveBeenCalledWith({ + gateId: '3', + projectKey: 'foo' + }); +}); + +it('correctly associates a project with the system default Quality Gate', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.setState({ + currentQualityGate: mockQualityGate({ id: '1' }), + selectedQualityGateId: USE_SYSTEM_DEFAULT + }); + wrapper.instance().handleSubmit(); + + expect(dissociateGateWithProject).toHaveBeenCalledWith({ + gateId: '1', + projectKey: 'foo' + }); +}); + +it("correctly doesn't do anything if the system default was selected, and the project had no prior Quality Gate associated with it", async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.setState({ currentQualityGate: undefined, selectedQualityGateId: USE_SYSTEM_DEFAULT }); + wrapper.instance().handleSubmit(); + + expect(associateGateWithProject).not.toHaveBeenCalled(); + expect(dissociateGateWithProject).not.toHaveBeenCalled(); +}); + +it('correctly handles WS errors', async () => { + (fetchQualityGates as jest.Mock).mockRejectedValueOnce(null); + (getGateForProject as jest.Mock).mockRejectedValueOnce(null); + (searchProjects as jest.Mock).mockRejectedValueOnce(null); + + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + expect(wrapper.state().allQualityGates).toBeUndefined(); + expect(wrapper.state().currentQualityGate).toBeUndefined(); + expect(wrapper.state().loading).toBe(false); + + const result = await wrapper.instance().isUsingDefault(mockQualityGate()); + expect(result).toBe(false); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateAppRenderer-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateAppRenderer-test.tsx new file mode 100644 index 00000000000..76ed1328e17 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateAppRenderer-test.tsx @@ -0,0 +1,111 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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 Radio from 'sonar-ui-common/components/controls/Radio'; +import Select from 'sonar-ui-common/components/controls/Select'; +import { submit } from 'sonar-ui-common/helpers/testUtils'; +import { mockQualityGate } from '../../../helpers/mocks/quality-gates'; +import { USE_SYSTEM_DEFAULT } from '../constants'; +import ProjectQualityGateAppRenderer, { + ProjectQualityGateAppRendererProps +} from '../ProjectQualityGateAppRenderer'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ loading: true })).toMatchSnapshot('loading'); + expect(shallowRender({ submitting: true })).toMatchSnapshot('submitting'); + expect( + shallowRender({ + currentQualityGate: mockQualityGate({ id: '2', isDefault: true }), + selectedQualityGateId: USE_SYSTEM_DEFAULT + }) + ).toMatchSnapshot('always use system default'); + expect( + shallowRender({ + selectedQualityGateId: '5' + }) + ).toMatchSnapshot('show warning'); + expect( + shallowRender({ + selectedQualityGateId: USE_SYSTEM_DEFAULT + }) + ).toMatchSnapshot('show warning if not using default'); + expect(shallowRender({ allQualityGates: undefined }).type()).toBeNull(); // no quality gates +}); + +it('should render select options correctly', () => { + return new Promise(resolve => { + const wrapper = shallowRender(); + const render = wrapper.find(Select).props().optionRenderer; + if (render) { + expect(render({ value: '1', label: 'Gate 1' })).toMatchSnapshot('default'); + resolve(); + } + }); +}); + +it('should correctly handle changes', () => { + const wrapper = shallowRender(); + const onSelect = jest.fn(selectedQualityGateId => { + wrapper.setProps({ selectedQualityGateId }); + }); + wrapper.setProps({ onSelect }); + + wrapper + .find(Radio) + .at(0) + .props() + .onCheck(USE_SYSTEM_DEFAULT); + expect(onSelect).toHaveBeenLastCalledWith(USE_SYSTEM_DEFAULT); + + wrapper + .find(Radio) + .at(1) + .props() + .onCheck('1'); + expect(onSelect).toHaveBeenLastCalledWith('1'); + + wrapper.find(Select).props().onChange!({ value: '2' }); + expect(onSelect).toHaveBeenLastCalledWith('2'); +}); + +it('should correctly handle form submission', () => { + const onSubmit = jest.fn(); + const wrapper = shallowRender({ onSubmit }); + submit(wrapper.find('form')); + expect(onSubmit).toBeCalled(); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Form-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Form-test.tsx.snap deleted file mode 100644 index e7620afb1a2..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Form-test.tsx.snap +++ /dev/null @@ -1,33 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders 1`] = ` -
- +
+ + + +
+ + save + +
+ + + +`; + +exports[`should render correctly: default 1`] = ` +
+ + + +
+
+

+ project_quality_gate.page +

+ + quality_gates.projects.help +
+ } + /> +
+
+
+

+ project_quality_gate.subtitle +

+
+

+ project_quality_gate.page.description +

+
+ +
+
+ project_quality_gate.always_use_default +
+
+ + current_noun + : + + qualitygate +
+
+
+
+
+ +
+
+ project_quality_gate.always_use_specific +
+
+ +
+
+
+ + project_quality_gate.requires_new_analysis + +
+
+ + save + +
+
+
+ +`; + +exports[`should render correctly: show warning if not using default 1`] = ` +
+ + + +
+
+

+ project_quality_gate.page +

+ + quality_gates.projects.help +
+ } + /> +
+ +
+

+ project_quality_gate.subtitle +

+
+

+ project_quality_gate.page.description +

+
+ +
+
+ project_quality_gate.always_use_default +
+
+ + current_noun + : + + qualitygate +
+
+
+
+
+ +
+
+ project_quality_gate.always_use_specific +
+
+ +
+
+
+
+
+ + save + + +
+ +
+ +`; + +exports[`should render select options correctly: default 1`] = ` + + Gate 1 + +`; diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/constants.ts b/server/sonar-web/src/main/js/apps/projectQualityGate/constants.ts new file mode 100644 index 00000000000..fa3f316af7f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/constants.ts @@ -0,0 +1,21 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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. + */ + +export const USE_SYSTEM_DEFAULT = '-1'; diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/routes.ts b/server/sonar-web/src/main/js/apps/projectQualityGate/routes.ts index d82abd3b146..0390755918b 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/routes.ts +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/routes.ts @@ -21,7 +21,7 @@ import { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent' const routes = [ { - indexRoute: { component: lazyLoadComponent(() => import('./App')) } + indexRoute: { component: lazyLoadComponent(() => import('./ProjectQualityGateApp')) } } ]; 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 ca73a073d83..bd49bac6d31 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -52,6 +52,8 @@ create_new_element=Create new element created=Created created_on=Created on critical=Critical +current=current +current_noun=Current customize=Customize date=Date days=Days @@ -1358,6 +1360,10 @@ project_quality_profile.successfully_updated={0} Quality Profile has been succes #------------------------------------------------------------------------------ project_quality_gate.default_qgate=Default project_quality_gate.successfully_updated=Quality Gate has been successfully updated. +project_quality_gate.subtitle=Manage project Quality Gate +project_quality_gate.always_use_default=Always use the instance default Quality Gate +project_quality_gate.always_use_specific=Always use a specific Quality Gate +project_quality_gate.requires_new_analysis=Changes will be applied after the next analysis. #------------------------------------------------------------------------------ # @@ -1485,7 +1491,7 @@ quality_gates.conditions=Conditions quality_gates.conditions.help=Both conditions on New Code and Overall Code have to be met by a project to pass the Quality Gate. quality_gates.conditions.help.link=See also: Clean as You Code quality_gates.projects=Projects -quality_gates.projects.help=The Default gate is applied to all projects not explicitly assigned to a gate. Quality Profile and Gate administrators can assign projects to a gate from the Quality Profile page. Project administrators can also choose a non-default gate. +quality_gates.projects.help=The Default gate is applied to all projects not explicitly assigned to a gate. Quality Gate administrators can assign projects to a non-default gate, or always make it follow the system default. Project administrators may choose any gate. quality_gates.add_condition=Add Condition quality_gates.condition_added=Successfully added condition. quality_gates.update_condition=Update Condition