]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-18891 Filter out security hotspots from rules facet on issues page (#8061)
authorvikvorona <viktor.vorona@sonarsource.com>
Thu, 20 Apr 2023 08:14:09 +0000 (10:14 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 20 Apr 2023 20:03:33 +0000 (20:03 +0000)
24 files changed:
server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
server/sonar-web/src/main/js/apps/coding-rules/components/AvailableSinceFacet.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx
server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx
server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.tsx
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/PeriodFilter.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/ScopeFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx
server/sonar-web/src/main/js/apps/issues/test-utils.tsx
server/sonar-web/src/main/js/components/facet/FacetBox.tsx
server/sonar-web/src/main/js/components/facet/FacetItemsList.tsx
server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx
server/sonar-web/src/main/js/components/facet/__tests__/Facet-it.tsx
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/ListStyleFacet-test.tsx.snap

index 22af6d71357fe2890e42f5597c761d2614e86cbd..ec11f62ad5b721ff73d914db3af2867490103b0c 100644 (file)
@@ -27,6 +27,7 @@ import {
   mockLoggedInUser,
   mockPaging,
   mockRawIssue,
+  mockRule,
   mockRuleDetails,
 } from '../../helpers/testMocks';
 import {
@@ -43,10 +44,12 @@ import {
   RawIssuesResponse,
   ReferencedComponent,
 } from '../../types/issues';
+import { SearchRulesQuery } from '../../types/rules';
 import { Standards } from '../../types/security';
 import {
   Dict,
   FlowType,
+  Rule,
   RuleActivation,
   RuleDetails,
   SnippetsByComponent,
@@ -67,7 +70,7 @@ import {
   setIssueTransition,
   setIssueType,
 } from '../issues';
-import { getRuleDetails } from '../rules';
+import { getRuleDetails, searchRules } from '../rules';
 import { dismissNotice, getCurrentUser, searchUsers } from '../users';
 
 function mockReferenceComponent(override?: Partial<ReferencedComponent>) {
@@ -103,6 +106,7 @@ export default class IssuesServiceMock {
   currentUser: LoggedInUser;
   standards?: Standards;
   defaultList: IssueData[];
+  rulesList: Rule[];
   list: IssueData[];
 
   constructor() {
@@ -439,11 +443,41 @@ export default class IssuesServiceMock {
         snippets: {},
       },
     ];
+    this.rulesList = [
+      mockRule({
+        key: 'simpleRuleId',
+        name: 'Simple rule',
+        lang: 'java',
+        langName: 'Java',
+        type: 'CODE_SMELL',
+      }),
+      mockRule({
+        key: 'advancedRuleId',
+        name: 'Advanced rule',
+        lang: 'web',
+        langName: 'HTML',
+        type: 'VULNERABILITY',
+      }),
+      mockRule({
+        key: 'cpp:S6069',
+        lang: 'cpp',
+        langName: 'C++',
+        name: 'Security hotspot rule',
+        type: 'SECURITY_HOTSPOT',
+      }),
+      mockRule({
+        key: 'tsql:S131',
+        name: '"CASE" expressions should end with "ELSE" clauses',
+        lang: 'tsql',
+        langName: 'T-SQL',
+      }),
+    ];
 
     this.list = cloneDeep(this.defaultList);
 
     (searchIssues as jest.Mock).mockImplementation(this.handleSearchIssues);
     (getRuleDetails as jest.Mock).mockImplementation(this.handleGetRuleDetails);
+    jest.mocked(searchRules).mockImplementation(this.handleSearchRules);
     (getIssueFlowSnippets as jest.Mock).mockImplementation(this.handleGetIssueFlowSnippets);
     (bulkChangeIssues as jest.Mock).mockImplementation(this.handleBulkChangeIssues);
     (getCurrentUser as jest.Mock).mockImplementation(this.handleGetCurrentUser);
@@ -511,6 +545,22 @@ export default class IssuesServiceMock {
     return this.reply(issue.snippets);
   };
 
+  handleSearchRules = (req: SearchRulesQuery) => {
+    const rules = this.rulesList.filter((rule) => {
+      const query = req.q?.toLowerCase() || '';
+      const nameMatches = rule.name.toLowerCase().includes(query);
+      const keyMatches = rule.key.toLowerCase().includes(query);
+      const isTypeRight = req.types?.includes(rule.type);
+      return isTypeRight && (nameMatches || keyMatches);
+    });
+    return this.reply({
+      p: 1,
+      ps: 30,
+      rules,
+      total: rules.length,
+    });
+  };
+
   handleGetRuleDetails = (parameters: {
     actives?: boolean;
     key: string;
@@ -709,6 +759,7 @@ export default class IssuesServiceMock {
         pageSize,
         total: filteredList.length,
       }),
+      rules: this.rulesList,
       users: [
         { login: 'login0' },
         { login: 'login1', name: 'Login 1' },
index e92c377b1738cf291a2dd9f1b9c3c9c40987c46d..410764f83cd7962060f7fe849dea788752143a68 100644 (file)
@@ -52,22 +52,24 @@ class AvailableSinceFacet extends React.PureComponent<Props & WrappedComponentPr
       : undefined;
 
   render() {
+    const { open, value } = this.props;
+
     return (
       <FacetBox property="availableSince">
         <FacetHeader
           name={translate('coding_rules.facet.available_since')}
           onClear={this.handleClear}
           onClick={this.handleHeaderClick}
-          open={this.props.open}
+          open={open}
           values={this.getValues()}
         />
 
-        {this.props.open && (
+        {open && (
           <DateInput
             name="available-since"
             onChange={this.handlePeriodChange}
             placeholder={translate('date')}
-            value={this.props.value}
+            value={value}
           />
         )}
       </FacetBox>
index fb924dfbe2a9dc8a9be2c27ad9c9fa279e915f8b..6b8fd8e1956da969fc98776239eb12518ff8562c 100644 (file)
@@ -93,7 +93,15 @@ export default class Facet extends React.PureComponent<Props> {
   };
 
   render() {
-    const { disabled, renderTextName = defaultRenderName, stats } = this.props;
+    const {
+      children,
+      disabled,
+      disabledHelper,
+      open,
+      property,
+      renderTextName = defaultRenderName,
+      stats,
+    } = this.props;
     const values = this.props.values.map(renderTextName);
     const items =
       this.props.options ||
@@ -107,25 +115,25 @@ export default class Facet extends React.PureComponent<Props> {
     return (
       <FacetBox
         className={classNames({ 'search-navigator-facet-box-forbidden': disabled })}
-        property={this.props.property}
+        property={property}
       >
         <FacetHeader
-          name={translate('coding_rules.facet', this.props.property)}
+          name={translate('coding_rules.facet', property)}
           disabled={disabled}
-          disabledHelper={this.props.disabledHelper}
+          disabledHelper={disabledHelper}
           onClear={this.handleClear}
           onClick={disabled ? undefined : this.handleHeaderClick}
-          open={this.props.open && !disabled}
+          open={open && !disabled}
           values={values}
         >
-          {this.props.children}
+          {children}
         </FacetHeader>
 
-        {this.props.open && items !== undefined && (
-          <FacetItemsList>{items.map(this.renderItem)}</FacetItemsList>
+        {open && items !== undefined && (
+          <FacetItemsList label={property}>{items.map(this.renderItem)}</FacetItemsList>
         )}
 
-        {this.props.open && this.props.renderFooter !== undefined && this.props.renderFooter()}
+        {open && this.props.renderFooter !== undefined && this.props.renderFooter()}
       </FacetBox>
     );
   }
index 1214b3469a1dffec34491183008b0a0be3d3cca5..9fbac37e959104ec437cf9acd6beecfb54cf4834 100644 (file)
@@ -153,7 +153,7 @@ export default class ProfileFacet extends React.PureComponent<Props> {
   };
 
   render() {
-    const { languages, referencedProfiles } = this.props;
+    const { languages, open, referencedProfiles } = this.props;
     let profiles = Object.values(referencedProfiles);
     if (languages.length > 0) {
       profiles = profiles.filter((profile) => languages.includes(profile.language));
@@ -164,13 +164,15 @@ export default class ProfileFacet extends React.PureComponent<Props> {
       (profile) => profile.languageName
     );
 
+    const property = 'profile';
+
     return (
-      <FacetBox property="profile">
+      <FacetBox property={property}>
         <FacetHeader
           name={translate('coding_rules.facet.qprofile')}
           onClear={this.handleClear}
           onClick={this.handleHeaderClick}
-          open={this.props.open}
+          open={open}
           values={this.getTextValue()}
         >
           <DocumentationTooltip
@@ -185,7 +187,7 @@ export default class ProfileFacet extends React.PureComponent<Props> {
           />
         </FacetHeader>
 
-        {this.props.open && <FacetItemsList>{profiles.map(this.renderItem)}</FacetItemsList>}
+        {open && <FacetItemsList label={property}>{profiles.map(this.renderItem)}</FacetItemsList>}
       </FacetBox>
     );
   }
index f42d1db33adcec8d8bae02ca20d7ba6610aff24a..d38472c3173ea8fdbff470cffba4d2c07995dc3e 100644 (file)
@@ -146,7 +146,7 @@ export default class DomainFacet extends React.PureComponent<Props> {
   };
 
   render() {
-    const { domain } = this.props;
+    const { domain, open } = this.props;
     const helperMessageKey = `component_measures.domain_facets.${domain.name}.help`;
     const helper = hasMessage(helperMessageKey) ? translate(helperMessageKey) : undefined;
     return (
@@ -155,12 +155,12 @@ export default class DomainFacet extends React.PureComponent<Props> {
           helper={helper}
           name={getLocalizedMetricDomain(domain.name)}
           onClick={this.handleHeaderClick}
-          open={this.props.open}
+          open={open}
           values={this.getValues()}
         />
 
-        {this.props.open && (
-          <FacetItemsList>
+        {open && (
+          <FacetItemsList label={domain.name}>
             {this.renderOverviewFacet()}
             {this.renderItemsFacet()}
           </FacetItemsList>
index 64f51904fc97dd867ad8e0ca3f2954ae6e21e228..741f09af76dc4dbc63d13048018df8309ae7c20e 100644 (file)
@@ -33,7 +33,7 @@ export default function ProjectOverviewFacet({ value, selected, onChange }: Prop
   const facetName = translate('component_measures.overview', value, 'facet');
   return (
     <FacetBox property={value}>
-      <FacetItemsList>
+      <FacetItemsList label={value}>
         <FacetItem
           active={value === selected}
           key={value}
index 49df50543e7cadd1b563debec6c8bf3785392763..604e785ec6816d26896402525a528ed45e76f823 100644 (file)
@@ -11,7 +11,9 @@ exports[`should display facet item list 1`] = `
     open={true}
     values={[]}
   />
-  <FacetItemsList>
+  <FacetItemsList
+    label="Reliability"
+  >
     <FacetItem
       active={false}
       halfWidth={false}
@@ -149,7 +151,9 @@ exports[`should display facet item list with bugs selected 1`] = `
       ]
     }
   />
-  <FacetItemsList>
+  <FacetItemsList
+    label="Reliability"
+  >
     <FacetItem
       active={false}
       halfWidth={false}
@@ -283,7 +287,9 @@ exports[`should not display subtitles of new measures if there is none 1`] = `
     open={true}
     values={[]}
   />
-  <FacetItemsList>
+  <FacetItemsList
+    label="Reliability"
+  >
     <FacetItem
       active={false}
       halfWidth={false}
@@ -364,7 +370,9 @@ exports[`should not display subtitles of new measures if there is none, even on
     open={true}
     values={[]}
   />
-  <FacetItemsList>
+  <FacetItemsList
+    label="Reliability"
+  >
     <FacetItem
       active={false}
       halfWidth={false}
index b555adc42e39089552f0a200064876b7ddc40411..212d1d4a38e39b14199c43f66aaaf97aadfdf3c2 100644 (file)
@@ -373,6 +373,7 @@ describe('issues app', () => {
       // Rule
       await user.click(ui.ruleFacet.get());
       await user.click(screen.getByRole('checkbox', { name: 'other' }));
+      expect(screen.getByRole('checkbox', { name: '(HTML) Advanced rule' })).toBeInTheDocument(); // Name should apply to the rule
 
       // Tag
       await user.click(ui.tagFacet.get());
@@ -463,6 +464,41 @@ describe('issues app', () => {
       expect(ui.issueItem2.get()).toBeInTheDocument();
       expect(ui.issueItem3.get()).toBeInTheDocument();
     });
+
+    it('should search for rules with proper types', async () => {
+      const user = userEvent.setup();
+
+      renderIssueApp();
+
+      await user.click(await ui.ruleFacet.find());
+      await user.type(ui.ruleFacetSearch.get(), 'rule');
+      expect(within(ui.ruleFacetList.get()).getAllByRole('checkbox')).toHaveLength(2);
+      expect(
+        within(ui.ruleFacetList.get()).getByRole('checkbox', {
+          name: /Advanced rule/,
+        })
+      ).toBeInTheDocument();
+      expect(
+        within(ui.ruleFacetList.get()).getByRole('checkbox', {
+          name: /Simple rule/,
+        })
+      ).toBeInTheDocument();
+
+      await user.click(ui.vulnerabilityIssueTypeFilter.get());
+      // after changing the issue type filter, search field is reset, so we type again
+      await user.type(ui.ruleFacetSearch.get(), 'rule');
+
+      expect(
+        within(ui.ruleFacetList.get()).getByRole('checkbox', {
+          name: /Advanced rule/,
+        })
+      ).toBeInTheDocument();
+      expect(
+        within(ui.ruleFacetList.get()).queryByRole('checkbox', {
+          name: /Simple rule/,
+        })
+      ).not.toBeInTheDocument();
+    });
   });
 });
 
index 73ea507fc33a5dea4b65cfa954bb3b14652030be..5918ccc99dd43cf7df6c994e23175ac102c3f49f 100644 (file)
@@ -287,18 +287,20 @@ export class CreationDateFacet extends React.PureComponent<Props & WrappedCompon
   }
 
   render() {
+    const { fetching, open } = this.props;
+
     return (
       <FacetBox property={this.property}>
         <FacetHeader
-          fetching={this.props.fetching}
+          fetching={fetching}
           name={translate('issues.facet', this.property)}
           onClear={this.handleClear}
           onClick={this.handleHeaderClick}
-          open={this.props.open}
+          open={open}
           values={this.getValues()}
         />
 
-        {this.props.open && this.renderInner()}
+        {open && this.renderInner()}
       </FacetBox>
     );
   }
index aa581aff0b1f9c39425073f052bc945811ef42f7..7bc7718e116c857afbe8ad10cbf3f40a3f90891c 100644 (file)
@@ -55,7 +55,7 @@ export default function PeriodFilter(props: PeriodFilterProps) {
 
   return (
     <FacetBox property={PROPERTY}>
-      <FacetItemsList>
+      <FacetItemsList label={PROPERTY}>
         <FacetItem
           active={newCodeSelected}
           loading={fetching}
index f7a9823e460b46df550d9823b1b2d3119a3a6d84..32578f5dc75edee1fb590afc26f05a387946ba0c 100644 (file)
@@ -27,7 +27,7 @@ import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHi
 import { translate } from '../../../helpers/l10n';
 import { IssueResolution } from '../../../types/issues';
 import { Dict } from '../../../types/types';
-import { formatFacetStat, Query } from '../utils';
+import { Query, formatFacetStat } from '../utils';
 
 interface Props {
   fetching: boolean;
@@ -115,23 +115,25 @@ export default class ResolutionFacet extends React.PureComponent<Props> {
   };
 
   render() {
-    const { resolutions, stats = {} } = this.props;
+    const { fetching, open, resolutions, stats = {} } = this.props;
     const values = resolutions.map((resolution) => this.getFacetItemName(resolution));
 
     return (
       <FacetBox property={this.property}>
         <FacetHeader
-          fetching={this.props.fetching}
+          fetching={fetching}
           name={translate('issues.facet', this.property)}
           onClear={this.handleClear}
           onClick={this.handleHeaderClick}
-          open={this.props.open}
+          open={open}
           values={values}
         />
 
-        {this.props.open && (
+        {open && (
           <>
-            <FacetItemsList>{RESOLUTIONS.map(this.renderItem)}</FacetItemsList>
+            <FacetItemsList label={this.property}>
+              {RESOLUTIONS.map(this.renderItem)}
+            </FacetItemsList>
             <MultipleSelectionHint
               options={Object.keys(stats).length}
               values={resolutions.length}
index 2f308f281024ef51f4164da2f7e812342b89536a..1da206faff1930b5603b4cc829e9127105f2129c 100644 (file)
@@ -21,33 +21,35 @@ import { omit } from 'lodash';
 import * as React from 'react';
 import { searchRules } from '../../../api/rules';
 import ListStyleFacet from '../../../components/facet/ListStyleFacet';
+import { ISSUE_TYPES } from '../../../helpers/constants';
 import { translate } from '../../../helpers/l10n';
-import { Facet, ReferencedRule } from '../../../types/issues';
+import { Facet, IssueType, ReferencedRule } from '../../../types/issues';
 import { Dict, Rule } from '../../../types/types';
 import { Query } from '../utils';
 
 interface Props {
   fetching: boolean;
-  languages: string[];
   loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>;
   onChange: (changes: Partial<Query>) => void;
   onToggle: (property: string) => void;
   open: boolean;
   query: Query;
   referencedRules: Dict<ReferencedRule>;
-  rules: string[];
   stats: Dict<number> | undefined;
 }
 
 export default class RuleFacet extends React.PureComponent<Props> {
   handleSearch = (query: string, page = 1) => {
-    const { languages } = this.props;
+    const { languages, types } = this.props.query;
     return searchRules({
       f: 'name,langName',
       languages: languages.length ? languages.join() : undefined,
       q: query,
       p: page,
       ps: 30,
+      types: types.length
+        ? types.join()
+        : ISSUE_TYPES.filter((type) => type !== IssueType.SecurityHotspot).join(),
       s: 'name',
       include_external: true,
     }).then((response) => ({
@@ -76,10 +78,12 @@ export default class RuleFacet extends React.PureComponent<Props> {
   };
 
   render() {
+    const { fetching, open, query, stats } = this.props;
+
     return (
       <ListStyleFacet<Rule>
         facetHeader={translate('issues.facet.rules')}
-        fetching={this.props.fetching}
+        fetching={fetching}
         getFacetItemText={this.getRuleName}
         getSearchResultKey={(rule) => rule.key}
         getSearchResultText={(rule) => rule.name}
@@ -87,14 +91,14 @@ export default class RuleFacet extends React.PureComponent<Props> {
         onChange={this.props.onChange}
         onSearch={this.handleSearch}
         onToggle={this.props.onToggle}
-        open={this.props.open}
+        open={open}
         property="rules"
-        query={omit(this.props.query, 'rules')}
+        query={omit(query, 'rules')}
         renderFacetItem={this.getRuleName}
         renderSearchResult={this.renderSearchResult}
         searchPlaceholder={translate('search.search_for_rules')}
-        stats={this.props.stats}
-        values={this.props.rules}
+        stats={stats}
+        values={query.rules}
       />
     );
   }
index 1d5af535e132f3c4b1663c3f6c49a79f35c26499..329c046b92d66194908831a1bedee8059b9f5295 100644 (file)
@@ -43,8 +43,10 @@ export default function ScopeFacet(props: ScopeFacetProps) {
   const { fetching, open, scopes = [], stats = {} } = props;
   const values = scopes.map((scope) => translate('issue.scope', scope));
 
+  const property = 'scopes';
+
   return (
-    <FacetBox property="scopes">
+    <FacetBox property={property}>
       <FacetHeader
         fetching={fetching}
         name={translate('issues.facet.scopes')}
@@ -56,7 +58,7 @@ export default function ScopeFacet(props: ScopeFacetProps) {
 
       {open && (
         <>
-          <FacetItemsList>
+          <FacetItemsList label={property}>
             {SOURCE_SCOPES.map(({ scope, qualifier }) => {
               const active = scopes.includes(scope);
               const stat = stats[scope];
index 9ac899bcec8acd873bfd320ad342fac2ae265b1f..f587a50b359e37fe18fbe9dd3c0484f90e29f0ff 100644 (file)
@@ -27,7 +27,7 @@ import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHi
 import SeverityHelper from '../../../components/shared/SeverityHelper';
 import { translate } from '../../../helpers/l10n';
 import { Dict } from '../../../types/types';
-import { formatFacetStat, Query } from '../utils';
+import { Query, formatFacetStat } from '../utils';
 
 interface Props {
   fetching: boolean;
@@ -93,23 +93,23 @@ export default class SeverityFacet extends React.PureComponent<Props> {
   };
 
   render() {
-    const { severities, stats = {} } = this.props;
+    const { fetching, open, severities, stats = {} } = this.props;
     const values = severities.map((severity) => translate('severity', severity));
 
     return (
       <FacetBox property={this.property}>
         <FacetHeader
-          fetching={this.props.fetching}
+          fetching={fetching}
           name={translate('issues.facet', this.property)}
           onClear={this.handleClear}
           onClick={this.handleHeaderClick}
-          open={this.props.open}
+          open={open}
           values={values}
         />
 
-        {this.props.open && (
+        {open && (
           <>
-            <FacetItemsList>{SEVERITIES.map(this.renderItem)}</FacetItemsList>
+            <FacetItemsList label={this.property}>{SEVERITIES.map(this.renderItem)}</FacetItemsList>
             <MultipleSelectionHint options={Object.keys(stats).length} values={severities.length} />
           </>
         )}
index a0be8e5c952743e17e561eba94b5b442b0c37b26..90460b7b8aaf6a48c4003a11d7ee09dfdd2c285b 100644 (file)
@@ -238,14 +238,12 @@ export class Sidebar extends React.PureComponent<Props> {
         />
         <RuleFacet
           fetching={this.props.loadingFacets.rules === true}
-          languages={query.languages}
           loadSearchResultCount={this.props.loadSearchResultCount}
           onChange={this.props.onFilterChange}
           onToggle={this.props.onFacetToggle}
           open={!!openFacets.rules}
           query={query}
           referencedRules={this.props.referencedRules}
-          rules={query.rules}
           stats={facets.rules}
         />
         <TagFacet
index 7c8be377cad9577810bbe862b3e40fc9a55a484c..acc5443f5a0eaf35b349d9317917cf3c7d2285ca 100644 (file)
@@ -40,7 +40,7 @@ import {
 import { Facet } from '../../../types/issues';
 import { SecurityStandard, Standards } from '../../../types/security';
 import { Dict } from '../../../types/types';
-import { formatFacetStat, Query, STANDARDS } from '../utils';
+import { Query, STANDARDS, formatFacetStat } from '../utils';
 
 interface Props {
   cwe: string[];
@@ -243,7 +243,15 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
       return null;
     }
     const categories = sortBy(Object.keys(stats), (key) => -stats[key]);
-    return this.renderFacetItemsList(stats, values, categories, renderName, renderName, onClick);
+    return this.renderFacetItemsList(
+      stats,
+      values,
+      categories,
+      valuesProp,
+      renderName,
+      renderName,
+      onClick
+    );
   };
 
   // eslint-disable-next-line max-params
@@ -251,6 +259,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
     stats: any,
     values: string[],
     categories: string[],
+    listLabel: ValuesProp,
     renderName: (standards: Standards, category: string) => React.ReactNode,
     renderTooltip: (standards: Standards, category: string) => string,
     onClick: (x: string, multiple?: boolean) => void
@@ -268,7 +277,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
     };
 
     return (
-      <FacetItemsList>
+      <FacetItemsList label={listLabel}>
         {categories.map((category) => (
           <FacetItem
             active={values.includes(category)}
@@ -334,7 +343,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
     const allItemShown = limitedList.length + selectedBelowLimit.length === sortedItems.length;
     return (
       <>
-        <FacetItemsList>
+        <FacetItemsList label={SecurityStandard.SONARSOURCE}>
           {limitedList.map((item) => (
             <FacetItem
               active={values.includes(item)}
@@ -350,7 +359,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
         {selectedBelowLimit.length > 0 && (
           <>
             {!allItemShown && <div className="note spacer-bottom text-center">⋯</div>}
-            <FacetItemsList>
+            <FacetItemsList label={SecurityStandard.SONARSOURCE}>
               {selectedBelowLimit.map((item) => (
                 <FacetItem
                   active={true}
@@ -390,19 +399,35 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
   }
 
   renderSubFacets() {
+    const {
+      cwe,
+      cweOpen,
+      cweStats,
+      fetchingCwe,
+      fetchingOwaspTop10,
+      'fetchingOwaspTop10-2021': fetchingOwaspTop102021,
+      fetchingSonarSourceSecurity,
+      owaspTop10,
+      owaspTop10Open,
+      'owaspTop10-2021Open': owaspTop102021Open,
+      'owaspTop10-2021': owaspTop102021,
+      query,
+      sonarsourceSecurity,
+      sonarsourceSecurityOpen,
+    } = this.props;
     return (
       <>
         <FacetBox className="is-inner" property={SecurityStandard.SONARSOURCE}>
           <FacetHeader
-            fetching={this.props.fetchingSonarSourceSecurity}
+            fetching={fetchingSonarSourceSecurity}
             name={translate('issues.facet.sonarsourceSecurity')}
             onClick={this.handleSonarSourceSecurityHeaderClick}
-            open={this.props.sonarsourceSecurityOpen}
-            values={this.props.sonarsourceSecurity.map((item) =>
+            open={sonarsourceSecurityOpen}
+            values={sonarsourceSecurity.map((item) =>
               renderSonarSourceSecurityCategory(this.state.standards, item)
             )}
           />
-          {this.props.sonarsourceSecurityOpen && (
+          {sonarsourceSecurityOpen && (
             <>
               {this.renderSonarSourceSecurityList()}
               {this.renderSonarSourceSecurityHint()}
@@ -411,15 +436,15 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
         </FacetBox>
         <FacetBox className="is-inner" property={SecurityStandard.OWASP_TOP10_2021}>
           <FacetHeader
-            fetching={this.props['fetchingOwaspTop10-2021']}
+            fetching={fetchingOwaspTop102021}
             name={translate('issues.facet.owaspTop10_2021')}
             onClick={this.handleOwaspTop102021HeaderClick}
-            open={this.props['owaspTop10-2021Open']}
-            values={this.props['owaspTop10-2021'].map((item) =>
+            open={owaspTop102021Open}
+            values={owaspTop102021.map((item) =>
               renderOwaspTop102021Category(this.state.standards, item)
             )}
           />
-          {this.props['owaspTop10-2021Open'] && (
+          {owaspTop102021Open && (
             <>
               {this.renderOwaspTop102021List()}
               {this.renderOwaspTop102021Hint()}
@@ -428,15 +453,13 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
         </FacetBox>
         <FacetBox className="is-inner" property={SecurityStandard.OWASP_TOP10}>
           <FacetHeader
-            fetching={this.props.fetchingOwaspTop10}
+            fetching={fetchingOwaspTop10}
             name={translate('issues.facet.owaspTop10')}
             onClick={this.handleOwaspTop10HeaderClick}
-            open={this.props.owaspTop10Open}
-            values={this.props.owaspTop10.map((item) =>
-              renderOwaspTop10Category(this.state.standards, item)
-            )}
+            open={owaspTop10Open}
+            values={owaspTop10.map((item) => renderOwaspTop10Category(this.state.standards, item))}
           />
-          {this.props.owaspTop10Open && (
+          {owaspTop10Open && (
             <>
               {this.renderOwaspTop10List()}
               {this.renderOwaspTop10Hint()}
@@ -446,7 +469,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
         <ListStyleFacet<string>
           className="is-inner"
           facetHeader={translate('issues.facet.cwe')}
-          fetching={this.props.fetchingCwe}
+          fetching={fetchingCwe}
           getFacetItemText={(item) => renderCWECategory(this.state.standards, item)}
           getSearchResultKey={(item) => item}
           getSearchResultText={(item) => renderCWECategory(this.state.standards, item)}
@@ -454,33 +477,35 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
           onChange={this.props.onChange}
           onSearch={this.handleCWESearch}
           onToggle={this.props.onToggle}
-          open={this.props.cweOpen}
+          open={cweOpen}
           property={SecurityStandard.CWE}
-          query={omit(this.props.query, 'cwe')}
+          query={omit(query, 'cwe')}
           renderFacetItem={(item) => renderCWECategory(this.state.standards, item)}
           renderSearchResult={(item, query) =>
             highlightTerm(renderCWECategory(this.state.standards, item), query)
           }
           searchPlaceholder={translate('search.search_for_cwe')}
-          stats={this.props.cweStats}
-          values={this.props.cwe}
+          stats={cweStats}
+          values={cwe}
         />
       </>
     );
   }
 
   render() {
+    const { open } = this.props;
+
     return (
       <FacetBox property={this.property}>
         <FacetHeader
           name={translate('issues.facet', this.property)}
           onClear={this.handleClear}
           onClick={this.handleHeaderClick}
-          open={this.props.open}
+          open={open}
           values={this.getValues()}
         />
 
-        {this.props.open && this.renderSubFacets()}
+        {open && this.renderSubFacets()}
       </FacetBox>
     );
   }
index acd60cc09c4d36103ee857234d786e93314b4f1a..d9143084c8623175c07280a2afe0782ff713d1a0 100644 (file)
@@ -27,7 +27,7 @@ import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHi
 import StatusHelper from '../../../components/shared/StatusHelper';
 import { translate } from '../../../helpers/l10n';
 import { Dict } from '../../../types/types';
-import { formatFacetStat, Query } from '../utils';
+import { Query, formatFacetStat } from '../utils';
 
 interface Props {
   fetching: boolean;
@@ -91,23 +91,23 @@ export default class StatusFacet extends React.PureComponent<Props> {
   };
 
   render() {
-    const { statuses, stats = {} } = this.props;
+    const { fetching, open, statuses, stats = {} } = this.props;
     const values = statuses.map((status) => translate('issue.status', status));
 
     return (
       <FacetBox property={this.property}>
         <FacetHeader
-          fetching={this.props.fetching}
+          fetching={fetching}
           name={translate('issues.facet', this.property)}
           onClear={this.handleClear}
           onClick={this.handleHeaderClick}
-          open={this.props.open}
+          open={open}
           values={values}
         />
 
-        {this.props.open && (
+        {open && (
           <>
-            <FacetItemsList>{STATUSES.map(this.renderItem)}</FacetItemsList>
+            <FacetItemsList label={this.property}>{STATUSES.map(this.renderItem)}</FacetItemsList>
             <MultipleSelectionHint options={Object.keys(stats).length} values={statuses.length} />
           </>
         )}
index f9f130e3c5c828fa7d446cc902f068e290347628..28b1a8f8668d278ced49628b9cb2105d8253a4ea 100644 (file)
@@ -28,7 +28,7 @@ import IssueTypeIcon from '../../../components/icons/IssueTypeIcon';
 import { ISSUE_TYPES } from '../../../helpers/constants';
 import { translate } from '../../../helpers/l10n';
 import { Dict } from '../../../types/types';
-import { formatFacetStat, Query } from '../utils';
+import { Query, formatFacetStat } from '../utils';
 
 interface Props {
   fetching: boolean;
@@ -99,23 +99,23 @@ export default class TypeFacet extends React.PureComponent<Props> {
   };
 
   render() {
-    const { types, stats = {} } = this.props;
+    const { fetching, open, types, stats = {} } = this.props;
     const values = types.map((type) => translate('issue.type', type));
 
     return (
       <FacetBox property={this.property}>
         <FacetHeader
-          fetching={this.props.fetching}
+          fetching={fetching}
           name={translate('issues.facet', this.property)}
           onClear={this.handleClear}
           onClick={this.handleHeaderClick}
-          open={this.props.open}
+          open={open}
           values={values}
         />
 
-        {this.props.open && (
+        {open && (
           <>
-            <FacetItemsList>
+            <FacetItemsList label={this.property}>
               {ISSUE_TYPES.filter((t) => t !== 'SECURITY_HOTSPOT').map(this.renderItem)}
             </FacetItemsList>
             <MultipleSelectionHint options={Object.keys(stats).length} values={types.length} />
index cae0215f91829acfdd25807ded0e680bac580e97..5d92bcddc32d5398d53825f85f813494ba155ead 100644 (file)
@@ -83,6 +83,9 @@ export const ui = {
   dateInputYearSelect: byRole('combobox', { name: 'Year:' }),
 
   clearAllFilters: byRole('button', { name: 'clear_all_filters' }),
+
+  ruleFacetList: byRole('list', { name: 'rules' }),
+  ruleFacetSearch: byRole('searchbox', { name: 'search.search_for_rules' }),
 };
 
 export async function waitOnDataLoaded() {
index a4b465f4ace2a966acbdabee98e53d09879a0ea6..a750f1ff3d7932df110397792a34fca63297b0f8 100644 (file)
@@ -27,12 +27,11 @@ export interface FacetBoxProps {
 }
 
 export default function FacetBox(props: FacetBoxProps) {
+  const { children, className, property } = props;
+
   return (
-    <div
-      className={classNames('search-navigator-facet-box', props.className)}
-      data-property={props.property}
-    >
-      {props.children}
+    <div className={classNames('search-navigator-facet-box', className)} data-property={property}>
+      {children}
     </div>
   );
 }
index 96f28953a0046f9b096552411e77fcf17737c0a9..e86348de14298d5945cae2f6a0d72133651b72ae 100644 (file)
@@ -21,17 +21,12 @@ import * as React from 'react';
 
 export interface FacetItemsListProps {
   children?: React.ReactNode;
-  title?: string;
+  label: string;
 }
 
-export default function FacetItemsList({ children, title }: FacetItemsListProps) {
+export default function FacetItemsList({ children, label }: FacetItemsListProps) {
   return (
-    <div className="search-navigator-facet-list" role="list">
-      {title && (
-        <div className="search-navigator-facet-list-title" role="presentation">
-          {title}
-        </div>
-      )}
+    <div className="search-navigator-facet-list" role="list" aria-label={label}>
       {children}
     </div>
   );
index a87da01e05042857f96e6b25644395da652ce44f..b49859ee10f1d50ded891ccd610103faadfd5319 100644 (file)
@@ -240,7 +240,15 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
   };
 
   renderList() {
-    const { stats, showMoreAriaLabel, showLessAriaLabel } = this.props;
+    const {
+      maxInitialItems,
+      maxItems,
+      property,
+      stats,
+      showMoreAriaLabel,
+      showLessAriaLabel,
+      values,
+    } = this.props;
 
     if (!stats) {
       return null;
@@ -256,20 +264,18 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
 
     const limitedList = this.state.showFullList
       ? sortedItems
-      : sortedItems.slice(0, this.props.maxInitialItems);
+      : sortedItems.slice(0, maxInitialItems);
 
     // make sure all selected items are displayed
     const selectedBelowLimit = this.state.showFullList
       ? []
-      : sortedItems
-          .slice(this.props.maxInitialItems)
-          .filter((item) => this.props.values.includes(item));
+      : sortedItems.slice(maxInitialItems).filter((item) => values.includes(item));
 
-    const mightHaveMoreResults = sortedItems.length >= this.props.maxItems;
+    const mightHaveMoreResults = sortedItems.length >= maxItems;
 
     return (
       <>
-        <FacetItemsList>
+        <FacetItemsList label={property}>
           {limitedList.map((item) => (
             <FacetItem
               active={this.props.values.includes(item)}
@@ -285,7 +291,7 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
         {selectedBelowLimit.length > 0 && (
           <>
             <div className="note spacer-bottom text-center">⋯</div>
-            <FacetItemsList>
+            <FacetItemsList label={property}>
               {selectedBelowLimit.map((item) => (
                 <FacetItem
                   active={true}
@@ -332,7 +338,7 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
   }
 
   renderSearchResults() {
-    const { showMoreAriaLabel } = this.props;
+    const { property, showMoreAriaLabel } = this.props;
     const { searching, searchMaxResults, searchResults, searchPaging } = this.state;
 
     if (!searching && (!searchResults || !searchResults.length)) {
@@ -346,7 +352,7 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
 
     return (
       <>
-        <FacetItemsList>
+        <FacetItemsList label={property}>
           {searchResults.map((result) => this.renderSearchResult(result))}
         </FacetItemsList>
         {searchMaxResults && (
@@ -386,31 +392,41 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
   }
 
   render() {
-    const { disabled, stats = {} } = this.props;
+    const {
+      className,
+      disabled,
+      disabledHelper,
+      facetHeader,
+      fetching,
+      open,
+      property,
+      stats = {},
+      values: propsValues,
+    } = this.props;
     const { query, searching, searchResults } = this.state;
-    const values = this.props.values.map((item) => this.props.getFacetItemText(item));
+    const values = propsValues.map((item) => this.props.getFacetItemText(item));
     const loadingResults =
       query !== '' && searching && (searchResults === undefined || searchResults.length === 0);
     const showList = !query || loadingResults;
     return (
       <FacetBox
-        className={classNames(this.props.className, {
+        className={classNames(className, {
           'search-navigator-facet-box-forbidden': disabled,
         })}
-        property={this.props.property}
+        property={property}
       >
         <FacetHeader
-          fetching={this.props.fetching}
-          name={this.props.facetHeader}
+          fetching={fetching}
+          name={facetHeader}
           disabled={disabled}
-          disabledHelper={this.props.disabledHelper}
+          disabledHelper={disabledHelper}
           onClear={this.handleClear}
           onClick={disabled ? undefined : this.handleHeaderClick}
-          open={this.props.open && !disabled}
+          open={open && !disabled}
           values={values}
         />
 
-        {this.props.open && !disabled && (
+        {open && !disabled && (
           <>
             {this.renderSearch()}
             {showList ? this.renderList() : this.renderSearchResults()}
index fe4030f2433aedcef3787ee0a64fcebba707447d..dd13d2969b5966ee7ba52be85b5fa18144f4b310 100644 (file)
@@ -75,11 +75,6 @@ it('should correctly render a disabled header', () => {
   expect(screen.queryByRole('checkbox', { name: 'foo' })).not.toBeInTheDocument();
 });
 
-it('should correctly render a facet item list with title', () => {
-  renderFacet(undefined, { open: true }, { title: 'My list title' });
-  expect(screen.getByText('My list title')).toBeInTheDocument();
-});
-
 function renderFacet(
   facetBoxProps: Partial<FacetBoxProps> = {},
   facetHeaderProps: Partial<FacetHeader['props']> = {},
@@ -90,8 +85,10 @@ function renderFacet(
     const [open, setOpen] = React.useState(facetHeaderProps.open ?? false);
     const [values, setValues] = React.useState(facetHeaderProps.values ?? undefined);
 
+    const property = 'foo';
+
     return (
-      <FacetBox property="foo" {...facetBoxProps}>
+      <FacetBox property={property} {...facetBoxProps}>
         <FacetHeader
           name="foo"
           onClick={() => setOpen(!open)}
@@ -100,7 +97,7 @@ function renderFacet(
         />
 
         {open && (
-          <FacetItemsList {...facetItemListProps}>
+          <FacetItemsList label={property} {...facetItemListProps}>
             <FacetItem
               active={true}
               name="Foo/Bar"
index 1fcb716aab0cf8124113687f59a1a646a8faf5ef..497402e96f61a9a88f47de9640576a12953b7832 100644 (file)
@@ -44,7 +44,9 @@ exports[`should display all selected items 1`] = `
     placeholder="search for foo..."
     value=""
   />
-  <FacetItemsList>
+  <FacetItemsList
+    label="foo"
+  >
     <FacetItem
       active={true}
       halfWidth={false}
@@ -73,7 +75,9 @@ exports[`should display all selected items 1`] = `
   >
     ⋯
   </div>
-  <FacetItemsList>
+  <FacetItemsList
+    label="foo"
+  >
     <FacetItem
       active={true}
       halfWidth={false}
@@ -119,7 +123,9 @@ exports[`should render 1`] = `
     placeholder="search for foo..."
     value=""
   />
-  <FacetItemsList>
+  <FacetItemsList
+    label="foo"
+  >
     <FacetItem
       active={false}
       halfWidth={false}
@@ -187,7 +193,9 @@ exports[`should search 1`] = `
     placeholder="search for foo..."
     value="query"
   />
-  <FacetItemsList>
+  <FacetItemsList
+    label="foo"
+  >
     <FacetItem
       active={false}
       halfWidth={false}
@@ -246,7 +254,9 @@ exports[`should search 2`] = `
     placeholder="search for foo..."
     value="query"
   />
-  <FacetItemsList>
+  <FacetItemsList
+    label="foo"
+  >
     <FacetItem
       active={false}
       halfWidth={false}
@@ -316,7 +326,9 @@ exports[`should search 3`] = `
     placeholder="search for foo..."
     value=""
   />
-  <FacetItemsList>
+  <FacetItemsList
+    label="foo"
+  >
     <FacetItem
       active={false}
       halfWidth={false}