aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorWouter Admiraal <wouter.admiraal@sonarsource.com>2020-01-03 09:29:51 +0100
committerSonarTech <sonartech@sonarsource.com>2020-01-06 20:46:13 +0100
commitccfd204b05b7e8b8c47273e5db78e9bd22128d60 (patch)
tree56c5adb3f0108f10d86f8169bb5375b2b8374756 /server
parente5d9b437e9ba5bbe933ee068dd31d7c716824428 (diff)
downloadsonarqube-ccfd204b05b7e8b8c47273e5db78e9bd22128d60.tar.gz
sonarqube-ccfd204b05b7e8b8c47273e5db78e9bd22128d60.zip
Improve test coverage
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx12
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx48
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectAdminPageExtension-test.tsx.snap51
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx20
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/BulkChangeModal-test.tsx108
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetails-test.tsx137
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleListItem-test.tsx62
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap253
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap18
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleListItem-test.tsx.snap145
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx84
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/Details-test.tsx131
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/DetailsContent-test.tsx (renamed from server/sonar-web/src/main/js/apps/sessions/components/LoginSonarCloud.css)64
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/DetailsHeader-test.tsx88
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/Details-test.tsx.snap53
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/DetailsContent-test.tsx.snap83
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/DetailsHeader-test.tsx.snap112
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsForm-test.tsx123
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsForm-test.tsx.snap129
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx177
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/ProfilesListRow-test.tsx47
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/ProfilesList-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/ProfilesListRow-test.tsx.snap465
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx27
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/LoginSonarCloud.tsx109
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/LoginContainer-test.tsx66
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/LoginSonarCloud-test.tsx64
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/LoginContainer-test.tsx.snap18
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/LoginSonarCloud-test.tsx.snap170
-rw-r--r--server/sonar-web/src/main/js/helpers/testMocks.ts21
-rw-r--r--server/sonar-web/src/main/js/store/__tests__/users-test.tsx119
-rw-r--r--server/sonar-web/src/main/js/store/users.ts6
34 files changed, 2225 insertions, 828 deletions
diff --git a/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx b/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx
index 323c45e2b3f..f90203d31a9 100644
--- a/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx
+++ b/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx
@@ -24,18 +24,22 @@ import { addGlobalErrorMessage } from '../../../store/globalMessages';
import NotFound from '../NotFound';
import Extension from './Extension';
-interface Props {
+export interface ProjectAdminPageExtensionProps {
component: T.Component;
location: Location;
params: { extensionKey: string; pluginKey: string };
}
-function ProjectAdminPageExtension(props: Props) {
- const { extensionKey, pluginKey } = props.params;
- const { component } = props;
+export function ProjectAdminPageExtension(props: ProjectAdminPageExtensionProps) {
+ const {
+ component,
+ params: { extensionKey, pluginKey }
+ } = props;
+
const extension =
component.configuration &&
(component.configuration.extensions || []).find(p => p.key === `${pluginKey}/${extensionKey}`);
+
return extension ? (
<Extension extension={extension} options={{ component }} />
) : (
diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx b/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx
new file mode 100644
index 00000000000..2ebfbd31649
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { mockComponent, mockLocation } from '../../../../helpers/testMocks';
+import {
+ ProjectAdminPageExtension,
+ ProjectAdminPageExtensionProps
+} from '../ProjectAdminPageExtension';
+
+it('should render correctly', () => {
+ expect(
+ shallowRender({
+ component: mockComponent({
+ configuration: { extensions: [{ key: 'foo/bar', name: 'Foo Bar' }] }
+ })
+ })
+ ).toMatchSnapshot('extension exists');
+ expect(shallowRender()).toMatchSnapshot('extension not found');
+});
+
+function shallowRender(props: Partial<ProjectAdminPageExtensionProps> = {}) {
+ return shallow(
+ <ProjectAdminPageExtension
+ component={mockComponent()}
+ location={mockLocation()}
+ params={{ extensionKey: 'bar', pluginKey: 'foo' }}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectAdminPageExtension-test.tsx.snap b/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectAdminPageExtension-test.tsx.snap
new file mode 100644
index 00000000000..a96ce322b6c
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/ProjectAdminPageExtension-test.tsx.snap
@@ -0,0 +1,51 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: extension exists 1`] = `
+<InjectIntl(withRouter(Connect(Extension)))
+ extension={
+ Object {
+ "key": "foo/bar",
+ "name": "Foo Bar",
+ }
+ }
+ options={
+ Object {
+ "component": Object {
+ "breadcrumbs": Array [],
+ "configuration": Object {
+ "extensions": Array [
+ Object {
+ "key": "foo/bar",
+ "name": "Foo Bar",
+ },
+ ],
+ },
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ },
+ }
+ }
+/>
+`;
+
+exports[`should render correctly: extension not found 1`] = `
+<NotFound
+ withContainer={false}
+/>
+`;
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx
index 288c42c1b93..b966ff8d361 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx
@@ -194,10 +194,15 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
render() {
const { action, profile, total } = this.props;
const header =
- // prettier-ignore
action === 'activate'
- ? `${translate('coding_rules.activate_in_quality_profile')} (${formatMeasure(total, 'INT')} ${translate('coding_rules._rules')})`
- : `${translate('coding_rules.deactivate_in_quality_profile')} (${formatMeasure(total, 'INT')} ${translate('coding_rules._rules')})`;
+ ? `${translate('coding_rules.activate_in_quality_profile')} (${formatMeasure(
+ total,
+ 'INT'
+ )} ${translate('coding_rules._rules')})`
+ : `${translate('coding_rules.deactivate_in_quality_profile')} (${formatMeasure(
+ total,
+ 'INT'
+ )} ${translate('coding_rules._rules')})`;
return (
<Modal contentLabel={header} onRequestClose={this.props.onClose} size="small">
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx
index 433d6cfa15f..cbc908598c4 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx
@@ -76,8 +76,8 @@ export default class RuleDetails extends React.PureComponent<Props, State> {
this.mounted = false;
}
- fetchRuleDetails = () =>
- getRuleDetails({
+ fetchRuleDetails = () => {
+ return getRuleDetails({
actives: true,
key: this.props.ruleKey,
organization: this.props.organization
@@ -93,6 +93,7 @@ export default class RuleDetails extends React.PureComponent<Props, State> {
}
}
);
+ };
handleRuleChange = (ruleDetails: T.RuleDetails) => {
if (this.mounted) {
@@ -119,8 +120,8 @@ export default class RuleDetails extends React.PureComponent<Props, State> {
});
};
- handleActivate = () =>
- this.fetchRuleDetails().then(() => {
+ handleActivate = () => {
+ return this.fetchRuleDetails().then(() => {
const { ruleKey, selectedProfile } = this.props;
if (selectedProfile && this.state.actives) {
const active = this.state.actives.find(active => active.qProfile === selectedProfile.key);
@@ -129,9 +130,10 @@ export default class RuleDetails extends React.PureComponent<Props, State> {
}
}
});
+ };
- handleDeactivate = () =>
- this.fetchRuleDetails().then(() => {
+ handleDeactivate = () => {
+ return this.fetchRuleDetails().then(() => {
const { ruleKey, selectedProfile } = this.props;
if (
selectedProfile &&
@@ -141,11 +143,13 @@ export default class RuleDetails extends React.PureComponent<Props, State> {
this.props.onDeactivate(selectedProfile.key, ruleKey);
}
});
+ };
- handleDelete = () =>
- deleteRule({ key: this.props.ruleKey, organization: this.props.organization }).then(() =>
+ handleDelete = () => {
+ return deleteRule({ key: this.props.ruleKey, organization: this.props.organization }).then(() =>
this.props.onDelete(this.props.ruleKey)
);
+ };
render() {
const { ruleDetails } = this.state;
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/BulkChangeModal-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/BulkChangeModal-test.tsx
index 0f7fde8e95a..5860828c892 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/BulkChangeModal-test.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/BulkChangeModal-test.tsx
@@ -19,23 +19,101 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
-import { mockQualityProfile } from '../../../../helpers/testMocks';
+import { submit, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { bulkActivateRules, bulkDeactivateRules } from '../../../../api/quality-profiles';
+import { mockLanguage, mockQualityProfile } from '../../../../helpers/testMocks';
import { Query } from '../../query';
import BulkChangeModal from '../BulkChangeModal';
-it('render correctly', () => {
+jest.mock('../../../../api/quality-profiles', () => ({
+ bulkActivateRules: jest.fn().mockResolvedValue({ failed: 0, succeeded: 2 }),
+ bulkDeactivateRules: jest.fn().mockResolvedValue({ failed: 2, succeeded: 0 })
+}));
+
+beforeEach(jest.clearAllMocks);
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('default');
+ expect(shallowRender({ profile: undefined })).toMatchSnapshot('no profile pre-selected');
+ expect(shallowRender({ action: 'deactivate' })).toMatchSnapshot('deactivate action');
expect(
- shallow(
- <BulkChangeModal
- action="activate"
- languages={{ js: { key: 'js', name: 'JavaScript' } }}
- onClose={jest.fn()}
- organization="foo"
- profile={mockQualityProfile()}
- query={{ languages: ['js'] } as Query}
- referencedProfiles={{ foo: mockQualityProfile() }}
- total={42}
- />
- )
- ).toMatchSnapshot();
+ shallowRender().setState({
+ results: [
+ { failed: 2, profile: 'foo', succeeded: 0 },
+ { failed: 0, profile: 'bar', succeeded: 2 }
+ ]
+ })
+ ).toMatchSnapshot('results');
+ expect(shallowRender().setState({ submitting: true })).toMatchSnapshot('submitting');
+ expect(shallowRender().setState({ finished: true })).toMatchSnapshot('finished');
+});
+
+it('should pre-select a profile if only 1 is available', () => {
+ const profile = mockQualityProfile({
+ actions: { edit: true },
+ isBuiltIn: false,
+ key: 'foo',
+ language: 'js'
+ });
+ const wrapper = shallowRender({ profile: undefined, referencedProfiles: { foo: profile } });
+ expect(wrapper.state().selectedProfiles).toEqual(['foo']);
});
+
+it('should handle profile selection', () => {
+ const wrapper = shallowRender();
+ wrapper.instance().handleProfileSelect([{ value: 'foo' }, { value: 'bar' }]);
+ expect(wrapper.state().selectedProfiles).toEqual(['foo', 'bar']);
+});
+
+it('should handle form submission', async () => {
+ const wrapper = shallowRender({ profile: undefined });
+ wrapper.setState({ selectedProfiles: ['foo', 'bar'] });
+
+ // Activate.
+ submit(wrapper.find('form'));
+ await waitAndUpdate(wrapper);
+ expect(bulkActivateRules).toBeCalledWith(expect.objectContaining({ targetKey: 'foo' }));
+
+ await waitAndUpdate(wrapper);
+ expect(bulkActivateRules).toBeCalledWith(expect.objectContaining({ targetKey: 'bar' }));
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state().results).toEqual([
+ { failed: 0, profile: 'foo', succeeded: 2 },
+ { failed: 0, profile: 'bar', succeeded: 2 }
+ ]);
+
+ // Deactivate.
+ wrapper.setProps({ action: 'deactivate' }).setState({ results: [] });
+ submit(wrapper.find('form'));
+ await waitAndUpdate(wrapper);
+ expect(bulkDeactivateRules).toBeCalledWith(expect.objectContaining({ targetKey: 'foo' }));
+
+ await waitAndUpdate(wrapper);
+ expect(bulkDeactivateRules).toBeCalledWith(expect.objectContaining({ targetKey: 'bar' }));
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state().results).toEqual([
+ { failed: 2, profile: 'foo', succeeded: 0 },
+ { failed: 2, profile: 'bar', succeeded: 0 }
+ ]);
+});
+
+function shallowRender(props: Partial<BulkChangeModal['props']> = {}) {
+ return shallow<BulkChangeModal>(
+ <BulkChangeModal
+ action="activate"
+ languages={{ js: mockLanguage() }}
+ onClose={jest.fn()}
+ organization={undefined}
+ profile={mockQualityProfile()}
+ query={{ languages: ['js'] } as Query}
+ referencedProfiles={{
+ foo: mockQualityProfile({ key: 'foo' }),
+ bar: mockQualityProfile({ key: 'bar' })
+ }}
+ total={42}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetails-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetails-test.tsx
index c721cedf1ab..ffe585f483c 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetails-test.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetails-test.tsx
@@ -17,32 +17,34 @@
* 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 sonarjs/no-duplicate-string */
import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
-import { updateRule } from '../../../../api/rules';
+import { deleteRule, getRuleDetails, updateRule } from '../../../../api/rules';
+import { mockQualityProfile } from '../../../../helpers/testMocks';
import RuleDetails from '../RuleDetails';
-jest.mock('../../../../api/rules', () => ({
- deleteRule: jest.fn(),
- getRuleDetails: jest.fn().mockResolvedValue({
- rule: getMockHelpers().mockRuleDetails(),
- actives: [
- {
- qProfile: 'key',
- inherit: 'NONE',
- severity: 'MAJOR',
- params: [],
- createdAt: '2017-06-16T16:13:38+0200',
- updatedAt: '2017-06-16T16:13:38+0200'
- }
- ]
- }),
- updateRule: jest.fn().mockResolvedValue({})
-}));
-
-const { mockQualityProfile } = getMockHelpers();
-const profile = mockQualityProfile();
+jest.mock('../../../../api/rules', () => {
+ const { mockRuleDetails } = jest.requireActual('../../../../helpers/testMocks');
+ return {
+ deleteRule: jest.fn().mockResolvedValue(null),
+ getRuleDetails: jest.fn().mockResolvedValue({
+ rule: mockRuleDetails(),
+ actives: [
+ {
+ qProfile: 'foo',
+ inherit: 'NONE',
+ severity: 'MAJOR',
+ params: [],
+ createdAt: '2017-06-16T16:13:38+0200',
+ updatedAt: '2017-06-16T16:13:38+0200'
+ }
+ ]
+ }),
+ updateRule: jest.fn().mockResolvedValue(null)
+ };
+});
beforeEach(() => {
jest.clearAllMocks();
@@ -50,18 +52,43 @@ beforeEach(() => {
it('should render correctly', async () => {
const wrapper = shallowRender();
- expect(wrapper).toMatchSnapshot();
+ expect(wrapper).toMatchSnapshot('loading');
+
await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
+ expect(wrapper).toMatchSnapshot('loaded');
+
+ expect(getRuleDetails).toBeCalledWith(
+ expect.objectContaining({
+ actives: true,
+ key: 'squid:S1337'
+ })
+ );
+});
+
+it('should correctly handle prop changes', async () => {
+ const ruleKey = 'foo:bar';
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+ jest.clearAllMocks();
+
+ wrapper.setProps({ ruleKey });
+ expect(getRuleDetails).toBeCalledWith(
+ expect.objectContaining({
+ actives: true,
+ key: ruleKey
+ })
+ );
});
it('should correctly handle tag changes', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
+
wrapper.instance().handleTagsChange(['foo', 'bar']);
const ruleDetails = wrapper.state('ruleDetails');
expect(ruleDetails && ruleDetails.tags).toEqual(['foo', 'bar']);
await waitAndUpdate(wrapper);
+
expect(updateRule).toHaveBeenCalledWith({
key: 'squid:S1337',
organization: undefined,
@@ -69,16 +96,64 @@ it('should correctly handle tag changes', async () => {
});
});
-function getMockHelpers() {
- // We use this little "force-requiring" instead of an import statement in
- // order to prevent a hoisting race condition while mocking. If we want to use
- // a mock helper in a Jest mock, we have to require it like this. Otherwise,
- // we get errors like:
- // ReferenceError: testMocks_1 is not defined
- return require.requireActual('../../../../helpers/testMocks');
-}
+it('should correctly handle rule changes', () => {
+ const wrapper = shallowRender();
+ const ruleChange = {
+ createdAt: '2019-02-01',
+ key: 'foo',
+ name: 'Foo',
+ repo: 'bar',
+ severity: 'MAJOR',
+ status: 'READY',
+ type: 'BUG' as T.RuleType
+ };
+
+ wrapper.instance().handleRuleChange(ruleChange);
+ expect(wrapper.state().ruleDetails).toBe(ruleChange);
+});
+
+it('should correctly handle activation', async () => {
+ const onActivate = jest.fn();
+ const wrapper = shallowRender({ onActivate });
+ await waitAndUpdate(wrapper);
+
+ wrapper.instance().handleActivate();
+ await waitAndUpdate(wrapper);
+ expect(onActivate).toBeCalledWith(
+ 'foo',
+ 'squid:S1337',
+ expect.objectContaining({
+ inherit: 'NONE',
+ severity: 'MAJOR'
+ })
+ );
+});
+
+it('should correctly handle deactivation', async () => {
+ const onDeactivate = jest.fn();
+ const selectedProfile = mockQualityProfile({ key: 'bar' });
+ const wrapper = shallowRender({ onDeactivate, selectedProfile });
+ await waitAndUpdate(wrapper);
+
+ wrapper.instance().handleDeactivate();
+ await waitAndUpdate(wrapper);
+ expect(onDeactivate).toBeCalledWith(selectedProfile.key, 'squid:S1337');
+});
+
+it('should correctly handle deletion', async () => {
+ const onDelete = jest.fn();
+ const wrapper = shallowRender({ onDelete });
+ await waitAndUpdate(wrapper);
+
+ wrapper.instance().handleDelete();
+ await waitAndUpdate(wrapper);
+ expect(deleteRule).toBeCalledWith(expect.objectContaining({ key: 'squid:S1337' }));
+ expect(onDelete).toBeCalledWith('squid:S1337');
+});
function shallowRender(props: Partial<RuleDetails['props']> = {}) {
+ const profile = mockQualityProfile({ key: 'foo' });
+
return shallow<RuleDetails>(
<RuleDetails
onActivate={jest.fn()}
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleListItem-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleListItem-test.tsx
index 7df50122cb7..9bf9bf8fa7b 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleListItem-test.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleListItem-test.tsx
@@ -19,26 +19,68 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
+import { Link } from 'react-router';
+import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { deactivateRule } from '../../../../api/quality-profiles';
import { mockEvent, mockQualityProfile, mockRule } from '../../../../helpers/testMocks';
import RuleListItem from '../RuleListItem';
-it('should render', () => {
- expect(shallowRender()).toMatchSnapshot();
+jest.mock('../../../../api/quality-profiles', () => ({
+ deactivateRule: jest.fn().mockResolvedValue(null)
+}));
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('default');
+ expect(shallowRender({})).toMatchSnapshot('with activation');
});
it('should open rule', () => {
const onOpen = jest.fn();
const wrapper = shallowRender({ onOpen });
- wrapper.find('Link').prop<Function>('onClick')(mockEvent({ button: 0 }));
+ wrapper.find(Link).simulate('click', mockEvent({ button: 0 }));
expect(onOpen).toBeCalledWith('javascript:S1067');
});
-it('should render deactivate button', () => {
- const wrapper = shallowRender();
- const instance = wrapper.instance();
+it('handle activation', () => {
+ const profile = mockQualityProfile();
+ const rule = mockRule();
+ const onActivate = jest.fn();
+ const wrapper = shallowRender({ onActivate, rule, selectedProfile: profile });
- expect(instance.renderDeactivateButton('NONE')).toMatchSnapshot();
- expect(instance.renderDeactivateButton('', 'coding_rules.need_extend_or_copy')).toMatchSnapshot();
+ wrapper.instance().handleActivate('MAJOR');
+ expect(onActivate).toBeCalledWith(profile.key, rule.key, {
+ severity: 'MAJOR',
+ inherit: 'NONE'
+ });
+});
+
+it('handle deactivation', async () => {
+ const profile = mockQualityProfile();
+ const rule = mockRule();
+ const onDeactivate = jest.fn();
+ const wrapper = shallowRender({ onDeactivate, rule, selectedProfile: profile });
+
+ wrapper.instance().handleDeactivate();
+ expect(deactivateRule).toBeCalledWith(
+ expect.objectContaining({
+ key: profile.key,
+ rule: rule.key
+ })
+ );
+ await waitAndUpdate(wrapper);
+ expect(onDeactivate).toBeCalledWith(profile.key, rule.key);
+});
+
+describe('#renderDeactivateButton', () => {
+ it('should render correctly', () => {
+ const wrapper = shallowRender();
+ const instance = wrapper.instance();
+
+ expect(instance.renderDeactivateButton('NONE')).toMatchSnapshot();
+ expect(
+ instance.renderDeactivateButton('', 'coding_rules.need_extend_or_copy')
+ ).toMatchSnapshot();
+ });
});
describe('renderActions', () => {
@@ -123,8 +165,8 @@ function shallowRender(props?: Partial<RuleListItem['props']>) {
onDeactivate={jest.fn()}
onFilterChange={jest.fn()}
onOpen={jest.fn()}
- organization="org"
- rule={mockRule()}
+ organization={undefined}
+ rule={mockRule({ key: 'javascript:S1067' })}
selected={false}
{...props}
/>
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap
index fed86bf741a..3dfb406d2a0 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap
@@ -1,6 +1,204 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`render correctly 1`] = `
+exports[`should render correctly: deactivate action 1`] = `
+<Modal
+ contentLabel="coding_rules.deactivate_in_quality_profile (42 coding_rules._rules)"
+ onRequestClose={[MockFunction]}
+ size="small"
+>
+ <form
+ onSubmit={[Function]}
+ >
+ <header
+ className="modal-head"
+ >
+ <h2>
+ coding_rules.deactivate_in_quality_profile (42 coding_rules._rules)
+ </h2>
+ </header>
+ <div
+ className="modal-body"
+ >
+ <div
+ className="modal-field"
+ >
+ <h3>
+ <label
+ htmlFor="coding-rules-bulk-change-profile"
+ >
+ coding_rules.deactivate_in
+ </label>
+ </h3>
+ <span>
+ name
+ —
+ are_you_sure
+ </span>
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <SubmitButton
+ disabled={false}
+ id="coding-rules-submit-bulk-change"
+ >
+ apply
+ </SubmitButton>
+ <ResetButtonLink
+ onClick={[MockFunction]}
+ >
+ cancel
+ </ResetButtonLink>
+ </footer>
+ </form>
+</Modal>
+`;
+
+exports[`should render correctly: default 1`] = `
+<Modal
+ contentLabel="coding_rules.activate_in_quality_profile (42 coding_rules._rules)"
+ onRequestClose={[MockFunction]}
+ size="small"
+>
+ <form
+ onSubmit={[Function]}
+ >
+ <header
+ className="modal-head"
+ >
+ <h2>
+ coding_rules.activate_in_quality_profile (42 coding_rules._rules)
+ </h2>
+ </header>
+ <div
+ className="modal-body"
+ >
+ <div
+ className="modal-field"
+ >
+ <h3>
+ <label
+ htmlFor="coding-rules-bulk-change-profile"
+ >
+ coding_rules.activate_in
+ </label>
+ </h3>
+ <span>
+ name
+ —
+ are_you_sure
+ </span>
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <SubmitButton
+ disabled={false}
+ id="coding-rules-submit-bulk-change"
+ >
+ apply
+ </SubmitButton>
+ <ResetButtonLink
+ onClick={[MockFunction]}
+ >
+ cancel
+ </ResetButtonLink>
+ </footer>
+ </form>
+</Modal>
+`;
+
+exports[`should render correctly: finished 1`] = `
+<Modal
+ contentLabel="coding_rules.activate_in_quality_profile (42 coding_rules._rules)"
+ onRequestClose={[MockFunction]}
+ size="small"
+>
+ <form
+ onSubmit={[Function]}
+ >
+ <header
+ className="modal-head"
+ >
+ <h2>
+ coding_rules.activate_in_quality_profile (42 coding_rules._rules)
+ </h2>
+ </header>
+ <div
+ className="modal-body"
+ />
+ <footer
+ className="modal-foot"
+ >
+ <ResetButtonLink
+ onClick={[MockFunction]}
+ >
+ close
+ </ResetButtonLink>
+ </footer>
+ </form>
+</Modal>
+`;
+
+exports[`should render correctly: no profile pre-selected 1`] = `
+<Modal
+ contentLabel="coding_rules.activate_in_quality_profile (42 coding_rules._rules)"
+ onRequestClose={[MockFunction]}
+ size="small"
+>
+ <form
+ onSubmit={[Function]}
+ >
+ <header
+ className="modal-head"
+ >
+ <h2>
+ coding_rules.activate_in_quality_profile (42 coding_rules._rules)
+ </h2>
+ </header>
+ <div
+ className="modal-body"
+ >
+ <div
+ className="modal-field"
+ >
+ <h3>
+ <label
+ htmlFor="coding-rules-bulk-change-profile"
+ >
+ coding_rules.activate_in
+ </label>
+ </h3>
+ <Select
+ multi={true}
+ onChange={[Function]}
+ options={Array []}
+ value={Array []}
+ />
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <SubmitButton
+ disabled={false}
+ id="coding-rules-submit-bulk-change"
+ >
+ apply
+ </SubmitButton>
+ <ResetButtonLink
+ onClick={[MockFunction]}
+ >
+ cancel
+ </ResetButtonLink>
+ </footer>
+ </form>
+</Modal>
+`;
+
+exports[`should render correctly: results 1`] = `
<Modal
contentLabel="coding_rules.activate_in_quality_profile (42 coding_rules._rules)"
onRequestClose={[MockFunction]}
@@ -19,6 +217,18 @@ exports[`render correctly 1`] = `
<div
className="modal-body"
>
+ <Alert
+ key="foo"
+ variant="warning"
+ >
+ coding_rules.bulk_change.warning.name.CSS.0.2
+ </Alert>
+ <Alert
+ key="bar"
+ variant="success"
+ >
+ coding_rules.bulk_change.success.name.CSS.2
+ </Alert>
<div
className="modal-field"
>
@@ -54,3 +264,44 @@ exports[`render correctly 1`] = `
</form>
</Modal>
`;
+
+exports[`should render correctly: submitting 1`] = `
+<Modal
+ contentLabel="coding_rules.activate_in_quality_profile (42 coding_rules._rules)"
+ onRequestClose={[MockFunction]}
+ size="small"
+>
+ <form
+ onSubmit={[Function]}
+ >
+ <header
+ className="modal-head"
+ >
+ <h2>
+ coding_rules.activate_in_quality_profile (42 coding_rules._rules)
+ </h2>
+ </header>
+ <div
+ className="modal-body"
+ />
+ <footer
+ className="modal-foot"
+ >
+ <i
+ className="spinner spacer-right"
+ />
+ <SubmitButton
+ disabled={true}
+ id="coding-rules-submit-bulk-change"
+ >
+ apply
+ </SubmitButton>
+ <ResetButtonLink
+ onClick={[MockFunction]}
+ >
+ cancel
+ </ResetButtonLink>
+ </footer>
+ </form>
+</Modal>
+`;
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap
index 46c661b1a68..b4b3885833b 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap
@@ -1,12 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render correctly 1`] = `
-<div
- className="coding-rule-details"
-/>
-`;
-
-exports[`should render correctly 2`] = `
+exports[`should render correctly: loaded 1`] = `
<div
className="coding-rule-details"
>
@@ -103,7 +97,7 @@ exports[`should render correctly 2`] = `
"createdAt": "2017-06-16T16:13:38+0200",
"inherit": "NONE",
"params": Array [],
- "qProfile": "key",
+ "qProfile": "foo",
"severity": "MAJOR",
"updatedAt": "2017-06-16T16:13:38+0200",
},
@@ -121,7 +115,7 @@ exports[`should render correctly 2`] = `
"isBuiltIn": false,
"isDefault": false,
"isInherited": false,
- "key": "key",
+ "key": "foo",
"language": "js",
"languageName": "JavaScript",
"name": "name",
@@ -202,3 +196,9 @@ exports[`should render correctly 2`] = `
</DeferredSpinner>
</div>
`;
+
+exports[`should render correctly: loading 1`] = `
+<div
+ className="coding-rule-details"
+/>
+`;
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleListItem-test.tsx.snap
index a48a11f7ef0..2a91409ee8c 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleListItem-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleListItem-test.tsx.snap
@@ -1,5 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`#renderDeactivateButton should render correctly 1`] = `
+<ConfirmButton
+ confirmButtonText="yes"
+ modalBody="coding_rules.deactivate.confirm"
+ modalHeader="coding_rules.deactivate"
+ onConfirm={[Function]}
+>
+ [Function]
+</ConfirmButton>
+`;
+
+exports[`#renderDeactivateButton should render correctly 2`] = `
+<Tooltip
+ overlay="coding_rules.need_extend_or_copy"
+>
+ <Button
+ className="coding-rules-detail-quality-profile-deactivate button-red"
+ disabled={true}
+ >
+ coding_rules.deactivate
+ </Button>
+</Tooltip>
+`;
+
exports[`renderActions should disable the button when I am on a built-in profile 1`] = `
<td
className="coding-rule-table-meta-cell coding-rule-activation-actions"
@@ -26,7 +50,6 @@ exports[`renderActions should render the activate button 1`] = `
className="coding-rules-detail-quality-profile-activate"
modalHeader="coding_rules.activate_in_quality_profile"
onDone={[Function]}
- organization="org"
profiles={
Array [
Object {
@@ -87,7 +110,7 @@ exports[`renderActions should render the deactivate button 1`] = `
</td>
`;
-exports[`should render 1`] = `
+exports[`should render correctly: default 1`] = `
<div
className="coding-rule"
data-rule="javascript:S1067"
@@ -108,7 +131,7 @@ exports[`should render 1`] = `
style={Object {}}
to={
Object {
- "pathname": "/organizations/org/rules",
+ "pathname": "/coding_rules",
"query": Object {
"open": "javascript:S1067",
"rule_key": "javascript:S1067",
@@ -187,26 +210,102 @@ exports[`should render 1`] = `
</div>
`;
-exports[`should render deactivate button 1`] = `
-<ConfirmButton
- confirmButtonText="yes"
- modalBody="coding_rules.deactivate.confirm"
- modalHeader="coding_rules.deactivate"
- onConfirm={[Function]}
->
- [Function]
-</ConfirmButton>
-`;
-
-exports[`should render deactivate button 2`] = `
-<Tooltip
- overlay="coding_rules.need_extend_or_copy"
+exports[`should render correctly: with activation 1`] = `
+<div
+ className="coding-rule"
+ data-rule="javascript:S1067"
>
- <Button
- className="coding-rules-detail-quality-profile-deactivate button-red"
- disabled={true}
+ <table
+ className="coding-rule-table"
>
- coding_rules.deactivate
- </Button>
-</Tooltip>
+ <tbody>
+ <tr>
+ <td>
+ <div
+ className="coding-rule-title"
+ >
+ <Link
+ className="link-no-underline"
+ onClick={[Function]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "open": "javascript:S1067",
+ "rule_key": "javascript:S1067",
+ },
+ }
+ }
+ >
+ Use foo
+ </Link>
+ </div>
+ </td>
+ <td
+ className="coding-rule-table-meta-cell"
+ >
+ <div
+ className="display-flex-center coding-rule-meta"
+ >
+ <span
+ className="display-inline-flex-center spacer-left note"
+ >
+ JavaScript
+ </span>
+ <Tooltip
+ overlay="coding_rules.type.tooltip.CODE_SMELL"
+ >
+ <span
+ className="display-inline-flex-center spacer-left note"
+ >
+ <IssueTypeIcon
+ query="CODE_SMELL"
+ />
+ <span
+ className="little-spacer-left text-middle"
+ >
+ issue.type.CODE_SMELL
+ </span>
+ </span>
+ </Tooltip>
+ <TagsList
+ allowUpdate={false}
+ className="display-inline-flex-center note spacer-left"
+ tags={
+ Array [
+ "x",
+ "a",
+ "b",
+ ]
+ }
+ />
+ <SimilarRulesFilter
+ onFilterChange={[MockFunction]}
+ rule={
+ Object {
+ "key": "javascript:S1067",
+ "lang": "js",
+ "langName": "JavaScript",
+ "name": "Use foo",
+ "severity": "MAJOR",
+ "status": "READY",
+ "sysTags": Array [
+ "a",
+ "b",
+ ],
+ "tags": Array [
+ "x",
+ ],
+ "type": "CODE_SMELL",
+ }
+ }
+ />
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</div>
`;
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx
index c6e95495a9b..082d35acc56 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx
@@ -23,7 +23,7 @@ import DocTooltip from '../../../components/docs/DocTooltip';
import Conditions from './Conditions';
import Projects from './Projects';
-interface Props {
+export interface DetailsContentProps {
isDefault?: boolean;
metrics: T.Dict<T.Metric>;
organization?: string;
@@ -33,48 +33,48 @@ interface Props {
qualityGate: T.QualityGate;
}
-export default class DetailsContent extends React.PureComponent<Props> {
- render() {
- const { isDefault, metrics, organization, qualityGate } = this.props;
- const conditions = qualityGate.conditions || [];
- const actions = qualityGate.actions || ({} as any);
+export function DetailsContent(props: DetailsContentProps) {
+ const { isDefault, metrics, organization, qualityGate } = props;
+ const conditions = qualityGate.conditions || [];
+ const actions = qualityGate.actions || ({} as any);
- return (
- <div className="layout-page-main-inner">
- <Conditions
- canEdit={actions.manageConditions}
- conditions={conditions}
- metrics={metrics}
- onAddCondition={this.props.onAddCondition}
- onRemoveCondition={this.props.onRemoveCondition}
- onSaveCondition={this.props.onSaveCondition}
- organization={organization}
- qualityGate={qualityGate}
- />
+ return (
+ <div className="layout-page-main-inner">
+ <Conditions
+ canEdit={actions.manageConditions}
+ conditions={conditions}
+ metrics={metrics}
+ onAddCondition={props.onAddCondition}
+ onRemoveCondition={props.onRemoveCondition}
+ onSaveCondition={props.onSaveCondition}
+ organization={organization}
+ qualityGate={qualityGate}
+ />
- <div className="quality-gate-section" id="quality-gate-projects">
- <header className="display-flex-center spacer-bottom">
- <h3>{translate('quality_gates.projects')}</h3>
- <DocTooltip
- className="spacer-left"
- doc={import(
- /* webpackMode: "eager" */ 'Docs/tooltips/quality-gates/quality-gate-projects.md'
- )}
- />
- </header>
- {isDefault ? (
- translate('quality_gates.projects_for_default')
- ) : (
- <Projects
- canEdit={actions.associateProjects}
- // pass unique key to re-mount the component when the quality gate changes
- key={qualityGate.id}
- organization={organization}
- qualityGate={qualityGate}
- />
- )}
- </div>
+ <div className="quality-gate-section" id="quality-gate-projects">
+ <header className="display-flex-center spacer-bottom">
+ <h3>{translate('quality_gates.projects')}</h3>
+ <DocTooltip
+ className="spacer-left"
+ doc={import(
+ /* webpackMode: "eager" */ 'Docs/tooltips/quality-gates/quality-gate-projects.md'
+ )}
+ />
+ </header>
+ {isDefault ? (
+ translate('quality_gates.projects_for_default')
+ ) : (
+ <Projects
+ canEdit={actions.associateProjects}
+ // pass unique key to re-mount the component when the quality gate changes
+ key={qualityGate.id}
+ organization={organization}
+ qualityGate={qualityGate}
+ />
+ )}
</div>
- );
- }
+ </div>
+ );
}
+
+export default React.memo(DetailsContent);
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/Details-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/Details-test.tsx
new file mode 100644
index 00000000000..15dd51e43da
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/Details-test.tsx
@@ -0,0 +1,131 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { fetchQualityGate } from '../../../../api/quality-gates';
+import { mockCondition, mockQualityGate } from '../../../../helpers/testMocks';
+import { addCondition, deleteCondition, replaceCondition } from '../../utils';
+import { Details } from '../Details';
+
+jest.mock('../../../../api/quality-gates', () => {
+ const { mockQualityGate } = jest.requireActual('../../../../helpers/testMocks');
+ return {
+ fetchQualityGate: jest.fn().mockResolvedValue(mockQualityGate())
+ };
+});
+
+jest.mock('../../utils', () => ({
+ checkIfDefault: jest.fn(() => false),
+ addCondition: jest.fn(qg => qg),
+ deleteCondition: jest.fn(qg => qg),
+ replaceCondition: jest.fn(qg => qg)
+}));
+
+beforeEach(jest.clearAllMocks);
+
+it('should render correctly', async () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot('loading');
+
+ await waitAndUpdate(wrapper);
+ expect(fetchQualityGate).toBeCalledWith({ id: '1' });
+ expect(wrapper).toMatchSnapshot('loaded');
+});
+
+it('should refresh if the QG id changes', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+ jest.clearAllMocks();
+
+ wrapper.setProps({ id: '2' });
+ expect(fetchQualityGate).toBeCalledWith({ id: '2' });
+});
+
+it('should fetch metrics on mount', () => {
+ const fetchMetrics = jest.fn();
+ shallowRender({ fetchMetrics });
+ expect(fetchMetrics).toBeCalled();
+});
+
+it('should correctly add/replace/remove conditions', async () => {
+ const qualityGate = mockQualityGate();
+ (fetchQualityGate as jest.Mock).mockResolvedValue(qualityGate);
+
+ const wrapper = shallowRender();
+ const instance = wrapper.instance();
+
+ instance.handleAddCondition(mockCondition());
+ expect(wrapper.state().qualityGate).toBeUndefined();
+
+ instance.handleSaveCondition(mockCondition(), mockCondition());
+ expect(wrapper.state().qualityGate).toBeUndefined();
+
+ instance.handleRemoveCondition(mockCondition());
+ expect(wrapper.state().qualityGate).toBeUndefined();
+
+ await waitAndUpdate(wrapper);
+
+ const newCondition = mockCondition({ metric: 'bugs', id: 2 });
+ instance.handleAddCondition(newCondition);
+ expect(addCondition).toBeCalledWith(qualityGate, newCondition);
+
+ const updatedCondition = mockCondition({ metric: 'new_bugs' });
+ instance.handleSaveCondition(newCondition, updatedCondition);
+ expect(replaceCondition).toBeCalledWith(qualityGate, newCondition, updatedCondition);
+
+ instance.handleRemoveCondition(newCondition);
+ expect(deleteCondition).toBeCalledWith(qualityGate, newCondition);
+});
+
+it('should correctly handle setting default', async () => {
+ const qualityGate = mockQualityGate();
+ (fetchQualityGate as jest.Mock).mockResolvedValue(qualityGate);
+
+ const onSetDefault = jest.fn();
+ const wrapper = shallowRender({ onSetDefault });
+
+ wrapper.instance().handleSetDefault();
+ expect(wrapper.state().qualityGate).toBeUndefined();
+
+ await waitAndUpdate(wrapper);
+
+ wrapper.instance().handleSetDefault();
+ expect(wrapper.state().qualityGate).toEqual(
+ expect.objectContaining({
+ id: qualityGate.id,
+ actions: { delete: false, setAsDefault: false }
+ })
+ );
+});
+
+function shallowRender(props: Partial<Details['props']> = {}) {
+ return shallow<Details>(
+ <Details
+ fetchMetrics={jest.fn()}
+ id="1"
+ metrics={{}}
+ onSetDefault={jest.fn()}
+ qualityGates={[mockQualityGate()]}
+ refreshQualityGates={jest.fn()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/LoginSonarCloud.css b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/DetailsContent-test.tsx
index 5b21e3c8d5d..78745a1def2 100644
--- a/server/sonar-web/src/main/js/apps/sessions/components/LoginSonarCloud.css
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/DetailsContent-test.tsx
@@ -17,47 +17,25 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-.sonarcloud-login-alert {
- margin: 10vh auto 5vh auto;
- width: 256px;
-}
-
-.sonarcloud-login-page {
- margin-top: 15vh;
- width: 216px;
- margin-left: auto;
- margin-right: auto;
- padding: calc(4 * var(--gridSize)) 20px;
-}
-
-.sonarcloud-login-alert ~ .sonarcloud-login-page {
- margin-top: 0;
-}
-
-.sonarcloud-login-page-large {
- width: 300px;
-}
-
-.sonarcloud-login-title {
- line-height: 1.5;
- font-size: var(--bigFontSize);
- font-weight: 300;
- width: 135px;
- margin: var(--gridSize) auto calc(3 * var(--gridSize));
-}
-
-.sonarcloud-oauth-providers.oauth-providers > ul {
- width: 186px;
-}
-
-.sonarcloud-oauth-providers.oauth-providers > ul > li {
- margin-bottom: var(--gridSize);
-}
-
-.sonarcloud-oauth-providers.oauth-providers .oauth-providers-help {
- right: -22px;
-}
-
-.sonarcloud-login-cancel {
- text-align: center;
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { mockQualityGate } from '../../../../helpers/testMocks';
+import { DetailsContent, DetailsContentProps } from '../DetailsContent';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('is not default');
+ expect(shallowRender({ isDefault: true })).toMatchSnapshot('is default');
+});
+
+function shallowRender(props: Partial<DetailsContentProps> = {}) {
+ return shallow(
+ <DetailsContent
+ metrics={{}}
+ onAddCondition={jest.fn()}
+ onRemoveCondition={jest.fn()}
+ onSaveCondition={jest.fn()}
+ qualityGate={mockQualityGate()}
+ {...props}
+ />
+ );
}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/DetailsHeader-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/DetailsHeader-test.tsx
new file mode 100644
index 00000000000..5dbf1d7a2f1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/DetailsHeader-test.tsx
@@ -0,0 +1,88 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { click, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { setQualityGateAsDefault } from '../../../../api/quality-gates';
+import { mockQualityGate } from '../../../../helpers/testMocks';
+import DetailsHeader from '../DetailsHeader';
+
+jest.mock('../../../../api/quality-gates', () => ({
+ setQualityGateAsDefault: jest.fn().mockResolvedValue(null)
+}));
+
+beforeEach(jest.clearAllMocks);
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('default');
+ expect(shallowRender({ qualityGate: mockQualityGate({ isBuiltIn: true }) })).toMatchSnapshot(
+ 'built-in'
+ );
+ expect(
+ shallowRender({
+ qualityGate: mockQualityGate({
+ actions: {
+ copy: true,
+ delete: true,
+ rename: true,
+ setAsDefault: true
+ }
+ })
+ })
+ ).toMatchSnapshot('admin actions');
+});
+
+it('should allow the QG to be set as the default', async () => {
+ const onSetDefault = jest.fn();
+ const refreshItem = jest.fn();
+ const refreshList = jest.fn();
+
+ const qualityGate = mockQualityGate({ id: 1, actions: { setAsDefault: true } });
+ const wrapper = shallowRender({ onSetDefault, qualityGate, refreshItem, refreshList });
+
+ click(wrapper.find('Button#quality-gate-toggle-default'));
+ expect(setQualityGateAsDefault).toBeCalledWith({ id: 1 });
+ expect(onSetDefault).toBeCalled();
+ await waitAndUpdate(wrapper);
+ expect(refreshItem).toBeCalled();
+ expect(refreshList).toBeCalled();
+
+ jest.clearAllMocks();
+
+ wrapper.setProps({ qualityGate: mockQualityGate({ ...qualityGate, isDefault: true }) });
+ click(wrapper.find('Button#quality-gate-toggle-default'));
+ expect(setQualityGateAsDefault).not.toBeCalled();
+ expect(onSetDefault).not.toBeCalled();
+ await waitAndUpdate(wrapper);
+ expect(refreshItem).not.toBeCalled();
+ expect(refreshList).not.toBeCalled();
+});
+
+function shallowRender(props: Partial<DetailsHeader['props']> = {}) {
+ return shallow<DetailsHeader>(
+ <DetailsHeader
+ onSetDefault={jest.fn()}
+ qualityGate={mockQualityGate()}
+ refreshItem={jest.fn().mockResolvedValue(null)}
+ refreshList={jest.fn().mockResolvedValue(null)}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/Details-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/Details-test.tsx.snap
new file mode 100644
index 00000000000..4cc1de5c82a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/Details-test.tsx.snap
@@ -0,0 +1,53 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: loaded 1`] = `
+<div
+ className="layout-page-main"
+>
+ <DeferredSpinner
+ loading={false}
+ timeout={200}
+ >
+ <HelmetWrapper
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="qualitygate"
+ />
+ <DetailsHeader
+ onSetDefault={[Function]}
+ qualityGate={
+ Object {
+ "id": 1,
+ "name": "qualitygate",
+ }
+ }
+ refreshItem={[Function]}
+ refreshList={[MockFunction]}
+ />
+ <Memo(DetailsContent)
+ isDefault={false}
+ metrics={Object {}}
+ onAddCondition={[Function]}
+ onRemoveCondition={[Function]}
+ onSaveCondition={[Function]}
+ qualityGate={
+ Object {
+ "id": 1,
+ "name": "qualitygate",
+ }
+ }
+ />
+ </DeferredSpinner>
+</div>
+`;
+
+exports[`should render correctly: loading 1`] = `
+<div
+ className="layout-page-main"
+>
+ <DeferredSpinner
+ loading={true}
+ timeout={200}
+ />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/DetailsContent-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/DetailsContent-test.tsx.snap
new file mode 100644
index 00000000000..8aa1b677220
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/DetailsContent-test.tsx.snap
@@ -0,0 +1,83 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: is default 1`] = `
+<div
+ className="layout-page-main-inner"
+>
+ <Connect(withAppState(Conditions))
+ conditions={Array []}
+ metrics={Object {}}
+ onAddCondition={[MockFunction]}
+ onRemoveCondition={[MockFunction]}
+ onSaveCondition={[MockFunction]}
+ qualityGate={
+ Object {
+ "id": 1,
+ "name": "qualitygate",
+ }
+ }
+ />
+ <div
+ className="quality-gate-section"
+ id="quality-gate-projects"
+ >
+ <header
+ className="display-flex-center spacer-bottom"
+ >
+ <h3>
+ quality_gates.projects
+ </h3>
+ <DocTooltip
+ className="spacer-left"
+ doc={Promise {}}
+ />
+ </header>
+ quality_gates.projects_for_default
+ </div>
+</div>
+`;
+
+exports[`should render correctly: is not default 1`] = `
+<div
+ className="layout-page-main-inner"
+>
+ <Connect(withAppState(Conditions))
+ conditions={Array []}
+ metrics={Object {}}
+ onAddCondition={[MockFunction]}
+ onRemoveCondition={[MockFunction]}
+ onSaveCondition={[MockFunction]}
+ qualityGate={
+ Object {
+ "id": 1,
+ "name": "qualitygate",
+ }
+ }
+ />
+ <div
+ className="quality-gate-section"
+ id="quality-gate-projects"
+ >
+ <header
+ className="display-flex-center spacer-bottom"
+ >
+ <h3>
+ quality_gates.projects
+ </h3>
+ <DocTooltip
+ className="spacer-left"
+ doc={Promise {}}
+ />
+ </header>
+ <Projects
+ key="1"
+ qualityGate={
+ Object {
+ "id": 1,
+ "name": "qualitygate",
+ }
+ }
+ />
+ </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/DetailsHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/DetailsHeader-test.tsx.snap
new file mode 100644
index 00000000000..8e593a3fd98
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/DetailsHeader-test.tsx.snap
@@ -0,0 +1,112 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: admin actions 1`] = `
+<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"
+ >
+ <div
+ className="pull-left display-flex-center"
+ >
+ <h2>
+ qualitygate
+ </h2>
+ </div>
+ <div
+ className="pull-right"
+ >
+ <ModalButton
+ modal={[Function]}
+ >
+ <Component />
+ </ModalButton>
+ <ModalButton
+ modal={[Function]}
+ >
+ <Component />
+ </ModalButton>
+ <Button
+ className="little-spacer-left"
+ id="quality-gate-toggle-default"
+ onClick={[Function]}
+ >
+ set_as_default
+ </Button>
+ <withRouter(DeleteQualityGateForm)
+ onDelete={[MockFunction]}
+ qualityGate={
+ Object {
+ "actions": Object {
+ "copy": true,
+ "delete": true,
+ "rename": true,
+ "setAsDefault": true,
+ },
+ "id": 1,
+ "name": "qualitygate",
+ }
+ }
+ />
+ </div>
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should render correctly: built-in 1`] = `
+<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"
+ >
+ <div
+ className="pull-left display-flex-center"
+ >
+ <h2>
+ qualitygate
+ </h2>
+ <BuiltInQualityGateBadge
+ className="spacer-left"
+ />
+ </div>
+ <div
+ className="pull-right"
+ />
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should render correctly: default 1`] = `
+<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"
+ >
+ <div
+ className="pull-left display-flex-center"
+ >
+ <h2>
+ qualitygate
+ </h2>
+ </div>
+ <div
+ className="pull-right"
+ />
+ </div>
+ </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx
index 2a05242e2fd..75dcbf4d59a 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx
@@ -62,21 +62,31 @@ export default class ProfilePermissionsForm extends React.PureComponent<Props, S
}
};
- handleUserAdd = (user: T.UserSelected) =>
+ handleUserAdd = (user: T.UserSelected) => {
+ const {
+ profile: { language, name },
+ organization
+ } = this.props;
addUser({
- language: this.props.profile.language,
+ language,
login: user.login,
- organization: this.props.organization,
- qualityProfile: this.props.profile.name
+ organization,
+ qualityProfile: name
}).then(() => this.props.onUserAdd(user), this.stopSubmitting);
+ };
- handleGroupAdd = (group: Group) =>
+ handleGroupAdd = (group: Group) => {
+ const {
+ profile: { language, name },
+ organization
+ } = this.props;
addGroup({
group: group.name,
- language: this.props.profile.language,
- organization: this.props.organization,
- qualityProfile: this.props.profile.name
+ language,
+ organization,
+ qualityProfile: name
}).then(() => this.props.onGroupAdd(group), this.stopSubmitting);
+ };
handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsForm-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsForm-test.tsx
index 73325c640d3..53e7d18843e 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsForm-test.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsForm-test.tsx
@@ -17,76 +17,95 @@
* 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-profiles', () => ({
- addUser: jest.fn(() => Promise.resolve()),
- addGroup: jest.fn(() => Promise.resolve())
-}));
-
import { shallow } from 'enzyme';
import * as React from 'react';
-import { submit } from 'sonar-ui-common/helpers/testUtils';
+import { submit, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { addGroup, addUser, searchGroups, searchUsers } from '../../../../api/quality-profiles';
+import { mockGroup, mockUser } from '../../../../helpers/testMocks';
import ProfilePermissionsForm from '../ProfilePermissionsForm';
+import ProfilePermissionsFormSelect from '../ProfilePermissionsFormSelect';
-const addUser = require('../../../../api/quality-profiles').addUser as jest.Mock<any>;
-const addGroup = require('../../../../api/quality-profiles').addGroup as jest.Mock<any>;
+jest.mock('../../../../api/quality-profiles', () => ({
+ addUser: jest.fn().mockResolvedValue(null),
+ addGroup: jest.fn().mockResolvedValue(null),
+ searchGroups: jest.fn().mockResolvedValue({ groups: [] }),
+ searchUsers: jest.fn().mockResolvedValue({ users: [] })
+}));
-const profile = { language: 'js', name: 'Sonar way' };
+const PROFILE = { language: 'js', name: 'Sonar way' };
-it('adds user', async () => {
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('default');
+ expect(shallowRender().setState({ submitting: true })).toMatchSnapshot('submitting');
+});
+
+it('correctly adds users', async () => {
const onUserAdd = jest.fn();
- const wrapper = shallow(
- <ProfilePermissionsForm
- onClose={jest.fn()}
- onGroupAdd={jest.fn()}
- onUserAdd={onUserAdd}
- organization="org"
- profile={profile}
- />
- );
- expect(wrapper).toMatchSnapshot();
+ const wrapper = shallowRender({ onUserAdd });
- wrapper.setState({ selected: { login: 'luke' } });
- expect(wrapper).toMatchSnapshot();
+ const user: T.UserSelected = { ...mockUser(), name: 'John doe', active: true, selected: true };
+ wrapper.instance().handleValueChange(user);
+ expect(wrapper.find(ProfilePermissionsFormSelect).prop('selected')).toBe(user);
submit(wrapper.find('form'));
expect(wrapper).toMatchSnapshot();
- expect(addUser).toBeCalledWith({
- language: 'js',
- login: 'luke',
- organization: 'org',
- qualityProfile: 'Sonar way'
- });
+ expect(addUser).toBeCalledWith(
+ expect.objectContaining({
+ language: PROFILE.language,
+ qualityProfile: PROFILE.name,
+ login: user.login
+ })
+ );
- await new Promise(setImmediate);
- expect(onUserAdd).toBeCalledWith({ login: 'luke' });
+ await waitAndUpdate(wrapper);
+ expect(onUserAdd).toBeCalledWith(user);
});
-it('adds group', async () => {
+it('correctly adds groups', async () => {
const onGroupAdd = jest.fn();
- const wrapper = shallow(
- <ProfilePermissionsForm
- onClose={jest.fn()}
- onGroupAdd={onGroupAdd}
- onUserAdd={jest.fn()}
- organization="org"
- profile={profile}
- />
- );
- expect(wrapper).toMatchSnapshot();
+ const wrapper = shallowRender({ onGroupAdd });
- wrapper.setState({ selected: { name: 'lambda' } });
- expect(wrapper).toMatchSnapshot();
+ const group = mockGroup();
+ wrapper.instance().handleValueChange(group);
+ expect(wrapper.find(ProfilePermissionsFormSelect).prop('selected')).toBe(group);
submit(wrapper.find('form'));
expect(wrapper).toMatchSnapshot();
- expect(addGroup).toBeCalledWith({
- group: 'lambda',
- language: 'js',
- organization: 'org',
- qualityProfile: 'Sonar way'
- });
+ expect(addGroup).toBeCalledWith(
+ expect.objectContaining({
+ language: PROFILE.language,
+ qualityProfile: PROFILE.name,
+ group: group.name
+ })
+ );
- await new Promise(setImmediate);
- expect(onGroupAdd).toBeCalledWith({ name: 'lambda' });
+ await waitAndUpdate(wrapper);
+ expect(onGroupAdd).toBeCalledWith(group);
});
+
+it('correctly handles search', () => {
+ const wrapper = shallowRender();
+ wrapper.instance().handleSearch('foo');
+
+ const parameters = {
+ language: PROFILE.language,
+ q: 'foo',
+ qualityProfile: PROFILE.name,
+ selected: 'deselected'
+ };
+
+ expect(searchUsers).toBeCalledWith(parameters);
+ expect(searchGroups).toBeCalledWith(parameters);
+});
+
+function shallowRender(props: Partial<ProfilePermissionsForm['props']> = {}) {
+ return shallow<ProfilePermissionsForm>(
+ <ProfilePermissionsForm
+ onClose={jest.fn()}
+ onGroupAdd={jest.fn()}
+ onUserAdd={jest.fn()}
+ profile={PROFILE}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsForm-test.tsx.snap
index 8d1e901d79a..8db35b9f7ee 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsForm-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsForm-test.tsx.snap
@@ -1,54 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`adds group 1`] = `
-<Modal
- contentLabel="quality_profiles.grant_permissions_to_user_or_group"
- onRequestClose={[MockFunction]}
->
- <header
- className="modal-head"
- >
- <h2>
- quality_profiles.grant_permissions_to_user_or_group
- </h2>
- </header>
- <form
- onSubmit={[Function]}
- >
- <div
- className="modal-body"
- >
- <div
- className="modal-field"
- >
- <label>
- quality_profiles.search_description
- </label>
- <ProfilePermissionsFormSelect
- onChange={[Function]}
- onSearch={[Function]}
- />
- </div>
- </div>
- <footer
- className="modal-foot"
- >
- <SubmitButton
- disabled={true}
- >
- add_verb
- </SubmitButton>
- <ResetButtonLink
- onClick={[MockFunction]}
- >
- cancel
- </ResetButtonLink>
- </footer>
- </form>
-</Modal>
-`;
-
-exports[`adds group 2`] = `
+exports[`correctly adds groups 1`] = `
<Modal
contentLabel="quality_profiles.grant_permissions_to_user_or_group"
onRequestClose={[MockFunction]}
@@ -77,7 +29,9 @@ exports[`adds group 2`] = `
onSearch={[Function]}
selected={
Object {
- "name": "lambda",
+ "id": 1,
+ "membersCount": 1,
+ "name": "Foo",
}
}
/>
@@ -86,8 +40,11 @@ exports[`adds group 2`] = `
<footer
className="modal-foot"
>
+ <i
+ className="spinner spacer-right"
+ />
<SubmitButton
- disabled={false}
+ disabled={true}
>
add_verb
</SubmitButton>
@@ -101,7 +58,7 @@ exports[`adds group 2`] = `
</Modal>
`;
-exports[`adds group 3`] = `
+exports[`correctly adds users 1`] = `
<Modal
contentLabel="quality_profiles.grant_permissions_to_user_or_group"
onRequestClose={[MockFunction]}
@@ -130,7 +87,11 @@ exports[`adds group 3`] = `
onSearch={[Function]}
selected={
Object {
- "name": "lambda",
+ "active": true,
+ "local": true,
+ "login": "john.doe",
+ "name": "John doe",
+ "selected": true,
}
}
/>
@@ -157,7 +118,7 @@ exports[`adds group 3`] = `
</Modal>
`;
-exports[`adds user 1`] = `
+exports[`should render correctly: default 1`] = `
<Modal
contentLabel="quality_profiles.grant_permissions_to_user_or_group"
onRequestClose={[MockFunction]}
@@ -205,7 +166,7 @@ exports[`adds user 1`] = `
</Modal>
`;
-exports[`adds user 2`] = `
+exports[`should render correctly: submitting 1`] = `
<Modal
contentLabel="quality_profiles.grant_permissions_to_user_or_group"
onRequestClose={[MockFunction]}
@@ -232,64 +193,6 @@ exports[`adds user 2`] = `
<ProfilePermissionsFormSelect
onChange={[Function]}
onSearch={[Function]}
- selected={
- Object {
- "login": "luke",
- }
- }
- />
- </div>
- </div>
- <footer
- className="modal-foot"
- >
- <SubmitButton
- disabled={false}
- >
- add_verb
- </SubmitButton>
- <ResetButtonLink
- onClick={[MockFunction]}
- >
- cancel
- </ResetButtonLink>
- </footer>
- </form>
-</Modal>
-`;
-
-exports[`adds user 3`] = `
-<Modal
- contentLabel="quality_profiles.grant_permissions_to_user_or_group"
- onRequestClose={[MockFunction]}
->
- <header
- className="modal-head"
- >
- <h2>
- quality_profiles.grant_permissions_to_user_or_group
- </h2>
- </header>
- <form
- onSubmit={[Function]}
- >
- <div
- className="modal-body"
- >
- <div
- className="modal-field"
- >
- <label>
- quality_profiles.search_description
- </label>
- <ProfilePermissionsFormSelect
- onChange={[Function]}
- onSearch={[Function]}
- selected={
- Object {
- "login": "luke",
- }
- }
/>
</div>
</div>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx
index fbabe10e52a..0a289b3e0ff 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx
@@ -29,113 +29,98 @@ import ProfileDate from '../components/ProfileDate';
import ProfileLink from '../components/ProfileLink';
import { Profile } from '../types';
-interface Props {
+export interface ProfilesListRowProps {
organization: string | null;
profile: Profile;
updateProfiles: () => Promise<void>;
}
-export default class ProfilesListRow extends React.PureComponent<Props> {
- renderName() {
- const { profile } = this.props;
- const offset = 25 * (profile.depth - 1);
- return (
- <div className="display-flex-center" style={{ paddingLeft: offset }}>
- <div>
- <ProfileLink
- language={profile.language}
- name={profile.name}
- organization={this.props.organization}>
- {profile.name}
- </ProfileLink>
- </div>
- {profile.isBuiltIn && <BuiltInQualityProfileBadge className="spacer-left" />}
- </div>
- );
- }
-
- renderProjects() {
- const { profile } = this.props;
+export function ProfilesListRow(props: ProfilesListRowProps) {
+ const { organization, profile } = props;
- if (profile.isDefault) {
- return (
- <DocTooltip
- doc={import(
- /* webpackMode: "eager" */ 'Docs/tooltips/quality-profiles/default-quality-profile.md'
- )}>
- <span className="badge">{translate('default')}</span>
- </DocTooltip>
- );
- }
+ const offset = 25 * (profile.depth - 1);
+ const activeRulesUrl = getRulesUrl(
+ {
+ qprofile: profile.key,
+ activation: 'true'
+ },
+ organization
+ );
+ const deprecatedRulesUrl = getRulesUrl(
+ {
+ qprofile: profile.key,
+ activation: 'true',
+ statuses: 'DEPRECATED'
+ },
+ organization
+ );
- return <span>{profile.projectCount}</span>;
- }
+ return (
+ <tr
+ className="quality-profiles-table-row text-middle"
+ data-key={profile.key}
+ data-name={profile.name}>
+ <td className="quality-profiles-table-name text-middle">
+ <div className="display-flex-center" style={{ paddingLeft: offset }}>
+ <div>
+ <ProfileLink
+ language={profile.language}
+ name={profile.name}
+ organization={organization}>
+ {profile.name}
+ </ProfileLink>
+ </div>
+ {profile.isBuiltIn && <BuiltInQualityProfileBadge className="spacer-left" />}
+ </div>
+ </td>
- renderRules() {
- const { profile } = this.props;
+ <td className="quality-profiles-table-projects thin nowrap text-middle text-right">
+ {profile.isDefault ? (
+ <DocTooltip
+ doc={import(
+ /* webpackMode: "eager" */ 'Docs/tooltips/quality-profiles/default-quality-profile.md'
+ )}>
+ <span className="badge">{translate('default')}</span>
+ </DocTooltip>
+ ) : (
+ <span>{profile.projectCount}</span>
+ )}
+ </td>
- const activeRulesUrl = getRulesUrl(
- {
- qprofile: profile.key,
- activation: 'true'
- },
- this.props.organization
- );
+ <td className="quality-profiles-table-rules thin nowrap text-middle text-right">
+ <div>
+ {profile.activeDeprecatedRuleCount > 0 && (
+ <span className="spacer-right">
+ <Tooltip overlay={translate('quality_profiles.deprecated_rules')}>
+ <Link className="badge badge-error" to={deprecatedRulesUrl}>
+ {profile.activeDeprecatedRuleCount}
+ </Link>
+ </Tooltip>
+ </span>
+ )}
- const deprecatedRulesUrl = getRulesUrl(
- {
- qprofile: profile.key,
- activation: 'true',
- statuses: 'DEPRECATED'
- },
- this.props.organization
- );
+ <Link to={activeRulesUrl}>{profile.activeRuleCount}</Link>
+ </div>
+ </td>
- return (
- <div>
- {profile.activeDeprecatedRuleCount > 0 && (
- <span className="spacer-right">
- <Tooltip overlay={translate('quality_profiles.deprecated_rules')}>
- <Link className="badge badge-error" to={deprecatedRulesUrl}>
- {profile.activeDeprecatedRuleCount}
- </Link>
- </Tooltip>
- </span>
- )}
+ <td className="quality-profiles-table-date thin nowrap text-middle text-right">
+ <ProfileDate date={profile.rulesUpdatedAt} />
+ </td>
- <Link to={activeRulesUrl}>{profile.activeRuleCount}</Link>
- </div>
- );
- }
+ <td className="quality-profiles-table-date thin nowrap text-middle text-right">
+ <ProfileDate date={profile.lastUsed} />
+ </td>
- render() {
- return (
- <tr
- className="quality-profiles-table-row text-middle"
- data-key={this.props.profile.key}
- data-name={this.props.profile.name}>
- <td className="quality-profiles-table-name text-middle">{this.renderName()}</td>
- <td className="quality-profiles-table-projects thin nowrap text-middle text-right">
- {this.renderProjects()}
- </td>
- <td className="quality-profiles-table-rules thin nowrap text-middle text-right">
- {this.renderRules()}
- </td>
- <td className="quality-profiles-table-date thin nowrap text-middle text-right">
- <ProfileDate date={this.props.profile.rulesUpdatedAt} />
- </td>
- <td className="quality-profiles-table-date thin nowrap text-middle text-right">
- <ProfileDate date={this.props.profile.lastUsed} />
- </td>
- <td className="quality-profiles-table-actions thin nowrap text-middle text-right">
- <ProfileActions
- fromList={true}
- organization={this.props.organization}
- profile={this.props.profile}
- updateProfiles={this.props.updateProfiles}
- />
- </td>
- </tr>
- );
- }
+ <td className="quality-profiles-table-actions thin nowrap text-middle text-right">
+ <ProfileActions
+ fromList={true}
+ organization={organization}
+ profile={profile}
+ updateProfiles={props.updateProfiles}
+ />
+ </td>
+ </tr>
+ );
}
+
+export default React.memo(ProfilesListRow);
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/ProfilesListRow-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/ProfilesListRow-test.tsx
new file mode 100644
index 00000000000..b4325e9b344
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/ProfilesListRow-test.tsx
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { mockQualityProfile } from '../../../../helpers/testMocks';
+import { ProfilesListRow, ProfilesListRowProps } from '../ProfilesListRow';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('default');
+ expect(shallowRender({ profile: mockQualityProfile({ isBuiltIn: true }) })).toMatchSnapshot(
+ 'built-in profile'
+ );
+ expect(shallowRender({ profile: mockQualityProfile({ isDefault: true }) })).toMatchSnapshot(
+ 'default profile'
+ );
+ expect(
+ shallowRender({ profile: mockQualityProfile({ activeDeprecatedRuleCount: 10 }) })
+ ).toMatchSnapshot('with deprecated rules');
+});
+
+function shallowRender(props: Partial<ProfilesListRowProps> = {}) {
+ return shallow(
+ <ProfilesListRow
+ organization={null}
+ profile={mockQualityProfile({ activeDeprecatedRuleCount: 0 })}
+ updateProfiles={jest.fn()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/ProfilesList-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/ProfilesList-test.tsx.snap
index de4e062bbe0..23e63a5ccbe 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/ProfilesList-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/ProfilesList-test.tsx.snap
@@ -62,7 +62,7 @@ exports[`should render correctly 1`] = `
</tr>
</thead>
<tbody>
- <ProfilesListRow
+ <Memo(ProfilesListRow)
key="key"
organization="foo"
profile={
@@ -132,7 +132,7 @@ exports[`should render correctly 1`] = `
</tr>
</thead>
<tbody>
- <ProfilesListRow
+ <Memo(ProfilesListRow)
key="key"
organization="foo"
profile={
@@ -223,7 +223,7 @@ exports[`should render correctly 2`] = `
</tr>
</thead>
<tbody>
- <ProfilesListRow
+ <Memo(ProfilesListRow)
key="key"
organization="foo"
profile={
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/ProfilesListRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/ProfilesListRow-test.tsx.snap
new file mode 100644
index 00000000000..60829b64142
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/ProfilesListRow-test.tsx.snap
@@ -0,0 +1,465 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: built-in profile 1`] = `
+<tr
+ className="quality-profiles-table-row text-middle"
+ data-key="key"
+ data-name="name"
+>
+ <td
+ className="quality-profiles-table-name text-middle"
+ >
+ <div
+ className="display-flex-center"
+ style={
+ Object {
+ "paddingLeft": 0,
+ }
+ }
+ >
+ <div>
+ <ProfileLink
+ language="js"
+ name="name"
+ organization={null}
+ >
+ name
+ </ProfileLink>
+ </div>
+ <BuiltInQualityProfileBadge
+ className="spacer-left"
+ />
+ </div>
+ </td>
+ <td
+ className="quality-profiles-table-projects thin nowrap text-middle text-right"
+ >
+ <span>
+ 3
+ </span>
+ </td>
+ <td
+ className="quality-profiles-table-rules thin nowrap text-middle text-right"
+ >
+ <div>
+ <span
+ className="spacer-right"
+ >
+ <Tooltip
+ overlay="quality_profiles.deprecated_rules"
+ >
+ <Link
+ className="badge badge-error"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "activation": "true",
+ "qprofile": "key",
+ "statuses": "DEPRECATED",
+ },
+ }
+ }
+ >
+ 2
+ </Link>
+ </Tooltip>
+ </span>
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "activation": "true",
+ "qprofile": "key",
+ },
+ }
+ }
+ >
+ 10
+ </Link>
+ </div>
+ </td>
+ <td
+ className="quality-profiles-table-date thin nowrap text-middle text-right"
+ >
+ <ProfileDate />
+ </td>
+ <td
+ className="quality-profiles-table-date thin nowrap text-middle text-right"
+ >
+ <ProfileDate />
+ </td>
+ <td
+ className="quality-profiles-table-actions thin nowrap text-middle text-right"
+ >
+ <withRouter(ProfileActions)
+ fromList={true}
+ organization={null}
+ profile={
+ Object {
+ "activeDeprecatedRuleCount": 2,
+ "activeRuleCount": 10,
+ "childrenCount": 0,
+ "depth": 1,
+ "isBuiltIn": true,
+ "isDefault": false,
+ "isInherited": false,
+ "key": "key",
+ "language": "js",
+ "languageName": "JavaScript",
+ "name": "name",
+ "organization": "foo",
+ "projectCount": 3,
+ }
+ }
+ updateProfiles={[MockFunction]}
+ />
+ </td>
+</tr>
+`;
+
+exports[`should render correctly: default 1`] = `
+<tr
+ className="quality-profiles-table-row text-middle"
+ data-key="key"
+ data-name="name"
+>
+ <td
+ className="quality-profiles-table-name text-middle"
+ >
+ <div
+ className="display-flex-center"
+ style={
+ Object {
+ "paddingLeft": 0,
+ }
+ }
+ >
+ <div>
+ <ProfileLink
+ language="js"
+ name="name"
+ organization={null}
+ >
+ name
+ </ProfileLink>
+ </div>
+ </div>
+ </td>
+ <td
+ className="quality-profiles-table-projects thin nowrap text-middle text-right"
+ >
+ <span>
+ 3
+ </span>
+ </td>
+ <td
+ className="quality-profiles-table-rules thin nowrap text-middle text-right"
+ >
+ <div>
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "activation": "true",
+ "qprofile": "key",
+ },
+ }
+ }
+ >
+ 10
+ </Link>
+ </div>
+ </td>
+ <td
+ className="quality-profiles-table-date thin nowrap text-middle text-right"
+ >
+ <ProfileDate />
+ </td>
+ <td
+ className="quality-profiles-table-date thin nowrap text-middle text-right"
+ >
+ <ProfileDate />
+ </td>
+ <td
+ className="quality-profiles-table-actions thin nowrap text-middle text-right"
+ >
+ <withRouter(ProfileActions)
+ fromList={true}
+ organization={null}
+ profile={
+ Object {
+ "activeDeprecatedRuleCount": 0,
+ "activeRuleCount": 10,
+ "childrenCount": 0,
+ "depth": 1,
+ "isBuiltIn": false,
+ "isDefault": false,
+ "isInherited": false,
+ "key": "key",
+ "language": "js",
+ "languageName": "JavaScript",
+ "name": "name",
+ "organization": "foo",
+ "projectCount": 3,
+ }
+ }
+ updateProfiles={[MockFunction]}
+ />
+ </td>
+</tr>
+`;
+
+exports[`should render correctly: default profile 1`] = `
+<tr
+ className="quality-profiles-table-row text-middle"
+ data-key="key"
+ data-name="name"
+>
+ <td
+ className="quality-profiles-table-name text-middle"
+ >
+ <div
+ className="display-flex-center"
+ style={
+ Object {
+ "paddingLeft": 0,
+ }
+ }
+ >
+ <div>
+ <ProfileLink
+ language="js"
+ name="name"
+ organization={null}
+ >
+ name
+ </ProfileLink>
+ </div>
+ </div>
+ </td>
+ <td
+ className="quality-profiles-table-projects thin nowrap text-middle text-right"
+ >
+ <DocTooltip
+ doc={Promise {}}
+ >
+ <span
+ className="badge"
+ >
+ default
+ </span>
+ </DocTooltip>
+ </td>
+ <td
+ className="quality-profiles-table-rules thin nowrap text-middle text-right"
+ >
+ <div>
+ <span
+ className="spacer-right"
+ >
+ <Tooltip
+ overlay="quality_profiles.deprecated_rules"
+ >
+ <Link
+ className="badge badge-error"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "activation": "true",
+ "qprofile": "key",
+ "statuses": "DEPRECATED",
+ },
+ }
+ }
+ >
+ 2
+ </Link>
+ </Tooltip>
+ </span>
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "activation": "true",
+ "qprofile": "key",
+ },
+ }
+ }
+ >
+ 10
+ </Link>
+ </div>
+ </td>
+ <td
+ className="quality-profiles-table-date thin nowrap text-middle text-right"
+ >
+ <ProfileDate />
+ </td>
+ <td
+ className="quality-profiles-table-date thin nowrap text-middle text-right"
+ >
+ <ProfileDate />
+ </td>
+ <td
+ className="quality-profiles-table-actions thin nowrap text-middle text-right"
+ >
+ <withRouter(ProfileActions)
+ fromList={true}
+ organization={null}
+ profile={
+ Object {
+ "activeDeprecatedRuleCount": 2,
+ "activeRuleCount": 10,
+ "childrenCount": 0,
+ "depth": 1,
+ "isBuiltIn": false,
+ "isDefault": true,
+ "isInherited": false,
+ "key": "key",
+ "language": "js",
+ "languageName": "JavaScript",
+ "name": "name",
+ "organization": "foo",
+ "projectCount": 3,
+ }
+ }
+ updateProfiles={[MockFunction]}
+ />
+ </td>
+</tr>
+`;
+
+exports[`should render correctly: with deprecated rules 1`] = `
+<tr
+ className="quality-profiles-table-row text-middle"
+ data-key="key"
+ data-name="name"
+>
+ <td
+ className="quality-profiles-table-name text-middle"
+ >
+ <div
+ className="display-flex-center"
+ style={
+ Object {
+ "paddingLeft": 0,
+ }
+ }
+ >
+ <div>
+ <ProfileLink
+ language="js"
+ name="name"
+ organization={null}
+ >
+ name
+ </ProfileLink>
+ </div>
+ </div>
+ </td>
+ <td
+ className="quality-profiles-table-projects thin nowrap text-middle text-right"
+ >
+ <span>
+ 3
+ </span>
+ </td>
+ <td
+ className="quality-profiles-table-rules thin nowrap text-middle text-right"
+ >
+ <div>
+ <span
+ className="spacer-right"
+ >
+ <Tooltip
+ overlay="quality_profiles.deprecated_rules"
+ >
+ <Link
+ className="badge badge-error"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "activation": "true",
+ "qprofile": "key",
+ "statuses": "DEPRECATED",
+ },
+ }
+ }
+ >
+ 10
+ </Link>
+ </Tooltip>
+ </span>
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "activation": "true",
+ "qprofile": "key",
+ },
+ }
+ }
+ >
+ 10
+ </Link>
+ </div>
+ </td>
+ <td
+ className="quality-profiles-table-date thin nowrap text-middle text-right"
+ >
+ <ProfileDate />
+ </td>
+ <td
+ className="quality-profiles-table-date thin nowrap text-middle text-right"
+ >
+ <ProfileDate />
+ </td>
+ <td
+ className="quality-profiles-table-actions thin nowrap text-middle text-right"
+ >
+ <withRouter(ProfileActions)
+ fromList={true}
+ organization={null}
+ profile={
+ Object {
+ "activeDeprecatedRuleCount": 10,
+ "activeRuleCount": 10,
+ "childrenCount": 0,
+ "depth": 1,
+ "isBuiltIn": false,
+ "isDefault": false,
+ "isInherited": false,
+ "key": "key",
+ "language": "js",
+ "languageName": "JavaScript",
+ "name": "name",
+ "organization": "foo",
+ "projectCount": 3,
+ }
+ }
+ updateProfiles={[MockFunction]}
+ />
+ </td>
+</tr>
+`;
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx b/server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx
index 893a586e3f6..1ba621cadb7 100644
--- a/server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx
+++ b/server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx
@@ -17,19 +17,16 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { Location } from 'history';
import * as React from 'react';
import { connect } from 'react-redux';
import { getReturnUrl } from 'sonar-ui-common/helpers/urls';
import { getIdentityProviders } from '../../../api/users';
-import { isSonarCloud } from '../../../helpers/system';
import { doLogin } from '../../../store/rootActions';
import Login from './Login';
-import LoginSonarCloud from './LoginSonarCloud';
interface OwnProps {
- location: {
- hash?: string;
- pathName: string;
+ location: Pick<Location, 'hash' | 'pathname' | 'query'> & {
query: { advanced?: string; return_to?: string };
};
}
@@ -44,7 +41,7 @@ interface State {
identityProviders?: T.IdentityProvider[];
}
-class LoginContainer extends React.PureComponent<Props, State> {
+export class LoginContainer extends React.PureComponent<Props, State> {
mounted = false;
state: State = {};
@@ -52,11 +49,9 @@ class LoginContainer extends React.PureComponent<Props, State> {
componentDidMount() {
this.mounted = true;
getIdentityProviders().then(
- identityProvidersResponse => {
+ ({ identityProviders }) => {
if (this.mounted) {
- this.setState({
- identityProviders: identityProvidersResponse.identityProviders
- });
+ this.setState({ identityProviders });
}
},
() => {}
@@ -78,21 +73,11 @@ class LoginContainer extends React.PureComponent<Props, State> {
render() {
const { location } = this.props;
const { identityProviders } = this.state;
+
if (!identityProviders) {
return null;
}
- if (isSonarCloud()) {
- return (
- <LoginSonarCloud
- identityProviders={identityProviders}
- onSubmit={this.handleSubmit}
- returnTo={getReturnUrl(location)}
- showForm={location.query['advanced'] !== undefined}
- />
- );
- }
-
return (
<Login
identityProviders={identityProviders}
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/LoginSonarCloud.tsx b/server/sonar-web/src/main/js/apps/sessions/components/LoginSonarCloud.tsx
deleted file mode 100644
index c16147c3b5c..00000000000
--- a/server/sonar-web/src/main/js/apps/sessions/components/LoginSonarCloud.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as classNames from 'classnames';
-import * as React from 'react';
-import { connect } from 'react-redux';
-import { Alert } from 'sonar-ui-common/components/ui/Alert';
-import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
-import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
-import { Store } from '../../../store/rootReducer';
-import LoginForm from './LoginForm';
-import './LoginSonarCloud.css';
-import OAuthProviders from './OAuthProviders';
-
-interface Props {
- identityProviders: T.IdentityProvider[];
- onSubmit: (login: string, password: string) => Promise<void>;
- returnTo: string;
- showForm?: boolean;
- authorizationError?: boolean;
- authenticationError?: boolean;
-}
-
-function formatLabel(name: string) {
- return translateWithParameters('login.with_x', name);
-}
-
-export function LoginSonarCloud({
- showForm,
- identityProviders,
- returnTo,
- onSubmit,
- authorizationError,
- authenticationError
-}: Props) {
- const displayForm = showForm || identityProviders.length <= 0;
- const displayErrorAction = authorizationError || authenticationError;
- return (
- <>
- {displayErrorAction && (
- <Alert className="sonarcloud-login-alert" display="block" variant="warning">
- {translate('login.unauthorized_access_alert')}
- </Alert>
- )}
- <div
- className={classNames('sonarcloud-login-page boxed-group boxed-group-inner', {
- 'sonarcloud-login-page-large': displayForm
- })}
- id="login_form">
- <div className="text-center">
- <img
- alt="SonarCloud logo"
- height={36}
- src={`${getBaseUrl()}/images/sonarcloud-square-logo.svg`}
- width={36}
- />
- <h1 className="sonarcloud-login-title">
- {translate('login.login_or_signup_to_sonarcloud')}
- </h1>
- </div>
-
- {displayForm ? (
- <LoginForm onSubmit={onSubmit} returnTo={returnTo} />
- ) : (
- <OAuthProviders
- className="sonarcloud-oauth-providers"
- formatLabel={formatLabel}
- identityProviders={identityProviders}
- returnTo={returnTo}
- />
- )}
-
- {displayErrorAction && (
- <div className="sonarcloud-login-cancel">
- <div className="horizontal-pipe-separator">
- <div className="horizontal-separator" />
- <span className="note">{translate('or')}</span>
- <div className="horizontal-separator" />
- </div>
- <a href={`${getBaseUrl()}/`}>{translate('go_back_to_homepage')}</a>
- </div>
- )}
- </div>
- </>
- );
-}
-
-const mapStateToProps = (state: Store) => ({
- authorizationError: state.appState.authorizationError,
- authenticationError: state.appState.authenticationError
-});
-
-export default connect(mapStateToProps)(LoginSonarCloud);
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/LoginContainer-test.tsx b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/LoginContainer-test.tsx
new file mode 100644
index 00000000000..81f0b90e9b7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/LoginContainer-test.tsx
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { getIdentityProviders } from '../../../../api/users';
+import { mockLocation } from '../../../../helpers/testMocks';
+import { LoginContainer } from '../LoginContainer';
+
+jest.mock('../../../../api/users', () => {
+ const { mockIdentityProvider } = jest.requireActual('../../../../helpers/testMocks');
+ return {
+ getIdentityProviders: jest
+ .fn()
+ .mockResolvedValue({ identityProviders: [mockIdentityProvider()] })
+ };
+});
+
+beforeEach(jest.clearAllMocks);
+
+it('should render correctly', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper).toMatchSnapshot();
+ expect(getIdentityProviders).toBeCalled();
+});
+
+it('should not provide any options if no IdPs are present', async () => {
+ (getIdentityProviders as jest.Mock).mockResolvedValue({});
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.type()).toBeNull();
+ expect(getIdentityProviders).toBeCalled();
+});
+
+it('should handle submission', () => {
+ const doLogin = jest.fn().mockResolvedValue(null);
+ const wrapper = shallowRender({ doLogin });
+ wrapper.instance().handleSubmit('user', 'pass');
+ expect(doLogin).toBeCalledWith('user', 'pass');
+});
+
+function shallowRender(props: Partial<LoginContainer['props']> = {}) {
+ return shallow<LoginContainer>(
+ <LoginContainer doLogin={jest.fn()} location={mockLocation()} {...props} />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/LoginSonarCloud-test.tsx b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/LoginSonarCloud-test.tsx
deleted file mode 100644
index f922a702efa..00000000000
--- a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/LoginSonarCloud-test.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import { LoginSonarCloud } from '../LoginSonarCloud';
-
-const identityProvider = {
- backgroundColor: '#000',
- iconPath: '/some/path',
- key: 'foo',
- name: 'foo'
-};
-
-it('logs in with identity provider', () => {
- const wrapper = shallow(
- <LoginSonarCloud identityProviders={[identityProvider]} onSubmit={jest.fn()} returnTo="" />
- );
- expect(wrapper).toMatchSnapshot();
-});
-
-it('logs in with simple form', () => {
- expect(
- shallow(
- <LoginSonarCloud
- identityProviders={[identityProvider]}
- onSubmit={jest.fn()}
- returnTo=""
- showForm={true}
- />
- )
- ).toMatchSnapshot();
- expect(
- shallow(<LoginSonarCloud identityProviders={[]} onSubmit={jest.fn()} returnTo="" />)
- ).toMatchSnapshot();
-});
-
-it("shows an warning message if there's an authorization error", () => {
- const wrapper = shallow(
- <LoginSonarCloud
- authorizationError={true}
- identityProviders={[identityProvider]}
- onSubmit={jest.fn()}
- returnTo=""
- />
- );
- expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/LoginContainer-test.tsx.snap b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/LoginContainer-test.tsx.snap
new file mode 100644
index 00000000000..dd0cc38ae08
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/LoginContainer-test.tsx.snap
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Login
+ identityProviders={
+ Array [
+ Object {
+ "backgroundColor": "#000000",
+ "iconPath": "/path/icon.svg",
+ "key": "github",
+ "name": "Github",
+ },
+ ]
+ }
+ onSubmit={[Function]}
+ returnTo="/"
+/>
+`;
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/LoginSonarCloud-test.tsx.snap b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/LoginSonarCloud-test.tsx.snap
deleted file mode 100644
index 56308c07525..00000000000
--- a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/LoginSonarCloud-test.tsx.snap
+++ /dev/null
@@ -1,170 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`logs in with identity provider 1`] = `
-<Fragment>
- <div
- className="sonarcloud-login-page boxed-group boxed-group-inner"
- id="login_form"
- >
- <div
- className="text-center"
- >
- <img
- alt="SonarCloud logo"
- height={36}
- src="/images/sonarcloud-square-logo.svg"
- width={36}
- />
- <h1
- className="sonarcloud-login-title"
- >
- login.login_or_signup_to_sonarcloud
- </h1>
- </div>
- <OAuthProviders
- className="sonarcloud-oauth-providers"
- formatLabel={[Function]}
- identityProviders={
- Array [
- Object {
- "backgroundColor": "#000",
- "iconPath": "/some/path",
- "key": "foo",
- "name": "foo",
- },
- ]
- }
- returnTo=""
- />
- </div>
-</Fragment>
-`;
-
-exports[`logs in with simple form 1`] = `
-<Fragment>
- <div
- className="sonarcloud-login-page boxed-group boxed-group-inner sonarcloud-login-page-large"
- id="login_form"
- >
- <div
- className="text-center"
- >
- <img
- alt="SonarCloud logo"
- height={36}
- src="/images/sonarcloud-square-logo.svg"
- width={36}
- />
- <h1
- className="sonarcloud-login-title"
- >
- login.login_or_signup_to_sonarcloud
- </h1>
- </div>
- <LoginForm
- onSubmit={[MockFunction]}
- returnTo=""
- />
- </div>
-</Fragment>
-`;
-
-exports[`logs in with simple form 2`] = `
-<Fragment>
- <div
- className="sonarcloud-login-page boxed-group boxed-group-inner sonarcloud-login-page-large"
- id="login_form"
- >
- <div
- className="text-center"
- >
- <img
- alt="SonarCloud logo"
- height={36}
- src="/images/sonarcloud-square-logo.svg"
- width={36}
- />
- <h1
- className="sonarcloud-login-title"
- >
- login.login_or_signup_to_sonarcloud
- </h1>
- </div>
- <LoginForm
- onSubmit={[MockFunction]}
- returnTo=""
- />
- </div>
-</Fragment>
-`;
-
-exports[`shows an warning message if there's an authorization error 1`] = `
-<Fragment>
- <Alert
- className="sonarcloud-login-alert"
- display="block"
- variant="warning"
- >
- login.unauthorized_access_alert
- </Alert>
- <div
- className="sonarcloud-login-page boxed-group boxed-group-inner"
- id="login_form"
- >
- <div
- className="text-center"
- >
- <img
- alt="SonarCloud logo"
- height={36}
- src="/images/sonarcloud-square-logo.svg"
- width={36}
- />
- <h1
- className="sonarcloud-login-title"
- >
- login.login_or_signup_to_sonarcloud
- </h1>
- </div>
- <OAuthProviders
- className="sonarcloud-oauth-providers"
- formatLabel={[Function]}
- identityProviders={
- Array [
- Object {
- "backgroundColor": "#000",
- "iconPath": "/some/path",
- "key": "foo",
- "name": "foo",
- },
- ]
- }
- returnTo=""
- />
- <div
- className="sonarcloud-login-cancel"
- >
- <div
- className="horizontal-pipe-separator"
- >
- <div
- className="horizontal-separator"
- />
- <span
- className="note"
- >
- or
- </span>
- <div
- className="horizontal-separator"
- />
- </div>
- <a
- href="/"
- >
- go_back_to_homepage
- </a>
- </div>
- </div>
-</Fragment>
-`;
diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts
index 1952315df76..c71548e3aba 100644
--- a/server/sonar-web/src/main/js/helpers/testMocks.ts
+++ b/server/sonar-web/src/main/js/helpers/testMocks.ts
@@ -388,6 +388,15 @@ export function mockLoggedInUser(overrides: Partial<T.LoggedInUser> = {}): T.Log
};
}
+export function mockGroup(overrides: Partial<T.Group> = {}): T.Group {
+ return {
+ id: 1,
+ membersCount: 1,
+ name: 'Foo',
+ ...overrides
+ };
+}
+
export function mockEvent(overrides = {}) {
return {
target: { blur() {} },
@@ -831,3 +840,15 @@ export function mockFlowLocation(overrides: Partial<T.FlowLocation> = {}): T.Flo
...overrides
};
}
+
+export function mockIdentityProvider(
+ overrides: Partial<T.IdentityProvider> = {}
+): T.IdentityProvider {
+ return {
+ backgroundColor: '#000000',
+ iconPath: '/path/icon.svg',
+ key: 'github',
+ name: 'Github',
+ ...overrides
+ };
+}
diff --git a/server/sonar-web/src/main/js/store/__tests__/users-test.tsx b/server/sonar-web/src/main/js/store/__tests__/users-test.tsx
new file mode 100644
index 00000000000..daf880c924b
--- /dev/null
+++ b/server/sonar-web/src/main/js/store/__tests__/users-test.tsx
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+/* eslint-disable sonarjs/no-duplicate-string */
+import { mockCurrentUser, mockLoggedInUser, mockUser } from '../../helpers/testMocks';
+import reducer, {
+ getCurrentUser,
+ getCurrentUserSetting,
+ getUserByLogin,
+ getUsersByLogins,
+ receiveCurrentUser,
+ setCurrentUserSettingAction,
+ setHomePageAction,
+ skipOnboardingAction,
+ State
+} from '../users';
+
+describe('reducer and actions', () => {
+ it('should allow to receive the current user', () => {
+ const initialState: State = createState();
+
+ const currentUser = mockCurrentUser();
+ const newState = reducer(initialState, receiveCurrentUser(currentUser));
+ expect(newState).toEqual(createState({ currentUser }));
+ });
+
+ it('should allow to skip the onboarding tutorials', () => {
+ const currentUser = mockLoggedInUser({ showOnboardingTutorial: true });
+ const initialState: State = createState({ currentUser });
+
+ const newState = reducer(initialState, skipOnboardingAction());
+ expect(newState).toEqual(
+ createState({ currentUser: { ...currentUser, showOnboardingTutorial: false } })
+ );
+ });
+
+ it('should allow to set the homepage', () => {
+ const homepage: T.HomePage = { type: 'PROJECTS' };
+ const currentUser = mockLoggedInUser({ homepage: undefined });
+ const initialState: State = createState({ currentUser });
+
+ const newState = reducer(initialState, setHomePageAction(homepage));
+ expect(newState).toEqual(
+ createState({ currentUser: { ...currentUser, homepage } as T.LoggedInUser })
+ );
+ });
+
+ it('should allow to set a user setting', () => {
+ const setting1: T.CurrentUserSetting = { key: 'notifications.optOut', value: '1' };
+ const setting2: T.CurrentUserSetting = { key: 'notifications.readDate', value: '2' };
+ const setting3: T.CurrentUserSetting = { key: 'notifications.optOut', value: '2' };
+ const currentUser = mockLoggedInUser();
+ const initialState: State = createState({ currentUser });
+
+ const newState = reducer(initialState, setCurrentUserSettingAction(setting1));
+ expect(newState).toEqual(
+ createState({ currentUser: { ...currentUser, settings: [setting1] } as T.LoggedInUser })
+ );
+
+ const newerState = reducer(newState, setCurrentUserSettingAction(setting2));
+ expect(newerState).toEqual(
+ createState({
+ currentUser: { ...currentUser, settings: [setting1, setting2] } as T.LoggedInUser
+ })
+ );
+
+ const newestState = reducer(newerState, setCurrentUserSettingAction(setting3));
+ expect(newestState).toEqual(
+ createState({
+ currentUser: { ...currentUser, settings: [setting3, setting2] } as T.LoggedInUser
+ })
+ );
+ });
+});
+
+describe('getters', () => {
+ const currentUser = mockLoggedInUser({ settings: [{ key: 'notifications.optOut', value: '1' }] });
+ const jane = mockUser({ login: 'jane', name: 'Jane Doe' });
+ const john = mockUser({ login: 'john' });
+ const state = createState({ currentUser, usersByLogin: { jane, john } });
+
+ test('getCurrentUser', () => {
+ expect(getCurrentUser(state)).toBe(currentUser);
+ });
+
+ test('getCurrentUserSetting', () => {
+ expect(getCurrentUserSetting(state, 'notifications.optOut')).toBe('1');
+ expect(getCurrentUserSetting(state, 'notifications.readDate')).toBeUndefined();
+ });
+
+ test('getUserByLogin', () => {
+ expect(getUserByLogin(state, 'jane')).toBe(jane);
+ expect(getUserByLogin(state, 'steve')).toBeUndefined();
+ });
+
+ test('getUsersByLogins', () => {
+ expect(getUsersByLogins(state, ['jane', 'john'])).toEqual([jane, john]);
+ });
+});
+
+function createState(overrides: Partial<State> = {}): State {
+ return { usersByLogin: {}, userLogins: [], currentUser: mockCurrentUser(), ...overrides };
+}
diff --git a/server/sonar-web/src/main/js/store/users.ts b/server/sonar-web/src/main/js/store/users.ts
index df95d3bce26..e5411684707 100644
--- a/server/sonar-web/src/main/js/store/users.ts
+++ b/server/sonar-web/src/main/js/store/users.ts
@@ -46,7 +46,7 @@ export function receiveCurrentUser(user: T.CurrentUser) {
return { type: Actions.ReceiveCurrentUser, user };
}
-function skipOnboardingAction() {
+export function skipOnboardingAction() {
return { type: Actions.SkipOnboardingAction };
}
@@ -58,11 +58,11 @@ export function skipOnboarding() {
);
}
-function setHomePageAction(homepage: T.HomePage) {
+export function setHomePageAction(homepage: T.HomePage) {
return { type: Actions.SetHomePageAction, homepage };
}
-function setCurrentUserSettingAction(setting: T.CurrentUserSetting) {
+export function setCurrentUserSettingAction(setting: T.CurrentUserSetting) {
return { type: Actions.SetCurrentUserSetting, setting };
}