@@ -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 }} /> | |||
) : ( |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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} | |||
/> | |||
`; |
@@ -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"> |
@@ -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; |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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()} |
@@ -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} | |||
/> |
@@ -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> | |||
`; |
@@ -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" | |||
/> | |||
`; |
@@ -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> | |||
`; |
@@ -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); |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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(); |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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> |
@@ -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); |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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={ |
@@ -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> | |||
`; |
@@ -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} |
@@ -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); |
@@ -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} /> | |||
); | |||
} |
@@ -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(); | |||
}); |
@@ -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="/" | |||
/> | |||
`; |
@@ -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> | |||
`; |
@@ -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 | |||
}; | |||
} |
@@ -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 }; | |||
} |
@@ -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 }; | |||
} | |||