diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2020-09-08 16:29:17 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2020-10-08 20:08:02 +0000 |
commit | 6ecb6c40842e94997ff5aaf729ecc1eb98251c3c (patch) | |
tree | 3ee2314eccf283fe6c6450cf6005c8516d3bede9 /server | |
parent | 0b9adba4230ea40e1b8d523981990b909b63dd1a (diff) | |
download | sonarqube-6ecb6c40842e94997ff5aaf729ecc1eb98251c3c.tar.gz sonarqube-6ecb6c40842e94997ff5aaf729ecc1eb98251c3c.zip |
SONAR-13856 Add 'Always use the Default' option at project level for QG
Diffstat (limited to 'server')
17 files changed, 1405 insertions, 570 deletions
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<Props> { - 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 ( - <div className="page page-limited" id="project-quality-gate"> - <Suggestions suggestions="project_quality_gate" /> - <Helmet defer={false} title={translate('project_quality_gate.page')} /> - <A11ySkipTarget anchor="qg_main" /> - <Header /> - {loading ? ( - <i className="spinner" /> - ) : ( - allGates && <Form allGates={allGates} gate={gate} onChange={this.handleChangeGate} /> - )} - </div> - ); - } -} 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<void>; -} - -interface State { - loading: boolean; -} - -interface Option { - isDefault?: boolean; - label: string; - value: string; -} - -export default class Form extends React.PureComponent<Props, State> { - 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 ( - <span> - <strong>{translate('default')}</strong> - {': '} - {option.label} - </span> - ); - } - - return <span>{option.label}</span>; - }; - - renderSelect() { - const { gate, allGates } = this.props; - - const options: Option[] = allGates.map(gate => ({ - value: String(gate.id), - label: gate.name, - isDefault: gate.isDefault - })); - - return ( - <Select - clearable={false} - disabled={this.state.loading} - onChange={this.handleChange} - optionRenderer={this.renderGateName} - options={options} - style={{ width: 300 }} - value={gate && String(gate.id)} - valueRenderer={this.renderGateName} - /> - ); - } - - render() { - return ( - <div> - {this.renderSelect()} - {this.state.loading && <i className="spinner spacer-left" />} - </div> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx deleted file mode 100644 index d2fee01125e..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/Header.tsx +++ /dev/null @@ -1,41 +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 HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip'; -import { translate } from 'sonar-ui-common/helpers/l10n'; - -export default function Header() { - return ( - <header className="page-header"> - <div className="page-title display-flex-center"> - <h1>{translate('project_quality_gate.page')}</h1> - <HelpTooltip - className="spacer-left" - overlay={ - <div className="big-padded-top big-padded-bottom"> - {translate('quality_gates.projects.help')} - </div> - } - /> - </div> - <div className="page-description">{translate('project_quality_gate.page.description')}</div> - </header> - ); -} diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx new file mode 100644 index 00000000000..98ec647b4bf --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx @@ -0,0 +1,192 @@ +/* + * 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 { translate } from 'sonar-ui-common/helpers/l10n'; +import { + associateGateWithProject, + dissociateGateWithProject, + fetchQualityGates, + getGateForProject, + searchProjects +} from '../../api/quality-gates'; +import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage'; +import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization'; +import { USE_SYSTEM_DEFAULT } from './constants'; +import ProjectQualityGateAppRenderer from './ProjectQualityGateAppRenderer'; + +interface Props { + component: T.Component; + onComponentChange: (changes: {}) => void; +} + +interface State { + allQualityGates?: T.QualityGate[]; + currentQualityGate?: T.QualityGate; + loading: boolean; + selectedQualityGateId: string; + submitting: boolean; +} + +export default class ProjectQualityGateApp extends React.PureComponent<Props, State> { + mounted = false; + state: State = { + loading: true, + selectedQualityGateId: USE_SYSTEM_DEFAULT, + submitting: false + }; + + 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; + }; + + isUsingDefault = async (qualityGate: T.QualityGate) => { + const { component } = this.props; + + if (!qualityGate.isDefault) { + return false; + } else { + // If this is the default Quality Gate, check if it was explicitly + // selected, or if we're inheriting the system default. + /* eslint-disable-next-line sonarjs/prefer-immediate-return */ + const selected = await searchProjects({ + gateName: qualityGate.name, + query: component.key + }) + .then(({ results }) => { + return Boolean(results.find(r => r.key === component.key)?.selected); + }) + .catch(() => false); + + // If it's NOT selected, it means we're following the system default. + return !selected; + } + }; + + fetchQualityGates = async () => { + const { component } = this.props; + this.setState({ loading: true }); + + const [allQualityGates, currentQualityGate] = await Promise.all([ + fetchQualityGates().then(({ qualitygates }) => qualitygates), + getGateForProject({ project: component.key }) + ]).catch(() => []); + + if (allQualityGates && currentQualityGate) { + const usingDefault = await this.isUsingDefault(currentQualityGate); + + if (this.mounted) { + this.setState({ + allQualityGates, + currentQualityGate, + selectedQualityGateId: usingDefault ? USE_SYSTEM_DEFAULT : currentQualityGate.id, + loading: false + }); + } + } else if (this.mounted) { + this.setState({ loading: false }); + } + }; + + handleSelect = (selectedQualityGateId: string) => { + this.setState({ selectedQualityGateId }); + }; + + handleSubmit = async () => { + const { component } = this.props; + const { allQualityGates, currentQualityGate, selectedQualityGateId } = this.state; + + if (allQualityGates === undefined || currentQualityGate === undefined) { + return; + } + + this.setState({ submitting: true }); + + if (selectedQualityGateId === USE_SYSTEM_DEFAULT) { + await dissociateGateWithProject({ + gateId: currentQualityGate.id, + projectKey: component.key + }).catch(() => { + /* noop */ + }); + } else { + await associateGateWithProject({ + gateId: selectedQualityGateId, + projectKey: component.key + }).catch(() => { + /* noop */ + }); + } + + if (this.mounted) { + addGlobalSuccessMessage(translate('project_quality_gate.successfully_updated')); + + const newGate = + selectedQualityGateId === USE_SYSTEM_DEFAULT + ? allQualityGates.find(gate => gate.isDefault) + : allQualityGates.find(gate => gate.id === selectedQualityGateId); + + if (newGate) { + this.setState({ currentQualityGate: newGate, submitting: false }); + this.props.onComponentChange({ qualityGate: newGate }); + } + } + }; + + render() { + if (!this.checkPermissions()) { + return null; + } + + const { + allQualityGates, + currentQualityGate, + loading, + selectedQualityGateId, + submitting + } = this.state; + + return ( + <ProjectQualityGateAppRenderer + allQualityGates={allQualityGates} + currentQualityGate={currentQualityGate} + loading={loading} + onSubmit={this.handleSubmit} + onSelect={this.handleSelect} + selectedQualityGateId={selectedQualityGateId} + submitting={submitting} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx new file mode 100644 index 00000000000..e8baa5364c9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx @@ -0,0 +1,166 @@ +/* + * 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 { SubmitButton } from 'sonar-ui-common/components/controls/buttons'; +import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip'; +import Radio from 'sonar-ui-common/components/controls/Radio'; +import Select from 'sonar-ui-common/components/controls/Select'; +import { Alert } from 'sonar-ui-common/components/ui/Alert'; +import { translate } from 'sonar-ui-common/helpers/l10n'; +import A11ySkipTarget from '../../app/components/a11y/A11ySkipTarget'; +import Suggestions from '../../app/components/embed-docs-modal/Suggestions'; +import BuiltInQualityGateBadge from '../quality-gates/components/BuiltInQualityGateBadge'; +import { USE_SYSTEM_DEFAULT } from './constants'; + +export interface ProjectQualityGateAppRendererProps { + allQualityGates?: T.QualityGate[]; + currentQualityGate?: T.QualityGate; + loading: boolean; + onSelect: (id: string) => void; + onSubmit: () => void; + selectedQualityGateId: string; + submitting: boolean; +} + +export default function ProjectQualityGateAppRenderer(props: ProjectQualityGateAppRendererProps) { + const { allQualityGates, currentQualityGate, loading, selectedQualityGateId, submitting } = props; + const defaultQualityGate = allQualityGates?.find(g => g.isDefault); + + if (loading) { + return <i className="spinner" />; + } + + if ( + allQualityGates === undefined || + defaultQualityGate === undefined || + currentQualityGate === undefined + ) { + return null; + } + + const usesDefault = selectedQualityGateId === USE_SYSTEM_DEFAULT; + const needsReanalysis = usesDefault + ? // currentQualityGate.isDefault is not always up to date. We need to check + // against defaultQualityGate explicitly. + defaultQualityGate.id !== currentQualityGate.id + : selectedQualityGateId !== currentQualityGate.id; + + const options = allQualityGates.map(g => ({ + label: g.name, + value: g.id + })); + + return ( + <div className="page page-limited" id="project-quality-gate"> + <Suggestions suggestions="project_quality_gate" /> + <Helmet defer={false} title={translate('project_quality_gate.page')} /> + <A11ySkipTarget anchor="qg_main" /> + + <header className="page-header"> + <div className="page-title display-flex-center"> + <h1>{translate('project_quality_gate.page')}</h1> + <HelpTooltip + className="spacer-left" + overlay={ + <div className="big-padded-top big-padded-bottom"> + {translate('quality_gates.projects.help')} + </div> + } + /> + </div> + </header> + + <div className="boxed-group"> + <h2 className="boxed-group-header">{translate('project_quality_gate.subtitle')}</h2> + + <form + className="boxed-group-inner" + onSubmit={e => { + e.preventDefault(); + props.onSubmit(); + }}> + <p className="big-spacer-bottom">{translate('project_quality_gate.page.description')}</p> + + <div className="big-spacer-bottom"> + <Radio + className="display-flex-start" + checked={usesDefault} + disabled={submitting} + onCheck={() => props.onSelect(USE_SYSTEM_DEFAULT)} + value={USE_SYSTEM_DEFAULT}> + <div className="spacer-left"> + <div className="little-spacer-bottom"> + {translate('project_quality_gate.always_use_default')} + </div> + <div className="display-flex-center"> + <span className="text-muted little-spacer-right"> + {translate('current_noun')}: + </span> + {defaultQualityGate.name} + {defaultQualityGate.isBuiltIn && ( + <BuiltInQualityGateBadge className="spacer-left" /> + )} + </div> + </div> + </Radio> + </div> + + <div className="big-spacer-bottom"> + <Radio + className="display-flex-start" + checked={!usesDefault} + disabled={submitting} + onCheck={value => props.onSelect(value)} + value={!usesDefault ? selectedQualityGateId : currentQualityGate.id}> + <div className="spacer-left"> + <div className="little-spacer-bottom"> + {translate('project_quality_gate.always_use_specific')} + </div> + <div className="display-flex-center"> + <Select + className="abs-width-300" + clearable={false} + disabled={submitting || usesDefault} + onChange={({ value }: { value: string }) => props.onSelect(value)} + options={options} + optionRenderer={option => <span>{option.label}</span>} + value={selectedQualityGateId} + /> + </div> + </div> + </Radio> + + {needsReanalysis && ( + <Alert className="big-spacer-top" variant="warning"> + {translate('project_quality_gate.requires_new_analysis')} + </Alert> + )} + </div> + + <div> + <SubmitButton disabled={submitting}>{translate('save')}</SubmitButton> + {submitting && <i className="spinner spacer-left" />} + </div> + </form> + </div> + </div> + ); +} 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<any>; - -const dissociateGateWithProject = require('../../../api/quality-gates') - .dissociateGateWithProject as jest.Mock<any>; - -const fetchQualityGates = require('../../../api/quality-gates').fetchQualityGates as jest.Mock<any>; - -const getGateForProject = require('../../../api/quality-gates').getGateForProject as jest.Mock<any>; - -const addGlobalSuccessMessage = require('../../../app/utils/addGlobalSuccessMessage') - .default as jest.Mock<any>; - -const handleRequiredAuthorization = require('../../../app/utils/handleRequiredAuthorization') - .default as jest.Mock<any>; - -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( - <App - component={{ ...component, configuration: undefined } as T.Component} - onComponentChange={jest.fn()} - /> - ); - expect(handleRequiredAuthorization).toBeCalled(); -}); - -it('fetches quality gates', () => { - fetchQualityGates.mockClear(); - getGateForProject.mockClear(); - shallow(<App component={component} onComponentChange={jest.fn()} />); - 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<Function>('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<Function>('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<Function>('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<Function>('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(<App component={component} onComponentChange={jest.fn()} />); - 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(<Form allGates={allGates} gate={foo} onChange={jest.fn()} />)).toMatchSnapshot(); -}); - -it('changes quality gate', () => { - const allGates = [randomGate('1'), randomGate('2')]; - const onChange = jest.fn(() => Promise.resolve()); - const wrapper = shallow(<Form allGates={allGates} onChange={onChange} />); - - wrapper.find('Select').prop<Function>('onChange')({ value: '2' }); - expect(onChange).lastCalledWith(undefined, '2'); - - wrapper.setProps({ gate: randomGate('1') }); - wrapper.find('Select').prop<Function>('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__/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<ProjectQualityGateApp['props']> = {}) { + return shallow<ProjectQualityGateApp>( + <ProjectQualityGateApp + component={mockComponent({ key: 'foo', configuration: { showQualityGates: true } })} + onComponentChange={jest.fn()} + {...props} + /> + ); +} 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<ProjectQualityGateAppRendererProps> = {}) { + return shallow<ProjectQualityGateAppRendererProps>( + <ProjectQualityGateAppRenderer + allQualityGates={[mockQualityGate(), mockQualityGate({ id: '2', isDefault: true })]} + currentQualityGate={mockQualityGate({ id: '1' })} + loading={false} + onSelect={jest.fn()} + onSubmit={jest.fn()} + selectedQualityGateId="1" + submitting={false} + {...props} + /> + ); +} 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`] = ` -<div> - <Select - clearable={false} - disabled={false} - onChange={[Function]} - optionRenderer={[Function]} - options={ - Array [ - Object { - "isDefault": undefined, - "label": "name-1", - "value": "1", - }, - Object { - "isDefault": undefined, - "label": "name-2", - "value": "2", - }, - ] - } - style={ - Object { - "width": 300, - } - } - value="1" - valueRenderer={[Function]} - /> -</div> -`; diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap deleted file mode 100644 index 234232fc945..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/Header-test.tsx.snap +++ /dev/null @@ -1,30 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders 1`] = ` -<header - className="page-header" -> - <div - className="page-title display-flex-center" - > - <h1> - project_quality_gate.page - </h1> - <HelpTooltip - className="spacer-left" - overlay={ - <div - className="big-padded-top big-padded-bottom" - > - quality_gates.projects.help - </div> - } - /> - </div> - <div - className="page-description" - > - project_quality_gate.page.description - </div> -</header> -`; diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateApp-test.tsx.snap new file mode 100644 index 00000000000..d7585406c6d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateApp-test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +<ProjectQualityGateAppRenderer + loading={true} + onSelect={[Function]} + onSubmit={[Function]} + selectedQualityGateId="-1" + submitting={false} +/> +`; diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateAppRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateAppRenderer-test.tsx.snap new file mode 100644 index 00000000000..26e95c11361 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateAppRenderer-test.tsx.snap @@ -0,0 +1,743 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: always use system default 1`] = ` +<div + className="page page-limited" + id="project-quality-gate" +> + <Suggestions + suggestions="project_quality_gate" + /> + <Helmet + defer={false} + encodeSpecialCharacters={true} + title="project_quality_gate.page" + /> + <A11ySkipTarget + anchor="qg_main" + /> + <header + className="page-header" + > + <div + className="page-title display-flex-center" + > + <h1> + project_quality_gate.page + </h1> + <HelpTooltip + className="spacer-left" + overlay={ + <div + className="big-padded-top big-padded-bottom" + > + quality_gates.projects.help + </div> + } + /> + </div> + </header> + <div + className="boxed-group" + > + <h2 + className="boxed-group-header" + > + project_quality_gate.subtitle + </h2> + <form + className="boxed-group-inner" + onSubmit={[Function]} + > + <p + className="big-spacer-bottom" + > + project_quality_gate.page.description + </p> + <div + className="big-spacer-bottom" + > + <Radio + checked={true} + className="display-flex-start" + disabled={false} + onCheck={[Function]} + value="-1" + > + <div + className="spacer-left" + > + <div + className="little-spacer-bottom" + > + project_quality_gate.always_use_default + </div> + <div + className="display-flex-center" + > + <span + className="text-muted little-spacer-right" + > + current_noun + : + </span> + qualitygate + </div> + </div> + </Radio> + </div> + <div + className="big-spacer-bottom" + > + <Radio + checked={false} + className="display-flex-start" + disabled={false} + onCheck={[Function]} + value="2" + > + <div + className="spacer-left" + > + <div + className="little-spacer-bottom" + > + project_quality_gate.always_use_specific + </div> + <div + className="display-flex-center" + > + <Select + className="abs-width-300" + clearable={false} + disabled={true} + onChange={[Function]} + optionRenderer={[Function]} + options={ + Array [ + Object { + "label": "qualitygate", + "value": "1", + }, + Object { + "label": "qualitygate", + "value": "2", + }, + ] + } + value="-1" + /> + </div> + </div> + </Radio> + </div> + <div> + <SubmitButton + disabled={false} + > + save + </SubmitButton> + </div> + </form> + </div> +</div> +`; + +exports[`should render correctly: default 1`] = ` +<div + className="page page-limited" + id="project-quality-gate" +> + <Suggestions + suggestions="project_quality_gate" + /> + <Helmet + defer={false} + encodeSpecialCharacters={true} + title="project_quality_gate.page" + /> + <A11ySkipTarget + anchor="qg_main" + /> + <header + className="page-header" + > + <div + className="page-title display-flex-center" + > + <h1> + project_quality_gate.page + </h1> + <HelpTooltip + className="spacer-left" + overlay={ + <div + className="big-padded-top big-padded-bottom" + > + quality_gates.projects.help + </div> + } + /> + </div> + </header> + <div + className="boxed-group" + > + <h2 + className="boxed-group-header" + > + project_quality_gate.subtitle + </h2> + <form + className="boxed-group-inner" + onSubmit={[Function]} + > + <p + className="big-spacer-bottom" + > + project_quality_gate.page.description + </p> + <div + className="big-spacer-bottom" + > + <Radio + checked={false} + className="display-flex-start" + disabled={false} + onCheck={[Function]} + value="-1" + > + <div + className="spacer-left" + > + <div + className="little-spacer-bottom" + > + project_quality_gate.always_use_default + </div> + <div + className="display-flex-center" + > + <span + className="text-muted little-spacer-right" + > + current_noun + : + </span> + qualitygate + </div> + </div> + </Radio> + </div> + <div + className="big-spacer-bottom" + > + <Radio + checked={true} + className="display-flex-start" + disabled={false} + onCheck={[Function]} + value="1" + > + <div + className="spacer-left" + > + <div + className="little-spacer-bottom" + > + project_quality_gate.always_use_specific + </div> + <div + className="display-flex-center" + > + <Select + className="abs-width-300" + clearable={false} + disabled={false} + onChange={[Function]} + optionRenderer={[Function]} + options={ + Array [ + Object { + "label": "qualitygate", + "value": "1", + }, + Object { + "label": "qualitygate", + "value": "2", + }, + ] + } + value="1" + /> + </div> + </div> + </Radio> + </div> + <div> + <SubmitButton + disabled={false} + > + save + </SubmitButton> + </div> + </form> + </div> +</div> +`; + +exports[`should render correctly: loading 1`] = ` +<i + className="spinner" +/> +`; + +exports[`should render correctly: show warning 1`] = ` +<div + className="page page-limited" + id="project-quality-gate" +> + <Suggestions + suggestions="project_quality_gate" + /> + <Helmet + defer={false} + encodeSpecialCharacters={true} + title="project_quality_gate.page" + /> + <A11ySkipTarget + anchor="qg_main" + /> + <header + className="page-header" + > + <div + className="page-title display-flex-center" + > + <h1> + project_quality_gate.page + </h1> + <HelpTooltip + className="spacer-left" + overlay={ + <div + className="big-padded-top big-padded-bottom" + > + quality_gates.projects.help + </div> + } + /> + </div> + </header> + <div + className="boxed-group" + > + <h2 + className="boxed-group-header" + > + project_quality_gate.subtitle + </h2> + <form + className="boxed-group-inner" + onSubmit={[Function]} + > + <p + className="big-spacer-bottom" + > + project_quality_gate.page.description + </p> + <div + className="big-spacer-bottom" + > + <Radio + checked={false} + className="display-flex-start" + disabled={false} + onCheck={[Function]} + value="-1" + > + <div + className="spacer-left" + > + <div + className="little-spacer-bottom" + > + project_quality_gate.always_use_default + </div> + <div + className="display-flex-center" + > + <span + className="text-muted little-spacer-right" + > + current_noun + : + </span> + qualitygate + </div> + </div> + </Radio> + </div> + <div + className="big-spacer-bottom" + > + <Radio + checked={true} + className="display-flex-start" + disabled={false} + onCheck={[Function]} + value="5" + > + <div + className="spacer-left" + > + <div + className="little-spacer-bottom" + > + project_quality_gate.always_use_specific + </div> + <div + className="display-flex-center" + > + <Select + className="abs-width-300" + clearable={false} + disabled={false} + onChange={[Function]} + optionRenderer={[Function]} + options={ + Array [ + Object { + "label": "qualitygate", + "value": "1", + }, + Object { + "label": "qualitygate", + "value": "2", + }, + ] + } + value="5" + /> + </div> + </div> + </Radio> + <Alert + className="big-spacer-top" + variant="warning" + > + project_quality_gate.requires_new_analysis + </Alert> + </div> + <div> + <SubmitButton + disabled={false} + > + save + </SubmitButton> + </div> + </form> + </div> +</div> +`; + +exports[`should render correctly: show warning if not using default 1`] = ` +<div + className="page page-limited" + id="project-quality-gate" +> + <Suggestions + suggestions="project_quality_gate" + /> + <Helmet + defer={false} + encodeSpecialCharacters={true} + title="project_quality_gate.page" + /> + <A11ySkipTarget + anchor="qg_main" + /> + <header + className="page-header" + > + <div + className="page-title display-flex-center" + > + <h1> + project_quality_gate.page + </h1> + <HelpTooltip + className="spacer-left" + overlay={ + <div + className="big-padded-top big-padded-bottom" + > + quality_gates.projects.help + </div> + } + /> + </div> + </header> + <div + className="boxed-group" + > + <h2 + className="boxed-group-header" + > + project_quality_gate.subtitle + </h2> + <form + className="boxed-group-inner" + onSubmit={[Function]} + > + <p + className="big-spacer-bottom" + > + project_quality_gate.page.description + </p> + <div + className="big-spacer-bottom" + > + <Radio + checked={true} + className="display-flex-start" + disabled={false} + onCheck={[Function]} + value="-1" + > + <div + className="spacer-left" + > + <div + className="little-spacer-bottom" + > + project_quality_gate.always_use_default + </div> + <div + className="display-flex-center" + > + <span + className="text-muted little-spacer-right" + > + current_noun + : + </span> + qualitygate + </div> + </div> + </Radio> + </div> + <div + className="big-spacer-bottom" + > + <Radio + checked={false} + className="display-flex-start" + disabled={false} + onCheck={[Function]} + value="1" + > + <div + className="spacer-left" + > + <div + className="little-spacer-bottom" + > + project_quality_gate.always_use_specific + </div> + <div + className="display-flex-center" + > + <Select + className="abs-width-300" + clearable={false} + disabled={true} + onChange={[Function]} + optionRenderer={[Function]} + options={ + Array [ + Object { + "label": "qualitygate", + "value": "1", + }, + Object { + "label": "qualitygate", + "value": "2", + }, + ] + } + value="-1" + /> + </div> + </div> + </Radio> + <Alert + className="big-spacer-top" + variant="warning" + > + project_quality_gate.requires_new_analysis + </Alert> + </div> + <div> + <SubmitButton + disabled={false} + > + save + </SubmitButton> + </div> + </form> + </div> +</div> +`; + +exports[`should render correctly: submitting 1`] = ` +<div + className="page page-limited" + id="project-quality-gate" +> + <Suggestions + suggestions="project_quality_gate" + /> + <Helmet + defer={false} + encodeSpecialCharacters={true} + title="project_quality_gate.page" + /> + <A11ySkipTarget + anchor="qg_main" + /> + <header + className="page-header" + > + <div + className="page-title display-flex-center" + > + <h1> + project_quality_gate.page + </h1> + <HelpTooltip + className="spacer-left" + overlay={ + <div + className="big-padded-top big-padded-bottom" + > + quality_gates.projects.help + </div> + } + /> + </div> + </header> + <div + className="boxed-group" + > + <h2 + className="boxed-group-header" + > + project_quality_gate.subtitle + </h2> + <form + className="boxed-group-inner" + onSubmit={[Function]} + > + <p + className="big-spacer-bottom" + > + project_quality_gate.page.description + </p> + <div + className="big-spacer-bottom" + > + <Radio + checked={false} + className="display-flex-start" + disabled={true} + onCheck={[Function]} + value="-1" + > + <div + className="spacer-left" + > + <div + className="little-spacer-bottom" + > + project_quality_gate.always_use_default + </div> + <div + className="display-flex-center" + > + <span + className="text-muted little-spacer-right" + > + current_noun + : + </span> + qualitygate + </div> + </div> + </Radio> + </div> + <div + className="big-spacer-bottom" + > + <Radio + checked={true} + className="display-flex-start" + disabled={true} + onCheck={[Function]} + value="1" + > + <div + className="spacer-left" + > + <div + className="little-spacer-bottom" + > + project_quality_gate.always_use_specific + </div> + <div + className="display-flex-center" + > + <Select + className="abs-width-300" + clearable={false} + disabled={true} + onChange={[Function]} + optionRenderer={[Function]} + options={ + Array [ + Object { + "label": "qualitygate", + "value": "1", + }, + Object { + "label": "qualitygate", + "value": "2", + }, + ] + } + value="1" + /> + </div> + </div> + </Radio> + </div> + <div> + <SubmitButton + disabled={true} + > + save + </SubmitButton> + <i + className="spinner spacer-left" + /> + </div> + </form> + </div> +</div> +`; + +exports[`should render select options correctly: default 1`] = ` +<span> + Gate 1 +</span> +`; diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/Header-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/constants.ts index 9730d14d916..fa3f316af7f 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/Header-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/constants.ts @@ -17,10 +17,5 @@ * 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(<Header />)).toMatchSnapshot(); -}); +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')) } } ]; |