]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-18733 Prevent reports generation for not-yet-analyzed branches
authorAmbroise C <ambroise.christea@sonarsource.com>
Thu, 30 Mar 2023 15:31:24 +0000 (17:31 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 4 Apr 2023 20:03:15 +0000 (20:03 +0000)
server/sonar-web/src/main/js/api/mocks/BranchesServiceMock.ts
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/projectRegulatoryReport/RegulatoryReport.tsx
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/projectRegulatoryReport/__tests__/RegulatoryReport-it.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index ef6a5db2c4aef1049006d639e5a53111df3b6901..c7fb23c56e04d9351bcdd103bb7f6faf722e8439 100644 (file)
@@ -39,6 +39,14 @@ export default class BranchesServiceMock {
     return Promise.resolve(this.branchLikes);
   };
 
+  emptyBranches = () => {
+    this.branchLikes = [];
+  };
+
+  addBranch = (branch: BranchLike) => {
+    this.branchLikes.push(branch);
+  };
+
   resetBranches = () => {
     this.branchLikes = cloneDeep(this.defaultBranchLikes);
   };
index a316e386a37cb187dd78d45c04e18b70d74586b9..62df8b7c4a57ad301ce2111a8d989d94edb81755 100644 (file)
@@ -24,12 +24,12 @@ import { FormattedMessage } from 'react-intl';
 import { getBranches } from '../../../../../../api/branches';
 import { getRegulatoryReportUrl } from '../../../../../../api/regulatory-report';
 import DocLink from '../../../../../../components/common/DocLink';
-import { ButtonLink } from '../../../../../../components/controls/buttons';
 import Select, { LabelValueSelectOption } from '../../../../../../components/controls/Select';
+import { ButtonLink } from '../../../../../../components/controls/buttons';
 import { Alert } from '../../../../../../components/ui/Alert';
 import {
   getBranchLikeDisplayName,
-  isBranch,
+  getBranchLikeKey,
   isMainBranch,
 } from '../../../../../../helpers/branch-like';
 import { translate } from '../../../../../../helpers/l10n';
@@ -45,7 +45,7 @@ interface Props {
 interface State {
   downloadStarted: boolean;
   selectedBranch: string;
-  branchLikesOptions: LabelValueSelectOption[];
+  branchOptions: LabelValueSelectOption[];
 }
 
 export default class RegulatoryReport extends React.PureComponent<Props, State> {
@@ -54,7 +54,7 @@ export default class RegulatoryReport extends React.PureComponent<Props, State>
     this.state = {
       downloadStarted: false,
       selectedBranch: '',
-      branchLikesOptions: [],
+      branchOptions: [],
     };
   }
 
@@ -62,31 +62,35 @@ export default class RegulatoryReport extends React.PureComponent<Props, State>
     const { component, branchLike } = this.props;
     getBranches(component.key)
       .then((data) => {
-        const mainBranch = data.find(isMainBranch);
+        const availableBranches = data.filter(
+          (br) => br.analysisDate && (isMainBranch(br) || br.excludedFromPurge)
+        );
+        const mainBranch = availableBranches.find(isMainBranch);
         const otherBranchSorted = orderBy(
-          data.filter(isBranch).filter((b) => !isMainBranch(b)),
+          availableBranches.filter((b) => !isMainBranch(b)),
           (b) => b.name
         );
         const sortedBranch = mainBranch ? [mainBranch, ...otherBranchSorted] : otherBranchSorted;
-        const options = sortedBranch
-          .filter((br) => br.excludedFromPurge)
-          .map((br) => {
-            return {
-              value: getBranchLikeDisplayName(br),
-              label: getBranchLikeDisplayName(br),
-            };
-          });
+        const options = sortedBranch.map((br) => {
+          return {
+            value: getBranchLikeDisplayName(br),
+            label: getBranchLikeDisplayName(br),
+          };
+        });
 
         let selectedBranch = '';
-        if (branchLike && isBranch(branchLike) && branchLike.excludedFromPurge) {
+        if (
+          branchLike &&
+          availableBranches.find((br) => getBranchLikeKey(br) === getBranchLikeKey(branchLike))
+        ) {
           selectedBranch = getBranchLikeDisplayName(branchLike);
         } else if (mainBranch) {
           selectedBranch = getBranchLikeDisplayName(mainBranch);
         }
-        this.setState({ selectedBranch, branchLikesOptions: options });
+        this.setState({ selectedBranch, branchOptions: options });
       })
       .catch(() => {
-        this.setState({ branchLikesOptions: [] });
+        this.setState({ branchOptions: [] });
       });
   }
 
@@ -96,7 +100,8 @@ export default class RegulatoryReport extends React.PureComponent<Props, State>
 
   render() {
     const { component, onClose } = this.props;
-    const { downloadStarted, selectedBranch, branchLikesOptions } = this.state;
+    const { downloadStarted, selectedBranch, branchOptions } = this.state;
+    const isDownloadButtonDisabled = downloadStarted || !selectedBranch;
 
     return (
       <>
@@ -113,37 +118,47 @@ export default class RegulatoryReport extends React.PureComponent<Props, State>
             </ul>
           </div>
           <p>{translate('regulatory_report.description2')}</p>
-          <div className="modal-field big-spacer-top">
-            <label htmlFor="regulatory-report-branch-select">
-              {translate('regulatory_page.select_branch')}
-            </label>
-            <Select
-              className="width-100"
-              inputId="regulatory-report-branch-select"
-              id="regulatory-report-branch-select-input"
-              onChange={this.onBranchSelect}
-              options={branchLikesOptions}
-              value={branchLikesOptions.find((o) => o.value === selectedBranch)}
-            />
-          </div>
-          <Alert variant="info">
-            <div>
-              {translate('regulatory_page.available_branches_info.only_keep_when_inactive')}
-            </div>
-            <div>
-              <FormattedMessage
-                id="regulatory_page.available_branches_info.more_info"
-                defaultMessage={translate('regulatory_page.available_branches_info.more_info')}
-                values={{
-                  doc_link: (
-                    <DocLink to="/analyzing-source-code/branches/branch-analysis/#inactive-branches">
-                      {translate('regulatory_page.available_branches_info.more_info.doc_link')}
-                    </DocLink>
-                  ),
-                }}
-              />
+          {branchOptions.length > 0 ? (
+            <>
+              <div className="modal-field big-spacer-top">
+                <label htmlFor="regulatory-report-branch-select">
+                  {translate('regulatory_page.select_branch')}
+                </label>
+                <Select
+                  className="width-100"
+                  inputId="regulatory-report-branch-select"
+                  id="regulatory-report-branch-select-input"
+                  onChange={this.onBranchSelect}
+                  options={branchOptions}
+                  value={branchOptions.find((o) => o.value === selectedBranch)}
+                />
+              </div>
+              <Alert variant="info">
+                <div>
+                  {translate('regulatory_page.available_branches_info.only_keep_when_inactive')}
+                </div>
+                <div>
+                  <FormattedMessage
+                    id="regulatory_page.available_branches_info.more_info"
+                    defaultMessage={translate('regulatory_page.available_branches_info.more_info')}
+                    values={{
+                      doc_link: (
+                        <DocLink to="/analyzing-source-code/branches/branch-analysis/#inactive-branches">
+                          {translate('regulatory_page.available_branches_info.more_info.doc_link')}
+                        </DocLink>
+                      ),
+                    }}
+                  />
+                </div>
+              </Alert>
+            </>
+          ) : (
+            <div className="big-spacer-top">
+              <Alert variant="warning">
+                <div>{translate('regulatory_page.no_available_branch')}</div>
+              </Alert>
             </div>
-          </Alert>
+          )}
           <div className="modal-field big-spacer-top">
             {downloadStarted && (
               <div>
@@ -155,7 +170,7 @@ export default class RegulatoryReport extends React.PureComponent<Props, State>
         <div className="modal-foot">
           <a
             className={classNames('button button-primary big-spacer-right', {
-              disabled: downloadStarted,
+              disabled: isDownloadButtonDisabled,
             })}
             download={[component.name, selectedBranch, 'regulatory report.zip']
               .filter((s) => !!s)
@@ -164,6 +179,7 @@ export default class RegulatoryReport extends React.PureComponent<Props, State>
             href={getRegulatoryReportUrl(component.key, selectedBranch)}
             target="_blank"
             rel="noopener noreferrer"
+            aria-disabled={isDownloadButtonDisabled}
           >
             {translate('download_verb')}
           </a>
index 95eff79d2a15889fe953623e869a80bdb0b984aa..fd6a18fda7ac48e21cbb8589583292df0da898d8 100644 (file)
@@ -21,7 +21,9 @@ import { screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import * as React from 'react';
 import BranchesServiceMock from '../../../../../../../api/mocks/BranchesServiceMock';
+import { mockBranch, mockMainBranch } from '../../../../../../../helpers/mocks/branch-like';
 import { renderComponent } from '../../../../../../../helpers/testReactTestingUtils';
+import { BranchLike } from '../../../../../../../types/branch-like';
 import RegulatoryReport from '../RegulatoryReport';
 
 jest.mock('../../../../../../../api/branches');
@@ -34,31 +36,86 @@ beforeAll(() => {
 
 afterEach(() => handler.resetBranches());
 
-it('should open the regulatory report page', async () => {
-  const user = userEvent.setup();
-  renderRegulatoryReportApp();
-  expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
-  expect(screen.getByText('regulatory_report.description1')).toBeInTheDocument();
-  expect(screen.getByText('regulatory_report.description2')).toBeInTheDocument();
-  expect(
-    screen.getByText('regulatory_page.available_branches_info.only_keep_when_inactive')
-  ).toBeInTheDocument();
-  expect(screen.getByText('regulatory_page.available_branches_info.more_info')).toBeInTheDocument();
-
-  const branchSelect = screen.getByRole('combobox', { name: 'regulatory_page.select_branch' });
-  expect(branchSelect).toBeInTheDocument();
-
-  await user.click(branchSelect);
-  await user.keyboard('[ArrowDown][Enter]');
-
-  const downloadButton = screen.getByText('download_verb');
-  expect(downloadButton).toBeInTheDocument();
-
-  expect(screen.queryByText('regulatory_page.download_start.sentence')).not.toBeInTheDocument();
-  await user.click(downloadButton);
-  expect(screen.getByText('regulatory_page.download_start.sentence')).toBeInTheDocument();
+describe('RegulatoryReport tests', () => {
+  it('should open the regulatory report page', async () => {
+    const user = userEvent.setup();
+    renderRegulatoryReportApp();
+    expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
+    expect(screen.getByText('regulatory_report.description1')).toBeInTheDocument();
+    expect(screen.getByText('regulatory_report.description2')).toBeInTheDocument();
+    expect(
+      screen.getByText('regulatory_page.available_branches_info.only_keep_when_inactive')
+    ).toBeInTheDocument();
+    expect(
+      screen.getByText('regulatory_page.available_branches_info.more_info')
+    ).toBeInTheDocument();
+
+    const branchSelect = screen.getByRole('combobox', { name: 'regulatory_page.select_branch' });
+    expect(branchSelect).toBeInTheDocument();
+
+    await user.click(branchSelect);
+    await user.keyboard('[ArrowDown][Enter]');
+
+    const downloadButton = screen.getByRole('link', { name: 'download_verb' });
+    expect(downloadButton).toBeInTheDocument();
+
+    expect(screen.queryByText('regulatory_page.download_start.sentence')).not.toBeInTheDocument();
+    await user.click(downloadButton);
+    expect(screen.getByText('regulatory_page.download_start.sentence')).toBeInTheDocument();
+  });
+
+  it('should display warning message if there is no available branch', async () => {
+    handler.emptyBranches();
+    renderRegulatoryReportApp();
+
+    expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
+
+    expect(screen.getByText('regulatory_page.no_available_branch')).toBeInTheDocument();
+
+    const downloadButton = screen.getByRole('link', { name: 'download_verb' });
+    expect(downloadButton).toBeInTheDocument();
+    expect(downloadButton).toHaveClass('disabled');
+  });
+
+  it('should automatically select passed branch if compatible', async () => {
+    const compatibleBranch = mockBranch({ name: 'compatible-branch' });
+    handler.addBranch(compatibleBranch);
+    renderRegulatoryReportApp(compatibleBranch);
+
+    expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
+
+    const downloadButton = screen.getByRole<HTMLAnchorElement>('link', { name: 'download_verb' });
+    expect(downloadButton).toBeInTheDocument();
+    expect(downloadButton).not.toHaveClass('disabled');
+    expect(downloadButton.href).toContain(compatibleBranch.name);
+  });
+
+  it('should automatically select main branch if present and passed branch is not compatible', async () => {
+    handler.emptyBranches();
+    const mainBranch = mockMainBranch({ name: 'main' });
+    const notCompatibleBranch = mockBranch({
+      name: 'not-compatible-branch',
+      excludedFromPurge: false,
+    });
+    handler.addBranch(mainBranch);
+    handler.addBranch(notCompatibleBranch);
+    renderRegulatoryReportApp(notCompatibleBranch);
+
+    expect(await screen.findByText('regulatory_report.page')).toBeInTheDocument();
+
+    const downloadButton = screen.getByRole<HTMLAnchorElement>('link', { name: 'download_verb' });
+    expect(downloadButton).toBeInTheDocument();
+    expect(downloadButton).not.toHaveClass('disabled');
+    expect(downloadButton.href).toContain(mainBranch.name);
+  });
 });
 
-function renderRegulatoryReportApp() {
-  renderComponent(<RegulatoryReport component={{ key: '', name: '' }} onClose={() => {}} />);
+function renderRegulatoryReportApp(branchLike?: BranchLike) {
+  renderComponent(
+    <RegulatoryReport
+      component={{ key: '', name: '' }}
+      branchLike={branchLike}
+      onClose={() => {}}
+    />
+  );
 }
index 444fcd3eb3af6a293f160c2a953815c1600544bd..6290e025737797a5ea27be161ccfe42f7ac23aba 100644 (file)
@@ -675,6 +675,7 @@ regulatory_report.bullet_point3=Lists of findings for both new and overall code
 regulatory_report.description2=The generation and download of the report may take a few minutes.
 regulatory_page.download_start.sentence=Your download should start shortly. This may take some time.
 regulatory_page.select_branch=Select Branch
+regulatory_page.no_available_branch=No branch has been analyzed yet, no report can be generated.
 regulatory_page.available_branches_info.only_keep_when_inactive=Only branches marked as "Keep when inactive" are available.
 regulatory_page.available_branches_info.more_info=For further details, please check the {doc_link}.
 regulatory_page.available_branches_info.more_info.doc_link=related documentation