diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2020-01-03 09:29:51 +0100 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2020-01-06 20:46:13 +0100 |
commit | ccfd204b05b7e8b8c47273e5db78e9bd22128d60 (patch) | |
tree | 56c5adb3f0108f10d86f8169bb5375b2b8374756 /server | |
parent | e5d9b437e9ba5bbe933ee068dd31d7c716824428 (diff) | |
download | sonarqube-ccfd204b05b7e8b8c47273e5db78e9bd22128d60.tar.gz sonarqube-ccfd204b05b7e8b8c47273e5db78e9bd22128d60.zip |
Improve test coverage
Diffstat (limited to 'server')
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 }; } |