Browse Source

SONAR-16085 Adding IT and change select from activation modal

tags/9.4.0.54424
Mathieu Suen 2 years ago
parent
commit
63ad64c7c2

+ 176
- 0
server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts View File

@@ -0,0 +1,176 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 { cloneDeep, countBy } from 'lodash';
import { mockQualityProfile, mockRule, mockRuleRepository } from '../../helpers/testMocks';
import { RuleRepository } from '../../types/coding-rules';
import { SearchRulesQuery } from '../../types/rules';
import { Rule } from '../../types/types';
import {
bulkActivateRules,
bulkDeactivateRules,
Profile,
searchQualityProfiles,
SearchQualityProfilesParameters,
SearchQualityProfilesResponse
} from '../quality-profiles';
import { getRulesApp, searchRules } from '../rules';

interface FacetFilter {
languages?: string;
}

const FACET_RULE_MAP: { [key: string]: keyof Rule } = {
languages: 'lang',
types: 'type'
};
export default class CodingRulesMock {
defaultRules: Rule[] = [];
rules: Rule[] = [];
qualityProfile: Profile[] = [];
repositories: RuleRepository[] = [];
isAdmin = false;
applyWithWarning = false;

constructor() {
this.repositories = [
mockRuleRepository({ key: 'repo1' }),
mockRuleRepository({ key: 'repo2' })
];
this.qualityProfile = [
mockQualityProfile({ key: 'p1', name: 'QP Foo', language: 'java', languageName: 'Java' }),
mockQualityProfile({ key: 'p2', name: 'QP Bar', language: 'js' }),
mockQualityProfile({ key: 'p3', name: 'QP FooBar', language: 'java', languageName: 'Java' })
];

this.defaultRules = [
mockRule({
key: 'rule1',
type: 'BUG',
lang: 'java',
langName: 'Java',
name: 'Awsome java rule'
}),
mockRule({ key: 'rule2', name: 'Hot hotspot', type: 'SECURITY_HOTSPOT' }),
mockRule({ key: 'rule3', name: 'Unknown rule' }),
mockRule({ key: 'rule4', type: 'BUG', lang: 'c', langName: 'C', name: 'Awsome C rule' })
];

(searchRules as jest.Mock).mockImplementation(this.handleSearchRules);
(searchQualityProfiles as jest.Mock).mockImplementation(this.handleSearchQualityProfiles);
(getRulesApp as jest.Mock).mockImplementation(this.handleGetRulesApp);
(bulkActivateRules as jest.Mock).mockImplementation(this.handleBulkActivateRules);
(bulkDeactivateRules as jest.Mock).mockImplementation(this.handleBulkDeactivateRules);

this.rules = cloneDeep(this.defaultRules);
}

filterFacet({ languages }: FacetFilter) {
let filteredRules = this.rules;
if (languages) {
filteredRules = filteredRules.filter(r => r.lang && languages.includes(r.lang));
}
return filteredRules;
}

setIsAdmin() {
this.isAdmin = true;
}

activateWithWarning() {
this.applyWithWarning = true;
}

reset() {
this.isAdmin = false;
this.applyWithWarning = false;
this.rules = cloneDeep(this.defaultRules);
}

allRulesName() {
return this.rules.map(r => r.name);
}

allQualityProfile(language: string) {
return this.qualityProfile.filter(qp => qp.language === language);
}

handleSearchRules = ({ facets, languages, p, ps }: SearchRulesQuery) => {
const countFacet = (facets || '').split(',').map((facet: keyof Rule) => {
const facetCount = countBy(this.rules.map(r => r[FACET_RULE_MAP[facet] || facet] as string));
return {
property: facet,
values: Object.keys(facetCount).map(val => ({ val, count: facetCount[val] }))
};
});
const currentPs = ps || 10;
const currentP = p || 1;
const filteredRules = this.filterFacet({ languages });
const responseRules = filteredRules.slice((currentP - 1) * currentPs, currentP * currentPs);
return this.reply({
total: filteredRules.length,
p: currentP,
ps: currentPs,
rules: responseRules,
facets: countFacet
});
};

handleBulkActivateRules = () => {
if (this.applyWithWarning) {
return this.reply({
succeeded: this.rules.length - 1,
failed: 1,
errors: [{ msg: 'c rule c:S6069 cannot be activated on cpp profile SonarSource' }]
});
}
return this.reply({
succeeded: this.rules.length,
failed: 0,
errors: []
});
};

handleBulkDeactivateRules = () => {
return this.reply({
succeeded: this.rules.length,
failed: 0
});
};

handleSearchQualityProfiles = ({ language }: SearchQualityProfilesParameters = {}): Promise<
SearchQualityProfilesResponse
> => {
let profiles: Profile[] = this.isAdmin
? this.qualityProfile.map(p => ({ ...p, actions: { edit: true } }))
: this.qualityProfile;
if (language) {
profiles = profiles.filter(p => p.language === language);
}
return this.reply({ profiles });
};

handleGetRulesApp = () => {
return this.reply({ canWrite: this.isAdmin, repositories: this.repositories });
};

reply<T>(response: T): Promise<T> {
return Promise.resolve(cloneDeep(response));
}
}

+ 2
- 28
server/sonar-web/src/main/js/api/rules.ts View File

@@ -20,40 +20,14 @@
import throwGlobalError from '../app/utils/throwGlobalError';
import { getJSON, post, postJSON } from '../helpers/request';
import { GetRulesAppResponse, SearchRulesResponse } from '../types/coding-rules';
import { SearchRulesQuery } from '../types/rules';
import { RuleActivation, RuleDetails } from '../types/types';

export function getRulesApp(): Promise<GetRulesAppResponse> {
return getJSON('/api/rules/app').catch(throwGlobalError);
}

export function searchRules(data: {
activation?: boolean | string;
active_severities?: string;
asc?: boolean | string;
available_since?: string;
cwe?: string;
f?: string;
facets?: string;
include_external?: boolean | string;
inheritance?: string;
is_template?: boolean | string;
languages?: string;
owaspTop10?: string;
p?: number;
ps?: number;
q?: string;
qprofile?: string;
repositories?: string;
rule_key?: string;
s?: string;
sansTop25?: string;
severities?: string;
sonarsourceSecurity?: string;
statuses?: string;
tags?: string;
template_key?: string;
types?: string;
}): Promise<SearchRulesResponse> {
export function searchRules(data: SearchRulesQuery): Promise<SearchRulesResponse> {
return getJSON('/api/rules/search', data).catch(throwGlobalError);
}


+ 197
- 0
server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts View File

@@ -0,0 +1,197 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import CodingRulesMock from '../../../api/mocks/CodingRulesMock';
import { mockLoggedInUser } from '../../../helpers/testMocks';
import { renderApp } from '../../../helpers/testReactTestingUtils';
import { CurrentUser } from '../../../types/users';
import routes from '../routes';

jest.mock('../../../api/rules');
jest.mock('../../../api/quality-profiles');

let handler: CodingRulesMock;

beforeAll(() => {
window.scrollTo = jest.fn();
handler = new CodingRulesMock();
});

afterEach(() => handler.reset());

jest.setTimeout(10_000);

it('should list all rules', async () => {
renderCodingRulesApp();

await waitFor(() => {
handler
.allRulesName()
.forEach(name => expect(screen.getByRole('link', { name })).toBeInTheDocument());
});
});

it('should have all type facet', async () => {
renderCodingRulesApp();

await waitFor(() => {
[
'issue.type.BUG',
'issue.type.VULNERABILITY',
'issue.type.CODE_SMELL',
'issue.type.SECURITY_HOTSPOT'
].forEach(name => expect(screen.getByRole('link', { name })).toBeInTheDocument());
});
});

it('select the correct quality profile for bulk change base on language search', async () => {
const user = userEvent.setup();
handler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser());
const selectQP = handler.allQualityProfile('js')[0];

await user.click(await screen.findByRole('link', { name: 'JavaScript' }));
await user.click(await screen.findByRole('button', { name: 'bulk_change' }));
await user.click(await screen.findByRole('link', { name: 'coding_rules.activate_in…' }));
const dialog = screen.getByRole('dialog', {
name: 'coding_rules.activate_in_quality_profile (2 coding_rules._rules)'
});

expect(dialog).toBeInTheDocument();
const dialogScreen = within(dialog);
expect(dialogScreen.getByText(`${selectQP.name} - ${selectQP.languageName}`)).toBeInTheDocument();
});

it('no quality profile for bulk cahnge base on language search', async () => {
const user = userEvent.setup();
handler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser());

await user.click(await screen.findByRole('link', { name: 'C' }));
await user.click(await screen.findByRole('button', { name: 'bulk_change' }));
await user.click(await screen.findByRole('link', { name: 'coding_rules.activate_in…' }));
const dialog = screen.getByRole('dialog', {
name: 'coding_rules.activate_in_quality_profile (1 coding_rules._rules)'
});

expect(dialog).toBeInTheDocument();
const dialogScreen = within(dialog);
await user.click(dialogScreen.getByRole('textbox', { name: 'coding_rules.activate_in' }));
expect(dialogScreen.getByText('coding_rules.bulk_change.no_quality_profile')).toBeInTheDocument();
});

it('should be able to bulk activate quality profile', async () => {
const user = userEvent.setup();
handler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser());

const selectQPSuccess = handler.allQualityProfile('java')[0];
const selectQPWarning = handler.allQualityProfile('java')[1];

await user.click(await screen.findByRole('button', { name: 'bulk_change' }));
await user.click(await screen.findByRole('link', { name: 'coding_rules.activate_in…' }));

const dialog = screen.getByRole('dialog', {
name: 'coding_rules.activate_in_quality_profile (4 coding_rules._rules)'
});
expect(dialog).toBeInTheDocument();

let dialogScreen = within(dialog);
await user.click(dialogScreen.getByRole('textbox', { name: 'coding_rules.activate_in' }));
await user.click(
dialogScreen.getByText(`${selectQPSuccess.name} - ${selectQPSuccess.languageName}`)
);
expect(
dialogScreen.getByText(`${selectQPSuccess.name} - ${selectQPSuccess.languageName}`)
).toBeInTheDocument();

await user.click(dialogScreen.getByRole('button', { name: 'apply' }));
expect(
dialogScreen.getByText(
`coding_rules.bulk_change.success.${selectQPSuccess.name}.${selectQPSuccess.languageName}.${
handler.allRulesName().length
}`
)
).toBeInTheDocument();

await user.click(dialogScreen.getByRole('button', { name: 'close' }));

// Try bulk change when quality profile has warnning.
handler.activateWithWarning();

await user.click(await screen.findByRole('button', { name: 'bulk_change' }));
await user.click(await screen.findByRole('link', { name: 'coding_rules.activate_in…' }));
dialogScreen = within(
screen.getByRole('dialog', {
name: 'coding_rules.activate_in_quality_profile (4 coding_rules._rules)'
})
);
await user.click(dialogScreen.getByRole('textbox', { name: 'coding_rules.activate_in' }));
await user.click(
dialogScreen.getByText(`${selectQPWarning.name} - ${selectQPWarning.languageName}`)
);
await user.click(dialogScreen.getByRole('button', { name: 'apply' }));
expect(
dialogScreen.getByText(
`coding_rules.bulk_change.warning.${selectQPWarning.name}.${
selectQPWarning.languageName
}.${handler.allRulesName().length - 1}.1`
)
).toBeInTheDocument();
});

it('should be able to bulk deactivate quality profile', async () => {
const user = userEvent.setup();
handler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser());

const selectQP = handler.allQualityProfile('java')[0];

await user.click(await screen.findByRole('button', { name: 'bulk_change' }));
await user.click(await screen.findByRole('link', { name: 'coding_rules.deactivate_in…' }));
const dialogScreen = within(
screen.getByRole('dialog', {
name: 'coding_rules.deactivate_in_quality_profile (4 coding_rules._rules)'
})
);
await user.click(dialogScreen.getByRole('textbox', { name: 'coding_rules.deactivate_in' }));

await user.click(dialogScreen.getByText(`${selectQP.name} - ${selectQP.languageName}`));
await user.click(dialogScreen.getByRole('button', { name: 'apply' }));
expect(
dialogScreen.getByText(
`coding_rules.bulk_change.success.${selectQP.name}.${selectQP.languageName}.${
handler.allRulesName().length
}`
)
).toBeInTheDocument();
});

function renderCodingRulesApp(currentUser?: CurrentUser) {
renderApp('coding_rules', routes, {
currentUser,
languages: {
js: { key: 'js', name: 'JavaScript' },
java: { key: 'java', name: 'Java' },
c: { key: 'c', name: 'C' }
}
});
}

+ 50
- 31
server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx View File

@@ -19,10 +19,11 @@
*/
import classNames from 'classnames';
import * as React from 'react';
import { components, OptionProps, OptionTypeBase, SingleValueProps } from 'react-select';
import { activateRule, Profile } from '../../../api/quality-profiles';
import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
import Modal from '../../../components/controls/Modal';
import SelectLegacy from '../../../components/controls/SelectLegacy';
import Select from '../../../components/controls/Select';
import SeverityHelper from '../../../components/shared/SeverityHelper';
import { Alert } from '../../../components/ui/Alert';
import { SEVERITIES } from '../../../helpers/constants';
@@ -40,9 +41,13 @@ interface Props {
rule: Rule | RuleDetails;
}

interface ProfileWithDeph extends Profile {
depth: number;
}

interface State {
params: Dict<string>;
profile: string;
profile?: ProfileWithDeph;
severity: string;
submitting: boolean;
}
@@ -55,7 +60,7 @@ export default class ActivationFormModal extends React.PureComponent<Props, Stat
const profilesWithDepth = this.getQualityProfilesWithDepth(props);
this.state = {
params: this.getParams(props),
profile: profilesWithDepth.length > 0 ? profilesWithDepth[0].key : '',
profile: profilesWithDepth.length > 0 ? profilesWithDepth[0] : undefined,
severity: props.activation ? props.activation.severity : props.rule.severity,
submitting: false
};
@@ -105,7 +110,7 @@ export default class ActivationFormModal extends React.PureComponent<Props, Stat
event.preventDefault();
this.setState({ submitting: true });
const data = {
key: this.state.profile,
key: this.state.profile?.key || '',
params: this.state.params,
rule: this.props.rule.key,
severity: this.state.severity
@@ -132,18 +137,14 @@ export default class ActivationFormModal extends React.PureComponent<Props, Stat
this.setState((state: State) => ({ params: { ...state.params, [name]: value } }));
};

handleProfileChange = ({ value }: { value: string }) => {
this.setState({ profile: value });
handleProfileChange = (profile: ProfileWithDeph) => {
this.setState({ profile });
};

handleSeverityChange = ({ value }: { value: string }) => {
handleSeverityChange = ({ value }: OptionTypeBase) => {
this.setState({ severity: value });
};

renderSeverityOption = ({ value }: { value: string }) => {
return <SeverityHelper severity={value} />;
};

render() {
const { activation, rule } = this.props;
const { profile, severity, submitting } = this.state;
@@ -152,6 +153,26 @@ export default class ActivationFormModal extends React.PureComponent<Props, Stat
const isCustomRule = !!(rule as RuleDetails).templateKey;
const activeInAllProfiles = profilesWithDepth.length <= 0;
const isUpdateMode = !!activation;
const serverityOption = SEVERITIES.map(severity => ({
label: translate('severity', severity),
value: severity
}));

function Option(props: OptionProps<OptionTypeBase, false>) {
return (
<components.Option {...props}>
<SeverityHelper severity={props.data.value} />
</components.Option>
);
}

function SingleValue(props: SingleValueProps<OptionTypeBase>) {
return (
<components.SingleValue {...props}>
<SeverityHelper className="coding-rules-severity-value" severity={props.data.value} />
</components.SingleValue>
);
}

return (
<Modal contentLabel={this.props.modalHeader} onRequestClose={this.props.onClose} size="small">
@@ -166,34 +187,32 @@ export default class ActivationFormModal extends React.PureComponent<Props, Stat
)}

<div className="modal-field">
<label>{translate('coding_rules.quality_profile')}</label>
<SelectLegacy
<label id="coding-rules-quality-profile-select">
{translate('coding_rules.quality_profile')}
</label>
<Select
className="js-profile"
clearable={false}
disabled={submitting || profilesWithDepth.length === 1}
aria-labelledby="coding-rules-quality-profile-select"
isClearable={false}
isDisabled={submitting || profilesWithDepth.length === 1}
onChange={this.handleProfileChange}
options={profilesWithDepth.map(profile => ({
label: ' '.repeat(profile.depth) + profile.name,
value: profile.key
}))}
getOptionLabel={p => ' '.repeat(p.depth) + p.name}
options={profilesWithDepth}
value={profile}
/>
</div>
<div className="modal-field">
<label>{translate('severity')}</label>
<SelectLegacy
<label id="coding-rules-severity-select">{translate('severity')}</label>
<Select
className="js-severity"
clearable={false}
disabled={submitting}
isClearable={false}
isDisabled={submitting}
aria-labelledby="coding-rules-severity-select"
onChange={this.handleSeverityChange}
optionRenderer={this.renderSeverityOption}
options={SEVERITIES.map(severity => ({
label: translate('severity', severity),
value: severity
}))}
searchable={false}
value={severity}
valueRenderer={this.renderSeverityOption}
components={{ Option, SingleValue }}
options={serverityOption}
isSearchable={false}
value={serverityOption.find(s => s.value === severity)}
/>
</div>
{isCustomRule ? (

+ 1
- 3
server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx View File

@@ -150,9 +150,7 @@ export class BulkChangeModal extends React.PureComponent<Props, State> {
renderResult = (result: ActivationResult) => {
const { profile: profileKey } = result;
const profile = this.props.referencedProfiles[profileKey];
if (!profile) {
return null;
}

const { languages } = this.props;
const language = languages[profile.language]
? languages[profile.language].name

+ 2
- 3
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx View File

@@ -38,7 +38,6 @@ import RuleDetailsProfiles from './RuleDetailsProfiles';
interface Props {
allowCustomRules?: boolean;
canWrite?: boolean;
hideQualityProfiles?: boolean;
onActivate: (profile: string, rule: string, activation: Activation) => void;
onDeactivate: (profile: string, rule: string) => void;
onDelete: (rule: string) => void;
@@ -156,7 +155,7 @@ export default class RuleDetails extends React.PureComponent<Props, State> {
return <div className="coding-rule-details" />;
}

const { allowCustomRules, canWrite, hideQualityProfiles, referencedProfiles } = this.props;
const { allowCustomRules, canWrite, referencedProfiles } = this.props;
const { params = [] } = ruleDetails;

const isCustom = !!ruleDetails.templateKey;
@@ -236,7 +235,7 @@ export default class RuleDetails extends React.PureComponent<Props, State> {
/>
)}

{!ruleDetails.isTemplate && !hideQualityProfiles && (
{!ruleDetails.isTemplate && (
<RuleDetailsProfiles
activations={this.state.actives}
canWrite={canWrite}

+ 28
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/ActivationFormModal-test.tsx View File

@@ -19,6 +19,7 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { activateRule } from '../../../../api/quality-profiles';
import {
mockQualityProfile,
mockRule,
@@ -28,6 +29,10 @@ import {
} from '../../../../helpers/testMocks';
import ActivationFormModal from '../ActivationFormModal';

jest.mock('../../../../api/quality-profiles', () => ({
activateRule: jest.fn().mockResolvedValueOnce({})
}));

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
expect(
@@ -47,6 +52,29 @@ it('should render correctly', () => {
expect(wrapper).toMatchSnapshot('submitting');
});

it('should activate rule on quality profile when submit', () => {
const wrapper = shallowRender();
wrapper.instance().handleFormSubmit(({
preventDefault: jest.fn()
} as any) as React.SyntheticEvent<HTMLFormElement>);
expect(activateRule).toHaveBeenCalledWith({
key: '',
params: {
'1': '1',
'2': '1'
},
rule: 'javascript:S1067',
severity: 'MAJOR'
});
});

it('should handle profile change correctly', () => {
const wrapper = shallowRender();
const qualityProfile = mockQualityProfile();
wrapper.instance().handleProfileChange(qualityProfile);
expect(wrapper.state().profile).toBe(qualityProfile);
});

function shallowRender(props: Partial<ActivationFormModal['props']> = {}) {
return shallow<ActivationFormModal>(
<ActivationFormModal

+ 0
- 123
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/BulkChangeModal-test.tsx View File

@@ -1,123 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 { bulkActivateRules, bulkDeactivateRules } from '../../../../api/quality-profiles';
import { mockLanguage, mockQualityProfile } from '../../../../helpers/testMocks';
import { submit, waitAndUpdate } from '../../../../helpers/testUtils';
import { Query } from '../../query';
import { BulkChangeModal } from '../BulkChangeModal';

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(
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([profile]);
});

it('should handle profile selection', () => {
const wrapper = shallowRender();
const profiles = [mockQualityProfile({ name: 'foo' }), mockQualityProfile({ name: 'bar' })];
wrapper.instance().handleProfileSelect(profiles);
expect(wrapper.state().selectedProfiles).toEqual(profiles);
});

it('should handle form submission', async () => {
const wrapper = shallowRender({ profile: undefined });
const profiles = [
mockQualityProfile({ name: 'foo', key: 'foo' }),
mockQualityProfile({ name: 'bar', key: 'bar' })
];
wrapper.setState({ selectedProfiles: profiles });

// 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()}
profile={mockQualityProfile()}
query={{ languages: ['js'] } as Query}
referencedProfiles={{
foo: mockQualityProfile({ key: 'foo' }),
bar: mockQualityProfile({ key: 'bar' })
}}
total={42}
{...props}
/>
);
}

+ 174
- 67
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/ActivationFormModal-test.tsx.snap View File

@@ -27,30 +27,42 @@ exports[`should render correctly: custom rule 1`] = `
<div
className="modal-field"
>
<label>
<label
id="coding-rules-quality-profile-select"
>
coding_rules.quality_profile
</label>
<SelectLegacy
<Select
aria-labelledby="coding-rules-quality-profile-select"
className="js-profile"
clearable={false}
disabled={false}
getOptionLabel={[Function]}
isClearable={false}
isDisabled={false}
onChange={[Function]}
options={Array []}
value=""
/>
</div>
<div
className="modal-field"
>
<label>
<label
id="coding-rules-severity-select"
>
severity
</label>
<SelectLegacy
<Select
aria-labelledby="coding-rules-severity-select"
className="js-severity"
clearable={false}
disabled={false}
components={
Object {
"Option": [Function],
"SingleValue": [Function],
}
}
isClearable={false}
isDisabled={false}
isSearchable={false}
onChange={[Function]}
optionRenderer={[Function]}
options={
Array [
Object {
@@ -75,9 +87,12 @@ exports[`should render correctly: custom rule 1`] = `
},
]
}
searchable={false}
value="MAJOR"
valueRenderer={[Function]}
value={
Object {
"label": "severity.MAJOR",
"value": "MAJOR",
}
}
/>
</div>
<div
@@ -136,30 +151,42 @@ exports[`should render correctly: default 1`] = `
<div
className="modal-field"
>
<label>
<label
id="coding-rules-quality-profile-select"
>
coding_rules.quality_profile
</label>
<SelectLegacy
<Select
aria-labelledby="coding-rules-quality-profile-select"
className="js-profile"
clearable={false}
disabled={false}
getOptionLabel={[Function]}
isClearable={false}
isDisabled={false}
onChange={[Function]}
options={Array []}
value=""
/>
</div>
<div
className="modal-field"
>
<label>
<label
id="coding-rules-severity-select"
>
severity
</label>
<SelectLegacy
<Select
aria-labelledby="coding-rules-severity-select"
className="js-severity"
clearable={false}
disabled={false}
components={
Object {
"Option": [Function],
"SingleValue": [Function],
}
}
isClearable={false}
isDisabled={false}
isSearchable={false}
onChange={[Function]}
optionRenderer={[Function]}
options={
Array [
Object {
@@ -184,9 +211,12 @@ exports[`should render correctly: default 1`] = `
},
]
}
searchable={false}
value="MAJOR"
valueRenderer={[Function]}
value={
Object {
"label": "severity.MAJOR",
"value": "MAJOR",
}
}
/>
</div>
<div
@@ -280,30 +310,42 @@ exports[`should render correctly: submitting 1`] = `
<div
className="modal-field"
>
<label>
<label
id="coding-rules-quality-profile-select"
>
coding_rules.quality_profile
</label>
<SelectLegacy
<Select
aria-labelledby="coding-rules-quality-profile-select"
className="js-profile"
clearable={false}
disabled={true}
getOptionLabel={[Function]}
isClearable={false}
isDisabled={true}
onChange={[Function]}
options={Array []}
value=""
/>
</div>
<div
className="modal-field"
>
<label>
<label
id="coding-rules-severity-select"
>
severity
</label>
<SelectLegacy
<Select
aria-labelledby="coding-rules-severity-select"
className="js-severity"
clearable={false}
disabled={true}
components={
Object {
"Option": [Function],
"SingleValue": [Function],
}
}
isClearable={false}
isDisabled={true}
isSearchable={false}
onChange={[Function]}
optionRenderer={[Function]}
options={
Array [
Object {
@@ -328,9 +370,12 @@ exports[`should render correctly: submitting 1`] = `
},
]
}
searchable={false}
value="MAJOR"
valueRenderer={[Function]}
value={
Object {
"label": "severity.MAJOR",
"value": "MAJOR",
}
}
/>
</div>
<div
@@ -422,30 +467,42 @@ exports[`should render correctly: update mode 1`] = `
<div
className="modal-field"
>
<label>
<label
id="coding-rules-quality-profile-select"
>
coding_rules.quality_profile
</label>
<SelectLegacy
<Select
aria-labelledby="coding-rules-quality-profile-select"
className="js-profile"
clearable={false}
disabled={false}
getOptionLabel={[Function]}
isClearable={false}
isDisabled={false}
onChange={[Function]}
options={Array []}
value=""
/>
</div>
<div
className="modal-field"
>
<label>
<label
id="coding-rules-severity-select"
>
severity
</label>
<SelectLegacy
<Select
aria-labelledby="coding-rules-severity-select"
className="js-severity"
clearable={false}
disabled={false}
components={
Object {
"Option": [Function],
"SingleValue": [Function],
}
}
isClearable={false}
isDisabled={false}
isSearchable={false}
onChange={[Function]}
optionRenderer={[Function]}
options={
Array [
Object {
@@ -470,9 +527,12 @@ exports[`should render correctly: update mode 1`] = `
},
]
}
searchable={false}
value="MAJOR"
valueRenderer={[Function]}
value={
Object {
"label": "severity.MAJOR",
"value": "MAJOR",
}
}
/>
</div>
<div
@@ -561,37 +621,81 @@ exports[`should render correctly: with deep profiles 1`] = `
<div
className="modal-field"
>
<label>
<label
id="coding-rules-quality-profile-select"
>
coding_rules.quality_profile
</label>
<SelectLegacy
<Select
aria-labelledby="coding-rules-quality-profile-select"
className="js-profile"
clearable={false}
disabled={true}
getOptionLabel={[Function]}
isClearable={false}
isDisabled={true}
onChange={[Function]}
options={
Array [
Object {
"label": "name",
"value": "key",
"actions": Object {
"edit": true,
},
"activeDeprecatedRuleCount": 2,
"activeRuleCount": 10,
"childrenCount": 0,
"depth": 0,
"isBuiltIn": false,
"isDefault": false,
"isInherited": false,
"key": "key",
"language": "js",
"languageName": "JavaScript",
"name": "name",
"projectCount": 3,
},
]
}
value="key"
value={
Object {
"actions": Object {
"edit": true,
},
"activeDeprecatedRuleCount": 2,
"activeRuleCount": 10,
"childrenCount": 0,
"depth": 0,
"isBuiltIn": false,
"isDefault": false,
"isInherited": false,
"key": "key",
"language": "js",
"languageName": "JavaScript",
"name": "name",
"projectCount": 3,
}
}
/>
</div>
<div
className="modal-field"
>
<label>
<label
id="coding-rules-severity-select"
>
severity
</label>
<SelectLegacy
<Select
aria-labelledby="coding-rules-severity-select"
className="js-severity"
clearable={false}
disabled={false}
components={
Object {
"Option": [Function],
"SingleValue": [Function],
}
}
isClearable={false}
isDisabled={false}
isSearchable={false}
onChange={[Function]}
optionRenderer={[Function]}
options={
Array [
Object {
@@ -616,9 +720,12 @@ exports[`should render correctly: with deep profiles 1`] = `
},
]
}
searchable={false}
value="MAJOR"
valueRenderer={[Function]}
value={
Object {
"label": "severity.MAJOR",
"value": "MAJOR",
}
}
/>
</div>
<div

+ 0
- 313
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap View File

@@ -1,313 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

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
id="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
id="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
id="coding-rules-bulk-change-profile"
>
coding_rules.activate_in
</label>
</h3>
<Select
aria-labelledby="coding-rules-bulk-change-profile"
getOptionLabel={[Function]}
getOptionValue={[Function]}
isClearable={false}
isMulti={true}
isSearchable={true}
noOptionsMessage={[Function]}
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]}
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"
>
<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"
>
<h3>
<label
id="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: 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>
`;

+ 2
- 1
server/sonar-web/src/main/js/apps/coding-rules/styles.css View File

@@ -101,7 +101,8 @@
line-height: 1;
}

.coding-rules-details-tag-edit-cancel {
.coding-rules-details-tag-edit-cancel,
.coding-rules-severity-value svg {
vertical-align: middle;
}


+ 2
- 3
server/sonar-web/src/main/js/helpers/testMocks.ts View File

@@ -23,6 +23,7 @@ import { createStore, Store } from 'redux';
import { DocumentationEntry } from '../apps/documentation/utils';
import { Exporter, Profile } from '../apps/quality-profiles/types';
import { AppState } from '../types/appstate';
import { RuleRepository } from '../types/coding-rules';
import { EditionKey } from '../types/editions';
import { RawIssue } from '../types/issues';
import { Language } from '../types/languages';
@@ -832,8 +833,6 @@ export function mockDumpStatus(props: Partial<DumpStatus> = {}): DumpStatus {
};
}

export function mockRuleRepository(
override: Partial<{ key: string; language: string; name: string }> = {}
) {
export function mockRuleRepository(override: Partial<RuleRepository> = {}) {
return { key: 'css', language: 'css', name: 'SonarQube', ...override };
}

+ 13
- 8
server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx View File

@@ -27,12 +27,13 @@ import { createMemoryHistory, Route, RouteComponent, RouteConfig, Router } from
import { Store } from 'redux';
import AppStateContextProvider from '../app/components/app-state/AppStateContextProvider';
import CurrentUserContextProvider from '../app/components/current-user/CurrentUserContextProvider';
import { LanguagesContext } from '../app/components/languages/LanguagesContext';
import { MetricsContext } from '../app/components/metrics/MetricsContext';
import getStore from '../app/utils/getStore';
import { RouteWithChildRoutes } from '../app/utils/startReactApp';
import { Store as State } from '../store/rootReducer';
import { AppState } from '../types/appstate';
import { Dict, Metric } from '../types/types';
import { Dict, Languages, Metric } from '../types/types';
import { CurrentUser } from '../types/users';
import { DEFAULT_METRICS } from './mocks/metrics';
import { mockAppState, mockCurrentUser } from './testMocks';
@@ -42,6 +43,7 @@ interface RenderContext {
store?: Store<State, any>;
history?: History;
appState?: AppState;
languages?: Languages;
currentUser?: CurrentUser;
}

@@ -56,7 +58,7 @@ export function renderComponentApp(
export function renderApp(
indexPath: string,
routes: RouteConfig,
context: RenderContext
context?: RenderContext
): RenderResult {
return renderRoutedApp(
<RouteWithChildRoutes path={indexPath} childRoutes={routes} />,
@@ -73,7 +75,8 @@ function renderRoutedApp(
metrics = DEFAULT_METRICS,
store = getStore(),
appState = mockAppState(),
history = createMemoryHistory()
history = createMemoryHistory(),
languages = {}
}: RenderContext = {}
): RenderResult {
history.push(`/${indexPath}`);
@@ -82,11 +85,13 @@ function renderRoutedApp(
<IntlProvider defaultLocale="en" locale="en">
<MetricsContext.Provider value={metrics}>
<Provider store={store}>
<CurrentUserContextProvider currentUser={currentUser}>
<AppStateContextProvider appState={appState}>
<Router history={history}>{children}</Router>
</AppStateContextProvider>
</CurrentUserContextProvider>
<LanguagesContext.Provider value={languages}>
<CurrentUserContextProvider currentUser={currentUser}>
<AppStateContextProvider appState={appState}>
<Router history={history}>{children}</Router>
</AppStateContextProvider>
</CurrentUserContextProvider>
</LanguagesContext.Provider>
</Provider>
</MetricsContext.Provider>
</IntlProvider>

+ 7
- 1
server/sonar-web/src/main/js/types/coding-rules.ts View File

@@ -19,9 +19,15 @@
*/
import { Dict, Rule, RuleActivation } from './types';

export interface RuleRepository {
key: string;
language: string;
name: string;
}

export interface GetRulesAppResponse {
canWrite?: boolean;
repositories: { key: string; language: string; name: string }[];
repositories: RuleRepository[];
}

export interface SearchRulesResponse {

+ 29
- 0
server/sonar-web/src/main/js/types/rules.ts View File

@@ -23,3 +23,32 @@ export enum RuleStatus {
Deprecated = 'DEPRECATED',
Removed = 'REMOVED'
}

export interface SearchRulesQuery {
activation?: boolean | string;
active_severities?: string;
asc?: boolean | string;
available_since?: string;
cwe?: string;
f?: string;
facets?: string;
include_external?: boolean | string;
inheritance?: string;
is_template?: boolean | string;
languages?: string;
owaspTop10?: string;
p?: number;
ps?: number;
q?: string;
qprofile?: string;
repositories?: string;
rule_key?: string;
s?: string;
sansTop25?: string;
severities?: string;
sonarsourceSecurity?: string;
statuses?: string;
tags?: string;
template_key?: string;
types?: string;
}

Loading…
Cancel
Save