]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-18424 Migrate rules page facet tests to RTL
author7PH <benjamin.raymond@sonarsource.com>
Thu, 13 Jul 2023 14:39:53 +0000 (16:39 +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/LanguageFacet.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RepositoryFacet.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/StandardFacet.tsx
server/sonar-web/src/main/js/apps/coding-rules/utils-test.tsx
server/sonar-web/src/main/js/types/rules.ts

index e76367f8005e5687e01c8879cffc1424bfa11af8..1132bbe8d1595267b53e8c75e9aa36a609b0dd32 100644 (file)
@@ -19,6 +19,7 @@
  */
 import { cloneDeep, countBy, pick, trim } from 'lodash';
 import { RuleDescriptionSections } from '../../apps/coding-rules/rule';
+import { getStandards } from '../../helpers/security-standard';
 import {
   mockCurrentUser,
   mockPaging,
@@ -30,6 +31,7 @@ import {
 import { RuleRepository, SearchRulesResponse } from '../../types/coding-rules';
 import { RawIssuesResponse } from '../../types/issues';
 import { SearchRulesQuery } from '../../types/rules';
+import { SecurityStandard, Standards } from '../../types/security';
 import { Dict, Rule, RuleActivation, RuleDetails, RulesUpdateRequest } from '../../types/types';
 import { NoticeType } from '../../types/users';
 import { getFacet } from '../issues';
@@ -48,6 +50,7 @@ import {
   createRule,
   deleteRule,
   getRuleDetails,
+  getRuleRepositories,
   getRuleTags,
   getRulesApp,
   searchRules,
@@ -55,19 +58,29 @@ import {
 } from '../rules';
 import { dismissNotice, getCurrentUser } from '../users';
 
-interface FacetFilter {
-  languages?: string;
-  tags?: string;
-  available_since?: string;
-  q?: string;
-  types?: string;
-  severities?: string;
-  is_template?: string | boolean;
-}
+type FacetFilter = Pick<
+  SearchRulesQuery,
+  | 'languages'
+  | 'tags'
+  | 'available_since'
+  | 'q'
+  | 'types'
+  | 'severities'
+  | 'repositories'
+  | 'qprofile'
+  | 'sonarsourceSecurity'
+  | 'owaspTop10'
+  | 'owaspTop10-2021'
+  | 'cwe'
+  | 'is_template'
+>;
 
 const FACET_RULE_MAP: { [key: string]: keyof Rule } = {
   languages: 'lang',
   types: 'type',
+  severities: 'severity',
+  statuses: 'status',
+  tags: 'tags',
 };
 
 export const RULE_TAGS_MOCK = ['awesome', 'cute', 'nice'];
@@ -82,11 +95,15 @@ export default class CodingRulesServiceMock {
   isAdmin = false;
   applyWithWarning = false;
   dismissedNoticesEP = false;
+  standardsToRules: Partial<{ [category in keyof Standards]: { [standard: string]: string[] } }> =
+    {};
+
+  qualityProfilesToRules: { [qp: string]: string[] } = {};
 
   constructor() {
     this.repositories = [
-      mockRuleRepository({ key: 'repo1' }),
-      mockRuleRepository({ key: 'repo2' }),
+      mockRuleRepository({ key: 'repo1', name: 'Repository 1' }),
+      mockRuleRepository({ key: 'repo2', name: 'Repository 2' }),
     ];
     this.qualityProfile = [
       mockQualityProfile({ key: 'p1', name: 'QP Foo', language: 'java', languageName: 'Java' }),
@@ -108,10 +125,12 @@ export default class CodingRulesServiceMock {
     this.defaultRules = [
       mockRuleDetails({
         key: 'rule1',
+        repo: 'repo1',
         type: 'BUG',
         lang: 'java',
         langName: 'Java',
         name: 'Awsome java rule',
+        tags: ['awesome'],
         params: [
           { key: '1', type: 'TEXT', htmlDesc: 'html description for key 1' },
           { key: '2', type: 'NUMBER', defaultValue: 'default value for key 2' },
@@ -119,7 +138,9 @@ export default class CodingRulesServiceMock {
       }),
       mockRuleDetails({
         key: 'rule2',
+        repo: 'repo1',
         name: 'Hot hotspot',
+        tags: ['awesome'],
         type: 'SECURITY_HOTSPOT',
         lang: 'js',
         descriptionSections: [
@@ -134,7 +155,13 @@ export default class CodingRulesServiceMock {
         ],
         langName: 'JavaScript',
       }),
-      mockRuleDetails({ key: 'rule3', name: 'Unknown rule', lang: 'js', langName: 'JavaScript' }),
+      mockRuleDetails({
+        key: 'rule3',
+        repo: 'repo2',
+        name: 'Unknown rule',
+        lang: 'js',
+        langName: 'JavaScript',
+      }),
       mockRuleDetails({
         key: 'rule4',
         type: 'BUG',
@@ -214,7 +241,7 @@ export default class CodingRulesServiceMock {
         severity: 'MINOR',
         lang: 'py',
         langName: 'Python',
-        tags: ['awesome'],
+        tags: ['awesome', 'cute'],
         name: 'Custom Rule based on rule8',
         params: [
           { key: '1', type: 'TEXT', htmlDesc: 'html description for key 1' },
@@ -248,11 +275,32 @@ export default class CodingRulesServiceMock {
       [this.defaultRules[0].key]: [mockRuleActivation({ qProfile: 'p1' })],
     };
 
+    this.standardsToRules = {
+      [SecurityStandard.SONARSOURCE]: {
+        'buffer-overflow': ['rule1', 'rule2', 'rule3', 'rule4', 'rule5', 'rule6'],
+      },
+      [SecurityStandard.OWASP_TOP10_2021]: {
+        a2: ['rule1', 'rule2', 'rule3', 'rule4', 'rule5'],
+      },
+      [SecurityStandard.OWASP_TOP10]: {
+        a3: ['rule1', 'rule2', 'rule3', 'rule4'],
+      },
+      [SecurityStandard.CWE]: {
+        '102': ['rule1', 'rule2', 'rule3'],
+        '297': ['rule1', 'rule4'],
+      },
+    };
+
+    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);
     jest.mocked(searchRules).mockImplementation(this.handleSearchRules);
     jest.mocked(getRuleDetails).mockImplementation(this.handleGetRuleDetails);
+    jest.mocked(getRuleRepositories).mockImplementation(this.handleGetRuleRepositories);
     jest.mocked(searchQualityProfiles).mockImplementation(this.handleSearchQualityProfiles);
     jest.mocked(getRulesApp).mockImplementation(this.handleGetRulesApp);
     jest.mocked(bulkActivateRules).mockImplementation(this.handleBulkActivateRules);
@@ -293,6 +341,12 @@ export default class CodingRulesServiceMock {
     types,
     tags,
     is_template,
+    repositories,
+    qprofile,
+    sonarsourceSecurity,
+    owaspTop10,
+    'owaspTop10-2021': owasp2021Top10,
+    cwe,
   }: FacetFilter) {
     let filteredRules = this.rules;
     if (types) {
@@ -312,10 +366,34 @@ export default class CodingRulesServiceMock {
     if (is_template !== undefined) {
       filteredRules = filteredRules.filter((r) => (is_template ? r.isTemplate : !r.isTemplate));
     }
+    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] ?? [];
+      filteredRules = filteredRules.filter((r) => matchingRules.includes(r.key));
+    }
+    if (owasp2021Top10) {
+      const matchingRules =
+        this.standardsToRules[SecurityStandard.OWASP_TOP10_2021]?.[owasp2021Top10] ?? [];
+      filteredRules = filteredRules.filter((r) => matchingRules.includes(r.key));
+    }
+    if (owaspTop10) {
+      const matchingRules = this.standardsToRules[SecurityStandard.OWASP_TOP10]?.[owaspTop10] ?? [];
+      filteredRules = filteredRules.filter((r) => matchingRules.includes(r.key));
+    }
+    if (cwe) {
+      const matchingRules = this.standardsToRules[SecurityStandard.CWE]?.[cwe] ?? [];
+      filteredRules = filteredRules.filter((r) => matchingRules.includes(r.key));
+    }
     if (q && q.length > 2) {
       filteredRules = filteredRules.filter((r) => r.name.includes(q));
     }
-
     if (tags) {
       filteredRules = filteredRules.filter((r) => r.tags && r.tags.some((t) => tags.includes(t)));
     }
@@ -383,6 +461,12 @@ export default class CodingRulesServiceMock {
     });
   };
 
+  handleGetRuleRepositories = (parameters: {
+    q: string;
+  }): Promise<Array<{ key: string; language: string; name: string }>> => {
+    return this.reply(this.repositories.filter((r) => r.name.includes(parameters.q)));
+  };
+
   handleUpdateRule = (data: RulesUpdateRequest): Promise<RuleDetails> => {
     const rule = this.rules.find((r) => r.key === data.key);
     if (rule === undefined) {
@@ -450,7 +534,7 @@ export default class CodingRulesServiceMock {
     return this.reply(undefined);
   };
 
-  handleSearchRules = ({
+  handleSearchRules = async ({
     facets,
     types,
     languages,
@@ -458,22 +542,54 @@ export default class CodingRulesServiceMock {
     ps,
     available_since,
     severities,
+    repositories,
+    qprofile,
+    sonarsourceSecurity,
+    owaspTop10,
+    'owaspTop10-2021': owasp2021Top10,
+    cwe,
     tags,
     q,
     rule_key,
     is_template,
   }: SearchRulesQuery): Promise<SearchRulesResponse> => {
-    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 standards = await getStandards();
+    const facetCounts: Array<{ property: string; values: { val: string; count: number }[] }> = [];
+    for (const facet of facets?.split(',') ?? []) {
+      // If we can count facet values from the list of rules
+      if (FACET_RULE_MAP[facet]) {
+        const counts = countBy(this.rules.map((r) => r[FACET_RULE_MAP[facet]]));
+        const values = Object.keys(counts).map((val) => ({ val, count: counts[val] }));
+        facetCounts.push({
+          property: facet,
+          values,
+        });
+      } else if (facet === 'repositories') {
+        facetCounts.push({
+          property: facet,
+          values: this.repositories.map((repo) => ({
+            val: repo.key,
+            count: this.rules.filter((r) => r.repo === repo.key).length,
+          })),
+        });
+      } else if (typeof (standards as Dict<object>)[facet] === 'object') {
+        // When a standards facet is requested, we return all the values with a count of 1
+        facetCounts.push({
+          property: facet,
+          values: Object.keys((standards as any)[facet]).map((val: string) => ({
+            val,
+            count: 1,
+          })),
+        });
+      } else {
+        facetCounts.push({
+          property: facet,
+          values: [],
+        });
+      }
+    }
+    const currentPs = ps ?? 10;
+    const currentP = p ?? 1;
     let filteredRules: Rule[] = [];
     if (rule_key) {
       filteredRules = this.getRulesWithoutDetails(this.rules).filter((r) => r.key === rule_key);
@@ -483,15 +599,21 @@ export default class CodingRulesServiceMock {
         available_since,
         q,
         severities,
+        repositories,
         types,
         tags,
         is_template,
+        qprofile,
+        sonarsourceSecurity,
+        owaspTop10,
+        'owaspTop10-2021': owasp2021Top10,
+        cwe,
       });
     }
     const responseRules = filteredRules.slice((currentP - 1) * currentPs, currentP * currentPs);
     return this.reply({
       rules: responseRules,
-      facets: countFacet,
+      facets: facetCounts,
       paging: mockPaging({
         total: filteredRules.length,
         pageIndex: currentP,
index 9dbe08d6be55a91b5b54d7a6dd5082b33c2e7b2e..39a5ef3e8e6efbbb886939f609ea2ffead297406 100644 (file)
@@ -82,7 +82,7 @@ describe('Rules app', () => {
   });
 
   describe('filtering', () => {
-    it('filters by facets', async () => {
+    it('combine facet filters', async () => {
       const { ui, user } = getPageObjects();
       const { pickDate } = dateInputEvent(user);
       renderCodingRulesApp(mockCurrentUser());
@@ -91,16 +91,117 @@ describe('Rules app', () => {
       expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10);
 
       // Filter by language facet
-      await user.click(ui.facetItem('py').get());
+      await act(async () => {
+        await user.type(ui.facetSearchInput('search.search_for_languages').get(), 'ja');
+        await user.click(ui.facetItem('JavaScript').get());
+      });
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(2);
+
+      // Clear language facet and search box, and filter by python language
+      await act(async () => {
+        await user.clear(ui.facetSearchInput('search.search_for_languages').get());
+        await user.click(ui.facetItem('py').get());
+      });
       expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(6);
 
       // Filter by date facet
-      await user.click(await ui.availableSinceFacet.find());
-      await pickDate(ui.availableSinceDateField.get(), parseDate('Nov 1, 2022'));
+      await act(async () => {
+        await user.click(await ui.availableSinceFacet.find());
+        await pickDate(ui.availableSinceDateField.get(), parseDate('Nov 1, 2022'));
+      });
       expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1);
 
       // Clear filters
-      await user.click(ui.clearAllFiltersButton.get());
+      await act(async () => {
+        await user.click(ui.clearAllFiltersButton.get());
+      });
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10);
+
+      // Filter by repository
+      await act(async () => {
+        await user.click(ui.repositoriesFacet.get());
+        await user.click(ui.facetItem('Repository 1').get());
+      });
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(2);
+
+      // Search second repository
+      await act(async () => {
+        await user.type(ui.facetSearchInput('search.search_for_repositories').get(), 'y 2');
+        await user.click(ui.facetItem('Repository 2').get());
+      });
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1);
+
+      // Clear filters
+      await act(async () => {
+        await user.click(ui.clearAllFiltersButton.get());
+      });
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10);
+
+      // Filter by quality profile
+      await act(async () => {
+        await user.click(ui.qpFacet.get());
+        await user.click(ui.facetItem('QP FooBar Java').get());
+      });
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(8);
+
+      // Filter by tag
+      await act(async () => {
+        await user.click(ui.facetClear('coding_rules.facet.qprofile').get()); // Clear quality profile facet
+        await user.click(ui.tagsFacet.get());
+        await user.click(ui.facetItem('awesome').get());
+      });
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(5);
+
+      // Search by tag
+      await act(async () => {
+        await user.type(ui.facetSearchInput('search.search_for_tags').get(), 'te');
+        await user.click(ui.facetItem('cute').get());
+      });
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1);
+    });
+
+    it('filter by standards', async () => {
+      const { ui, user } = getPageObjects();
+      renderCodingRulesApp(mockCurrentUser());
+      await ui.appLoaded();
+
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10);
+      await act(async () => {
+        await user.click(ui.standardsFacet.get());
+        await user.click(ui.facetItem('Buffer Overflow').get());
+      });
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(6);
+
+      await act(async () => {
+        await user.click(ui.standardsOwasp2021Top10Facet.get());
+        await user.click(ui.facetItem('A2 - Cryptographic Failures').get());
+        await user.click(ui.standardsOwasp2021Top10Facet.get()); // Close facet
+      });
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(5);
+
+      await act(async () => {
+        await user.click(ui.standardsOwasp2017Top10Facet.get());
+        await user.click(ui.facetItem('A3 - Sensitive Data Exposure').get());
+      });
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(4);
+
+      await act(async () => {
+        await user.click(ui.standardsCweFacet.get());
+        await user.click(ui.facetItem('CWE-102 - Struts: Duplicate Validation Forms').get());
+      });
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(3);
+
+      await act(async () => {
+        await user.type(ui.facetSearchInput('search.search_for_cwe').get(), 'Certificate');
+        await user.click(
+          ui.facetItem('CWE-297 - Improper Validation of Certificate with Host Mismatch').get()
+        );
+      });
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(2);
+
+      await act(async () => {
+        await user.click(ui.facetClear('issues.facet.standards').get());
+      });
       expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10);
     });
 
index eabd93eaa728c5474d9936542063fcbd22b85b85..1e6fc871fbfe35f8c6ef946565fb0b5fef3d4d6d 100644 (file)
@@ -31,7 +31,7 @@ interface Props extends BasicProps {
   languages: Languages;
 }
 
-export class LanguageFacet extends React.PureComponent<Props> {
+class LanguageFacet extends React.PureComponent<Props> {
   getLanguageName = (languageKey: string) => {
     const language = this.props.languages[languageKey];
     return language ? language.name : languageKey;
index 321e49e4c65294e133f7f768be1106b2eac98ca4..b8c16f7ceb5342020c9012aa4502ce3ac2f95e0a 100644 (file)
@@ -35,7 +35,7 @@ interface Props extends BasicProps, StateProps {
   referencedRepositories: Dict<{ key: string; language: string; name: string }>;
 }
 
-export class RepositoryFacet extends React.PureComponent<Props> {
+class RepositoryFacet extends React.PureComponent<Props> {
   getLanguageName = (languageKey: string) => {
     const { languages } = this.props;
     const language = languages[languageKey];
index 28f61ce9234e5a2c5a0551c964db582fa080539d..3b6382f99ea786f914a640b7bdddd88f8a572f82 100644 (file)
@@ -28,6 +28,7 @@ import FacetItemsList from '../../../components/facet/FacetItemsList';
 import ListStyleFacet from '../../../components/facet/ListStyleFacet';
 import ListStyleFacetFooter from '../../../components/facet/ListStyleFacetFooter';
 import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';
+import DeferredSpinner from '../../../components/ui/DeferredSpinner';
 import { translate } from '../../../helpers/l10n';
 import { highlightTerm } from '../../../helpers/search';
 import {
@@ -171,22 +172,6 @@ export class StandardFacet extends React.PureComponent<Props, State> {
     return `facet_${property}`;
   };
 
-  handleHeaderClick = () => {
-    this.props.onToggle(this.property);
-  };
-
-  handleOwaspTop10HeaderClick = () => {
-    this.props.onToggle('owaspTop10');
-  };
-
-  handleOwaspTop102021HeaderClick = () => {
-    this.props.onToggle('owaspTop10-2021');
-  };
-
-  handleSonarSourceSecurityHeaderClick = () => {
-    this.props.onToggle('sonarsourceSecurity');
-  };
-
   handleClear = () => {
     this.props.onChange({
       [this.property]: [],
@@ -241,7 +226,7 @@ export class StandardFacet extends React.PureComponent<Props, State> {
       : Promise.resolve({});
   };
 
-  renderList = (
+  renderOwaspList = (
     statsProp: StatsProp,
     valuesProp: ValuesProp,
     renderName: (standards: Standards, category: string) => string,
@@ -251,7 +236,7 @@ export class StandardFacet extends React.PureComponent<Props, State> {
     const values = this.props[valuesProp];
 
     if (!stats) {
-      return null;
+      return <DeferredSpinner className="sw-ml-4" />;
     }
 
     const categories = sortBy(Object.keys(stats), (key) => -stats[key]);
@@ -314,7 +299,7 @@ export class StandardFacet extends React.PureComponent<Props, State> {
   };
 
   renderOwaspTop10List() {
-    return this.renderList(
+    return this.renderOwaspList(
       'owaspTop10Stats',
       SecurityStandard.OWASP_TOP10,
       renderOwaspTop10Category,
@@ -323,7 +308,7 @@ export class StandardFacet extends React.PureComponent<Props, State> {
   }
 
   renderOwaspTop102021List() {
-    return this.renderList(
+    return this.renderOwaspList(
       'owaspTop10-2021Stats',
       SecurityStandard.OWASP_TOP10_2021,
       renderOwaspTop102021Category,
@@ -336,7 +321,7 @@ export class StandardFacet extends React.PureComponent<Props, State> {
     const values = this.props.sonarsourceSecurity;
 
     if (!stats) {
-      return null;
+      return <DeferredSpinner className="sw-ml-4" />;
     }
 
     const sortedItems = sortBy(
@@ -440,7 +425,7 @@ export class StandardFacet extends React.PureComponent<Props, State> {
             fetching={fetchingSonarSourceSecurity}
             id={this.getFacetHeaderId(SecurityStandard.SONARSOURCE)}
             name={translate('issues.facet.sonarsourceSecurity')}
-            onClick={this.handleSonarSourceSecurityHeaderClick}
+            onClick={() => this.props.onToggle('sonarsourceSecurity')}
             open={sonarsourceSecurityOpen}
             values={sonarsourceSecurity.map((item) =>
               renderSonarSourceSecurityCategory(this.state.standards, item)
@@ -460,7 +445,7 @@ export class StandardFacet extends React.PureComponent<Props, State> {
             fetching={fetchingOwaspTop102021}
             id={this.getFacetHeaderId(SecurityStandard.OWASP_TOP10_2021)}
             name={translate('issues.facet.owaspTop10_2021')}
-            onClick={this.handleOwaspTop102021HeaderClick}
+            onClick={() => this.props.onToggle('owaspTop10-2021')}
             open={owaspTop102021Open}
             values={owaspTop102021.map((item) =>
               renderOwaspTop102021Category(this.state.standards, item)
@@ -480,7 +465,7 @@ export class StandardFacet extends React.PureComponent<Props, State> {
             fetching={fetchingOwaspTop10}
             id={this.getFacetHeaderId(SecurityStandard.OWASP_TOP10)}
             name={translate('issues.facet.owaspTop10')}
-            onClick={this.handleOwaspTop10HeaderClick}
+            onClick={() => this.props.onToggle('owaspTop10')}
             open={owaspTop10Open}
             values={owaspTop10.map((item) => renderOwaspTop10Category(this.state.standards, item))}
           />
@@ -528,7 +513,7 @@ export class StandardFacet extends React.PureComponent<Props, State> {
           id={this.getFacetHeaderId(this.property)}
           name={translate('issues.facet', this.property)}
           onClear={this.handleClear}
-          onClick={this.handleHeaderClick}
+          onClick={() => this.props.onToggle(this.property)}
           open={open}
           values={this.getValues()}
         />
index 54d92c3ce74df0f350b5e67a8829e39ba8bc8489..d7e9723e9036cb0c649688ecc55a8d8ce7cbf8b3 100644 (file)
@@ -52,9 +52,14 @@ const selectors = {
   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'),
 
index fb53a9f364a9678884b572783f0e21930c83bb11..4e85cb1e1a7bfe9566b2507db4239914205174c0 100644 (file)
@@ -37,6 +37,7 @@ export interface SearchRulesQuery {
   is_template?: boolean | string;
   languages?: string;
   owaspTop10?: string;
+  ['owaspTop10-2021']?: string;
   p?: number;
   ps?: number;
   q?: string;