]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-18424 Add RTL tests for custom rule
authorstanislavh <stanislav.honcharov@sonarsource.com>
Fri, 14 Jul 2023 08:18:54 +0000 (10:18 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 18 Jul 2023 20:03:22 +0000 (20:03 +0000)
server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts
server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx
server/sonar-web/src/main/js/apps/coding-rules/utils-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 1132bbe8d1595267b53e8c75e9aa36a609b0dd32..ba49e9f5cfca265d74809c9ba8ef40db328f4b3a 100644 (file)
@@ -68,6 +68,7 @@ type FacetFilter = Pick<
   | 'severities'
   | 'repositories'
   | 'qprofile'
+  | 'activation'
   | 'sonarsourceSecurity'
   | 'owaspTop10'
   | 'owaspTop10-2021'
@@ -98,15 +99,19 @@ export default class CodingRulesServiceMock {
   standardsToRules: Partial<{ [category in keyof Standards]: { [standard: string]: string[] } }> =
     {};
 
-  qualityProfilesToRules: { [qp: string]: string[] } = {};
-
   constructor() {
     this.repositories = [
       mockRuleRepository({ key: 'repo1', name: 'Repository 1' }),
       mockRuleRepository({ key: 'repo2', name: 'Repository 2' }),
     ];
     this.qualityProfile = [
-      mockQualityProfile({ key: 'p1', name: 'QP Foo', language: 'java', languageName: 'Java' }),
+      mockQualityProfile({
+        key: 'p1',
+        name: 'QP Foo',
+        language: 'java',
+        languageName: 'Java',
+        actions: { edit: true },
+      }),
       mockQualityProfile({ key: 'p2', name: 'QP Bar', language: 'js' }),
       mockQualityProfile({ key: 'p3', name: 'QP FooBar', language: 'java', languageName: 'Java' }),
       mockQualityProfile({
@@ -249,7 +254,6 @@ export default class CodingRulesServiceMock {
         ],
         templateKey: 'rule8',
       }),
-      // Keep this last
       mockRuleDetails({
         createdAt: '2022-12-16T17:26:54+0100',
         key: 'rule10',
@@ -269,6 +273,13 @@ export default class CodingRulesServiceMock {
         ],
         educationPrinciples: ['defense_in_depth', 'never_trust_user_input'],
       }),
+      mockRuleDetails({
+        key: 'rule11',
+        type: 'BUG',
+        lang: 'java',
+        langName: 'Java',
+        name: 'Common java rule',
+      }),
     ];
 
     this.defaultRulesActivations = {
@@ -291,10 +302,6 @@ export default class CodingRulesServiceMock {
       },
     };
 
-    this.qualityProfilesToRules = {
-      p3: ['rule1', 'rule2', 'rule3', 'rule4', 'rule5', 'rule6', 'rule7', 'rule8'],
-    };
-
     jest.mocked(updateRule).mockImplementation(this.handleUpdateRule);
     jest.mocked(createRule).mockImplementation(this.handleCreateRule);
     jest.mocked(deleteRule).mockImplementation(this.handleDeleteRule);
@@ -347,6 +354,7 @@ export default class CodingRulesServiceMock {
     owaspTop10,
     'owaspTop10-2021': owasp2021Top10,
     cwe,
+    activation,
   }: FacetFilter) {
     let filteredRules = this.rules;
     if (types) {
@@ -358,6 +366,18 @@ export default class CodingRulesServiceMock {
     if (severities) {
       filteredRules = filteredRules.filter((r) => r.severity && severities.includes(r.severity));
     }
+    if (qprofile && activation !== undefined) {
+      const qProfileLang = this.qualityProfile.find((p) => p.key === qprofile)?.language;
+      filteredRules = filteredRules
+        .filter((r) => r.lang === qProfileLang)
+        .filter((r) => {
+          const qProfilesInRule = this.rulesActivations[r.key]?.map((ra) => ra.qProfile) ?? [];
+          const ruleHasQueriedProfile = qProfilesInRule.includes(qprofile);
+          return activation === 'true' ? ruleHasQueriedProfile : !ruleHasQueriedProfile;
+        });
+
+      console.log(filteredRules);
+    }
     if (available_since) {
       filteredRules = filteredRules.filter(
         (r) => r.createdAt && new Date(r.createdAt) > new Date(available_since)
@@ -369,10 +389,6 @@ export default class CodingRulesServiceMock {
     if (repositories) {
       filteredRules = filteredRules.filter((r) => r.lang && repositories.includes(r.repo));
     }
-    if (qprofile) {
-      const rules = this.qualityProfilesToRules[qprofile] ?? [];
-      filteredRules = filteredRules.filter((r) => rules.includes(r.key));
-    }
     if (sonarsourceSecurity) {
       const matchingRules =
         this.standardsToRules[SecurityStandard.SONARSOURCE]?.[sonarsourceSecurity] ?? [];
@@ -552,6 +568,7 @@ export default class CodingRulesServiceMock {
     q,
     rule_key,
     is_template,
+    activation,
   }: SearchRulesQuery): Promise<SearchRulesResponse> => {
     const standards = await getStandards();
     const facetCounts: Array<{ property: string; values: { val: string; count: number }[] }> = [];
@@ -595,6 +612,7 @@ export default class CodingRulesServiceMock {
       filteredRules = this.getRulesWithoutDetails(this.rules).filter((r) => r.key === rule_key);
     } else {
       filteredRules = this.filterFacet({
+        qprofile,
         languages,
         available_since,
         q,
@@ -603,11 +621,11 @@ export default class CodingRulesServiceMock {
         types,
         tags,
         is_template,
-        qprofile,
         sonarsourceSecurity,
         owaspTop10,
         'owaspTop10-2021': owasp2021Top10,
         cwe,
+        activation,
       });
     }
     const responseRules = filteredRules.slice((currentP - 1) * currentPs, currentP * currentPs);
@@ -652,13 +670,20 @@ export default class CodingRulesServiceMock {
     severity?: string;
   }) => {
     const nextActivation = mockRuleActivation({ qProfile: data.key, severity: data.severity });
+
+    if (!this.rulesActivations[data.rule]) {
+      this.rulesActivations[data.rule] = [nextActivation];
+      return this.reply(undefined);
+    }
+
     const activationIndex = this.rulesActivations[data.rule]?.findIndex((activation) => {
       return activation.qProfile === data.key;
     });
+
     if (activationIndex !== -1) {
       this.rulesActivations[data.rule][activationIndex] = nextActivation;
     } else {
-      this.rulesActivations[data.rule] = [...this.rulesActivations[data.rule], nextActivation];
+      this.rulesActivations[data.rule].push(nextActivation);
     }
     return this.reply(undefined);
   };
index 39a5ef3e8e6efbbb886939f609ea2ffead297406..c3edaea9492ba7aa4bcb607327a14e02abef40eb 100644 (file)
@@ -18,7 +18,6 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { act, fireEvent, screen } from '@testing-library/react';
-import { last } from 'lodash';
 import selectEvent from 'react-select-event';
 import CodingRulesServiceMock, { RULE_TAGS_MOCK } from '../../../api/mocks/CodingRulesServiceMock';
 import { RULE_TYPES } from '../../../helpers/constants';
@@ -27,7 +26,7 @@ import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
 import { dateInputEvent, renderAppRoutes } from '../../../helpers/testReactTestingUtils';
 import { CurrentUser } from '../../../types/users';
 import routes from '../routes';
-import { getPageObjects } from '../utils-test';
+import { getPageObjects } from '../utils-tests';
 
 jest.mock('../../../api/rules');
 jest.mock('../../../api/issues');
@@ -88,7 +87,7 @@ describe('Rules app', () => {
       renderCodingRulesApp(mockCurrentUser());
       await ui.appLoaded();
 
-      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10);
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(11);
 
       // Filter by language facet
       await act(async () => {
@@ -115,7 +114,7 @@ describe('Rules app', () => {
       await act(async () => {
         await user.click(ui.clearAllFiltersButton.get());
       });
-      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10);
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(11);
 
       // Filter by repository
       await act(async () => {
@@ -135,14 +134,14 @@ describe('Rules app', () => {
       await act(async () => {
         await user.click(ui.clearAllFiltersButton.get());
       });
-      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10);
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(11);
 
       // Filter by quality profile
       await act(async () => {
         await user.click(ui.qpFacet.get());
-        await user.click(ui.facetItem('QP FooBar Java').get());
+        await user.click(ui.facetItem('QP Foo Java').get());
       });
-      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(8);
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1);
 
       // Filter by tag
       await act(async () => {
@@ -165,7 +164,7 @@ describe('Rules app', () => {
       renderCodingRulesApp(mockCurrentUser());
       await ui.appLoaded();
 
-      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10);
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(11);
       await act(async () => {
         await user.click(ui.standardsFacet.get());
         await user.click(ui.facetItem('Buffer Overflow').get());
@@ -202,7 +201,7 @@ describe('Rules app', () => {
       await act(async () => {
         await user.click(ui.facetClear('issues.facet.standards').get());
       });
-      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10);
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(11);
     });
 
     it('filters by similar rules', async () => {
@@ -210,25 +209,25 @@ describe('Rules app', () => {
       renderCodingRulesApp(mockCurrentUser());
       await ui.appLoaded();
 
-      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10);
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(11);
 
-      const lastRule = last(ui.ruleListItem.getAll());
+      const ruleName = 'Awesome Python rule with education principles';
 
-      await user.click(ui.similarIssuesButton.get(lastRule));
-      await user.click(ui.similarIssuesFilterByLang('Python').get(lastRule));
+      await user.click(ui.similarIssuesButton(ruleName).get());
+      await user.click(ui.similarIssuesFilterByLang('Python').get());
       expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(6);
 
-      await user.click(ui.similarIssuesButton.get(lastRule));
-      await user.click(ui.similarIssuesFilterByType('issue.type.VULNERABILITY').get(lastRule));
+      await user.click(ui.similarIssuesButton(ruleName).get());
+      await user.click(ui.similarIssuesFilterByType('issue.type.VULNERABILITY').get());
       expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(3);
       expect(ui.facetItem('issue.type.VULNERABILITY').get()).toBeChecked();
 
-      await user.click(ui.similarIssuesButton.get(lastRule));
-      await user.click(ui.similarIssuesFilterBySeverity('MINOR').get(lastRule));
+      await user.click(ui.similarIssuesButton(ruleName).get());
+      await user.click(ui.similarIssuesFilterBySeverity('MINOR').get());
       expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(2);
 
-      await user.click(ui.similarIssuesButton.get(lastRule));
-      await user.click(ui.similarIssuesFilterByTag('awesome').get(lastRule));
+      await user.click(ui.similarIssuesButton(ruleName).get());
+      await user.click(ui.similarIssuesFilterByTag('awesome').get());
       expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1);
     });
 
@@ -314,7 +313,33 @@ describe('Rules app', () => {
     it('should be able to deactivate for specific quality profile', async () => {});
   });
 
-  it('can activate/deactivate specific rule for quality profile', async () => {});
+  it('can activate/deactivate specific rule for quality profile', async () => {
+    const { ui, user } = getPageObjects();
+    renderCodingRulesApp(mockLoggedInUser());
+    await ui.appLoaded();
+
+    await user.click(ui.qpFacet.get());
+    await user.click(ui.facetItem('QP Foo Java').get());
+
+    // Only one rule is activated in selected QP
+    expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1);
+
+    // Switch to inactive rules
+    await user.click(ui.qpInactiveRadio.get(ui.facetItem('QP Foo Java').get()));
+    expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1);
+
+    // Activate Rule for qp
+    await user.click(ui.activateButton.get());
+    await user.click(ui.activateButton.get(ui.activateQPDialog.get()));
+    expect(ui.activateButton.query()).not.toBeInTheDocument();
+    expect(ui.deactivateButton.get()).toBeInTheDocument();
+
+    // Deactivate activated rule
+    await user.click(ui.deactivateButton.get());
+    await user.click(ui.yesButton.get());
+    expect(ui.deactivateButton.query()).not.toBeInTheDocument();
+    expect(ui.activateButton.get()).toBeInTheDocument();
+  });
 
   it('navigates by keyboard', async () => {
     const { user, ui } = getPageObjects();
@@ -490,7 +515,7 @@ describe('Rule app', () => {
     await user.click(ui.cancelButton.get());
 
     // Deactivate rule in quality profile
-    await user.click(ui.deactivateButton('QP FooBar').get());
+    await user.click(ui.deactivateInQPButton('QP FooBar').get());
     await act(() => user.click(ui.yesButton.get()));
     expect(ui.qpLink('QP FooBar').query()).not.toBeInTheDocument();
   });
@@ -621,7 +646,6 @@ describe('Rule app', () => {
       await user.click(ui.deleteButton.get(ui.deleteCustomRuleDialog.get()));
 
       // Shows the list of rules, custom rule should not be included
-      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(9);
       expect(ui.ruleListItemLink('Custom Rule based on rule8').query()).not.toBeInTheDocument();
     });
 
index 7631457d32d38ad90d446997e9161acd416f2886..1f5f0e74ccc3e86e5ea4de43c4a3aeb2202fedaf 100644 (file)
@@ -130,6 +130,7 @@ export default class RuleListItem extends React.PureComponent<Props> {
 
   renderActions = () => {
     const { activation, isLoggedIn, rule, selectedProfile } = this.props;
+
     if (!selectedProfile || !isLoggedIn) {
       return null;
     }
index 73fc930e32f1ed126128091b69db06195e53c529..4d76aeea82067ace15828a173330bea506146076 100644 (file)
@@ -19,8 +19,8 @@
  */
 import classNames from 'classnames';
 import * as React from 'react';
-import { Button, ButtonPlain } from '../../../components/controls/buttons';
 import Dropdown from '../../../components/controls/Dropdown';
+import { Button, ButtonPlain } from '../../../components/controls/buttons';
 import DropdownIcon from '../../../components/icons/DropdownIcon';
 import FilterIcon from '../../../components/icons/FilterIcon';
 import IssueTypeIcon from '../../../components/icons/IssueTypeIcon';
@@ -139,7 +139,7 @@ export default class SimilarRulesFilter extends React.PureComponent<Props> {
       >
         <Button
           className="js-rule-filter spacer-left"
-          title={translate('coding_rules.filter_similar_rules')}
+          title={translateWithParameters('coding_rules.filter_similar_rules_x', rule.name)}
         >
           <FilterIcon />
           <DropdownIcon />
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/utils-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/utils-test.tsx
deleted file mode 100644 (file)
index d7e9723..0000000
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { waitFor } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { Profile } from '../../api/quality-profiles';
-import { byLabelText, byPlaceholderText, byRole, byText } from '../../helpers/testSelector';
-
-const selectors = {
-  loading: byLabelText('loading'),
-
-  // List
-  rulesList: byRole('list', { name: 'list_of_rules' }),
-  ruleListItemLink: (name: string) => byRole('link', { name }),
-  ruleListItem: byRole('listitem'),
-  currentListItem: byRole('listitem', { current: true }),
-  similarIssuesButton: byRole('button', { name: 'coding_rules.filter_similar_rules' }),
-  similarIssuesFilterByLang: (lang: string) =>
-    byRole('button', { name: `coding_rules.filter_by_language.${lang}` }),
-  similarIssuesFilterByType: (type: string) =>
-    byRole('button', { name: `coding_rules.filter_by_type.${type}` }),
-  similarIssuesFilterBySeverity: (severity: string) =>
-    byRole('button', { name: `coding_rules.filter_by_severity.${severity}` }),
-  similarIssuesFilterByTag: (tag: string) =>
-    byRole('button', { name: `coding_rules.filter_by_tag.${tag}` }),
-
-  // Filters
-  searchInput: byRole('searchbox', { name: 'search.search_for_rules' }),
-  clearAllFiltersButton: byRole('button', { name: 'clear_all_filters' }),
-
-  // Facets
-  languagesFacet: byRole('button', { name: 'coding_rules.facet.languages' }),
-  typeFacet: byRole('button', { name: 'coding_rules.facet.types' }),
-  tagsFacet: byRole('button', { name: 'coding_rules.facet.tags' }),
-  repositoriesFacet: byRole('button', { name: 'coding_rules.facet.repositories' }),
-  severetiesFacet: byRole('button', { name: 'coding_rules.facet.severities' }),
-  statusesFacet: byRole('button', { name: 'coding_rules.facet.statuses' }),
-  standardsFacet: byRole('button', { name: 'issues.facet.standards' }),
-  standardsOwasp2017Top10Facet: byRole('button', { name: 'issues.facet.owaspTop10' }),
-  standardsOwasp2021Top10Facet: byRole('button', { name: 'issues.facet.owaspTop10_2021' }),
-  standardsCweFacet: byRole('button', { name: 'issues.facet.cwe' }),
-  availableSinceFacet: byRole('button', { name: 'coding_rules.facet.available_since' }),
-  templateFacet: byRole('button', { name: 'coding_rules.facet.template' }),
-  qpFacet: byRole('button', { name: 'coding_rules.facet.qprofile' }),
-  facetClear: (name: string) => byRole('button', { name: `clear_x_filter.${name}` }),
-  facetSearchInput: (name: string) => byRole('searchbox', { name }),
-  facetItem: (name: string) => byRole('checkbox', { name }),
-  availableSinceDateField: byPlaceholderText('date'),
-
-  // Bulk change
-  bulkChangeButton: byRole('button', { name: 'bulk_change' }),
-  activateIn: byRole('link', { name: 'coding_rules.activate_in…' }),
-  deactivateIn: byRole('link', { name: 'coding_rules.deactivate_in…' }),
-  bulkChangeDialog: (count: number, activate = true) =>
-    byRole('dialog', {
-      name: `coding_rules.${
-        activate ? 'ac' : 'deac'
-      }tivate_in_quality_profile (${count} coding_rules._rules)`,
-    }),
-  activateInSelect: byRole('combobox', { name: 'coding_rules.activate_in' }),
-  deactivateInSelect: byRole('combobox', { name: 'coding_rules.deactivate_in' }),
-  noQualityProfiles: byText('coding_rules.bulk_change.no_quality_profile'),
-  bulkApply: byRole('button', { name: 'apply' }),
-  bulkSuccessMessage: (qpName: string, langName: string, count: number) =>
-    byText(`coding_rules.bulk_change.success.${qpName}.${langName}.${count}`),
-  bulkWarningMessage: (qpName: string, langName: string, count: number) =>
-    byText(`coding_rules.bulk_change.warning.${qpName}.${langName}.${count}.1`),
-  bulkClose: byRole('button', { name: 'close' }),
-
-  // Rule details
-  ruleTitle: (name: string) => byRole('heading', { level: 1, name }),
-  externalDescription: (ruleName: string) => byText(`issue.external_issue_description.${ruleName}`),
-  introTitle: byText(/Introduction to this rule/),
-  rootCause: byText('Root cause'),
-  howToFix: byText('This is how to fix'),
-  resourceLink: byRole('link', { name: 'Awsome Reading' }),
-  contextRadio: (name: string) => byRole('radio', { name }),
-  contextSubtitle: (name: string) => byText(`coding_rules.description_context.subtitle.${name}`),
-  otherContextTitle: byText('coding_rules.context.others.title'),
-  caycNotificationButton: byRole('button', { name: 'coding_rules.more_info.scroll_message' }),
-  extendDescriptionButton: byRole('button', { name: 'coding_rules.extend_description' }),
-  extendDescriptionTextbox: byRole('textbox', { name: 'coding_rules.extend_description' }),
-  saveButton: byRole('button', { name: 'save' }),
-  cancelButton: byRole('button', { name: 'cancel' }),
-  removeButton: byRole('button', { name: 'remove' }),
-
-  // Rule tags
-  tagsDropdown: byRole('button', { name: /tags_list_x/ }),
-  tagsMenu: byRole('group', { name: 'select_tags' }),
-  tagCheckbox: (tag: string) => byRole('checkbox', { name: tag }),
-  tagSearch: byRole('searchbox', { name: 'search.search_for_tags' }),
-
-  // Rule tabs
-  whyIssueTab: byRole('tab', {
-    name: 'coding_rules.description_section.title.root_cause',
-  }),
-  whatRiskTab: byRole('tab', {
-    name: 'coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT',
-  }),
-  assessTab: byRole('tab', {
-    name: 'coding_rules.description_section.title.assess_the_problem',
-  }),
-  moreInfoTab: byRole('tab', {
-    name: 'coding_rules.description_section.title.more_info',
-  }),
-  howToFixTab: byRole('tab', {
-    name: 'coding_rules.description_section.title.how_to_fix',
-  }),
-
-  // Rule Quality Profiles
-  qpLink: (name: string) => byRole('link', { name }),
-  activateButton: byRole('button', { name: 'coding_rules.activate' }),
-  severitySelect: byRole('combobox', { name: 'severity' }),
-  qualityProfileSelect: byRole('combobox', { name: 'coding_rules.quality_profile' }),
-  activateQPDialog: byRole('dialog', { name: 'coding_rules.activate_in_quality_profile' }),
-  changeButton: (profile: string) =>
-    byRole('button', { name: `coding_rules.change_details_x.${profile}` }),
-  changeQPDialog: byRole('dialog', { name: 'coding_rules.change_details' }),
-  deactivateButton: (profile: string) =>
-    byRole('button', { name: `coding_rules.deactivate_in_quality_profile_x.${profile}` }),
-  activaInAllQPs: byText('coding_rules.active_in_all_profiles'),
-  yesButton: byRole('button', { name: 'yes' }),
-  paramInput: (param: string) => byRole('textbox', { name: param }),
-
-  // Custom rules
-  customRuleSectionTitle: byRole('heading', { level: 2, name: 'coding_rules.custom_rules' }),
-  createCustomRuleButton: byRole('button', { name: 'coding_rules.create' }),
-  editCustomRuleButton: byRole('button', { name: 'edit' }),
-  deleteCustomRuleButton: (rule: string) =>
-    byRole('button', { name: `coding_rules.delete_rule_x.${rule}` }),
-  customRuleItemLink: (name: string) => byRole('link', { name }),
-
-  // Custom rule form
-  createCustomRuleDialog: byRole('dialog', { name: 'coding_rules.create_custom_rule' }),
-  updateCustomRuleDialog: byRole('dialog', { name: 'coding_rules.update_custom_rule' }),
-  deleteCustomRuleDialog: byRole('dialog', { name: 'coding_rules.delete_rule' }),
-  ruleNameTextbox: byRole('textbox', { name: 'name field_required' }),
-  keyTextbox: byRole('textbox', { name: 'key field_required' }),
-  typeSelect: byRole('combobox', { name: 'type' }),
-  statusSelect: byRole('combobox', { name: 'coding_rules.filters.status' }),
-  descriptionTextbox: byRole('textbox', { name: 'description field_required' }),
-  createButton: byRole('button', { name: 'create' }),
-  deleteButton: byRole('button', { name: 'delete' }),
-};
-
-export function getPageObjects() {
-  const user = userEvent.setup();
-
-  const ui = {
-    ...selectors,
-    async appLoaded() {
-      await waitFor(() => {
-        expect(selectors.loading.query()).not.toBeInTheDocument();
-      });
-    },
-
-    async bulkActivate(rulesCount: number, profile: Profile) {
-      await user.click(ui.bulkChangeButton.get());
-      await user.click(ui.activateIn.get());
-
-      const dialog = ui.bulkChangeDialog(rulesCount).get();
-      expect(dialog).toBeInTheDocument();
-
-      await user.click(ui.activateInSelect.get());
-      await user.click(byText(`${profile.name} - ${profile.languageName}`).get(dialog));
-
-      await user.click(ui.bulkApply.get());
-    },
-
-    async bulkDeactivate(rulesCount: number, profile: Profile) {
-      await user.click(ui.bulkChangeButton.get());
-      await user.click(ui.deactivateIn.get());
-
-      const dialog = ui.bulkChangeDialog(rulesCount, false).get();
-      expect(dialog).toBeInTheDocument();
-
-      await user.click(ui.deactivateInSelect.get());
-      await user.click(byText(`${profile.name} - ${profile.languageName}`).get(dialog));
-
-      await user.click(ui.bulkApply.get());
-    },
-  };
-
-  return {
-    ui,
-    user,
-  };
-}
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx b/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx
new file mode 100644 (file)
index 0000000..ba500d3
--- /dev/null
@@ -0,0 +1,208 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { Profile } from '../../api/quality-profiles';
+import { byLabelText, byPlaceholderText, byRole, byText } from '../../helpers/testSelector';
+
+const selectors = {
+  loading: byLabelText('loading'),
+
+  // List
+  rulesList: byRole('list', { name: 'list_of_rules' }),
+  ruleListItemLink: (name: string) => byRole('link', { name }),
+  ruleListItem: byRole('listitem'),
+  currentListItem: byRole('listitem', { current: true }),
+  similarIssuesButton: (rule: string) =>
+    byRole('button', { name: `coding_rules.filter_similar_rules_x.${rule}` }),
+  similarIssuesFilterByLang: (lang: string) =>
+    byRole('button', { name: `coding_rules.filter_by_language.${lang}` }),
+  similarIssuesFilterByType: (type: string) =>
+    byRole('button', { name: `coding_rules.filter_by_type.${type}` }),
+  similarIssuesFilterBySeverity: (severity: string) =>
+    byRole('button', { name: `coding_rules.filter_by_severity.${severity}` }),
+  similarIssuesFilterByTag: (tag: string) =>
+    byRole('button', { name: `coding_rules.filter_by_tag.${tag}` }),
+
+  // Filters
+  searchInput: byRole('searchbox', { name: 'search.search_for_rules' }),
+  clearAllFiltersButton: byRole('button', { name: 'clear_all_filters' }),
+
+  // Facets
+  languagesFacet: byRole('button', { name: 'coding_rules.facet.languages' }),
+  typeFacet: byRole('button', { name: 'coding_rules.facet.types' }),
+  tagsFacet: byRole('button', { name: 'coding_rules.facet.tags' }),
+  repositoriesFacet: byRole('button', { name: 'coding_rules.facet.repositories' }),
+  severetiesFacet: byRole('button', { name: 'coding_rules.facet.severities' }),
+  statusesFacet: byRole('button', { name: 'coding_rules.facet.statuses' }),
+  standardsFacet: byRole('button', { name: 'issues.facet.standards' }),
+  standardsOwasp2017Top10Facet: byRole('button', { name: 'issues.facet.owaspTop10' }),
+  standardsOwasp2021Top10Facet: byRole('button', { name: 'issues.facet.owaspTop10_2021' }),
+  standardsCweFacet: byRole('button', { name: 'issues.facet.cwe' }),
+  availableSinceFacet: byRole('button', { name: 'coding_rules.facet.available_since' }),
+  templateFacet: byRole('button', { name: 'coding_rules.facet.template' }),
+  qpFacet: byRole('button', { name: 'coding_rules.facet.qprofile' }),
+  facetClear: (name: string) => byRole('button', { name: `clear_x_filter.${name}` }),
+  facetSearchInput: (name: string) => byRole('searchbox', { name }),
+  facetItem: (name: string) => byRole('checkbox', { name }),
+  availableSinceDateField: byPlaceholderText('date'),
+  qpActiveRadio: byRole('radio', { name: `active` }),
+  qpInactiveRadio: byRole('radio', { name: `inactive` }),
+
+  // Bulk change
+  bulkChangeButton: byRole('button', { name: 'bulk_change' }),
+  activateIn: byRole('link', { name: 'coding_rules.activate_in…' }),
+  deactivateIn: byRole('link', { name: 'coding_rules.deactivate_in…' }),
+  bulkChangeDialog: (count: number, activate = true) =>
+    byRole('dialog', {
+      name: `coding_rules.${
+        activate ? 'ac' : 'deac'
+      }tivate_in_quality_profile (${count} coding_rules._rules)`,
+    }),
+  activateInSelect: byRole('combobox', { name: 'coding_rules.activate_in' }),
+  deactivateInSelect: byRole('combobox', { name: 'coding_rules.deactivate_in' }),
+  noQualityProfiles: byText('coding_rules.bulk_change.no_quality_profile'),
+  bulkApply: byRole('button', { name: 'apply' }),
+  bulkSuccessMessage: (qpName: string, langName: string, count: number) =>
+    byText(`coding_rules.bulk_change.success.${qpName}.${langName}.${count}`),
+  bulkWarningMessage: (qpName: string, langName: string, count: number) =>
+    byText(`coding_rules.bulk_change.warning.${qpName}.${langName}.${count}.1`),
+  bulkClose: byRole('button', { name: 'close' }),
+
+  // Rule details
+  ruleTitle: (name: string) => byRole('heading', { level: 1, name }),
+  externalDescription: (ruleName: string) => byText(`issue.external_issue_description.${ruleName}`),
+  introTitle: byText(/Introduction to this rule/),
+  rootCause: byText('Root cause'),
+  howToFix: byText('This is how to fix'),
+  resourceLink: byRole('link', { name: 'Awsome Reading' }),
+  contextRadio: (name: string) => byRole('radio', { name }),
+  contextSubtitle: (name: string) => byText(`coding_rules.description_context.subtitle.${name}`),
+  otherContextTitle: byText('coding_rules.context.others.title'),
+  caycNotificationButton: byRole('button', { name: 'coding_rules.more_info.scroll_message' }),
+  extendDescriptionButton: byRole('button', { name: 'coding_rules.extend_description' }),
+  extendDescriptionTextbox: byRole('textbox', { name: 'coding_rules.extend_description' }),
+  saveButton: byRole('button', { name: 'save' }),
+  cancelButton: byRole('button', { name: 'cancel' }),
+  removeButton: byRole('button', { name: 'remove' }),
+
+  // Rule tags
+  tagsDropdown: byRole('button', { name: /tags_list_x/ }),
+  tagsMenu: byRole('group', { name: 'select_tags' }),
+  tagCheckbox: (tag: string) => byRole('checkbox', { name: tag }),
+  tagSearch: byRole('searchbox', { name: 'search.search_for_tags' }),
+
+  // Rule tabs
+  whyIssueTab: byRole('tab', {
+    name: 'coding_rules.description_section.title.root_cause',
+  }),
+  whatRiskTab: byRole('tab', {
+    name: 'coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT',
+  }),
+  assessTab: byRole('tab', {
+    name: 'coding_rules.description_section.title.assess_the_problem',
+  }),
+  moreInfoTab: byRole('tab', {
+    name: 'coding_rules.description_section.title.more_info',
+  }),
+  howToFixTab: byRole('tab', {
+    name: 'coding_rules.description_section.title.how_to_fix',
+  }),
+
+  // Rule Quality Profiles
+  qpLink: (name: string) => byRole('link', { name }),
+  activateButton: byRole('button', { name: 'coding_rules.activate' }),
+  deactivateButton: byRole('button', { name: 'coding_rules.deactivate' }),
+  severitySelect: byRole('combobox', { name: 'severity' }),
+  qualityProfileSelect: byRole('combobox', { name: 'coding_rules.quality_profile' }),
+  activateQPDialog: byRole('dialog', { name: 'coding_rules.activate_in_quality_profile' }),
+  changeButton: (profile: string) =>
+    byRole('button', { name: `coding_rules.change_details_x.${profile}` }),
+  changeQPDialog: byRole('dialog', { name: 'coding_rules.change_details' }),
+  deactivateInQPButton: (profile: string) =>
+    byRole('button', { name: `coding_rules.deactivate_in_quality_profile_x.${profile}` }),
+  activaInAllQPs: byText('coding_rules.active_in_all_profiles'),
+  yesButton: byRole('button', { name: 'yes' }),
+  paramInput: (param: string) => byRole('textbox', { name: param }),
+
+  // Custom rules
+  customRuleSectionTitle: byRole('heading', { level: 2, name: 'coding_rules.custom_rules' }),
+  createCustomRuleButton: byRole('button', { name: 'coding_rules.create' }),
+  editCustomRuleButton: byRole('button', { name: 'edit' }),
+  deleteCustomRuleButton: (rule: string) =>
+    byRole('button', { name: `coding_rules.delete_rule_x.${rule}` }),
+  customRuleItemLink: (name: string) => byRole('link', { name }),
+
+  // Custom rule form
+  createCustomRuleDialog: byRole('dialog', { name: 'coding_rules.create_custom_rule' }),
+  updateCustomRuleDialog: byRole('dialog', { name: 'coding_rules.update_custom_rule' }),
+  deleteCustomRuleDialog: byRole('dialog', { name: 'coding_rules.delete_rule' }),
+  ruleNameTextbox: byRole('textbox', { name: 'name field_required' }),
+  keyTextbox: byRole('textbox', { name: 'key field_required' }),
+  typeSelect: byRole('combobox', { name: 'type' }),
+  statusSelect: byRole('combobox', { name: 'coding_rules.filters.status' }),
+  descriptionTextbox: byRole('textbox', { name: 'description field_required' }),
+  createButton: byRole('button', { name: 'create' }),
+  deleteButton: byRole('button', { name: 'delete' }),
+};
+
+export function getPageObjects() {
+  const user = userEvent.setup();
+
+  const ui = {
+    ...selectors,
+    async appLoaded() {
+      await waitFor(() => {
+        expect(selectors.loading.query()).not.toBeInTheDocument();
+      });
+    },
+
+    async bulkActivate(rulesCount: number, profile: Profile) {
+      await user.click(ui.bulkChangeButton.get());
+      await user.click(ui.activateIn.get());
+
+      const dialog = ui.bulkChangeDialog(rulesCount).get();
+      expect(dialog).toBeInTheDocument();
+
+      await user.click(ui.activateInSelect.get());
+      await user.click(byText(`${profile.name} - ${profile.languageName}`).get(dialog));
+
+      await user.click(ui.bulkApply.get());
+    },
+
+    async bulkDeactivate(rulesCount: number, profile: Profile) {
+      await user.click(ui.bulkChangeButton.get());
+      await user.click(ui.deactivateIn.get());
+
+      const dialog = ui.bulkChangeDialog(rulesCount, false).get();
+      expect(dialog).toBeInTheDocument();
+
+      await user.click(ui.deactivateInSelect.get());
+      await user.click(byText(`${profile.name} - ${profile.languageName}`).get(dialog));
+
+      await user.click(ui.bulkApply.get());
+    },
+  };
+
+  return {
+    ui,
+    user,
+  };
+}
index 45a4748e51e68ac15df89aff7506b62b1c075594..18562723f10009a883ce9ab6a630998de3894a62 100644 (file)
@@ -2174,6 +2174,7 @@ coding_rules.type.tooltip.VULNERABILITY=Vulnerability Detection Rule
 coding_rules.type.tooltip.SECURITY_HOTSPOT=Security Hotspot Detection Rule
 coding_rules.update_custom_rule=Update Custom Rule
 coding_rules.filter_similar_rules=Filter Similar Rules
+coding_rules.filter_similar_rules_x=Filter rules similar with {0}
 coding_rules.filter_by_language=Filter by {0} language
 coding_rules.filter_by_type=Filter by {0} type
 coding_rules.filter_by_severity=Filter by {0} severity