diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-07-20 13:27:32 +0200 |
---|---|---|
committer | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-07-25 09:20:30 +0200 |
commit | 106437a53c371a202d7fbd669472ba7096ae2f71 (patch) | |
tree | 60db17cd6c1cf3413a556eeadf28654f92c21f35 | |
parent | 1ef941b5c7b4faa45f7f19461a8b1d81ccac540d (diff) | |
download | sonarqube-106437a53c371a202d7fbd669472ba7096ae2f71.tar.gz sonarqube-106437a53c371a202d7fbd669472ba7096ae2f71.zip |
SONAR-9565 Move the Quality Gates link to organization level
21 files changed, 404 insertions, 93 deletions
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js index 6535201176f..7d7437238bf 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js @@ -20,6 +20,7 @@ import React from 'react'; import { Link } from 'react-router'; import { translate } from '../../../../helpers/l10n'; +import { getQualityGatesUrl } from '../../../../helpers/urls'; import { isMySet } from '../../../../apps/issues/utils'; export default class GlobalNavMenu extends React.PureComponent { @@ -98,7 +99,7 @@ export default class GlobalNavMenu extends React.PureComponent { renderQualityGatesLink() { return ( <li> - <Link to="/quality_gates" activeClassName="active"> + <Link to={getQualityGatesUrl()} activeClassName="active"> {translate('quality_gates.page')} </Link> </li> @@ -158,7 +159,7 @@ export default class GlobalNavMenu extends React.PureComponent { {this.renderIssuesLink()} {!organizationsEnabled && this.renderRulesLink()} {!organizationsEnabled && this.renderProfilesLink()} - {this.renderQualityGatesLink()} + {!organizationsEnabled && this.renderQualityGatesLink()} {this.renderAdministrationLink()} {this.renderMore()} </ul> diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap index 3315afa83e2..f3541ea9d6d 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap @@ -55,7 +55,11 @@ exports[`should show administration menu if the user has the rights 1`] = ` activeClassName="active" onlyActiveOnIndex={false} style={Object {}} - to="/quality_gates" + to={ + Object { + "pathname": "/quality_gates", + } + } > quality_gates.page </Link> @@ -129,7 +133,11 @@ exports[`should work with extensions 1`] = ` activeClassName="active" onlyActiveOnIndex={false} style={Object {}} - to="/quality_gates" + to={ + Object { + "pathname": "/quality_gates", + } + } > quality_gates.page </Link> diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js index 0d8939752c6..be4670195a1 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js @@ -26,6 +26,7 @@ import ContextNavBar from '../../../components/nav/ContextNavBar'; import NavBarTabs from '../../../components/nav/NavBarTabs'; import OrganizationIcon from '../../../components/icons-components/OrganizationIcon'; import { isMySet } from '../../issues/utils'; +import { getQualityGatesUrl } from '../../../helpers/urls'; import type { Organization } from '../../../store/organizations/duck'; const ADMIN_PATHS = [ @@ -228,6 +229,11 @@ export default class OrganizationNavigation extends React.PureComponent { {translate('coding_rules.page')} </Link> </li> + <li> + <Link to={getQualityGatesUrl(organization.key)} activeClassName="active"> + {translate('quality_gates.page')} + </Link> + </li> {this.renderExtensions(moreActive)} {organization.canAdmin && this.renderAdministration(adminActive)} </NavBarTabs> diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap index 389fae8e92f..e3acf365788 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap @@ -87,6 +87,20 @@ exports[`admin 1`] = ` coding_rules.page </Link> </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/organizations/foo/quality_gates", + } + } + > + quality_gates.page + </Link> + </li> <li className="dropdown" > @@ -257,6 +271,20 @@ exports[`regular user 1`] = ` coding_rules.page </Link> </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/organizations/foo/quality_gates", + } + } + > + quality_gates.page + </Link> + </li> </NavBarTabs> </ContextNavBar> `; @@ -348,6 +376,20 @@ exports[`undeletable org 1`] = ` coding_rules.page </Link> </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/organizations/foo/quality_gates", + } + } + > + quality_gates.page + </Link> + </li> <li className="dropdown" > diff --git a/server/sonar-web/src/main/js/apps/organizations/routes.js b/server/sonar-web/src/main/js/apps/organizations/routes.js index 90c22b3270b..ecdd76eb7e1 100644 --- a/server/sonar-web/src/main/js/apps/organizations/routes.js +++ b/server/sonar-web/src/main/js/apps/organizations/routes.js @@ -31,6 +31,7 @@ import OrganizationPermissions from './components/OrganizationPermissions'; import OrganizationPermissionTemplates from './components/OrganizationPermissionTemplates'; import OrganizationProjectsManagement from './components/OrganizationProjectsManagement'; import OrganizationDelete from './components/OrganizationDelete'; +import qualityGatesRoutes from '../quality-gates/routes'; import qualityProfilesRoutes from '../quality-profiles/routes'; import issuesRoutes from '../issues/routes'; @@ -80,6 +81,11 @@ const routes = [ childRoutes: qualityProfilesRoutes }, { + path: 'quality_gates', + component: OrganizationContainer, + childRoutes: qualityGatesRoutes + }, + { path: 'extension/:pluginKey/:extensionKey', component: OrganizationPageExtension }, diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js index 1a57590524f..4d84a3f2e23 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js @@ -43,7 +43,7 @@ const Meta = ({ component, history, measures, areThereCustomOrganizations, route const shouldShowQualityProfiles = !isView && !isDeveloper && hasQualityProfiles; const shouldShowQualityGate = !isView && !isDeveloper && hasQualityGate; - const shouldShowOrganizationKey = component.organization != null && areThereCustomOrganizations; + const hasOrganization = component.organization != null && areThereCustomOrganizations; return ( <div className="overview-meta"> @@ -58,7 +58,11 @@ const Meta = ({ component, history, measures, areThereCustomOrganizations, route {isProject && <AnalysesList project={component.key} history={history} router={router} />} - {shouldShowQualityGate && <MetaQualityGate gate={qualityGate} />} + {shouldShowQualityGate && + <MetaQualityGate + gate={qualityGate} + organization={hasOrganization && component.organization} + />} {shouldShowQualityProfiles && <MetaQualityProfiles @@ -71,7 +75,7 @@ const Meta = ({ component, history, measures, areThereCustomOrganizations, route <MetaKey component={component} /> - {shouldShowOrganizationKey && <MetaOrganizationKey component={component} />} + {hasOrganization && <MetaOrganizationKey component={component} />} </div> ); }; diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityGate.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityGate.js index 3bdda5734d1..f9c3a1db5f6 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityGate.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityGate.js @@ -22,7 +22,7 @@ import { Link } from 'react-router'; import { translate } from '../../../helpers/l10n'; import { getQualityGateUrl } from '../../../helpers/urls'; -const MetaQualityGate = ({ gate }) => { +const MetaQualityGate = ({ gate, organization }) => { return ( <div className="overview-meta-card"> <h4 className="overview-meta-header"> @@ -35,7 +35,7 @@ const MetaQualityGate = ({ gate }) => { <span className="note spacer-right"> {'(' + translate('default') + ')'} </span>} - <Link to={getQualityGateUrl(gate.key)}> + <Link to={getQualityGateUrl(gate.key, organization)}> {gate.name} </Link> </li> diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Details.js b/server/sonar-web/src/main/js/apps/quality-gates/components/Details.js index 8fd787e3030..8f52c7bbe69 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Details.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Details.js @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React, { Component } from 'react'; +import React from 'react'; import Helmet from 'react-helmet'; import { fetchQualityGate, @@ -29,8 +29,9 @@ import DetailsContent from './DetailsContent'; import RenameView from '../views/rename-view'; import CopyView from '../views/copy-view'; import DeleteView from '../views/delete-view'; +import { getQualityGatesUrl, getQualityGateUrl } from '../../../helpers/urls'; -export default class Details extends Component { +export default class Details extends React.PureComponent { componentDidMount() { this.fetchDetails(); } @@ -62,14 +63,14 @@ export default class Details extends Component { } handleCopyClick() { - const { qualityGate, onCopy } = this.props; + const { qualityGate, onCopy, organization } = this.props; const { router } = this.context; new CopyView({ qualityGate, onCopy: newQualityGate => { onCopy(newQualityGate); - router.push(`/quality_gates/show/${newQualityGate.id}`); + router.push(getQualityGateUrl(newQualityGate.id, organization && organization.key)); } }).render(); } @@ -85,14 +86,13 @@ export default class Details extends Component { } handleDeleteClick() { - const { qualityGate, onDelete } = this.props; + const { qualityGate, onDelete, organization } = this.props; const { router } = this.context; - new DeleteView({ qualityGate, onDelete: qualityGate => { onDelete(qualityGate); - router.replace('/quality_gates'); + router.replace(getQualityGatesUrl(organization && organization.key)); } }).render(); } @@ -115,6 +115,7 @@ export default class Details extends Component { onCopy={this.handleCopyClick.bind(this)} onSetAsDefault={this.handleSetAsDefaultClick.bind(this)} onDelete={this.handleDeleteClick.bind(this)} + organization={this.props.organization} /> <DetailsContent diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js index 3613be4e567..5c826c2c302 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.js @@ -17,12 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React, { Component } from 'react'; +import React from 'react'; import Conditions from './Conditions'; import Projects from './Projects'; import { translate } from '../../../helpers/l10n'; -export default class DetailsContent extends Component { +export default class DetailsContent extends React.PureComponent { render() { const { gate, canEdit, metrics } = this.props; const { onAddCondition, onDeleteCondition, onSaveCondition } = this.props; diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.js b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.js index 4d518e83238..2f6091184d6 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.js @@ -20,62 +20,62 @@ import React from 'react'; import { translate } from '../../../helpers/l10n'; -export default function DetailsHeader({ - qualityGate, - edit, - onRename, - onCopy, - onSetAsDefault, - onDelete -}) { - function handleRenameClick(e) { +export default class DetailsHeader extends React.PureComponent { + handleRenameClick = e => { e.preventDefault(); - onRename(); - } + this.props.onRename(); + }; - function handleCopyClick(e) { + handleCopyClick = e => { e.preventDefault(); - onCopy(); - } + this.props.onCopy(); + }; - function handleSetAsDefaultClick(e) { + handleSetAsDefaultClick = e => { e.preventDefault(); - onSetAsDefault(); - } + this.props.onSetAsDefault(); + }; - function handleDeleteClick(e) { + handleDeleteClick = e => { e.preventDefault(); - onDelete(); - } + this.props.onDelete(); + }; + + render() { + const { qualityGate, edit } = this.props; - return ( - <div className="layout-page-header-panel layout-page-main-header issues-main-header"> - <div className="layout-page-header-panel-inner layout-page-main-header-inner"> - <div className="layout-page-main-inner"> - <h2 className="pull-left"> - {qualityGate.name} - </h2> - {edit && - <div className="pull-right"> - <div className="button-group"> - <button id="quality-gate-rename" onClick={handleRenameClick}> - {translate('rename')} - </button> - <button id="quality-gate-copy" onClick={handleCopyClick}> - {translate('copy')} - </button> - <button id="quality-gate-toggle-default" onClick={handleSetAsDefaultClick}> - {qualityGate.isDefault - ? translate('unset_as_default') - : translate('set_as_default')} - </button> - <button id="quality-gate-delete" className="button-red" onClick={handleDeleteClick}> - {translate('delete')} - </button> - </div> - </div>} + return ( + <div className="layout-page-header-panel layout-page-main-header issues-main-header"> + <div className="layout-page-header-panel-inner layout-page-main-header-inner"> + <div className="layout-page-main-inner"> + <h2 className="pull-left"> + {qualityGate.name} + </h2> + {edit && + <div className="pull-right"> + <div className="button-group"> + <button id="quality-gate-rename" onClick={this.handleRenameClick}> + {translate('rename')} + </button> + <button id="quality-gate-copy" onClick={this.handleCopyClick}> + {translate('copy')} + </button> + <button id="quality-gate-toggle-default" onClick={this.handleSetAsDefaultClick}> + {qualityGate.isDefault + ? translate('unset_as_default') + : translate('set_as_default')} + </button> + <button + id="quality-gate-delete" + className="button-red" + onClick={this.handleDeleteClick}> + {translate('delete')} + </button> + </div> + </div>} + </div> </div> </div> - </div> - ); + ); + } } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/List.js b/server/sonar-web/src/main/js/apps/quality-gates/components/List.js index 9729392a1b8..b6a4d3a8c23 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/List.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/List.js @@ -20,14 +20,15 @@ import React from 'react'; import { Link } from 'react-router'; import { translate } from '../../../helpers/l10n'; +import { getQualityGateUrl } from '../../../helpers/urls'; -export default function List({ qualityGates }) { +export default function List({ organization, qualityGates }) { return ( <div className="list-group"> {qualityGates.map(qualityGate => <Link key={qualityGate.id} - to={`/quality_gates/show/${qualityGate.id}`} + to={getQualityGateUrl(qualityGate.id, organization && organization.key)} activeClassName="active" className="list-group-item" data-id={qualityGate.id}> diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.js b/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.js index a5c96cd7967..33a9c447d13 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.js @@ -17,10 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React, { Component } from 'react'; +import React from 'react'; import ProjectsView from '../views/gate-projects-view'; -export default class Projects extends Component { +export default class Projects extends React.PureComponent { componentDidMount() { this.renderView(); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js index ff9d95d7bd7..3ac0da688cc 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js @@ -26,9 +26,14 @@ import { fetchQualityGates as fetchQualityGatesAPI } from '../../../api/quality-gates'; import { translate } from '../../../helpers/l10n'; +import { getQualityGateUrl } from '../../../helpers/urls'; import '../styles.css'; export default class QualityGatesApp extends Component { + static contextTypes = { + router: React.PropTypes.object.isRequired + }; + state = {}; componentDidMount() { @@ -45,37 +50,35 @@ export default class QualityGatesApp extends Component { } handleAdd(qualityGate) { - const { addQualityGate } = this.props; + const { addQualityGate, organization } = this.props; const { router } = this.context; addQualityGate(qualityGate); - router.push(`/quality_gates/show/${qualityGate.id}`); + router.push(getQualityGateUrl(qualityGate.id, organization && organization.key)); } render() { - const { children, qualityGates, edit } = this.props; + const { children, qualityGates, edit, organization } = this.props; const defaultTitle = translate('quality_gates.page'); + const top = organization ? 95 : 30; return ( <div className="layout-page"> <Helmet defaultTitle={defaultTitle} titleTemplate={'%s - ' + defaultTitle} /> <div className="layout-page-side-outer"> - <div className="layout-page-side" style={{ top: 30 }}> + <div className="layout-page-side" style={{ top }}> <div className="layout-page-side-inner"> <div className="layout-page-filters"> <ListHeader canEdit={edit} onAdd={this.handleAdd.bind(this)} /> - {qualityGates && <List qualityGates={qualityGates} />} + {qualityGates && <List organization={organization} qualityGates={qualityGates} />} </div> </div> </div> </div> - {!!qualityGates && children} + {qualityGates != null && + React.Children.map(children, child => React.cloneElement(child, { organization }))} </div> ); } } - -QualityGatesApp.contextTypes = { - router: React.PropTypes.object.isRequired -}; diff --git a/server/sonar-web/src/main/js/helpers/__tests__/urls-test.js b/server/sonar-web/src/main/js/helpers/__tests__/urls-test.js index c2047a572a4..0f5fb66e39a 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/urls-test.js +++ b/server/sonar-web/src/main/js/helpers/__tests__/urls-test.js @@ -17,7 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { getComponentUrl, getComponentIssuesUrl, getComponentDrilldownUrl } from '../urls'; +import { + getComponentUrl, + getComponentIssuesUrl, + getComponentDrilldownUrl, + getQualityGatesUrl, + getQualityGateUrl +} from '../urls'; const SIMPLE_COMPONENT_KEY = 'sonarqube'; const COMPLEX_COMPONENT_KEY = 'org.sonarsource.sonarqube:sonarqube'; @@ -77,10 +83,30 @@ describe('#getComponentDrilldownUrl', () => { }); }); - it('should encode component key', () => { + it('should not encode component key', () => { expect(getComponentDrilldownUrl(COMPLEX_COMPONENT_KEY, METRIC)).toEqual({ pathname: '/component_measures/metric/' + METRIC, query: { id: COMPLEX_COMPONENT_KEY } }); }); }); + +describe('#getQualityGate(s)Url', () => { + it('should take organization key into account', () => { + expect(getQualityGatesUrl()).toEqual({ pathname: '/quality_gates' }); + expect(getQualityGatesUrl('foo')).toEqual({ pathname: '/organizations/foo/quality_gates' }); + expect(getQualityGateUrl('bar')).toEqual({ pathname: '/quality_gates/show/bar' }); + expect(getQualityGateUrl('bar', 'foo')).toEqual({ + pathname: '/organizations/foo/quality_gates/show/bar' + }); + }); + + it('should encode keys', () => { + expect(getQualityGatesUrl(COMPLEX_COMPONENT_KEY)).toEqual({ + pathname: '/organizations/' + COMPLEX_COMPONENT_KEY_ENCODED + '/quality_gates' + }); + expect(getQualityGateUrl(COMPLEX_COMPONENT_KEY)).toEqual({ + pathname: '/quality_gates/show/' + COMPLEX_COMPONENT_KEY_ENCODED + }); + }); +}); diff --git a/server/sonar-web/src/main/js/helpers/urls.js b/server/sonar-web/src/main/js/helpers/urls.js index 07103e12255..27c2ec6ee6c 100644 --- a/server/sonar-web/src/main/js/helpers/urls.js +++ b/server/sonar-web/src/main/js/helpers/urls.js @@ -100,16 +100,14 @@ export function getQualityProfileUrl(name, language, organization) { return getProfilePath(name, language, organization); } -/** - * Generate URL for a quality gate - * @param {string} key - * @returns {Object} - */ -export function getQualityGateUrl(key) { - return { - pathname: '/quality_gates/show/' + encodeURIComponent(key) - }; -} +export const getQualityGateUrl = (key: string, organization?: string) => ({ + pathname: getQualityGatesUrl(organization).pathname + '/show/' + encodeURIComponent(key) +}); + +export const getQualityGatesUrl = (organization?: string) => ({ + pathname: + (organization ? '/organizations/' + encodeURIComponent(organization) : '') + '/quality_gates' +}); /** * Generate URL for the rules page diff --git a/tests/src/test/java/org/sonarqube/pageobjects/Navigation.java b/tests/src/test/java/org/sonarqube/pageobjects/Navigation.java index 6bdf9be6156..b71307e1587 100644 --- a/tests/src/test/java/org/sonarqube/pageobjects/Navigation.java +++ b/tests/src/test/java/org/sonarqube/pageobjects/Navigation.java @@ -31,7 +31,6 @@ import javax.annotation.Nullable; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.html5.WebStorage; -import org.sonarqube.pageobjects.issues.Issue; import org.sonarqube.tests.Tester; import org.sonarqube.pageobjects.issues.IssuesPage; import org.sonarqube.pageobjects.licenses.LicensesPage; @@ -109,6 +108,16 @@ public class Navigation { return open(url, ProjectLinksPage.class); } + public QualityGatePage openQualityGates() { + String url = "/quality_gates"; + return open(url, QualityGatePage.class); + } + + public QualityGatePage openQualityGates(String organization) { + String url = "/organizations/" + organization + "/quality_gates"; + return open(url, QualityGatePage.class); + } + public ProjectQualityGatePage openProjectQualityGate(String projectKey) { // TODO encode projectKey String url = "/project/quality_gate?id=" + projectKey; diff --git a/tests/src/test/java/org/sonarqube/pageobjects/ProjectDashboardPage.java b/tests/src/test/java/org/sonarqube/pageobjects/ProjectDashboardPage.java index 60969c43fa5..673e037c193 100644 --- a/tests/src/test/java/org/sonarqube/pageobjects/ProjectDashboardPage.java +++ b/tests/src/test/java/org/sonarqube/pageobjects/ProjectDashboardPage.java @@ -22,6 +22,7 @@ package org.sonarqube.pageobjects; import com.codeborne.selenide.ElementsCollection; import com.codeborne.selenide.SelenideElement; import java.util.Arrays; +import org.openqa.selenium.By; import static com.codeborne.selenide.Condition.exist; import static com.codeborne.selenide.Condition.hasText; @@ -29,6 +30,7 @@ import static com.codeborne.selenide.Condition.text; import static com.codeborne.selenide.Condition.visible; import static com.codeborne.selenide.Selenide.$; import static com.codeborne.selenide.Selenide.$$; +import static org.assertj.core.api.Assertions.assertThat; public class ProjectDashboardPage { @@ -97,4 +99,11 @@ public class ProjectDashboardPage { tagsInput.sendKeys(charSequences); return this; } + + public ProjectDashboardPage hasQualityGateLink(String name, String link) { + SelenideElement elem = $(".overview-meta-header").should(exist) + .parent().find(By.linkText(name)).should(exist); + assertThat(elem.attr("href")).endsWith(link); + return this; + } } diff --git a/tests/src/test/java/org/sonarqube/pageobjects/QualityGatePage.java b/tests/src/test/java/org/sonarqube/pageobjects/QualityGatePage.java new file mode 100644 index 00000000000..3d7162f898d --- /dev/null +++ b/tests/src/test/java/org/sonarqube/pageobjects/QualityGatePage.java @@ -0,0 +1,46 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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. + */ +package org.sonarqube.pageobjects; + +import com.codeborne.selenide.Condition; + +import static com.codeborne.selenide.Selenide.$; +import static com.codeborne.selenide.Selenide.$$; + +public class QualityGatePage { + public QualityGatePage() { + $(".quality-gates-results").shouldBe(Condition.visible); + } + + public QualityGatePage countQualityGates(Integer count) { + $$(".quality-gates-results .list-group-item").shouldHaveSize(count); + return this; + } + + public QualityGatePage canCreateQG() { + $("#quality-gate-add").should(Condition.exist).shouldBe(Condition.visible); + return this; + } + + public QualityGatePage canNotCreateQG() { + $("#quality-gate-add").shouldNot(Condition.exist); + return this; + } +} diff --git a/tests/src/test/java/org/sonarqube/tests/Category6Suite.java b/tests/src/test/java/org/sonarqube/tests/Category6Suite.java index 69ea5870d25..206b264c24c 100644 --- a/tests/src/test/java/org/sonarqube/tests/Category6Suite.java +++ b/tests/src/test/java/org/sonarqube/tests/Category6Suite.java @@ -40,6 +40,7 @@ import org.sonarqube.tests.projectAdministration.ProjectKeyUpdateTest; import org.sonarqube.tests.projectAdministration.ProjectProvisioningTest; import org.sonarqube.tests.projectSearch.LeakProjectsPageTest; import org.sonarqube.tests.projectSearch.SearchProjectsTest; +import org.sonarqube.tests.qualityGate.OrganizationQualityGateUiTest; import org.sonarqube.tests.qualityProfile.BuiltInQualityProfilesTest; import org.sonarqube.tests.qualityProfile.CustomQualityProfilesTest; import org.sonarqube.tests.qualityProfile.OrganizationQualityProfilesUiTest; @@ -61,6 +62,7 @@ import static util.ItUtils.xooPlugin; OrganizationIssuesPageTest.class, OrganizationMembershipTest.class, OrganizationMembershipUiTest.class, + OrganizationQualityGateUiTest.class, OrganizationQualityProfilesUiTest.class, OrganizationTest.class, RootUserOnOrganizationTest.class, diff --git a/tests/src/test/java/org/sonarqube/tests/qualityGate/OrganizationQualityGateUiTest.java b/tests/src/test/java/org/sonarqube/tests/qualityGate/OrganizationQualityGateUiTest.java new file mode 100644 index 00000000000..01ab1189d87 --- /dev/null +++ b/tests/src/test/java/org/sonarqube/tests/qualityGate/OrganizationQualityGateUiTest.java @@ -0,0 +1,113 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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. + */ +package org.sonarqube.tests.qualityGate; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.sonar.orchestrator.Orchestrator; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.openqa.selenium.By; +import org.sonarqube.pageobjects.ProjectDashboardPage; +import org.sonarqube.pageobjects.QualityGatePage; +import org.sonarqube.tests.Category6Suite; +import org.sonarqube.tests.Tester; +import org.sonarqube.ws.Organizations; +import org.sonarqube.ws.WsUsers; +import util.issue.IssueRule; + +import static com.codeborne.selenide.Selenide.$; +import static util.ItUtils.restoreProfile; +import static util.ItUtils.runProjectAnalysis; +import static org.assertj.core.api.Assertions.assertThat; + +public class OrganizationQualityGateUiTest { + @ClassRule + public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR; + + @Rule + public Tester tester = new Tester(orchestrator); + + @Rule + public IssueRule issueRule = IssueRule.from(orchestrator); + + private Organizations.Organization organization; + private WsUsers.CreateWsResponse.User user; + + @Before + public void setUp() throws Exception { + organization = tester.organizations().generate(); + user = tester.users().generate(); + tester.organizations().addMember(organization, user); + restoreProfile(orchestrator, getClass().getResource("/issue/with-many-rules.xml"), organization.getKey()); + } + + @Test + public void should_have_a_link_to_quality_gates() { + tester.openBrowser() + .logIn().submitCredentials(user.getLogin()) + .openQualityGates(organization.getKey()); + + SelenideElement element = $(".navbar-context .navbar-nav") + .find(By.linkText("Quality Gates")) + .should(Condition.exist); + assertThat(element.attr("href")).endsWith("/organizations/" + organization.getKey() + "/quality_gates"); + } + + @Test + public void should_display_available_quality_gates() { + QualityGatePage page = tester.openBrowser() + .logIn().submitCredentials(user.getLogin()) + .openQualityGates(organization.getKey()); + page.countQualityGates(1); + } + + @Test + public void should_not_allow_random_user_to_create() { + tester.openBrowser() + .logIn().submitCredentials(user.getLogin()) + .openQualityGates(organization.getKey()) + .canNotCreateQG(); + tester.openBrowser() + .logIn().submitCredentials("admin") + .openQualityGates(organization.getKey()) + .canCreateQG(); + } + + @Test + public void quality_gate_link_on_project_dashboard_should_have_organization_context() { + String project = tester.projects().generate(organization).getKey(); + runProjectAnalysis(orchestrator, "shared/xoo-multi-modules-sample", + "sonar.projectKey", project, + "sonar.organization", organization.getKey(), + "sonar.login", "admin", + "sonar.password", "admin", + "sonar.scm.disabled", "false", + "sonar.scm.provider", "xoo"); + + String link = "/organizations/" + organization.getKey() + "/quality_gates/show/1"; + ProjectDashboardPage page = tester.openBrowser() + .logIn().submitCredentials(user.getLogin()) + .openProjectDashboard(project); + page.hasQualityGateLink("SonarQube way", link); + } +} diff --git a/tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateUiTest.java b/tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateUiTest.java index 4404dc7ca80..acb207f5271 100644 --- a/tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateUiTest.java +++ b/tests/src/test/java/org/sonarqube/tests/qualityGate/QualityGateUiTest.java @@ -19,8 +19,12 @@ */ package org.sonarqube.tests.qualityGate; +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; import com.sonar.orchestrator.Orchestrator; import com.sonar.orchestrator.build.SonarScanner; +import org.junit.Rule; +import org.openqa.selenium.By; import org.sonarqube.tests.Category1Suite; import java.util.Date; import javax.annotation.Nullable; @@ -37,8 +41,11 @@ import org.sonar.wsclient.qualitygate.QualityGateCondition; import org.sonar.wsclient.qualitygate.UpdateCondition; import org.sonarqube.pageobjects.Navigation; import org.sonarqube.pageobjects.ProjectActivityPage; +import org.sonarqube.tests.Tester; +import static com.codeborne.selenide.Selenide.$; import static org.apache.commons.lang.time.DateUtils.addDays; +import static org.assertj.core.api.Assertions.assertThat; import static util.ItUtils.projectDir; import static util.ItUtils.resetPeriod; import static util.ItUtils.setServerProperty; @@ -49,6 +56,9 @@ public class QualityGateUiTest { @ClassRule public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR; + @Rule + public Tester tester = new Tester(orchestrator); + private static long DEFAULT_QUALITY_GATE; @BeforeClass @@ -102,6 +112,32 @@ public class QualityGateUiTest { runSelenese(orchestrator, "/qualityGate/QualityGateUiTest/should_display_quality_gates_page.html"); } + @Test + public void should_have_a_global_link_to_quality_gates() { + String login = tester.users().generate().getLogin(); + tester.openBrowser() + .logIn().submitCredentials(login) + .openQualityGates(); + + SelenideElement element = $(".navbar-global .navbar-nav") + .find(By.linkText("Quality Gates")) + .should(Condition.exist); + assertThat(element.attr("href")).endsWith("/quality_gates"); + } + + @Test + public void should_not_allow_random_user_to_create() { + String login = tester.users().generate().getLogin(); + tester.openBrowser() + .logIn().submitCredentials(login) + .openQualityGates() + .canNotCreateQG(); + tester.openBrowser() + .logIn().submitCredentials("admin") + .openQualityGates() + .canCreateQG(); + } + private void scanSampleWithDate(String date) { scanSample(date, null); } |