From 63ad64c7c2b365736eb4c9c91a68d9580c549d69 Mon Sep 17 00:00:00 2001 From: Mathieu Suen Date: Wed, 23 Mar 2022 18:32:30 +0100 Subject: SONAR-16085 Adding IT and change select from activation modal --- .../src/main/js/api/mocks/CodingRulesMock.ts | 176 ++++++++++++ server/sonar-web/src/main/js/api/rules.ts | 30 +- .../apps/coding-rules/__tests__/CodingRules-it.ts | 197 +++++++++++++ .../components/ActivationFormModal.tsx | 81 ++++-- .../coding-rules/components/BulkChangeModal.tsx | 4 +- .../apps/coding-rules/components/RuleDetails.tsx | 5 +- .../__tests__/ActivationFormModal-test.tsx | 28 ++ .../components/__tests__/BulkChangeModal-test.tsx | 123 -------- .../ActivationFormModal-test.tsx.snap | 241 +++++++++++----- .../__snapshots__/BulkChangeModal-test.tsx.snap | 313 --------------------- .../src/main/js/apps/coding-rules/styles.css | 3 +- server/sonar-web/src/main/js/helpers/testMocks.ts | 5 +- .../src/main/js/helpers/testReactTestingUtils.tsx | 21 +- server/sonar-web/src/main/js/types/coding-rules.ts | 8 +- server/sonar-web/src/main/js/types/rules.ts | 29 ++ 15 files changed, 683 insertions(+), 581 deletions(-) create mode 100644 server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts create mode 100644 server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts delete mode 100644 server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/BulkChangeModal-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap (limited to 'server/sonar-web/src') diff --git a/server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts b/server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts new file mode 100644 index 00000000000..198e6cf585c --- /dev/null +++ b/server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts @@ -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(response: T): Promise { + return Promise.resolve(cloneDeep(response)); + } +} diff --git a/server/sonar-web/src/main/js/api/rules.ts b/server/sonar-web/src/main/js/api/rules.ts index a2ada44f0a8..419e7ed7eef 100644 --- a/server/sonar-web/src/main/js/api/rules.ts +++ b/server/sonar-web/src/main/js/api/rules.ts @@ -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 { 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 { +export function searchRules(data: SearchRulesQuery): Promise { return getJSON('/api/rules/search', data).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts new file mode 100644 index 00000000000..a8de3c4f436 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts @@ -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' } + } + }); +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx index 76f8c2f22e9..97a0c93be8e 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx @@ -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; - profile: string; + profile?: ProfileWithDeph; severity: string; submitting: boolean; } @@ -55,7 +60,7 @@ export default class ActivationFormModal extends React.PureComponent 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 ({ 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 ; - }; - render() { const { activation, rule } = this.props; const { profile, severity, submitting } = this.state; @@ -152,6 +153,26 @@ export default class ActivationFormModal extends React.PureComponent ({ + label: translate('severity', severity), + value: severity + })); + + function Option(props: OptionProps) { + return ( + + + + ); + } + + function SingleValue(props: SingleValueProps) { + return ( + + + + ); + } return ( @@ -166,34 +187,32 @@ export default class ActivationFormModal extends React.PureComponent - - + {translate('coding_rules.quality_profile')} + + ({ - 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)} /> {isCustomRule ? ( diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx index 32c3b9b691d..9221083055e 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx @@ -150,9 +150,7 @@ export class BulkChangeModal extends React.PureComponent { 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 diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx index 64987cf37ce..96f59bce97d 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx @@ -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 { return
; } - 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 { /> )} - {!ruleDetails.isTemplate && !hideQualityProfiles && ( + {!ruleDetails.isTemplate && ( ({ + 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); + 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 = {}) { return shallow( ({ - 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 = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/ActivationFormModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/ActivationFormModal-test.tsx.snap index 772f62dee6b..ccfb4ebf108 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/ActivationFormModal-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/ActivationFormModal-test.tsx.snap @@ -27,30 +27,42 @@ exports[`should render correctly: custom rule 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-

- coding_rules.deactivate_in_quality_profile (42 coding_rules._rules) -

-
-
-
-

- -

- - name - — - are_you_sure - -
-
-
- - apply - - - cancel - -
-
- -`; - -exports[`should render correctly: default 1`] = ` - -
-
-

- coding_rules.activate_in_quality_profile (42 coding_rules._rules) -

-
-
-
-

- -

- - name - — - are_you_sure - -
-
-
- - apply - - - cancel - -
-
-
-`; - -exports[`should render correctly: finished 1`] = ` - -
-
-

- coding_rules.activate_in_quality_profile (42 coding_rules._rules) -

-
-
-
- - close - -
- - -`; - -exports[`should render correctly: no profile pre-selected 1`] = ` - -
-
-

- coding_rules.activate_in_quality_profile (42 coding_rules._rules) -

-
-
-
-

- -

-