]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-18431 Migrate activity tests to RTL
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Tue, 28 Feb 2023 11:32:55 +0000 (12:32 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 3 Mar 2023 20:02:58 +0000 (20:02 +0000)
54 files changed:
server/sonar-web/src/main/js/api/mocks/ProjectActivityServiceMock.ts
server/sonar-web/src/main/js/api/mocks/TimeMachineServiceMock.ts [new file with mode: 0644]
server/sonar-web/src/main/js/api/projectActivity.ts
server/sonar-web/src/main/js/apps/overview/branches/ActivityPanel.tsx
server/sonar-web/src/main/js/apps/overview/branches/__tests__/Analysis-test.tsx
server/sonar-web/src/main/js/apps/overview/branches/__tests__/Event-test.tsx
server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/ActivityPanel-test.tsx.snap
server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/actions-test.ts.snap
server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.ts
server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.ts
server/sonar-web/src/main/js/apps/projectActivity/components/Event.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/Events.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppRenderer.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFilters.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityPageFilters-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityDateInput-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityPageFilters-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/AddEventForm-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/ChangeEventForm-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/RemoveEventForm-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/AddEventForm-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/ChangeEventForm-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/RemoveEventForm-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css
server/sonar-web/src/main/js/apps/projectActivity/utils.ts
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchAnalysisList-test.tsx
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchAnalysisListRenderer-test.tsx
server/sonar-web/src/main/js/components/activity-graph/AddGraphMetric.tsx
server/sonar-web/src/main/js/components/activity-graph/DefinitionChangeEventInner.tsx
server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx
server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx
server/sonar-web/src/main/js/components/activity-graph/GraphsZoom.tsx
server/sonar-web/src/main/js/components/activity-graph/RichQualityGateEventInner.tsx
server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx
server/sonar-web/src/main/js/components/activity-graph/__tests__/DataTableModal-it.tsx
server/sonar-web/src/main/js/components/activity-graph/__tests__/EventInner-it.tsx
server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/utils-test.ts.snap
server/sonar-web/src/main/js/components/activity-graph/__tests__/utils-test.ts
server/sonar-web/src/main/js/components/activity-graph/utils.ts
server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx
server/sonar-web/src/main/js/helpers/mocks/project-activity.ts
server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
server/sonar-web/src/main/js/types/project-activity.ts

index 5cc18a19e2b61da08467c953a004d04dfa3ebc39..9905b4ab63988535ecc0b0424205e3dd722e560a 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { cloneDeep, uniqueId } from 'lodash';
+import { chunk, cloneDeep, uniqueId } from 'lodash';
+import { parseDate } from '../../helpers/dates';
 import { mockAnalysis, mockAnalysisEvent } from '../../helpers/mocks/project-activity';
-import { Analysis } from '../../types/project-activity';
+import { BranchParameters } from '../../types/branch-like';
+import { Analysis, ProjectAnalysisEventCategory } from '../../types/project-activity';
 import {
   changeEvent,
   createEvent,
@@ -28,81 +30,121 @@ import {
   getProjectActivity,
 } from '../projectActivity';
 
-export class ProjectActivityServiceMock {
-  readOnlyAnalysisList: Analysis[];
-  analysisList: Analysis[];
-
-  constructor(analyses?: Analysis[]) {
-    this.readOnlyAnalysisList = analyses || [
-      mockAnalysis({
-        key: 'AXJMbIUGPAOIsUIE3eNT',
-        date: '2017-03-03T09:36:01+0100',
-        projectVersion: '1.1',
-        buildString: '1.1.0.2',
-        events: [
-          mockAnalysisEvent({ category: 'VERSION', key: 'IsUIEAXJMbIUGPAO3eND', name: '1.1' }),
-        ],
-      }),
-      mockAnalysis({
-        key: 'AXJMbIUGPAOIsUIE3eND',
-        date: '2017-03-02T09:36:01+0100',
-        projectVersion: '1.1',
-        buildString: '1.1.0.1',
-      }),
-      mockAnalysis({
-        key: 'AXJMbIUGPAOIsUIE3eNE',
-        date: '2017-03-01T10:36:01+0100',
-        projectVersion: '1.0',
-        buildString: '1.0.0.2',
-        events: [
-          mockAnalysisEvent({ category: 'VERSION', key: 'IUGPAOAXJMbIsUIE3eNE', name: '1.0' }),
-        ],
+const PAGE_SIZE = 10;
+const DEFAULT_PAGE = 0;
+const UNKNOWN_PROJECT = 'unknown';
+
+const defaultAnalysesList = [
+  mockAnalysis({
+    key: 'AXJMbIUGPAOIsUIE3eNT',
+    date: parseDate('2017-03-03T22:00:00.000Z').toDateString(),
+    projectVersion: '1.1',
+    buildString: '1.1.0.2',
+    events: [
+      mockAnalysisEvent({
+        category: ProjectAnalysisEventCategory.Version,
+        key: 'IsUIEAXJMbIUGPAO3eND',
+        name: '1.1',
       }),
-      mockAnalysis({
-        key: 'AXJMbIUGPAOIsUIE3eNC',
-        date: '2017-03-01T09:36:01+0100',
-        projectVersion: '1.0',
-        buildString: '1.0.0.1',
+    ],
+  }),
+  mockAnalysis({
+    key: 'AXJMbIUGPAOIsUIE3eND',
+    date: parseDate('2017-03-02T22:00:00.000Z').toDateString(),
+    projectVersion: '1.1',
+    buildString: '1.1.0.1',
+  }),
+  mockAnalysis({
+    key: 'AXJMbIUGPAOIsUIE3eNE',
+    date: parseDate('2017-03-01T22:00:00.000Z').toDateString(),
+    projectVersion: '1.0',
+    events: [
+      mockAnalysisEvent({
+        category: ProjectAnalysisEventCategory.Version,
+        key: 'IUGPAOAXJMbIsUIE3eNE',
+        name: '1.0',
       }),
-    ];
+    ],
+  }),
+  mockAnalysis({
+    key: 'AXJMbIUGPAOIsUIE3eNC',
+    date: parseDate('2017-02-28T22:00:00.000Z').toDateString(),
+    projectVersion: '1.0',
+    buildString: '1.0.0.1',
+  }),
+];
+
+export class ProjectActivityServiceMock {
+  #analysisList: Analysis[];
 
-    this.analysisList = cloneDeep(this.readOnlyAnalysisList);
+  constructor() {
+    this.#analysisList = cloneDeep(defaultAnalysesList);
 
-    (getProjectActivity as jest.Mock).mockImplementation(this.getActivityHandler);
-    (deleteAnalysis as jest.Mock).mockImplementation(this.deleteAnalysisHandler);
-    (createEvent as jest.Mock).mockImplementation(this.createEventHandler);
-    (changeEvent as jest.Mock).mockImplementation(this.changeEventHandler);
-    (deleteEvent as jest.Mock).mockImplementation(this.deleteEventHandler);
+    jest.mocked(getProjectActivity).mockImplementation(this.getActivityHandler);
+    jest.mocked(deleteAnalysis).mockImplementation(this.deleteAnalysisHandler);
+    jest.mocked(createEvent).mockImplementation(this.createEventHandler);
+    jest.mocked(changeEvent).mockImplementation(this.changeEventHandler);
+    jest.mocked(deleteEvent).mockImplementation(this.deleteEventHandler);
   }
 
   reset = () => {
-    this.analysisList = cloneDeep(this.readOnlyAnalysisList);
+    this.#analysisList = cloneDeep(defaultAnalysesList);
+  };
+
+  getAnalysesList = () => {
+    return this.#analysisList;
+  };
+
+  setAnalysesList = (analyses: Analysis[]) => {
+    this.#analysisList = analyses;
   };
 
-  getActivityHandler = () => {
+  getActivityHandler = (
+    data: {
+      project: string;
+      statuses?: string;
+      category?: string;
+      from?: string;
+      p?: number;
+      ps?: number;
+    } & BranchParameters
+  ) => {
+    const { project, ps = PAGE_SIZE, p = DEFAULT_PAGE, category, from } = data;
+
+    if (project === UNKNOWN_PROJECT) {
+      throw new Error(`Could not find project "${UNKNOWN_PROJECT}"`);
+    }
+
+    let analyses = category
+      ? this.#analysisList.filter((a) => a.events.some((e) => e.category === category))
+      : this.#analysisList;
+
+    if (from) {
+      const fromTime = parseDate(from).getTime();
+      analyses = analyses.filter((a) => parseDate(a.date).getTime() >= fromTime);
+    }
+
+    const analysesChunked = chunk(analyses, ps);
+
     return this.reply({
-      analyses: this.analysisList,
-      paging: {
-        pageIndex: 1,
-        pageSize: 100,
-        total: this.analysisList.length,
-      },
+      paging: { pageSize: ps, total: analyses.length, pageIndex: p },
+      analyses: analysesChunked[p - 1] ?? [],
     });
   };
 
   deleteAnalysisHandler = (analysisKey: string) => {
-    const i = this.analysisList.findIndex(({ key }) => key === analysisKey);
-    if (i !== undefined) {
-      this.analysisList.splice(i, 1);
-      return this.reply();
+    const i = this.#analysisList.findIndex(({ key }) => key === analysisKey);
+    if (i === undefined) {
+      throw new Error(`Could not find analysis with key: ${analysisKey}`);
     }
-    throw new Error(`Could not find analysis with key: ${analysisKey}`);
+    this.#analysisList.splice(i, 1);
+    return this.reply(undefined);
   };
 
   createEventHandler = (
     analysisKey: string,
     name: string,
-    category = 'OTHER',
+    category = ProjectAnalysisEventCategory.Other,
     description?: string
   ) => {
     const analysis = this.findAnalysis(analysisKey);
@@ -136,12 +178,12 @@ export class ProjectActivityServiceMock {
 
     analysis.events.splice(eventIndex, 1);
 
-    return this.reply();
+    return this.reply(undefined);
   };
 
   findEvent = (eventKey: string): [number, string] => {
     let analysisKey;
-    const eventIndex = this.analysisList.reduce((acc, { key, events }) => {
+    const eventIndex = this.#analysisList.reduce((acc, { key, events }) => {
       if (acc === undefined) {
         const i = events.findIndex(({ key }) => key === eventKey);
         if (i > -1) {
@@ -161,7 +203,7 @@ export class ProjectActivityServiceMock {
   };
 
   findAnalysis = (analysisKey: string) => {
-    const analysis = this.analysisList.find(({ key }) => key === analysisKey);
+    const analysis = this.#analysisList.find(({ key }) => key === analysisKey);
 
     if (analysis !== undefined) {
       return analysis;
@@ -170,7 +212,7 @@ export class ProjectActivityServiceMock {
     throw new Error(`Could not find analysis with key: ${analysisKey}`);
   };
 
-  reply<T>(response?: T): Promise<T | void> {
-    return Promise.resolve(response ? cloneDeep(response) : undefined);
+  reply<T>(response: T): Promise<T> {
+    return Promise.resolve(cloneDeep(response));
   }
 }
diff --git a/server/sonar-web/src/main/js/api/mocks/TimeMachineServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/TimeMachineServiceMock.ts
new file mode 100644 (file)
index 0000000..cda4ebd
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * 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 { chunk, cloneDeep, times } from 'lodash';
+import { parseDate } from '../../helpers/dates';
+import { mockHistoryItem, mockMeasureHistory } from '../../helpers/mocks/project-activity';
+import { BranchParameters } from '../../types/branch-like';
+import { MetricKey } from '../../types/metrics';
+import { MeasureHistory } from '../../types/project-activity';
+import { getAllTimeMachineData, getTimeMachineData, TimeMachineResponse } from '../time-machine';
+
+const PAGE_SIZE = 10;
+const DEFAULT_PAGE = 0;
+const HISTORY_COUNT = 10;
+const START_DATE = '2016-01-01T00:00:00.000Z';
+
+const defaultMeasureHistory = [
+  MetricKey.bugs,
+  MetricKey.code_smells,
+  MetricKey.confirmed_issues,
+  MetricKey.vulnerabilities,
+  MetricKey.blocker_violations,
+  MetricKey.lines_to_cover,
+  MetricKey.uncovered_lines,
+  MetricKey.security_hotspots_reviewed,
+  MetricKey.coverage,
+  MetricKey.duplicated_lines_density,
+  MetricKey.test_success_density,
+].map((metric) => {
+  return mockMeasureHistory({
+    metric,
+    history: times(HISTORY_COUNT, (i) => {
+      const date = parseDate(START_DATE);
+      date.setDate(date.getDate() + i);
+      return mockHistoryItem({ value: i.toString(), date });
+    }),
+  });
+});
+
+export class TimeMachineServiceMock {
+  #measureHistory: MeasureHistory[];
+
+  constructor() {
+    this.#measureHistory = cloneDeep(defaultMeasureHistory);
+
+    jest.mocked(getTimeMachineData).mockImplementation(this.handleGetTimeMachineData);
+    jest.mocked(getAllTimeMachineData).mockImplementation(this.handleGetAllTimeMachineData);
+  }
+
+  handleGetTimeMachineData = (
+    data: {
+      component: string;
+      from?: string;
+      metrics: string;
+      p?: number;
+      ps?: number;
+      to?: string;
+    } & BranchParameters
+  ) => {
+    const { ps = PAGE_SIZE, p = DEFAULT_PAGE } = data;
+
+    const measureHistoryChunked = chunk(this.#measureHistory, ps);
+
+    return this.reply({
+      paging: { pageSize: ps, total: this.#measureHistory.length, pageIndex: p },
+      measures: measureHistoryChunked[p - 1] ? this.map(measureHistoryChunked[p - 1]) : [],
+    });
+  };
+
+  handleGetAllTimeMachineData = (
+    data: {
+      component: string;
+      metrics: string;
+      from?: string;
+      p?: number;
+      to?: string;
+    } & BranchParameters,
+    _prev?: TimeMachineResponse
+  ) => {
+    const { p = DEFAULT_PAGE } = data;
+    return this.reply({
+      paging: { pageSize: PAGE_SIZE, total: this.#measureHistory.length, pageIndex: p },
+      measures: this.map(this.#measureHistory),
+    });
+  };
+
+  setMeasureHistory = (list: MeasureHistory[]) => {
+    this.#measureHistory = list;
+  };
+
+  map = (list: MeasureHistory[]) => {
+    return list.map((item) => ({
+      ...item,
+      history: item.history.map((h) => ({ ...h, date: h.date.toDateString() })),
+    }));
+  };
+
+  reset = () => {
+    this.#measureHistory = cloneDeep(defaultMeasureHistory);
+  };
+
+  reply<T>(response: T): Promise<T> {
+    return Promise.resolve(cloneDeep(response));
+  }
+}
index a9164852f13bf0c254eeb1b6df458254a8ac1b3f..41414bf129a8a1b4bffa2873281969d6fa564c21 100644 (file)
 import { throwGlobalError } from '../helpers/error';
 import { getJSON, post, postJSON, RequestData } from '../helpers/request';
 import { BranchParameters } from '../types/branch-like';
-import { Analysis } from '../types/project-activity';
+import {
+  Analysis,
+  ApplicationAnalysisEventCategory,
+  ProjectAnalysisEventCategory,
+} from '../types/project-activity';
 import { Paging } from '../types/types';
 
 export enum ProjectActivityStatuses {
@@ -46,7 +50,7 @@ interface CreateEventResponse {
   analysis: string;
   key: string;
   name: string;
-  category: string;
+  category: ProjectAnalysisEventCategory | ApplicationAnalysisEventCategory;
   description?: string;
 }
 
index fcd003814d9c46d1dd550accd64e2fbce3070d6f..41edf6d1698e21cf6d6958ced23c37c0c177f95f 100644 (file)
@@ -95,7 +95,7 @@ export function ActivityPanel(props: ActivityPanelProps) {
         <div className="display-flex-row">
           <div className="display-flex-column flex-1">
             <div className="overview-panel-padded display-flex-column flex-1">
-              <GraphsHeader graph={graph} metrics={metrics} updateGraph={props.onGraphChange} />
+              <GraphsHeader graph={graph} metrics={metrics} onUpdateGraph={props.onGraphChange} />
               <GraphsHistory
                 analyses={[]}
                 ariaLabel={translateWithParameters(
index be118e2ee42681c1f64d7f158eec84d422841b85..147732263e6f460d3a1778cf3411ee778ab583d3 100644 (file)
@@ -21,6 +21,7 @@ import { shallow } from 'enzyme';
 import * as React from 'react';
 import { mockAnalysis } from '../../../../helpers/mocks/project-activity';
 import { ComponentQualifier } from '../../../../types/component';
+import { ProjectAnalysisEventCategory } from '../../../../types/project-activity';
 import { Analysis, AnalysisProps } from '../Analysis';
 
 it('should render correctly', () => {
@@ -33,8 +34,8 @@ function shallowRender(props: Partial<AnalysisProps> = {}) {
     <Analysis
       analysis={mockAnalysis({
         events: [
-          { key: '1', category: 'OTHER', name: 'test' },
-          { key: '2', category: 'VERSION', name: '6.5-SNAPSHOT' },
+          { key: '1', category: ProjectAnalysisEventCategory.Other, name: 'test' },
+          { key: '2', category: ProjectAnalysisEventCategory.Version, name: '6.5-SNAPSHOT' },
         ],
       })}
       qualifier={ComponentQualifier.Project}
index e8f9bb2ea9a4d5b82b4236afd67908c2b7b38306..ac63c1703947f4ae3dabf768c7ca515a0e8f1b25 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { AnalysisEvent } from '../../../../types/project-activity';
+import { AnalysisEvent, ProjectAnalysisEventCategory } from '../../../../types/project-activity';
 import { Event } from '../Event';
 
 it('should render an event correctly', () => {
   expect(
-    shallow(<Event event={{ key: '1', category: 'OTHER', name: 'test' }} />)
+    shallow(
+      <Event event={{ key: '1', category: ProjectAnalysisEventCategory.Other, name: 'test' }} />
+    )
   ).toMatchSnapshot();
 });
 
 it('should render a version correctly', () => {
   expect(
-    shallow(<Event event={{ key: '2', category: 'VERSION', name: '6.5-SNAPSHOT' }} />)
+    shallow(
+      <Event
+        event={{ key: '2', category: ProjectAnalysisEventCategory.Version, name: '6.5-SNAPSHOT' }}
+      />
+    )
   ).toMatchSnapshot();
 });
 
 it('should render rich quality gate event', () => {
   const event: AnalysisEvent = {
-    category: 'QUALITY_GATE',
+    category: ProjectAnalysisEventCategory.QualityGate,
     key: 'foo1234',
     name: '',
     qualityGate: {
index 609f69f5ca913b2e66b50d35754ad7f619c4abef..5ed1dbc57116dd2d922fe1f5dfc63f93e39a8328 100644 (file)
@@ -34,7 +34,7 @@ exports[`should render correctly 1`] = `
                 },
               ]
             }
-            updateGraph={[MockFunction]}
+            onUpdateGraph={[MockFunction]}
           />
           <GraphsHistory
             analyses={[]}
@@ -204,7 +204,7 @@ exports[`should render correctly 2`] = `
                 },
               ]
             }
-            updateGraph={[MockFunction]}
+            onUpdateGraph={[MockFunction]}
           />
           <GraphsHistory
             analyses={[]}
index 4e39a9042c16ce59126cd25223444158fd976a58..27cba954bc16df343a99595c281831b3d394dc94 100644 (file)
@@ -5,7 +5,7 @@ exports[`addCustomEvent should correctly add a custom event 1`] = `
   "date": 2016-10-27T10:21:15.000Z,
   "events": [
     {
-      "category": "Custom",
+      "category": "OTHER",
       "key": "Enew",
       "name": "Foo",
     },
index 0cef1d14347f8c322b12be4de0a261a51c37853e..b5f2967e9004e3a340f15cf076def46e0b4ef18a 100644 (file)
@@ -19,6 +19,7 @@
  */
 import { DEFAULT_GRAPH } from '../../../components/activity-graph/utils';
 import { parseDate } from '../../../helpers/dates';
+import { ProjectAnalysisEventCategory } from '../../../types/project-activity';
 import * as actions from '../actions';
 
 const ANALYSES = [
@@ -28,7 +29,7 @@ const ANALYSES = [
     events: [
       {
         key: 'E1',
-        category: 'VERSION',
+        category: ProjectAnalysisEventCategory.Version,
         name: '6.5-SNAPSHOT',
       },
     ],
@@ -44,12 +45,12 @@ const ANALYSES = [
     events: [
       {
         key: 'E2',
-        category: 'OTHER',
+        category: ProjectAnalysisEventCategory.Other,
         name: 'foo',
       },
       {
         key: 'E3',
-        category: 'OTHER',
+        category: ProjectAnalysisEventCategory.Other,
         name: 'foo',
       },
     ],
@@ -59,7 +60,7 @@ const ANALYSES = [
 const newEvent = {
   key: 'Enew',
   name: 'Foo',
-  category: 'Custom',
+  category: ProjectAnalysisEventCategory.Other,
 };
 
 const emptyState = {
@@ -100,12 +101,18 @@ describe('deleteEvent', () => {
 describe('changeEvent', () => {
   it('should correctly update an event', () => {
     expect(
-      actions.changeEvent('A1', { key: 'E1', name: 'changed', category: 'VERSION' })(state)
-        .analyses[0]
+      actions.changeEvent('A1', {
+        key: 'E1',
+        name: 'changed',
+        category: ProjectAnalysisEventCategory.Version,
+      })(state).analyses[0]
     ).toMatchSnapshot();
     expect(
-      actions.changeEvent('A2', { key: 'E2', name: 'foo', category: 'VERSION' })(state).analyses[1]
-        .events
+      actions.changeEvent('A2', {
+        key: 'E2',
+        name: 'foo',
+        category: ProjectAnalysisEventCategory.Version,
+      })(state).analyses[1].events
     ).toHaveLength(0);
   });
 });
index 49df8ecb0b30ff1664a2270cc24924b40a1a2f79..8b802d8fd3a1091ffb968a87d1e864bc1e558deb 100644 (file)
@@ -19,7 +19,7 @@
  */
 import { DEFAULT_GRAPH } from '../../../components/activity-graph/utils';
 import * as dates from '../../../helpers/dates';
-import { GraphType } from '../../../types/project-activity';
+import { GraphType, ProjectAnalysisEventCategory } from '../../../types/project-activity';
 import * as utils from '../utils';
 
 jest.mock('date-fns', () => {
@@ -38,13 +38,21 @@ const ANALYSES = [
   {
     key: 'AVyMjlK1HjR_PLDzRbB9',
     date: dates.parseDate('2017-06-09T13:06:10.000Z'),
-    events: [{ key: 'AVyM9oI1HjR_PLDzRciU', category: 'VERSION', name: '1.1-SNAPSHOT' }],
+    events: [
+      {
+        key: 'AVyM9oI1HjR_PLDzRciU',
+        category: ProjectAnalysisEventCategory.Version,
+        name: '1.1-SNAPSHOT',
+      },
+    ],
   },
   { key: 'AVyM9n3cHjR_PLDzRciT', date: dates.parseDate('2017-06-09T11:12:27.000Z'), events: [] },
   {
     key: 'AVyMjlK1HjR_PLDzRbB9',
     date: dates.parseDate('2017-06-09T11:12:27.000Z'),
-    events: [{ key: 'AVyM9oI1HjR_PLDzRciU', category: 'VERSION', name: '1.1' }],
+    events: [
+      { key: 'AVyM9oI1HjR_PLDzRciU', category: ProjectAnalysisEventCategory.Version, name: '1.1' },
+    ],
   },
   {
     key: 'AVxZtCpH7841nF4RNEMI',
@@ -52,7 +60,7 @@ const ANALYSES = [
     events: [
       {
         key: 'AVxZtC-N7841nF4RNEMJ',
-        category: 'QUALITY_PROFILE',
+        category: ProjectAnalysisEventCategory.QualityProfile,
         name: 'Changes in "Default - SonarSource conventions" (Java)',
       },
     ],
@@ -62,10 +70,10 @@ const ANALYSES = [
     key: 'AVwQF7kwl-nNFgFWOJ3V',
     date: dates.parseDate('2017-05-16T07:09:59.000Z'),
     events: [
-      { key: 'AVyM9oI1HjR_PLDzRciU', category: 'VERSION', name: '1.0' },
+      { key: 'AVyM9oI1HjR_PLDzRciU', category: ProjectAnalysisEventCategory.Version, name: '1.0' },
       {
         key: 'AVwQF7zXl-nNFgFWOJ3W',
-        category: 'QUALITY_PROFILE',
+        category: ProjectAnalysisEventCategory.QualityProfile,
         name: 'Changes in "Default - SonarSource conventions" (Java)',
       },
     ],
index 1a0071ddc03ff53a4ab95f324d56952a43dcdc82..19bff70ecf15e7c7ca1f17b70d98f876f14e0cae 100644 (file)
@@ -21,7 +21,7 @@ import * as React from 'react';
 import EventInner from '../../../components/activity-graph/EventInner';
 import { DeleteButton, EditButton } from '../../../components/controls/buttons';
 import { translate } from '../../../helpers/l10n';
-import { AnalysisEvent } from '../../../types/project-activity';
+import { AnalysisEvent, ProjectAnalysisEventCategory } from '../../../types/project-activity';
 import ChangeEventForm from './forms/ChangeEventForm';
 import RemoveEventForm from './forms/RemoveEventForm';
 
@@ -34,14 +34,14 @@ export interface EventProps {
   onDelete?: (analysisKey: string, event: string) => Promise<void>;
 }
 
-export function Event(props: EventProps) {
+function Event(props: EventProps) {
   const { analysisKey, event, canAdmin, isFirst } = props;
 
   const [changing, setChanging] = React.useState(false);
   const [deleting, setDeleting] = React.useState(false);
 
-  const isOther = event.category === 'OTHER';
-  const isVersion = event.category === 'VERSION';
+  const isOther = event.category === ProjectAnalysisEventCategory.Other;
+  const isVersion = event.category === ProjectAnalysisEventCategory.Version;
   const canChange = (isOther || isVersion) && props.onChange;
   const canDelete = (isOther || (isVersion && !isFirst)) && props.onDelete;
   const showActions = canAdmin && (canChange || canDelete);
index 4310574a1a055e807580d16570426a33166f1611..470ef7ccc80d80532d1e19695ece86dcf98ba989 100644 (file)
@@ -19,7 +19,7 @@
  */
 import { sortBy } from 'lodash';
 import * as React from 'react';
-import { AnalysisEvent } from '../../../types/project-activity';
+import { AnalysisEvent, ProjectAnalysisEventCategory } from '../../../types/project-activity';
 import Event from './Event';
 
 export interface EventsProps {
@@ -31,13 +31,13 @@ export interface EventsProps {
   onDelete?: (analysis: string, event: string) => Promise<void>;
 }
 
-export function Events(props: EventsProps) {
+function Events(props: EventsProps) {
   const { analysisKey, canAdmin, events, isFirst } = props;
 
   const sortedEvents = sortBy(
     events,
     // versions last
-    (event) => (event.category === 'VERSION' ? 1 : 0),
+    (event) => (event.category === ProjectAnalysisEventCategory.Version ? 1 : 0),
     // then the rest sorted by category
     'category'
   );
index e523374a060b7f291fd7f1ea431a52ec3bbbb5a5..130b24c6cfb69d3affbcdfd935156600fd8f9313 100644 (file)
@@ -19,7 +19,6 @@
  */
 import classNames from 'classnames';
 import { isEqual } from 'date-fns';
-import { throttle } from 'lodash';
 import * as React from 'react';
 import Tooltip from '../../../components/controls/Tooltip';
 import DateFormatter from '../../../components/intl/DateFormatter';
@@ -31,103 +30,35 @@ import { activityQueryChanged, getAnalysesByVersionByDay, Query } from '../utils
 import ProjectActivityAnalysis from './ProjectActivityAnalysis';
 
 interface Props {
-  addCustomEvent: (analysis: string, name: string, category?: string) => Promise<void>;
-  addVersion: (analysis: string, version: string) => Promise<void>;
+  onAddCustomEvent: (analysis: string, name: string, category?: string) => Promise<void>;
+  onAddVersion: (analysis: string, version: string) => Promise<void>;
   analyses: ParsedAnalysis[];
   analysesLoading: boolean;
   canAdmin?: boolean;
   canDeleteAnalyses?: boolean;
-  changeEvent: (event: string, name: string) => Promise<void>;
-  deleteAnalysis: (analysis: string) => Promise<void>;
-  deleteEvent: (analysis: string, event: string) => Promise<void>;
+  onChangeEvent: (event: string, name: string) => Promise<void>;
+  onDeleteAnalysis: (analysis: string) => Promise<void>;
+  onDeleteEvent: (analysis: string, event: string) => Promise<void>;
   initializing: boolean;
   leakPeriodDate?: Date;
   project: { qualifier: string };
   query: Query;
-  updateQuery: (changes: Partial<Query>) => void;
+  onUpdateQuery: (changes: Partial<Query>) => void;
 }
 
-const LIST_MARGIN_TOP = 36;
+const LIST_MARGIN_TOP = 24;
 
 export default class ProjectActivityAnalysesList extends React.PureComponent<Props> {
-  analyses?: HTMLCollectionOf<HTMLElement>;
-  badges?: HTMLCollectionOf<HTMLElement>;
   scrollContainer?: HTMLUListElement | null;
 
-  constructor(props: Props) {
-    super(props);
-    this.handleScroll = throttle(this.handleScroll, 20);
-  }
-
-  componentDidMount() {
-    this.badges = document.getElementsByClassName(
-      'project-activity-version-badge'
-    ) as HTMLCollectionOf<HTMLElement>;
-    this.analyses = document.getElementsByClassName(
-      'project-activity-analysis'
-    ) as HTMLCollectionOf<HTMLElement>;
-  }
-
   componentDidUpdate(prevProps: Props) {
-    if (!this.scrollContainer) {
-      return;
-    }
-    if (activityQueryChanged(prevProps.query, this.props.query)) {
-      this.resetScrollTop(0, true);
+    if (this.scrollContainer && activityQueryChanged(prevProps.query, this.props.query)) {
+      this.scrollContainer.scrollTop = 0;
     }
   }
 
-  handleScroll = () => this.updateStickyBadges(true);
-
-  resetScrollTop = (newScrollTop: number, forceBadgeAlignement?: boolean) => {
-    if (this.scrollContainer) {
-      this.scrollContainer.scrollTop = newScrollTop;
-    }
-    if (this.badges) {
-      for (let i = 1; i < this.badges.length; i++) {
-        this.badges[i].removeAttribute('originOffsetTop');
-        this.badges[i].classList.remove('sticky');
-      }
-    }
-    this.updateStickyBadges(forceBadgeAlignement);
-  };
-
-  updateStickyBadges = (forceBadgeAlignement?: boolean) => {
-    if (!this.scrollContainer || !this.badges) {
-      return;
-    }
-
-    const { scrollTop } = this.scrollContainer;
-    if (scrollTop == null) {
-      return;
-    }
-
-    let newScrollTop;
-    for (let i = 1; i < this.badges.length; i++) {
-      const badge = this.badges[i];
-      let originOffsetTop = badge.getAttribute('originOffsetTop');
-      if (originOffsetTop == null) {
-        // Set the originOffsetTop attribute, to avoid using getBoundingClientRect
-        originOffsetTop = String(badge.offsetTop);
-        badge.setAttribute('originOffsetTop', originOffsetTop);
-      }
-      if (Number(originOffsetTop) < scrollTop + 18 + i * 2) {
-        if (forceBadgeAlignement && !badge.classList.contains('sticky')) {
-          newScrollTop = originOffsetTop;
-        }
-        badge.classList.add('sticky');
-      } else {
-        badge.classList.remove('sticky');
-      }
-    }
-
-    if (forceBadgeAlignement && newScrollTop != null) {
-      this.scrollContainer.scrollTop = Number(newScrollTop) - 6;
-    }
-  };
-
-  updateSelectedDate = (date: Date) => {
-    this.props.updateQuery({ selectedDate: date });
+  handleUpdateSelectedDate = (date: Date) => {
+    this.props.onUpdateQuery({ selectedDate: date });
   };
 
   shouldRenderBaselineMarker(analysis: ParsedAnalysis): boolean {
@@ -143,20 +74,20 @@ export default class ProjectActivityAnalysesList extends React.PureComponent<Pro
 
     return (
       <ProjectActivityAnalysis
-        addCustomEvent={this.props.addCustomEvent}
-        addVersion={this.props.addVersion}
+        onAddCustomEvent={this.props.onAddCustomEvent}
+        onAddVersion={this.props.onAddVersion}
         analysis={analysis}
         canAdmin={this.props.canAdmin}
         canCreateVersion={this.props.project.qualifier === ComponentQualifier.Project}
         canDeleteAnalyses={this.props.canDeleteAnalyses}
-        changeEvent={this.props.changeEvent}
-        deleteAnalysis={this.props.deleteAnalysis}
-        deleteEvent={this.props.deleteEvent}
+        onChangeEvent={this.props.onChangeEvent}
+        onDeleteAnalysis={this.props.onDeleteAnalysis}
+        onDeleteEvent={this.props.onDeleteEvent}
         isBaseline={this.shouldRenderBaselineMarker(analysis)}
         isFirst={analysis.key === firstAnalysisKey}
         key={analysis.key}
         selected={analysis.date.valueOf() === selectedDate}
-        updateSelectedDate={this.updateSelectedDate}
+        onUpdateSelectedDate={this.handleUpdateSelectedDate}
       />
     );
   }
@@ -183,7 +114,6 @@ export default class ProjectActivityAnalysesList extends React.PureComponent<Pro
     return (
       <ul
         className="project-activity-versions-list"
-        onScroll={this.handleScroll}
         ref={(element) => (this.scrollContainer = element)}
         style={{
           marginTop:
index c7b97ff4f7c72848d0a844be978691a965414759..7a0a99fed679f28cf8d3a716dbfc1939d2eb2961 100644 (file)
@@ -38,22 +38,22 @@ import AddEventForm from './forms/AddEventForm';
 import RemoveAnalysisForm from './forms/RemoveAnalysisForm';
 
 export interface ProjectActivityAnalysisProps extends WrappedComponentProps {
-  addCustomEvent: (analysis: string, name: string, category?: string) => Promise<void>;
-  addVersion: (analysis: string, version: string) => Promise<void>;
+  onAddCustomEvent: (analysis: string, name: string, category?: string) => Promise<void>;
+  onAddVersion: (analysis: string, version: string) => Promise<void>;
   analysis: ParsedAnalysis;
   canAdmin?: boolean;
   canDeleteAnalyses?: boolean;
   canCreateVersion: boolean;
-  changeEvent: (event: string, name: string) => Promise<void>;
-  deleteAnalysis: (analysis: string) => Promise<void>;
-  deleteEvent: (analysis: string, event: string) => Promise<void>;
+  onChangeEvent: (event: string, name: string) => Promise<void>;
+  onDeleteAnalysis: (analysis: string) => Promise<void>;
+  onDeleteEvent: (analysis: string, event: string) => Promise<void>;
   isBaseline: boolean;
   isFirst: boolean;
   selected: boolean;
-  updateSelectedDate: (date: Date) => void;
+  onUpdateSelectedDate: (date: Date) => void;
 }
 
-export function ProjectActivityAnalysis(props: ProjectActivityAnalysisProps) {
+function ProjectActivityAnalysis(props: ProjectActivityAnalysisProps) {
   let node: HTMLLIElement | null = null;
 
   const {
@@ -89,7 +89,7 @@ export function ProjectActivityAnalysis(props: ProjectActivityAnalysisProps) {
       className={classNames('project-activity-analysis bordered-top bordered-bottom', {
         selected,
       })}
-      onClick={() => props.updateSelectedDate(analysis.date)}
+      onClick={() => props.onUpdateSelectedDate(analysis.date)}
       ref={(ref) => (node = ref)}
     >
       <div className="display-flex-center display-flex-space-between">
@@ -102,7 +102,7 @@ export function ProjectActivityAnalysis(props: ProjectActivityAnalysisProps) {
                   'project_activity.show_analysis_X_on_graph',
                   analysis.buildString || formatDate(parsedDate, formatterOption)
                 )}
-                onClick={() => props.updateSelectedDate(analysis.date)}
+                onClick={() => props.onUpdateSelectedDate(analysis.date)}
               >
                 <time className="text-middle" dateTime={parsedDate.toISOString()}>
                   {formattedTime}
@@ -163,7 +163,7 @@ export function ProjectActivityAnalysis(props: ProjectActivityAnalysisProps) {
 
               {addVersionForm && (
                 <AddEventForm
-                  addEvent={props.addVersion}
+                  addEvent={props.onAddVersion}
                   addEventButtonText="project_activity.add_version"
                   analysis={analysis}
                   onClose={() => setAddVersionForm(false)}
@@ -172,7 +172,7 @@ export function ProjectActivityAnalysis(props: ProjectActivityAnalysisProps) {
 
               {addEventForm && (
                 <AddEventForm
-                  addEvent={props.addCustomEvent}
+                  addEvent={props.onAddCustomEvent}
                   addEventButtonText="project_activity.add_custom_event"
                   analysis={analysis}
                   onClose={() => setAddEventForm(false)}
@@ -182,7 +182,7 @@ export function ProjectActivityAnalysis(props: ProjectActivityAnalysisProps) {
               {removeAnalysisForm && (
                 <RemoveAnalysisForm
                   analysis={analysis}
-                  deleteAnalysis={props.deleteAnalysis}
+                  deleteAnalysis={props.onDeleteAnalysis}
                   onClose={() => setRemoveAnalysisForm(false)}
                 />
               )}
@@ -197,8 +197,8 @@ export function ProjectActivityAnalysis(props: ProjectActivityAnalysisProps) {
           canAdmin={canAdmin}
           events={analysis.events}
           isFirst={isFirst}
-          onChange={props.changeEvent}
-          onDelete={props.deleteEvent}
+          onChange={props.onChangeEvent}
+          onDelete={props.onDeleteEvent}
         />
       )}
 
index a676f602f5aaf0b5e7886f68e73570c167bbaaab..d4b7ff0af5ca3c6aa58954ef7044a45cdf4d3a9b 100644 (file)
@@ -43,7 +43,12 @@ import { serializeStringArray } from '../../../helpers/query';
 import { BranchLike } from '../../../types/branch-like';
 import { ComponentQualifier, isPortfolioLike } from '../../../types/component';
 import { MetricKey } from '../../../types/metrics';
-import { GraphType, MeasureHistory, ParsedAnalysis } from '../../../types/project-activity';
+import {
+  GraphType,
+  MeasureHistory,
+  ParsedAnalysis,
+  ProjectAnalysisEventCategory,
+} from '../../../types/project-activity';
 import { Component, Dict, Metric, Paging, RawQuery } from '../../../types/types';
 import * as actions from '../actions';
 import {
@@ -77,7 +82,7 @@ export const PROJECT_ACTIVITY_GRAPH = 'sonar_project_activity.graph';
 const ACTIVITY_PAGE_SIZE_FIRST_BATCH = 100;
 const ACTIVITY_PAGE_SIZE = 500;
 
-export class ProjectActivityApp extends React.PureComponent<Props, State> {
+class ProjectActivityApp extends React.PureComponent<Props, State> {
   mounted = false;
 
   constructor(props: Props) {
@@ -116,7 +121,7 @@ export class ProjectActivityApp extends React.PureComponent<Props, State> {
     this.mounted = false;
   }
 
-  addCustomEvent = (analysisKey: string, name: string, category?: string) => {
+  handleAddCustomEvent = (analysisKey: string, name: string, category?: string) => {
     return createEvent(analysisKey, name, category).then(({ analysis, ...event }) => {
       if (this.mounted) {
         this.setState(actions.addCustomEvent(analysis, event));
@@ -124,11 +129,11 @@ export class ProjectActivityApp extends React.PureComponent<Props, State> {
     });
   };
 
-  addVersion = (analysis: string, version: string) => {
-    return this.addCustomEvent(analysis, version, 'VERSION');
+  handleAddVersion = (analysis: string, version: string) => {
+    return this.handleAddCustomEvent(analysis, version, ProjectAnalysisEventCategory.Version);
   };
 
-  changeEvent = (eventKey: string, name: string) => {
+  handleChangeEvent = (eventKey: string, name: string) => {
     return changeEvent(eventKey, name).then(({ analysis, ...event }) => {
       if (this.mounted) {
         this.setState(actions.changeEvent(analysis, event));
@@ -136,7 +141,7 @@ export class ProjectActivityApp extends React.PureComponent<Props, State> {
     });
   };
 
-  deleteAnalysis = (analysis: string) => {
+  handleDeleteAnalysis = (analysis: string) => {
     return deleteAnalysis(analysis).then(() => {
       if (this.mounted) {
         this.updateGraphData(
@@ -148,7 +153,7 @@ export class ProjectActivityApp extends React.PureComponent<Props, State> {
     });
   };
 
-  deleteEvent = (analysis: string, event: string) => {
+  handleDeleteEvent = (analysis: string, event: string) => {
     return deleteEvent(event).then(() => {
       if (this.mounted) {
         this.setState(actions.deleteEvent(analysis, event));
@@ -334,7 +339,7 @@ export class ProjectActivityApp extends React.PureComponent<Props, State> {
     );
   };
 
-  updateQuery = (newQuery: Query) => {
+  handleUpdateQuery = (newQuery: Query) => {
     const query = serializeUrlQuery({
       ...this.state.query,
       ...newQuery,
@@ -353,20 +358,20 @@ export class ProjectActivityApp extends React.PureComponent<Props, State> {
     const metrics = this.filterMetrics();
     return (
       <ProjectActivityAppRenderer
-        addCustomEvent={this.addCustomEvent}
-        addVersion={this.addVersion}
+        onAddCustomEvent={this.handleAddCustomEvent}
+        onAddVersion={this.handleAddVersion}
         analyses={this.state.analyses}
         analysesLoading={this.state.analysesLoading}
-        changeEvent={this.changeEvent}
-        deleteAnalysis={this.deleteAnalysis}
-        deleteEvent={this.deleteEvent}
+        onChangeEvent={this.handleChangeEvent}
+        onDeleteAnalysis={this.handleDeleteAnalysis}
+        onDeleteEvent={this.handleDeleteEvent}
         graphLoading={!this.state.initialized || this.state.graphLoading}
         initializing={!this.state.initialized}
         measuresHistory={this.state.measuresHistory}
         metrics={metrics}
         project={this.props.component}
         query={this.state.query}
-        updateQuery={this.updateQuery}
+        onUpdateQuery={this.handleUpdateQuery}
       />
     );
   }
index 4bb0d646efeb9dc83a07310332ecb6124ee67aa7..ed619464a56c72bf0cc26126ae20451492c1fa36 100644 (file)
@@ -23,6 +23,7 @@ import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
 import Suggestions from '../../../components/embed-docs-modal/Suggestions';
 import { parseDate } from '../../../helpers/dates';
 import { translate } from '../../../helpers/l10n';
+import { ComponentQualifier } from '../../../types/component';
 import { MeasureHistory, ParsedAnalysis } from '../../../types/project-activity';
 import { Component, Metric } from '../../../types/types';
 import { Query } from '../utils';
@@ -32,27 +33,28 @@ import ProjectActivityGraphs from './ProjectActivityGraphs';
 import ProjectActivityPageFilters from './ProjectActivityPageFilters';
 
 interface Props {
-  addCustomEvent: (analysis: string, name: string, category?: string) => Promise<void>;
-  addVersion: (analysis: string, version: string) => Promise<void>;
+  onAddCustomEvent: (analysis: string, name: string, category?: string) => Promise<void>;
+  onAddVersion: (analysis: string, version: string) => Promise<void>;
   analyses: ParsedAnalysis[];
   analysesLoading: boolean;
-  changeEvent: (event: string, name: string) => Promise<void>;
-  deleteAnalysis: (analysis: string) => Promise<void>;
-  deleteEvent: (analysis: string, event: string) => Promise<void>;
+  onChangeEvent: (event: string, name: string) => Promise<void>;
+  onDeleteAnalysis: (analysis: string) => Promise<void>;
+  onDeleteEvent: (analysis: string, event: string) => Promise<void>;
   graphLoading: boolean;
   initializing: boolean;
   project: Pick<Component, 'configuration' | 'key' | 'leakPeriodDate' | 'qualifier'>;
   metrics: Metric[];
   measuresHistory: MeasureHistory[];
   query: Query;
-  updateQuery: (changes: Partial<Query>) => void;
+  onUpdateQuery: (changes: Partial<Query>) => void;
 }
 
 export default function ProjectActivityAppRenderer(props: Props) {
   const { analyses, measuresHistory, query } = props;
   const { configuration } = props.project;
   const canAdmin =
-    (props.project.qualifier === 'TRK' || props.project.qualifier === 'APP') &&
+    (props.project.qualifier === ComponentQualifier.Project ||
+      props.project.qualifier === ComponentQualifier.Application) &&
     (configuration ? configuration.showHistory : false);
   const canDeleteAnalyses = configuration ? configuration.showHistory : false;
   return (
@@ -67,28 +69,28 @@ export default function ProjectActivityAppRenderer(props: Props) {
         from={query.from}
         project={props.project}
         to={query.to}
-        updateQuery={props.updateQuery}
+        updateQuery={props.onUpdateQuery}
       />
 
       <div className="layout-page project-activity-page">
         <div className="layout-page-side-outer project-activity-page-side-outer boxed-group">
           <ProjectActivityAnalysesList
-            addCustomEvent={props.addCustomEvent}
-            addVersion={props.addVersion}
+            onAddCustomEvent={props.onAddCustomEvent}
+            onAddVersion={props.onAddVersion}
             analyses={analyses}
             analysesLoading={props.analysesLoading}
             canAdmin={canAdmin}
             canDeleteAnalyses={canDeleteAnalyses}
-            changeEvent={props.changeEvent}
-            deleteAnalysis={props.deleteAnalysis}
-            deleteEvent={props.deleteEvent}
+            onChangeEvent={props.onChangeEvent}
+            onDeleteAnalysis={props.onDeleteAnalysis}
+            onDeleteEvent={props.onDeleteEvent}
             initializing={props.initializing}
             leakPeriodDate={
               props.project.leakPeriodDate ? parseDate(props.project.leakPeriodDate) : undefined
             }
             project={props.project}
             query={query}
-            updateQuery={props.updateQuery}
+            onUpdateQuery={props.onUpdateQuery}
           />
         </div>
         <div className="project-activity-layout-page-main">
@@ -102,7 +104,7 @@ export default function ProjectActivityAppRenderer(props: Props) {
             metrics={props.metrics}
             project={props.project.key}
             query={query}
-            updateQuery={props.updateQuery}
+            updateQuery={props.onUpdateQuery}
           />
         </div>
       </div>
index 7014540d32aaf6c84a9944a91cb21e1285f23438..3f54362aef64d7f98f009a4661ee7fb04fa8218a 100644 (file)
@@ -148,13 +148,13 @@ export default class ProjectActivityGraphs extends React.PureComponent<Props, St
       .map((graph) => graph[0].type);
   };
 
-  addCustomMetric = (metric: string) => {
+  handleAddCustomMetric = (metric: string) => {
     const customMetrics = [...this.props.query.customMetrics, metric];
     saveActivityGraph(PROJECT_ACTIVITY_GRAPH, this.props.project, GraphType.custom, customMetrics);
     this.props.updateQuery({ customMetrics });
   };
 
-  removeCustomMetric = (removedMetric: string) => {
+  handleRemoveCustomMetric = (removedMetric: string) => {
     const customMetrics = this.props.query.customMetrics.filter(
       (metric) => metric !== removedMetric
     );
@@ -162,7 +162,7 @@ export default class ProjectActivityGraphs extends React.PureComponent<Props, St
     this.props.updateQuery({ customMetrics });
   };
 
-  updateGraph = (graph: GraphType) => {
+  handleUpdateGraph = (graph: GraphType) => {
     saveActivityGraph(PROJECT_ACTIVITY_GRAPH, this.props.project, graph);
     if (isCustomGraph(graph) && this.props.query.customMetrics.length <= 0) {
       const { customGraphs } = getActivityGraph(PROJECT_ACTIVITY_GRAPH, this.props.project);
@@ -172,7 +172,7 @@ export default class ProjectActivityGraphs extends React.PureComponent<Props, St
     }
   };
 
-  updateGraphZoom = (graphStartDate?: Date, graphEndDate?: Date) => {
+  handleUpdateGraphZoom = (graphStartDate?: Date, graphEndDate?: Date) => {
     if (graphEndDate !== undefined && graphStartDate !== undefined) {
       const msDiff = Math.abs(graphEndDate.valueOf() - graphStartDate.valueOf());
       // 12 hours minimum between the two dates
@@ -185,7 +185,9 @@ export default class ProjectActivityGraphs extends React.PureComponent<Props, St
     this.updateQueryDateRange([graphStartDate, graphEndDate]);
   };
 
-  updateSelectedDate = (selectedDate?: Date) => this.props.updateQuery({ selectedDate });
+  handleUpdateSelectedDate = (selectedDate?: Date) => {
+    this.props.updateQuery({ selectedDate });
+  };
 
   updateQueryDateRange = (dates: Array<Date | undefined>) => {
     if (dates[0] === undefined || dates[1] === undefined) {
@@ -197,35 +199,35 @@ export default class ProjectActivityGraphs extends React.PureComponent<Props, St
   };
 
   render() {
-    const { leakPeriodDate, loading, metrics, query } = this.props;
+    const { analyses, leakPeriodDate, loading, measuresHistory, metrics, query } = this.props;
     const { graphEndDate, graphStartDate, series } = this.state;
 
     return (
       <div className="project-activity-layout-page-main-inner boxed-group boxed-group-inner">
         <GraphsHeader
-          addCustomMetric={this.addCustomMetric}
+          onAddCustomMetric={this.handleAddCustomMetric}
           className="big-spacer-bottom"
           graph={query.graph}
           metrics={metrics}
           metricsTypeFilter={this.getMetricsTypeFilter()}
-          removeCustomMetric={this.removeCustomMetric}
-          selectedMetrics={this.props.query.customMetrics}
-          updateGraph={this.updateGraph}
+          onRemoveCustomMetric={this.handleRemoveCustomMetric}
+          selectedMetrics={query.customMetrics}
+          onUpdateGraph={this.handleUpdateGraph}
         />
         <GraphsHistory
-          analyses={this.props.analyses}
+          analyses={analyses}
           graph={query.graph}
           graphEndDate={graphEndDate}
           graphStartDate={graphStartDate}
           graphs={this.state.graphs}
           leakPeriodDate={leakPeriodDate}
           loading={loading}
-          measuresHistory={this.props.measuresHistory}
-          removeCustomMetric={this.removeCustomMetric}
-          selectedDate={this.props.query.selectedDate}
+          measuresHistory={measuresHistory}
+          removeCustomMetric={this.handleRemoveCustomMetric}
+          selectedDate={query.selectedDate}
           series={series}
-          updateGraphZoom={this.updateGraphZoom}
-          updateSelectedDate={this.updateSelectedDate}
+          updateGraphZoom={this.handleUpdateGraphZoom}
+          updateSelectedDate={this.handleUpdateSelectedDate}
         />
         <GraphsZoom
           graphEndDate={graphEndDate}
@@ -235,7 +237,7 @@ export default class ProjectActivityGraphs extends React.PureComponent<Props, St
           metricsType={getSeriesMetricType(series)}
           series={series}
           showAreas={[GraphType.coverage, GraphType.duplications].includes(query.graph)}
-          updateGraphZoom={this.updateGraphZoom}
+          onUpdateGraphZoom={this.handleUpdateGraphZoom}
         />
       </div>
     );
index df69b89b786cbb597e24c17a13404191ea231c0e..c934d236443f8ef0140d22341128bebc5623cdee 100644 (file)
 import * as React from 'react';
 import Select from '../../../components/controls/Select';
 import { translate } from '../../../helpers/l10n';
-import { ComponentQualifier } from '../../../types/component';
+import { ComponentQualifier, isPortfolioLike } from '../../../types/component';
+import {
+  ApplicationAnalysisEventCategory,
+  ProjectAnalysisEventCategory,
+} from '../../../types/project-activity';
 import { Component } from '../../../types/types';
-import { APPLICATION_EVENT_TYPES, EVENT_TYPES, Query } from '../utils';
+import { Query } from '../utils';
 import ProjectActivityDateInput from './ProjectActivityDateInput';
 
 interface ProjectActivityPageFiltersProps {
@@ -37,7 +41,9 @@ export default function ProjectActivityPageFilters(props: ProjectActivityPageFil
   const { project, category, from, to, updateQuery } = props;
 
   const isApp = project.qualifier === ComponentQualifier.Application;
-  const eventTypes = isApp ? APPLICATION_EVENT_TYPES : EVENT_TYPES;
+  const eventTypes = isApp
+    ? Object.values(ApplicationAnalysisEventCategory)
+    : Object.values(ProjectAnalysisEventCategory);
   const options = eventTypes.map((category) => ({
     label: translate('event.category', category),
     value: category,
@@ -52,14 +58,15 @@ export default function ProjectActivityPageFilters(props: ProjectActivityPageFil
 
   return (
     <div className="page-header display-flex-start">
-      {!([ComponentQualifier.Portfolio, ComponentQualifier.SubPortfolio] as string[]).includes(
-        project.qualifier
-      ) && (
+      {!isPortfolioLike(project.qualifier) && (
         <div className="display-flex-column big-spacer-right">
           <label className="text-bold little-spacer-bottom" htmlFor="filter-events">
             {translate('project_activity.filter_events')}
           </label>
           <Select
+            // For some reason, not setting this aria-label makes some tests fail. They cannot seem to link
+            // the label above with this input.
+            aria-label={translate('project_activity.filter_events')}
             className={isApp ? 'input-large' : 'input-medium'}
             id="filter-events"
             isClearable={true}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.tsx
deleted file mode 100644 (file)
index 44a3230..0000000
+++ /dev/null
@@ -1,35 +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 * as React from 'react';
-import ListFooter from '../../../components/controls/ListFooter';
-import { Paging } from '../../../types/types';
-
-interface Props {
-  analyses: unknown[];
-  fetchMoreActivity: () => void;
-  paging?: Paging;
-}
-
-export default function ProjectActivityPageFooter({ analyses, fetchMoreActivity, paging }: Props) {
-  if (!paging || analyses.length === 0) {
-    return null;
-  }
-  return <ListFooter count={analyses.length} loadMore={fetchMoreActivity} total={paging.total} />;
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx
deleted file mode 100644 (file)
index 3382624..0000000
+++ /dev/null
@@ -1,140 +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 { shallow } from 'enzyme';
-import * as React from 'react';
-import { DEFAULT_GRAPH } from '../../../../components/activity-graph/utils';
-import { parseDate } from '../../../../helpers/dates';
-import { mockParsedAnalysis } from '../../../../helpers/mocks/project-activity';
-import { ComponentQualifier } from '../../../../types/component';
-import ProjectActivityAnalysesList from '../ProjectActivityAnalysesList';
-
-jest.mock('date-fns', () => {
-  const actual = jest.requireActual('date-fns');
-  return {
-    ...actual,
-    startOfDay: (date: Date) => {
-      const startDay = new Date(date);
-      startDay.setUTCHours(0, 0, 0, 0);
-      return startDay;
-    },
-  };
-});
-
-jest.mock('../../../../helpers/dates', () => {
-  const actual = jest.requireActual('../../../../helpers/dates');
-  return { ...actual, toShortNotSoISOString: (date: string) => 'ISO.' + date };
-});
-
-const DATE = parseDate('2016-10-27T16:33:50+0000');
-
-const DEFAULT_QUERY = {
-  category: '',
-  customMetrics: [],
-  graph: DEFAULT_GRAPH,
-  project: 'org.sonarsource.sonarqube:sonarqube',
-};
-
-it('should render correctly', () => {
-  expect(shallowRender()).toMatchSnapshot('default');
-  expect(shallowRender({ project: { qualifier: ComponentQualifier.Application } })).toMatchSnapshot(
-    'application'
-  );
-  expect(shallowRender({ analyses: [], initializing: true })).toMatchSnapshot('loading');
-  expect(shallowRender({ analyses: [] })).toMatchSnapshot('no analyses');
-});
-
-it('should correctly filter analyses by category', () => {
-  const wrapper = shallowRender();
-  wrapper.setProps({ query: { ...DEFAULT_QUERY, category: 'QUALITY_GATE' } });
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should correctly filter analyses by date range', () => {
-  const wrapper = shallowRender();
-  wrapper.setProps({
-    query: {
-      ...DEFAULT_QUERY,
-      from: DATE,
-      to: DATE,
-    },
-  });
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should correctly update the selected date', () => {
-  const selectedDate = new Date();
-  const updateQuery = jest.fn();
-  const wrapper = shallowRender({ updateQuery });
-  wrapper.instance().updateSelectedDate(selectedDate);
-  expect(updateQuery).toHaveBeenCalledWith({ selectedDate });
-});
-
-it('should correctly reset scroll if filters change', () => {
-  const wrapper = shallowRender();
-  const scrollContainer = document.createElement('ul');
-  scrollContainer.scrollTop = 100;
-
-  // Saves us a call to mount().
-  wrapper.instance().scrollContainer = scrollContainer;
-
-  wrapper.setProps({ query: { ...DEFAULT_QUERY, category: 'OTHER' } });
-  expect(scrollContainer.scrollTop).toBe(0);
-});
-
-function shallowRender(props: Partial<ProjectActivityAnalysesList['props']> = {}) {
-  return shallow<ProjectActivityAnalysesList>(
-    <ProjectActivityAnalysesList
-      addCustomEvent={jest.fn().mockResolvedValue(undefined)}
-      addVersion={jest.fn().mockResolvedValue(undefined)}
-      analyses={[
-        mockParsedAnalysis({
-          key: 'A1',
-          date: DATE,
-          events: [{ key: 'E1', category: 'VERSION', name: '6.5-SNAPSHOT' }],
-        }),
-        mockParsedAnalysis({ key: 'A2', date: parseDate('2016-10-27T12:21:15+0000') }),
-        mockParsedAnalysis({
-          key: 'A3',
-          date: parseDate('2016-10-26T12:17:29+0000'),
-          events: [
-            { key: 'E2', category: 'VERSION', name: '6.4' },
-            { key: 'E3', category: 'OTHER', name: 'foo' },
-          ],
-        }),
-        mockParsedAnalysis({
-          key: 'A4',
-          date: parseDate('2016-10-24T16:33:50+0000'),
-          events: [{ key: 'E1', category: 'QUALITY_GATE', name: 'Quality gate changed to red...' }],
-        }),
-      ]}
-      analysesLoading={false}
-      canAdmin={false}
-      changeEvent={jest.fn().mockResolvedValue(undefined)}
-      deleteAnalysis={jest.fn().mockResolvedValue(undefined)}
-      deleteEvent={jest.fn().mockResolvedValue(undefined)}
-      initializing={false}
-      leakPeriodDate={parseDate('2016-10-27T12:21:15+0000')}
-      project={{ qualifier: ComponentQualifier.Project }}
-      query={DEFAULT_QUERY}
-      updateQuery={jest.fn()}
-      {...props}
-    />
-  );
-}
index 718179107daa19d5384fe898ce5d8ce0878ef15d..d18daecdff6c366fb83139fa7f3351dd8ba6752f 100644 (file)
  */
 import { screen, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
-import { keyBy } from 'lodash';
+import { keyBy, times } from 'lodash';
 import React from 'react';
+import { act } from 'react-dom/test-utils';
 import { Route } from 'react-router-dom';
+import selectEvent from 'react-select-event';
 import { byLabelText, byRole, byText } from 'testing-library-selector';
 import { ProjectActivityServiceMock } from '../../../../api/mocks/ProjectActivityServiceMock';
-import { getAllTimeMachineData } from '../../../../api/time-machine';
+import { TimeMachineServiceMock } from '../../../../api/mocks/TimeMachineServiceMock';
+import { parseDate } from '../../../../helpers/dates';
 import { mockComponent } from '../../../../helpers/mocks/component';
+import {
+  mockAnalysis,
+  mockAnalysisEvent,
+  mockHistoryItem,
+  mockMeasureHistory,
+} from '../../../../helpers/mocks/project-activity';
 import { get } from '../../../../helpers/storage';
-import { mockMetric, mockPaging } from '../../../../helpers/testMocks';
-import { renderAppWithComponentContext } from '../../../../helpers/testReactTestingUtils';
+import { mockMetric } from '../../../../helpers/testMocks';
+import {
+  dateInputEvent,
+  renderAppWithComponentContext,
+} from '../../../../helpers/testReactTestingUtils';
 import { ComponentQualifier } from '../../../../types/component';
-import { MetricKey } from '../../../../types/metrics';
-import { GraphType } from '../../../../types/project-activity';
+import { MetricKey, MetricType } from '../../../../types/metrics';
+import {
+  ApplicationAnalysisEventCategory,
+  GraphType,
+  ProjectAnalysisEventCategory,
+} from '../../../../types/project-activity';
 import ProjectActivityAppContainer from '../ProjectActivityApp';
 
 jest.mock('../../../../api/projectActivity');
-
-jest.mock('../../../../api/time-machine', () => ({
-  getAllTimeMachineData: jest.fn(),
-}));
+jest.mock('../../../../api/time-machine');
 
 jest.mock('../../../../helpers/storage', () => ({
   ...jest.requireActual('../../../../helpers/storage'),
   get: jest.fn(),
+  save: jest.fn(),
 }));
 
-let handler: ProjectActivityServiceMock;
+const projectActivityHandler = new ProjectActivityServiceMock();
+const timeMachineHandler = new TimeMachineServiceMock();
+
+beforeEach(() => {
+  jest.clearAllMocks();
+  projectActivityHandler.reset();
+  timeMachineHandler.reset();
+  timeMachineHandler.setMeasureHistory(
+    [
+      MetricKey.bugs,
+      MetricKey.reliability_rating,
+      MetricKey.code_smells,
+      MetricKey.sqale_rating,
+      MetricKey.security_hotspots_reviewed,
+      MetricKey.security_review_rating,
+    ].map((metric) =>
+      mockMeasureHistory({
+        metric,
+        history: projectActivityHandler
+          .getAnalysesList()
+          .map(({ date }) => mockHistoryItem({ value: '3', date: parseDate(date) })),
+      })
+    )
+  );
+});
 
-beforeAll(() => {
-  handler = new ProjectActivityServiceMock();
-  (getAllTimeMachineData as jest.Mock).mockResolvedValue({
-    measures: [
-      {
-        metric: MetricKey.reliability_rating,
-        history: handler.analysisList.map(({ date }) => ({ date, value: '2.0' })),
-      },
-      {
-        metric: MetricKey.bugs,
-        history: handler.analysisList.map(({ date }) => ({ date, value: '10' })),
-      },
-    ],
-    paging: mockPaging(),
+describe('rendering', () => {
+  it('should render issues as default graph', async () => {
+    const { ui } = getPageObject();
+    renderProjectActivityAppContainer();
+    await ui.appLoaded();
+
+    expect(ui.graphTypeIssues.get()).toBeInTheDocument();
+    expect(ui.graphs.getAll().length).toBe(1);
   });
-});
 
-beforeEach(jest.clearAllMocks);
-
-afterEach(() => handler.reset());
-
-const ui = {
-  // Graph types.
-  graphTypeIssues: byText('project_activity.graphs.issues'),
-  graphTypeCustom: byText('project_activity.graphs.custom'),
-
-  // Add metrics.
-  addMetricBtn: byRole('button', { name: 'project_activity.graphs.custom.add' }),
-  reviewedHotspotsCheckbox: byRole('checkbox', { name: MetricKey.security_hotspots_reviewed }),
-  reviewRatingCheckbox: byRole('checkbox', { name: MetricKey.security_review_rating }),
-
-  // Analysis interactions.
-  cogBtn: (id: string) => byRole('button', { name: `project_activity.analysis_X_actions.${id}` }),
-  seeDetailsBtn: (time: string) =>
-    byRole('button', { name: `project_activity.show_analysis_X_on_graph.${time}` }),
-  addCustomEventBtn: byRole('button', { name: 'project_activity.add_custom_event' }),
-  addVersionEvenBtn: byRole('button', { name: 'project_activity.add_version' }),
-  deleteAnalysisBtn: byRole('button', { name: 'project_activity.delete_analysis' }),
-  editEventBtn: byRole('button', { name: 'project_activity.events.tooltip.edit' }),
-  deleteEventBtn: byRole('button', { name: 'project_activity.events.tooltip.delete' }),
-
-  // Event modal.
-  nameInput: byLabelText('name'),
-  saveBtn: byRole('button', { name: 'save' }),
-  changeBtn: byRole('button', { name: 'change_verb' }),
-  deleteBtn: byRole('button', { name: 'delete' }),
-
-  // Misc.
-  loading: byLabelText('loading'),
-  baseline: byText('project_activity.new_code_period_start'),
-  bugsPopupCell: byRole('cell', { name: 'bugs' }),
-};
-
-it('should render issues as default graph', async () => {
-  renderProjectActivityAppContainer();
-
-  expect(await ui.graphTypeIssues.find()).toBeInTheDocument();
-});
+  it('should correctly show the baseline marker', async () => {
+    const { ui } = getPageObject();
+    renderProjectActivityAppContainer(
+      mockComponent({
+        leakPeriodDate: parseDate('2017-03-01T22:00:00.000Z').toDateString(),
+        breadcrumbs: [
+          { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project },
+        ],
+      })
+    );
+    await ui.appLoaded();
 
-it('should reload custom graph from local storage', async () => {
-  (get as jest.Mock).mockImplementation((namespace: string) =>
-    // eslint-disable-next-line jest/no-conditional-in-test
-    namespace.includes('.custom') ? 'bugs,code_smells' : GraphType.custom
-  );
-  renderProjectActivityAppContainer();
+    expect(ui.baseline.get()).toBeInTheDocument();
+  });
+
+  it('should only show certain security hotspot-related metrics for a project', async () => {
+    const { ui } = getPageObject();
+    renderProjectActivityAppContainer(
+      mockComponent({
+        breadcrumbs: [
+          { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project },
+        ],
+      })
+    );
 
-  expect(await ui.graphTypeCustom.find()).toBeInTheDocument();
+    await ui.changeGraphType(GraphType.custom);
+    await ui.openMetricsDropdown();
+    expect(ui.metricCheckbox(MetricKey.security_hotspots_reviewed).get()).toBeInTheDocument();
+    expect(ui.metricCheckbox(MetricKey.security_review_rating).query()).not.toBeInTheDocument();
+  });
+
+  it.each([ComponentQualifier.Portfolio, ComponentQualifier.SubPortfolio])(
+    'should only show certain security hotspot-related metrics for a %s',
+    async (qualifier) => {
+      const { ui } = getPageObject();
+      renderProjectActivityAppContainer(
+        mockComponent({
+          qualifier,
+          breadcrumbs: [{ key: 'breadcrumb', name: 'breadcrumb', qualifier }],
+        })
+      );
+
+      await ui.changeGraphType(GraphType.custom);
+      await ui.openMetricsDropdown();
+      expect(ui.metricCheckbox(MetricKey.security_review_rating).get()).toBeInTheDocument();
+      expect(
+        ui.metricCheckbox(MetricKey.security_hotspots_reviewed).query()
+      ).not.toBeInTheDocument();
+    }
+  );
 });
 
-it.each([
-  ['OTHER', ui.addCustomEventBtn, 'Custom event name', 'Custom event updated name'],
-  ['VERSION', ui.addVersionEvenBtn, '1.1-SNAPSHOT', '1.1--SNAPSHOT'],
-])(
-  'should correctly create, update, and delete %s events',
-  async (_, btn, initialValue, updatedValue) => {
-    const user = userEvent.setup();
+describe('CRUD', () => {
+  it('should correctly create, update, and delete "VERSION" events', async () => {
+    const { ui } = getPageObject();
+    const initialValue = '1.1-SNAPSHOT';
+    const updatedValue = '1.1--SNAPSHOT';
     renderProjectActivityAppContainer(
       mockComponent({
         breadcrumbs: [
@@ -131,104 +160,348 @@ it.each([
         configuration: { showHistory: true },
       })
     );
-    await waitOnDataLoaded();
-
-    await user.click(ui.cogBtn('1.1.0.1').get());
-    await user.click(btn.get());
-    await user.type(ui.nameInput.get(), initialValue);
-    await user.click(ui.saveBtn.get());
+    await ui.appLoaded();
 
+    await ui.addVersionEvent('1.1.0.1', initialValue);
     expect(screen.getAllByText(initialValue)[0]).toBeInTheDocument();
 
-    await user.click(ui.editEventBtn.getAll()[1]);
-    await user.clear(ui.nameInput.get());
-    await user.type(ui.nameInput.get(), updatedValue);
-    await user.click(ui.changeBtn.get());
+    await act(async () => {
+      await ui.updateEvent(1, updatedValue);
+      expect(screen.getAllByText(updatedValue)[0]).toBeInTheDocument();
+    });
 
-    expect(screen.getAllByText(updatedValue)[0]).toBeInTheDocument();
+    await ui.deleteEvent(0);
+    expect(screen.queryByText(updatedValue)).not.toBeInTheDocument();
+  });
 
-    await user.click(ui.deleteEventBtn.getAll()[0]);
-    await user.click(ui.deleteBtn.get());
+  it('should correctly create, update, and delete "OTHER" events', async () => {
+    const { ui } = getPageObject();
+    const initialValue = 'Custom event name';
+    const updatedValue = 'Custom event updated name';
+    renderProjectActivityAppContainer(
+      mockComponent({
+        breadcrumbs: [
+          { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project },
+        ],
+        configuration: { showHistory: true },
+      })
+    );
+    await ui.appLoaded();
 
-    expect(screen.queryByText(updatedValue)).not.toBeInTheDocument();
-  }
-);
+    await act(async () => {
+      await ui.addCustomEvent('1.1.0.1', initialValue);
+      expect(screen.getByText(initialValue)).toBeInTheDocument();
+    });
 
-it('should correctly allow deletion of specific analyses', async () => {
-  const user = userEvent.setup();
-  renderProjectActivityAppContainer(
-    mockComponent({
-      breadcrumbs: [
-        { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project },
-      ],
-      configuration: { showHistory: true },
-    })
-  );
-  await waitOnDataLoaded();
+    await act(async () => {
+      await ui.updateEvent(1, updatedValue);
+      expect(screen.getByText(updatedValue)).toBeInTheDocument();
+    });
 
-  // Most recent analysis is not deletable.
-  await user.click(ui.cogBtn('1.1.0.2').get());
-  expect(ui.deleteAnalysisBtn.query()).not.toBeInTheDocument();
+    await ui.deleteEvent(0);
+    expect(screen.queryByText(updatedValue)).not.toBeInTheDocument();
+  });
 
-  await user.click(ui.cogBtn('1.1.0.1').get());
-  await user.click(ui.deleteAnalysisBtn.get());
-  await user.click(ui.deleteBtn.get());
+  it('should correctly allow deletion of specific analyses', async () => {
+    const { ui } = getPageObject();
+    renderProjectActivityAppContainer(
+      mockComponent({
+        breadcrumbs: [
+          { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project },
+        ],
+        configuration: { showHistory: true },
+      })
+    );
+    await ui.appLoaded();
 
-  expect(screen.queryByText('1.1.0.1')).not.toBeInTheDocument();
+    // Most recent analysis is not deletable.
+    await ui.openCogMenu('1.1.0.2');
+    expect(ui.deleteAnalysisBtn.query()).not.toBeInTheDocument();
+
+    await ui.deleteAnalysis('1.1.0.1');
+    expect(screen.queryByText('1.1.0.1')).not.toBeInTheDocument();
+  });
 });
 
-it('should correctly show the baseline marker', async () => {
-  renderProjectActivityAppContainer(
-    mockComponent({
-      leakPeriodDate: '2017-03-01T10:36:01+0100',
-      breadcrumbs: [
-        { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project },
-      ],
-    })
-  );
-  await waitOnDataLoaded();
+describe('data loading', () => {
+  function getMock(namespace: string) {
+    // eslint-disable-next-line jest/no-conditional-in-test
+    return namespace.includes('.custom') ? 'bugs,code_smells' : GraphType.custom;
+  }
 
-  expect(ui.baseline.get()).toBeInTheDocument();
-});
+  it('should load all analyses', async () => {
+    const count = 1000;
+    projectActivityHandler.setAnalysesList(
+      times(count, (i) => {
+        return mockAnalysis({
+          key: `analysis-${i}`,
+          date: '2016-01-01T00:00:00+0200',
+        });
+      })
+    );
+    const { ui } = getPageObject();
+    renderProjectActivityAppContainer();
+    await ui.appLoaded();
+
+    expect(ui.activityItem.getAll().length).toBe(count);
+  });
+
+  it('should reload custom graph from local storage', async () => {
+    jest.mocked(get).mockImplementationOnce(getMock).mockImplementationOnce(getMock);
+    const { ui } = getPageObject();
+    renderProjectActivityAppContainer();
+    await ui.appLoaded();
 
-it.each([
-  [ComponentQualifier.Project, ui.reviewedHotspotsCheckbox, ui.reviewRatingCheckbox],
-  [ComponentQualifier.Portfolio, ui.reviewRatingCheckbox, ui.reviewedHotspotsCheckbox],
-  [ComponentQualifier.SubPortfolio, ui.reviewRatingCheckbox, ui.reviewedHotspotsCheckbox],
-])(
-  'should only show certain security hotspot-related metrics for a component with qualifier %s',
-  async (qualifier, visible, invisible) => {
-    const user = userEvent.setup();
+    expect(ui.graphTypeCustom.get()).toBeInTheDocument();
+  });
+
+  it('should correctly fetch the top level component when dealing with sub portfolios', async () => {
+    const { ui } = getPageObject();
     renderProjectActivityAppContainer(
       mockComponent({
-        qualifier,
-        breadcrumbs: [{ key: 'breadcrumb', name: 'breadcrumb', qualifier }],
+        key: 'unknown',
+        qualifier: ComponentQualifier.SubPortfolio,
+        breadcrumbs: [
+          { key: 'foo', name: 'foo', qualifier: ComponentQualifier.Portfolio },
+          { key: 'unknown', name: 'unknown', qualifier: ComponentQualifier.SubPortfolio },
+        ],
       })
     );
+    await ui.appLoaded();
 
-    await user.click(ui.addMetricBtn.get());
+    // If it didn't fail, it means we correctly queried for project "foo".
+    expect(ui.activityItem.getAll().length).toBe(4);
+  });
+});
 
-    expect(visible.get()).toBeInTheDocument();
-    expect(invisible.query()).not.toBeInTheDocument();
-  }
-);
+describe('filtering', () => {
+  it('should correctly filter by event category', async () => {
+    projectActivityHandler.setAnalysesList([
+      mockAnalysis({
+        key: `analysis-1`,
+        events: [],
+      }),
+      mockAnalysis({
+        key: `analysis-2`,
+        events: [
+          mockAnalysisEvent({ key: '1', category: ProjectAnalysisEventCategory.QualityGate }),
+        ],
+      }),
+      mockAnalysis({
+        key: `analysis-3`,
+        events: [mockAnalysisEvent({ key: '2', category: ProjectAnalysisEventCategory.Version })],
+      }),
+      mockAnalysis({
+        key: `analysis-4`,
+        events: [mockAnalysisEvent({ key: '3', category: ProjectAnalysisEventCategory.Version })],
+      }),
+    ]);
+
+    const { ui } = getPageObject();
+    renderProjectActivityAppContainer();
+    await ui.appLoaded();
+
+    await ui.filterByCategory(ProjectAnalysisEventCategory.Version);
+    expect(ui.activityItem.getAll().length).toBe(2);
+
+    await ui.filterByCategory(ProjectAnalysisEventCategory.QualityGate);
+    expect(ui.activityItem.getAll().length).toBe(1);
+  });
 
-it('should allow analyses to be clicked', async () => {
-  const user = userEvent.setup();
-  renderProjectActivityAppContainer();
-  await waitOnDataLoaded();
+  it('should correctly filter by date range', async () => {
+    projectActivityHandler.setAnalysesList(
+      times(20, (i) => {
+        const date = parseDate('2016-01-01T00:00:00.000Z');
+        date.setDate(date.getDate() + i);
+        return mockAnalysis({
+          key: `analysis-${i}`,
+          date: date.toDateString(),
+        });
+      })
+    );
+    const { ui } = getPageObject();
+    renderProjectActivityAppContainer();
+    await ui.appLoaded();
+
+    expect(ui.activityItem.getAll().length).toBe(20);
+
+    await ui.setDateRange('2016-01-10');
+    expect(ui.activityItem.getAll().length).toBe(11);
+    await ui.resetDateFilters();
 
-  expect(ui.bugsPopupCell.query()).not.toBeInTheDocument();
+    expect(ui.activityItem.getAll().length).toBe(20);
 
-  await user.click(ui.seeDetailsBtn('1.0.0.1').get());
+    await ui.setDateRange('2016-01-10', '2016-01-11');
+    expect(ui.activityItem.getAll().length).toBe(2);
+    await ui.resetDateFilters();
 
-  expect(ui.bugsPopupCell.get()).toBeInTheDocument();
+    await ui.setDateRange(undefined, '2016-01-08');
+    expect(ui.activityItem.getAll().length).toBe(8);
+  });
 });
 
-async function waitOnDataLoaded() {
-  await waitFor(() => {
-    expect(ui.loading.query()).not.toBeInTheDocument();
+describe('graph interactions', () => {
+  it('should allow analyses to be clicked to see details for the analysis', async () => {
+    const { ui } = getPageObject();
+    renderProjectActivityAppContainer();
+    await ui.appLoaded();
+
+    expect(ui.bugsPopupCell.query()).not.toBeInTheDocument();
+    await act(async () => {
+      await ui.showDetails('1.1.0.1');
+    });
+    expect(ui.bugsPopupCell.get()).toBeInTheDocument();
   });
+
+  it('should correctly handle customizing the graph', async () => {
+    const { ui } = getPageObject();
+    renderProjectActivityAppContainer();
+    await ui.appLoaded();
+
+    await ui.changeGraphType(GraphType.custom);
+
+    expect(ui.noDataText.get()).toBeInTheDocument();
+
+    // Add metrics.
+    await ui.openMetricsDropdown();
+    await ui.toggleMetric(MetricKey.bugs);
+    await ui.toggleMetric(MetricKey.security_hotspots_reviewed);
+    await ui.closeMetricsDropdown();
+
+    expect(ui.graphs.getAll()).toHaveLength(2);
+
+    // Remove metrics.
+    await ui.openMetricsDropdown();
+    await ui.toggleMetric(MetricKey.bugs);
+    await ui.toggleMetric(MetricKey.security_hotspots_reviewed);
+    await ui.closeMetricsDropdown();
+
+    expect(ui.noDataText.get()).toBeInTheDocument();
+
+    await ui.changeGraphType(GraphType.issues);
+
+    expect(ui.graphs.getAll()).toHaveLength(1);
+  });
+});
+
+function getPageObject() {
+  const user = userEvent.setup();
+  const ui = {
+    // Graph types.
+    graphTypeSelect: byLabelText('project_activity.graphs.choose_type'),
+    graphTypeIssues: byText('project_activity.graphs.issues'),
+    graphTypeCustom: byText('project_activity.graphs.custom'),
+
+    // Graphs.
+    graphs: byLabelText('project_activity.graphs.explanation_x', { exact: false }),
+    noDataText: byText('project_activity.graphs.custom.no_history'),
+
+    // Add metrics.
+    addMetricBtn: byRole('button', { name: 'project_activity.graphs.custom.add' }),
+    metricCheckbox: (name: MetricKey) => byRole('checkbox', { name }),
+
+    // Filtering.
+    categorySelect: byRole('combobox', { name: 'project_activity.filter_events' }),
+    resetDatesBtn: byRole('button', { name: 'project_activity.reset_dates' }),
+    fromDateInput: byRole('textbox', { name: 'start_date' }),
+    toDateInput: byRole('textbox', { name: 'end_date' }),
+
+    // Analysis interactions.
+    activityItem: byLabelText(/project_activity.show_analysis_X_on_graph/),
+    cogBtn: (id: string) => byRole('button', { name: `project_activity.analysis_X_actions.${id}` }),
+    seeDetailsBtn: (time: string) =>
+      byRole('button', { name: `project_activity.show_analysis_X_on_graph.${time}` }),
+    addCustomEventBtn: byRole('button', { name: 'project_activity.add_custom_event' }),
+    addVersionEvenBtn: byRole('button', { name: 'project_activity.add_version' }),
+    deleteAnalysisBtn: byRole('button', { name: 'project_activity.delete_analysis' }),
+    editEventBtn: byRole('button', { name: 'project_activity.events.tooltip.edit' }),
+    deleteEventBtn: byRole('button', { name: 'project_activity.events.tooltip.delete' }),
+
+    // Event modal.
+    nameInput: byLabelText('name'),
+    saveBtn: byRole('button', { name: 'save' }),
+    changeBtn: byRole('button', { name: 'change_verb' }),
+    deleteBtn: byRole('button', { name: 'delete' }),
+
+    // Misc.
+    loading: byLabelText('loading'),
+    baseline: byText('project_activity.new_code_period_start'),
+    bugsPopupCell: byRole('cell', { name: 'bugs' }),
+  };
+
+  return {
+    user,
+    ui: {
+      ...ui,
+      async appLoaded() {
+        await waitFor(() => {
+          expect(ui.loading.query()).not.toBeInTheDocument();
+        });
+      },
+      async changeGraphType(type: GraphType) {
+        await selectEvent.select(ui.graphTypeSelect.get(), [`project_activity.graphs.${type}`]);
+      },
+      async openMetricsDropdown() {
+        await user.click(ui.addMetricBtn.get());
+      },
+      async toggleMetric(metric: MetricKey) {
+        await user.click(ui.metricCheckbox(metric).get());
+      },
+      async closeMetricsDropdown() {
+        await user.keyboard('{Escape}');
+      },
+      async openCogMenu(id: string) {
+        await user.click(ui.cogBtn(id).get());
+      },
+      async deleteAnalysis(id: string) {
+        await user.click(ui.cogBtn(id).get());
+        await user.click(ui.deleteAnalysisBtn.get());
+        await user.click(ui.deleteBtn.get());
+      },
+      async addVersionEvent(id: string, value: string) {
+        await user.click(ui.cogBtn(id).get());
+        await user.click(ui.addVersionEvenBtn.get());
+        await user.type(ui.nameInput.get(), value);
+        await user.click(ui.saveBtn.get());
+      },
+      async addCustomEvent(id: string, value: string) {
+        await user.click(ui.cogBtn(id).get());
+        await user.click(ui.addCustomEventBtn.get());
+        await user.type(ui.nameInput.get(), value);
+        await user.click(ui.saveBtn.get());
+      },
+      async updateEvent(index: number, value: string) {
+        await user.click(ui.editEventBtn.getAll()[index]);
+        await user.clear(ui.nameInput.get());
+        await user.type(ui.nameInput.get(), value);
+        await user.click(ui.changeBtn.get());
+      },
+      async deleteEvent(index: number) {
+        await user.click(ui.deleteEventBtn.getAll()[index]);
+        await user.click(ui.deleteBtn.get());
+      },
+      async showDetails(id: string) {
+        await user.click(ui.seeDetailsBtn(id).get());
+      },
+      async filterByCategory(
+        category: ProjectAnalysisEventCategory | ApplicationAnalysisEventCategory
+      ) {
+        await selectEvent.select(ui.categorySelect.get(), [`event.category.${category}`]);
+      },
+      async setDateRange(from?: string, to?: string) {
+        const dateInput = dateInputEvent(user);
+        if (from) {
+          await dateInput.pickDate(ui.fromDateInput.get(), parseDate(from));
+        }
+        if (to) {
+          await dateInput.pickDate(ui.toDateInput.get(), parseDate(to));
+        }
+      },
+      async resetDateFilters() {
+        await user.click(ui.resetDatesBtn.get());
+      },
+    },
+  };
 }
 
 function renderProjectActivityAppContainer(
@@ -242,9 +515,10 @@ function renderProjectActivityAppContainer(
     {
       metrics: keyBy(
         [
-          mockMetric({ key: MetricKey.bugs, type: 'INT' }),
+          mockMetric({ key: MetricKey.bugs, type: MetricType.Integer }),
+          mockMetric({ key: MetricKey.code_smells, type: MetricType.Integer }),
           mockMetric({ key: MetricKey.security_hotspots_reviewed }),
-          mockMetric({ key: MetricKey.security_review_rating, type: 'RATING' }),
+          mockMetric({ key: MetricKey.security_review_rating, type: MetricType.Rating }),
         ],
         'key'
       ),
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.tsx
deleted file mode 100644 (file)
index 6a770aa..0000000
+++ /dev/null
@@ -1,35 +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 { shallow } from 'enzyme';
-import * as React from 'react';
-import { parseDate } from '../../../../helpers/dates';
-import ProjectActivityDateInput from '../ProjectActivityDateInput';
-
-it('should render correctly the date inputs', () => {
-  expect(
-    shallow(
-      <ProjectActivityDateInput
-        from={parseDate('2016-10-27T12:21:15+0000')}
-        onChange={() => {}}
-        to={parseDate('2016-12-27T12:21:15+0000')}
-      />
-    )
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.tsx
deleted file mode 100644 (file)
index 2d523d0..0000000
+++ /dev/null
@@ -1,86 +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 { shallow } from 'enzyme';
-import * as React from 'react';
-import { DEFAULT_GRAPH } from '../../../../components/activity-graph/utils';
-import { parseDate } from '../../../../helpers/dates';
-import ProjectActivityGraphs from '../ProjectActivityGraphs';
-
-const ANALYSES = [
-  {
-    key: 'A1',
-    date: parseDate('2016-10-27T16:33:50+0200'),
-    events: [{ key: 'E1', category: 'VERSION', name: '6.5-SNAPSHOT' }],
-  },
-  {
-    key: 'A2',
-    date: parseDate('2016-10-27T12:21:15+0200'),
-    events: [],
-  },
-  {
-    key: 'A3',
-    date: parseDate('2016-10-26T12:17:29+0200'),
-    events: [
-      { key: 'E2', category: 'VERSION', name: '6.4' },
-      { key: 'E3', category: 'OTHER', name: 'foo' },
-    ],
-  },
-];
-
-const METRICS = [{ id: '1', key: 'code_smells', name: 'Code Smells', type: 'INT' }];
-
-const DEFAULT_PROPS: ProjectActivityGraphs['props'] = {
-  analyses: ANALYSES,
-  leakPeriodDate: parseDate('2017-05-16T13:50:02+0200'),
-  loading: false,
-  measuresHistory: [
-    {
-      metric: 'code_smells',
-      history: [
-        { date: parseDate('2016-10-26T12:17:29+0200'), value: '2286' },
-        { date: parseDate('2016-10-27T12:21:15+0200'), value: '1749' },
-        { date: parseDate('2016-10-27T16:33:50+0200'), value: '500' },
-      ],
-    },
-  ],
-  metrics: METRICS,
-  project: 'foo',
-  query: {
-    category: '',
-    customMetrics: [],
-    graph: DEFAULT_GRAPH,
-    project: 'org.sonarsource.sonarqube:sonarqube',
-  },
-  updateQuery: () => {},
-};
-
-it('should render correctly the graph and legends', () => {
-  expect(shallow(<ProjectActivityGraphs {...DEFAULT_PROPS} />)).toMatchSnapshot();
-});
-
-it('should render correctly with filter history on dates', () => {
-  const wrapper = shallow(
-    <ProjectActivityGraphs
-      {...DEFAULT_PROPS}
-      query={{ ...DEFAULT_PROPS.query, from: parseDate('2016-10-27T12:21:15+0200') }}
-    />
-  );
-  expect(wrapper.state()).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityPageFilters-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityPageFilters-test.tsx
deleted file mode 100644 (file)
index 1316a3f..0000000
+++ /dev/null
@@ -1,36 +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 { shallow } from 'enzyme';
-import * as React from 'react';
-import { parseDate } from '../../../../helpers/dates';
-import ProjectActivityPageFilters from '../ProjectActivityPageFilters';
-
-it('should render correctly the list of series', () => {
-  expect(
-    shallow(
-      <ProjectActivityPageFilters
-        category=""
-        from={parseDate('2016-10-27T12:21:15+0200')}
-        project={{ qualifier: 'TRK' }}
-        updateQuery={() => {}}
-      />
-    )
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.tsx.snap
deleted file mode 100644 (file)
index 7deee3c..0000000
+++ /dev/null
@@ -1,617 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should correctly filter analyses by category 1`] = `
-<ul
-  className="project-activity-versions-list"
-  onScroll={[Function]}
-  style={
-    {
-      "marginTop": 36,
-    }
-  }
->
-  <li
-    key="E2"
-  >
-    <div
-      className="project-activity-version-badge first"
-    >
-      <Tooltip
-        mouseEnterDelay={0.5}
-        overlay="version 6.4"
-      >
-        <h2
-          className="analysis-version"
-        >
-          6.4
-        </h2>
-      </Tooltip>
-    </div>
-    <ul
-      className="project-activity-days-list"
-    >
-      <li
-        className="project-activity-day"
-        data-day="ISO.1477267200000"
-        key="1477267200000"
-      >
-        <h3>
-          <DateFormatter
-            date={1477267200000}
-            long={true}
-          />
-        </h3>
-        <ul
-          className="project-activity-analyses-list"
-        >
-          <injectIntl(ProjectActivityAnalysis)
-            addCustomEvent={[MockFunction]}
-            addVersion={[MockFunction]}
-            analysis={
-              {
-                "date": 2016-10-24T16:33:50.000Z,
-                "events": [
-                  {
-                    "category": "QUALITY_GATE",
-                    "key": "E1",
-                    "name": "Quality gate changed to red...",
-                  },
-                ],
-                "key": "A4",
-                "projectVersion": "1.0",
-              }
-            }
-            canAdmin={false}
-            canCreateVersion={true}
-            changeEvent={[MockFunction]}
-            deleteAnalysis={[MockFunction]}
-            deleteEvent={[MockFunction]}
-            isBaseline={false}
-            isFirst={false}
-            key="A4"
-            selected={false}
-            updateSelectedDate={[Function]}
-          />
-        </ul>
-      </li>
-    </ul>
-  </li>
-</ul>
-`;
-
-exports[`should correctly filter analyses by date range 1`] = `
-<ul
-  className="project-activity-versions-list"
-  onScroll={[Function]}
-  style={
-    {
-      "marginTop": 36,
-    }
-  }
->
-  <li
-    key="E1"
-  >
-    <div
-      className="project-activity-version-badge first"
-    >
-      <Tooltip
-        mouseEnterDelay={0.5}
-        overlay="version 6.5-SNAPSHOT"
-      >
-        <h2
-          className="analysis-version"
-        >
-          6.5-SNAPSHOT
-        </h2>
-      </Tooltip>
-    </div>
-    <ul
-      className="project-activity-days-list"
-    >
-      <li
-        className="project-activity-day"
-        data-day="ISO.1477526400000"
-        key="1477526400000"
-      >
-        <h3>
-          <DateFormatter
-            date={1477526400000}
-            long={true}
-          />
-        </h3>
-        <ul
-          className="project-activity-analyses-list"
-        >
-          <injectIntl(ProjectActivityAnalysis)
-            addCustomEvent={[MockFunction]}
-            addVersion={[MockFunction]}
-            analysis={
-              {
-                "date": 2016-10-27T16:33:50.000Z,
-                "events": [
-                  {
-                    "category": "VERSION",
-                    "key": "E1",
-                    "name": "6.5-SNAPSHOT",
-                  },
-                ],
-                "key": "A1",
-                "projectVersion": "1.0",
-              }
-            }
-            canAdmin={false}
-            canCreateVersion={true}
-            changeEvent={[MockFunction]}
-            deleteAnalysis={[MockFunction]}
-            deleteEvent={[MockFunction]}
-            isBaseline={false}
-            isFirst={true}
-            key="A1"
-            selected={false}
-            updateSelectedDate={[Function]}
-          />
-        </ul>
-      </li>
-    </ul>
-  </li>
-</ul>
-`;
-
-exports[`should render correctly: application 1`] = `
-<ul
-  className="project-activity-versions-list"
-  onScroll={[Function]}
-  style={
-    {
-      "marginTop": undefined,
-    }
-  }
->
-  <li
-    key="E1"
-  >
-    <div
-      className="project-activity-version-badge first"
-    >
-      <Tooltip
-        mouseEnterDelay={0.5}
-        overlay="version 6.5-SNAPSHOT"
-      >
-        <h2
-          className="analysis-version"
-        >
-          6.5-SNAPSHOT
-        </h2>
-      </Tooltip>
-    </div>
-    <ul
-      className="project-activity-days-list"
-    >
-      <li
-        className="project-activity-day"
-        data-day="ISO.1477526400000"
-        key="1477526400000"
-      >
-        <h3>
-          <DateFormatter
-            date={1477526400000}
-            long={true}
-          />
-        </h3>
-        <ul
-          className="project-activity-analyses-list"
-        >
-          <injectIntl(ProjectActivityAnalysis)
-            addCustomEvent={[MockFunction]}
-            addVersion={[MockFunction]}
-            analysis={
-              {
-                "date": 2016-10-27T16:33:50.000Z,
-                "events": [
-                  {
-                    "category": "VERSION",
-                    "key": "E1",
-                    "name": "6.5-SNAPSHOT",
-                  },
-                ],
-                "key": "A1",
-                "projectVersion": "1.0",
-              }
-            }
-            canAdmin={false}
-            canCreateVersion={false}
-            changeEvent={[MockFunction]}
-            deleteAnalysis={[MockFunction]}
-            deleteEvent={[MockFunction]}
-            isBaseline={false}
-            isFirst={true}
-            key="A1"
-            selected={false}
-            updateSelectedDate={[Function]}
-          />
-          <injectIntl(ProjectActivityAnalysis)
-            addCustomEvent={[MockFunction]}
-            addVersion={[MockFunction]}
-            analysis={
-              {
-                "date": 2016-10-27T12:21:15.000Z,
-                "events": [],
-                "key": "A2",
-                "projectVersion": "1.0",
-              }
-            }
-            canAdmin={false}
-            canCreateVersion={false}
-            changeEvent={[MockFunction]}
-            deleteAnalysis={[MockFunction]}
-            deleteEvent={[MockFunction]}
-            isBaseline={true}
-            isFirst={false}
-            key="A2"
-            selected={false}
-            updateSelectedDate={[Function]}
-          />
-        </ul>
-      </li>
-    </ul>
-  </li>
-  <li
-    key="E2"
-  >
-    <div
-      className="project-activity-version-badge"
-    >
-      <Tooltip
-        mouseEnterDelay={0.5}
-        overlay="version 6.4"
-      >
-        <h2
-          className="analysis-version"
-        >
-          6.4
-        </h2>
-      </Tooltip>
-    </div>
-    <ul
-      className="project-activity-days-list"
-    >
-      <li
-        className="project-activity-day"
-        data-day="ISO.1477440000000"
-        key="1477440000000"
-      >
-        <h3>
-          <DateFormatter
-            date={1477440000000}
-            long={true}
-          />
-        </h3>
-        <ul
-          className="project-activity-analyses-list"
-        >
-          <injectIntl(ProjectActivityAnalysis)
-            addCustomEvent={[MockFunction]}
-            addVersion={[MockFunction]}
-            analysis={
-              {
-                "date": 2016-10-26T12:17:29.000Z,
-                "events": [
-                  {
-                    "category": "VERSION",
-                    "key": "E2",
-                    "name": "6.4",
-                  },
-                  {
-                    "category": "OTHER",
-                    "key": "E3",
-                    "name": "foo",
-                  },
-                ],
-                "key": "A3",
-                "projectVersion": "1.0",
-              }
-            }
-            canAdmin={false}
-            canCreateVersion={false}
-            changeEvent={[MockFunction]}
-            deleteAnalysis={[MockFunction]}
-            deleteEvent={[MockFunction]}
-            isBaseline={false}
-            isFirst={false}
-            key="A3"
-            selected={false}
-            updateSelectedDate={[Function]}
-          />
-        </ul>
-      </li>
-      <li
-        className="project-activity-day"
-        data-day="ISO.1477267200000"
-        key="1477267200000"
-      >
-        <h3>
-          <DateFormatter
-            date={1477267200000}
-            long={true}
-          />
-        </h3>
-        <ul
-          className="project-activity-analyses-list"
-        >
-          <injectIntl(ProjectActivityAnalysis)
-            addCustomEvent={[MockFunction]}
-            addVersion={[MockFunction]}
-            analysis={
-              {
-                "date": 2016-10-24T16:33:50.000Z,
-                "events": [
-                  {
-                    "category": "QUALITY_GATE",
-                    "key": "E1",
-                    "name": "Quality gate changed to red...",
-                  },
-                ],
-                "key": "A4",
-                "projectVersion": "1.0",
-              }
-            }
-            canAdmin={false}
-            canCreateVersion={false}
-            changeEvent={[MockFunction]}
-            deleteAnalysis={[MockFunction]}
-            deleteEvent={[MockFunction]}
-            isBaseline={false}
-            isFirst={false}
-            key="A4"
-            selected={false}
-            updateSelectedDate={[Function]}
-          />
-        </ul>
-      </li>
-    </ul>
-  </li>
-</ul>
-`;
-
-exports[`should render correctly: default 1`] = `
-<ul
-  className="project-activity-versions-list"
-  onScroll={[Function]}
-  style={
-    {
-      "marginTop": 36,
-    }
-  }
->
-  <li
-    key="E1"
-  >
-    <div
-      className="project-activity-version-badge first"
-    >
-      <Tooltip
-        mouseEnterDelay={0.5}
-        overlay="version 6.5-SNAPSHOT"
-      >
-        <h2
-          className="analysis-version"
-        >
-          6.5-SNAPSHOT
-        </h2>
-      </Tooltip>
-    </div>
-    <ul
-      className="project-activity-days-list"
-    >
-      <li
-        className="project-activity-day"
-        data-day="ISO.1477526400000"
-        key="1477526400000"
-      >
-        <h3>
-          <DateFormatter
-            date={1477526400000}
-            long={true}
-          />
-        </h3>
-        <ul
-          className="project-activity-analyses-list"
-        >
-          <injectIntl(ProjectActivityAnalysis)
-            addCustomEvent={[MockFunction]}
-            addVersion={[MockFunction]}
-            analysis={
-              {
-                "date": 2016-10-27T16:33:50.000Z,
-                "events": [
-                  {
-                    "category": "VERSION",
-                    "key": "E1",
-                    "name": "6.5-SNAPSHOT",
-                  },
-                ],
-                "key": "A1",
-                "projectVersion": "1.0",
-              }
-            }
-            canAdmin={false}
-            canCreateVersion={true}
-            changeEvent={[MockFunction]}
-            deleteAnalysis={[MockFunction]}
-            deleteEvent={[MockFunction]}
-            isBaseline={false}
-            isFirst={true}
-            key="A1"
-            selected={false}
-            updateSelectedDate={[Function]}
-          />
-          <injectIntl(ProjectActivityAnalysis)
-            addCustomEvent={[MockFunction]}
-            addVersion={[MockFunction]}
-            analysis={
-              {
-                "date": 2016-10-27T12:21:15.000Z,
-                "events": [],
-                "key": "A2",
-                "projectVersion": "1.0",
-              }
-            }
-            canAdmin={false}
-            canCreateVersion={true}
-            changeEvent={[MockFunction]}
-            deleteAnalysis={[MockFunction]}
-            deleteEvent={[MockFunction]}
-            isBaseline={true}
-            isFirst={false}
-            key="A2"
-            selected={false}
-            updateSelectedDate={[Function]}
-          />
-        </ul>
-      </li>
-    </ul>
-  </li>
-  <li
-    key="E2"
-  >
-    <div
-      className="project-activity-version-badge"
-    >
-      <Tooltip
-        mouseEnterDelay={0.5}
-        overlay="version 6.4"
-      >
-        <h2
-          className="analysis-version"
-        >
-          6.4
-        </h2>
-      </Tooltip>
-    </div>
-    <ul
-      className="project-activity-days-list"
-    >
-      <li
-        className="project-activity-day"
-        data-day="ISO.1477440000000"
-        key="1477440000000"
-      >
-        <h3>
-          <DateFormatter
-            date={1477440000000}
-            long={true}
-          />
-        </h3>
-        <ul
-          className="project-activity-analyses-list"
-        >
-          <injectIntl(ProjectActivityAnalysis)
-            addCustomEvent={[MockFunction]}
-            addVersion={[MockFunction]}
-            analysis={
-              {
-                "date": 2016-10-26T12:17:29.000Z,
-                "events": [
-                  {
-                    "category": "VERSION",
-                    "key": "E2",
-                    "name": "6.4",
-                  },
-                  {
-                    "category": "OTHER",
-                    "key": "E3",
-                    "name": "foo",
-                  },
-                ],
-                "key": "A3",
-                "projectVersion": "1.0",
-              }
-            }
-            canAdmin={false}
-            canCreateVersion={true}
-            changeEvent={[MockFunction]}
-            deleteAnalysis={[MockFunction]}
-            deleteEvent={[MockFunction]}
-            isBaseline={false}
-            isFirst={false}
-            key="A3"
-            selected={false}
-            updateSelectedDate={[Function]}
-          />
-        </ul>
-      </li>
-      <li
-        className="project-activity-day"
-        data-day="ISO.1477267200000"
-        key="1477267200000"
-      >
-        <h3>
-          <DateFormatter
-            date={1477267200000}
-            long={true}
-          />
-        </h3>
-        <ul
-          className="project-activity-analyses-list"
-        >
-          <injectIntl(ProjectActivityAnalysis)
-            addCustomEvent={[MockFunction]}
-            addVersion={[MockFunction]}
-            analysis={
-              {
-                "date": 2016-10-24T16:33:50.000Z,
-                "events": [
-                  {
-                    "category": "QUALITY_GATE",
-                    "key": "E1",
-                    "name": "Quality gate changed to red...",
-                  },
-                ],
-                "key": "A4",
-                "projectVersion": "1.0",
-              }
-            }
-            canAdmin={false}
-            canCreateVersion={true}
-            changeEvent={[MockFunction]}
-            deleteAnalysis={[MockFunction]}
-            deleteEvent={[MockFunction]}
-            isBaseline={false}
-            isFirst={false}
-            key="A4"
-            selected={false}
-            updateSelectedDate={[Function]}
-          />
-        </ul>
-      </li>
-    </ul>
-  </li>
-</ul>
-`;
-
-exports[`should render correctly: loading 1`] = `
-<div
-  className="boxed-group-inner"
->
-  <div
-    className="text-center"
-  >
-    <i
-      className="spinner"
-    />
-  </div>
-</div>
-`;
-
-exports[`should render correctly: no analyses 1`] = `
-<div
-  className="boxed-group-inner"
->
-  <span
-    className="note"
-  >
-    no_results
-  </span>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityDateInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityDateInput-test.tsx.snap
deleted file mode 100644 (file)
index d1fa488..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly the date inputs 1`] = `
-<div
-  className="display-flex-end"
->
-  <DateRangeInput
-    onChange={[Function]}
-    value={
-      {
-        "from": 2016-10-27T12:21:15.000Z,
-        "to": 2016-12-27T12:21:15.000Z,
-      }
-    }
-  />
-  <Button
-    className="spacer-left"
-    disabled={false}
-    onClick={[Function]}
-  >
-    project_activity.reset_dates
-  </Button>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.tsx.snap
deleted file mode 100644 (file)
index 5049227..0000000
+++ /dev/null
@@ -1,220 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly the graph and legends 1`] = `
-<div
-  className="project-activity-layout-page-main-inner boxed-group boxed-group-inner"
->
-  <GraphsHeader
-    addCustomMetric={[Function]}
-    className="big-spacer-bottom"
-    graph="issues"
-    metrics={
-      [
-        {
-          "id": "1",
-          "key": "code_smells",
-          "name": "Code Smells",
-          "type": "INT",
-        },
-      ]
-    }
-    removeCustomMetric={[Function]}
-    selectedMetrics={[]}
-    updateGraph={[Function]}
-  />
-  <GraphsHistory
-    analyses={
-      [
-        {
-          "date": 2016-10-27T14:33:50.000Z,
-          "events": [
-            {
-              "category": "VERSION",
-              "key": "E1",
-              "name": "6.5-SNAPSHOT",
-            },
-          ],
-          "key": "A1",
-        },
-        {
-          "date": 2016-10-27T10:21:15.000Z,
-          "events": [],
-          "key": "A2",
-        },
-        {
-          "date": 2016-10-26T10:17:29.000Z,
-          "events": [
-            {
-              "category": "VERSION",
-              "key": "E2",
-              "name": "6.4",
-            },
-            {
-              "category": "OTHER",
-              "key": "E3",
-              "name": "foo",
-            },
-          ],
-          "key": "A3",
-        },
-      ]
-    }
-    graph="issues"
-    graphs={
-      [
-        [
-          {
-            "data": [
-              {
-                "x": 2016-10-26T10:17:29.000Z,
-                "y": 2286,
-              },
-              {
-                "x": 2016-10-27T10:21:15.000Z,
-                "y": 1749,
-              },
-              {
-                "x": 2016-10-27T14:33:50.000Z,
-                "y": 500,
-              },
-            ],
-            "name": "code_smells",
-            "translatedName": "Code Smells",
-            "type": "INT",
-          },
-        ],
-      ]
-    }
-    leakPeriodDate={2017-05-16T11:50:02.000Z}
-    loading={false}
-    measuresHistory={
-      [
-        {
-          "history": [
-            {
-              "date": 2016-10-26T10:17:29.000Z,
-              "value": "2286",
-            },
-            {
-              "date": 2016-10-27T10:21:15.000Z,
-              "value": "1749",
-            },
-            {
-              "date": 2016-10-27T14:33:50.000Z,
-              "value": "500",
-            },
-          ],
-          "metric": "code_smells",
-        },
-      ]
-    }
-    removeCustomMetric={[Function]}
-    series={
-      [
-        {
-          "data": [
-            {
-              "x": 2016-10-26T10:17:29.000Z,
-              "y": 2286,
-            },
-            {
-              "x": 2016-10-27T10:21:15.000Z,
-              "y": 1749,
-            },
-            {
-              "x": 2016-10-27T14:33:50.000Z,
-              "y": 500,
-            },
-          ],
-          "name": "code_smells",
-          "translatedName": "Code Smells",
-          "type": "INT",
-        },
-      ]
-    }
-    updateGraphZoom={[Function]}
-    updateSelectedDate={[Function]}
-  />
-  <GraphsZoom
-    leakPeriodDate={2017-05-16T11:50:02.000Z}
-    loading={false}
-    metricsType="INT"
-    series={
-      [
-        {
-          "data": [
-            {
-              "x": 2016-10-26T10:17:29.000Z,
-              "y": 2286,
-            },
-            {
-              "x": 2016-10-27T10:21:15.000Z,
-              "y": 1749,
-            },
-            {
-              "x": 2016-10-27T14:33:50.000Z,
-              "y": 500,
-            },
-          ],
-          "name": "code_smells",
-          "translatedName": "Code Smells",
-          "type": "INT",
-        },
-      ]
-    }
-    showAreas={false}
-    updateGraphZoom={[Function]}
-  />
-</div>
-`;
-
-exports[`should render correctly with filter history on dates 1`] = `
-{
-  "graphEndDate": undefined,
-  "graphStartDate": 2016-10-27T10:21:15.000Z,
-  "graphs": [
-    [
-      {
-        "data": [
-          {
-            "x": 2016-10-26T10:17:29.000Z,
-            "y": 2286,
-          },
-          {
-            "x": 2016-10-27T10:21:15.000Z,
-            "y": 1749,
-          },
-          {
-            "x": 2016-10-27T14:33:50.000Z,
-            "y": 500,
-          },
-        ],
-        "name": "code_smells",
-        "translatedName": "Code Smells",
-        "type": "INT",
-      },
-    ],
-  ],
-  "series": [
-    {
-      "data": [
-        {
-          "x": 2016-10-26T10:17:29.000Z,
-          "y": 2286,
-        },
-        {
-          "x": 2016-10-27T10:21:15.000Z,
-          "y": 1749,
-        },
-        {
-          "x": 2016-10-27T14:33:50.000Z,
-          "y": 500,
-        },
-      ],
-      "name": "code_smells",
-      "translatedName": "Code Smells",
-      "type": "INT",
-    },
-  ],
-}
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityPageFilters-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityPageFilters-test.tsx.snap
deleted file mode 100644 (file)
index 5a5d10a..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly the list of series 1`] = `
-<div
-  className="page-header display-flex-start"
->
-  <div
-    className="display-flex-column big-spacer-right"
-  >
-    <label
-      className="text-bold little-spacer-bottom"
-      htmlFor="filter-events"
-    >
-      project_activity.filter_events
-    </label>
-    <Select
-      className="input-medium"
-      id="filter-events"
-      isClearable={true}
-      isSearchable={false}
-      onChange={[Function]}
-      options={
-        [
-          {
-            "label": "event.category.VERSION",
-            "value": "VERSION",
-          },
-          {
-            "label": "event.category.QUALITY_GATE",
-            "value": "QUALITY_GATE",
-          },
-          {
-            "label": "event.category.QUALITY_PROFILE",
-            "value": "QUALITY_PROFILE",
-          },
-          {
-            "label": "event.category.OTHER",
-            "value": "OTHER",
-          },
-        ]
-      }
-      value={[]}
-    />
-  </div>
-  <ProjectActivityDateInput
-    from={2016-10-27T10:21:15.000Z}
-    onChange={[Function]}
-  />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/AddEventForm-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/AddEventForm-test.tsx
deleted file mode 100644 (file)
index 0d2e0d5..0000000
+++ /dev/null
@@ -1,41 +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 { shallow } from 'enzyme';
-import * as React from 'react';
-import AddEventForm from '../AddEventForm';
-
-it('should render correctly', () => {
-  expect(
-    shallow(
-      <AddEventForm
-        addEvent={jest.fn()}
-        addEventButtonText="add"
-        analysis={{
-          key: '1',
-          date: new Date('2019-01-14T15:44:51.000Z'),
-          events: [{ key: '2', category: 'VERSION', name: '1.0' }],
-          projectVersion: '1.0',
-          manualNewCodePeriodBaseline: false,
-        }}
-        onClose={jest.fn()}
-      />
-    )
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/ChangeEventForm-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/ChangeEventForm-test.tsx
deleted file mode 100644 (file)
index 5d22262..0000000
+++ /dev/null
@@ -1,35 +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 { shallow } from 'enzyme';
-import * as React from 'react';
-import ChangeEventForm from '../ChangeEventForm';
-
-it('should render correctly', () => {
-  expect(
-    shallow(
-      <ChangeEventForm
-        changeEvent={jest.fn()}
-        event={{ category: 'VERSION', key: '1', name: '1.0' }}
-        header="change"
-        onClose={jest.fn()}
-      />
-    )
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/RemoveEventForm-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/RemoveEventForm-test.tsx
deleted file mode 100644 (file)
index cf712eb..0000000
+++ /dev/null
@@ -1,56 +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 { shallow } from 'enzyme';
-import * as React from 'react';
-import ConfirmModal from '../../../../../components/controls/ConfirmModal';
-import { mockAnalysisEvent } from '../../../../../helpers/mocks/project-activity';
-import RemoveEventForm, { RemoveEventFormProps } from '../RemoveEventForm';
-
-it('should render correctly', () => {
-  expect(shallowRender()).toMatchSnapshot();
-});
-
-it('should correctly confirm', () => {
-  const onConfirm = jest.fn();
-  const wrapper = shallowRender({ onConfirm });
-  wrapper.find(ConfirmModal).prop('onConfirm')();
-  expect(onConfirm).toHaveBeenCalledWith('foo', 'bar');
-});
-
-it('should correctly cancel', () => {
-  const onClose = jest.fn();
-  const wrapper = shallowRender({ onClose });
-  wrapper.find(ConfirmModal).prop('onClose')();
-  expect(onClose).toHaveBeenCalled();
-});
-
-function shallowRender(props: Partial<RemoveEventFormProps> = {}) {
-  return shallow(
-    <RemoveEventForm
-      analysisKey="foo"
-      event={mockAnalysisEvent({ key: 'bar' })}
-      header="Remove foo"
-      onClose={jest.fn()}
-      onConfirm={jest.fn()}
-      removeEventQuestion="Remove foo?"
-      {...props}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/AddEventForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/AddEventForm-test.tsx.snap
deleted file mode 100644 (file)
index 41f6352..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<ConfirmModal
-  confirmButtonText="save"
-  confirmDisable={true}
-  header="add"
-  onClose={[MockFunction]}
-  onConfirm={[Function]}
-  size="small"
->
-  <div
-    className="modal-field"
-  >
-    <label
-      htmlFor="name"
-    >
-      name
-    </label>
-    <input
-      autoFocus={true}
-      id="name"
-      onChange={[Function]}
-      type="text"
-      value=""
-    />
-  </div>
-</ConfirmModal>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/ChangeEventForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/ChangeEventForm-test.tsx.snap
deleted file mode 100644 (file)
index 8b00505..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<ConfirmModal
-  confirmButtonText="change_verb"
-  confirmDisable={true}
-  header="change"
-  onClose={[MockFunction]}
-  onConfirm={[Function]}
-  size="small"
->
-  <div
-    className="modal-field"
-  >
-    <label
-      htmlFor="name"
-    >
-      name
-    </label>
-    <input
-      autoFocus={true}
-      id="name"
-      onChange={[Function]}
-      type="text"
-      value="1.0"
-    />
-  </div>
-</ConfirmModal>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/RemoveEventForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/RemoveEventForm-test.tsx.snap
deleted file mode 100644 (file)
index 8d69aad..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<ConfirmModal
-  confirmButtonText="delete"
-  header="Remove foo"
-  isDestructive={true}
-  onClose={[MockFunction]}
-  onConfirm={[Function]}
->
-  Remove foo?
-</ConfirmModal>
-`;
index ec678f869d8f0a086271e1e52ce3657cfb2e83af..fabdaf7335bcace53e8b9185c762864281b04359 100644 (file)
@@ -55,8 +55,7 @@
   overflow: auto;
   flex-grow: 1;
   flex-shrink: 0;
-  padding: calc(2 * var(--gridSize)) calc(2 * var(--gridSize)) calc(2 * var(--gridSize))
-    calc(1.5 * var(--gridSize));
+  padding: 0 calc(2 * var(--gridSize)) calc(2 * var(--gridSize)) calc(1.5 * var(--gridSize));
 }
 
 .project-activity-day {
 }
 
 .project-activity-version-badge {
-  margin-left: calc(-1.5 * var(--gridSize));
-  padding-top: var(--gridSize);
-  padding-bottom: var(--gridSize);
-  background-color: white;
-}
-
-.project-activity-version-badge.sticky,
-.project-activity-version-badge.first {
-  position: absolute;
-  top: 0;
+  position: sticky;
+  top: calc(-3 * var(--gridSize));
   left: calc(1.5 * var(--gridSize));
   right: calc(2 * var(--gridSize));
+  margin-left: calc(-1.5 * var(--gridSize));
+  background-color: white;
   padding-top: calc(3 * var(--gridSize));
+  padding-bottom: var(--gridSize);
   z-index: var(--belowNormalZIndex);
 }
 
-.project-activity-version-badge.sticky + .project-activity-days-list {
-  padding-top: 36px;
+.project-activity-version-badge.first {
+  top: 0;
+  padding-top: 0;
 }
 
 .project-activity-version-badge .analysis-version {
index 67cc97cb1038ae509af47ac8c418a220202a6f36..c6d689e2a41a7b5876f99e7854977ca4f57894be 100644 (file)
@@ -43,9 +43,6 @@ export interface Query {
   to?: Date;
 }
 
-export const EVENT_TYPES = ['VERSION', 'QUALITY_GATE', 'QUALITY_PROFILE', 'OTHER'];
-export const APPLICATION_EVENT_TYPES = ['QUALITY_GATE', 'DEFINITION_CHANGE', 'OTHER'];
-
 export function activityQueryChanged(prevQuery: Query, nextQuery: Query) {
   return prevQuery.category !== nextQuery.category || datesQueryChanged(prevQuery, nextQuery);
 }
@@ -62,10 +59,6 @@ export function historyQueryChanged(prevQuery: Query, nextQuery: Query) {
   return prevQuery.graph !== nextQuery.graph;
 }
 
-export function selectedDateQueryChanged(prevQuery: Query, nextQuery: Query) {
-  return !isEqual(prevQuery.selectedDate, nextQuery.selectedDate);
-}
-
 interface AnalysesByDay {
   byDay: Dict<ParsedAnalysis[]>;
   version: string | null;
index 588c9b4409e8c8ab3cc4cdf48e2902a59179fae8..1bd39ffd4b09905dd6203b71e537497e7ad3a4d4 100644 (file)
@@ -24,6 +24,7 @@ import { getProjectActivity } from '../../../../api/projectActivity';
 import { toShortNotSoISOString } from '../../../../helpers/dates';
 import { mockAnalysis, mockAnalysisEvent } from '../../../../helpers/mocks/project-activity';
 import { waitAndUpdate } from '../../../../helpers/testUtils';
+import { ProjectAnalysisEventCategory } from '../../../../types/project-activity';
 import BranchAnalysisList from '../BranchAnalysisList';
 
 jest.mock('date-fns', () => {
@@ -70,7 +71,10 @@ it('should render correctly', async () => {
         date: '2017-03-02T08:36:01',
         events: [
           mockAnalysisEvent(),
-          mockAnalysisEvent({ category: 'VERSION', qualityGate: undefined }),
+          mockAnalysisEvent({
+            category: ProjectAnalysisEventCategory.Version,
+            qualityGate: undefined,
+          }),
         ],
         projectVersion: '4.1',
       }),
index c3b12396834384feff683f117e88057181272cf3..1dc6e061f7d188c41685b79b7e53a1c0eea54fc0 100644 (file)
@@ -20,6 +20,7 @@
 import { shallow } from 'enzyme';
 import * as React from 'react';
 import { mockAnalysisEvent, mockParsedAnalysis } from '../../../../helpers/mocks/project-activity';
+import { ProjectAnalysisEventCategory } from '../../../../types/project-activity';
 import BranchAnalysisListRenderer, {
   BranchAnalysisListRendererProps,
 } from '../BranchAnalysisListRenderer';
@@ -58,7 +59,7 @@ const analyses = [
     date: new Date('2017-03-02T08:36:01Z'),
     events: [
       mockAnalysisEvent(),
-      mockAnalysisEvent({ category: 'VERSION', qualityGate: undefined }),
+      mockAnalysisEvent({ category: ProjectAnalysisEventCategory.Version, qualityGate: undefined }),
     ],
     projectVersion: '4.1',
   }),
index e0996f4a6fc27db9af3b4b9911df02a109d6132d..00529d77f7e2d7b0af7e30e1b2b3cc1de30995f9 100644 (file)
@@ -17,7 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { find, sortBy } from 'lodash';
+import { sortBy } from 'lodash';
 import * as React from 'react';
 import { Button } from '../../components/controls/buttons';
 import Dropdown from '../../components/controls/Dropdown';
@@ -28,10 +28,10 @@ import { Metric } from '../../types/types';
 import AddGraphMetricPopup from './AddGraphMetricPopup';
 
 interface Props {
-  addMetric: (metric: string) => void;
   metrics: Metric[];
   metricsTypeFilter?: string[];
-  removeMetric: (metric: string) => void;
+  onAddMetric: (metric: string) => void;
+  onRemoveMetric: (metric: string) => void;
   selectedMetrics: string[];
 }
 
@@ -77,13 +77,14 @@ export default class AddGraphMetric extends React.PureComponent<Props, State> {
       .map((metric) => metric.key);
   };
 
-  getSelectedMetricsElements = (metrics: Metric[], selectedMetrics?: string[]) => {
-    const selected = selectedMetrics || this.props.selectedMetrics;
-    return metrics.filter((metric) => selected.includes(metric.key)).map((metric) => metric.key);
+  getSelectedMetricsElements = (metrics: Metric[], selectedMetrics: string[]) => {
+    return metrics
+      .filter((metric) => selectedMetrics.includes(metric.key))
+      .map((metric) => metric.key);
   };
 
   getLocalizedMetricNameFromKey = (key: string) => {
-    const metric = find(this.props.metrics, { key });
+    const metric = this.props.metrics.find((m) => m.key === key);
     return metric === undefined ? key : getLocalizedMetricName(metric);
   };
 
@@ -93,7 +94,7 @@ export default class AddGraphMetric extends React.PureComponent<Props, State> {
   };
 
   onSelect = (metric: string) => {
-    this.props.addMetric(metric);
+    this.props.onAddMetric(metric);
     this.setState((state) => {
       return {
         selectedMetrics: sortBy([...state.selectedMetrics, metric]),
@@ -103,7 +104,7 @@ export default class AddGraphMetric extends React.PureComponent<Props, State> {
   };
 
   onUnselect = (metric: string) => {
-    this.props.removeMetric(metric);
+    this.props.onRemoveMetric(metric);
     this.setState((state) => {
       return {
         metrics: sortBy([...state.metrics, metric]),
index 345168e6a0bd102d42493d89f7b3f3e011200013..9723fb1fa6e900dddb75d25f7dbd93f19c2d0401 100644 (file)
@@ -24,9 +24,14 @@ import { translate } from '../../helpers/l10n';
 import { limitComponentName } from '../../helpers/path';
 import { getProjectUrl } from '../../helpers/urls';
 import { BranchLike } from '../../types/branch-like';
-import { AnalysisEvent } from '../../types/project-activity';
+import {
+  AnalysisEvent,
+  ApplicationAnalysisEventCategory,
+  DefinitionChangeType,
+} from '../../types/project-activity';
 import Link from '../common/Link';
 import { ButtonLink } from '../controls/buttons';
+import ClickEventBoundary from '../controls/ClickEventBoundary';
 import BranchIcon from '../icons/BranchIcon';
 import DropdownIcon from '../icons/DropdownIcon';
 
@@ -34,7 +39,10 @@ export type DefinitionChangeEvent = AnalysisEvent &
   Required<Pick<AnalysisEvent, 'definitionChange'>>;
 
 export function isDefinitionChangeEvent(event: AnalysisEvent): event is DefinitionChangeEvent {
-  return event.category === 'DEFINITION_CHANGE' && event.definitionChange !== undefined;
+  return (
+    event.category === ApplicationAnalysisEventCategory.DefinitionChange &&
+    event.definitionChange !== undefined
+  );
 }
 
 interface Props {
@@ -47,25 +55,21 @@ interface State {
   expanded: boolean;
 }
 
+const NAME_MAX_LENGTH = 28;
+
 export class DefinitionChangeEventInner extends React.PureComponent<Props, State> {
   state: State = { expanded: false };
 
-  stopPropagation = (event: React.MouseEvent<HTMLAnchorElement>) => {
-    event.stopPropagation();
-  };
-
   toggleProjectsList = () => {
     this.setState((state) => ({ expanded: !state.expanded }));
   };
 
   renderProjectLink = (project: { key: string; name: string }, branch: string | undefined) => (
-    <Link
-      onClick={this.stopPropagation}
-      title={project.name}
-      to={getProjectUrl(project.key, branch)}
-    >
-      {limitComponentName(project.name, 28)}
-    </Link>
+    <ClickEventBoundary>
+      <Link title={project.name} to={getProjectUrl(project.key, branch)}>
+        {limitComponentName(project.name, NAME_MAX_LENGTH)}
+      </Link>
+    </ClickEventBoundary>
   );
 
   renderBranch = (branch = translate('branches.main_branch')) => (
@@ -76,7 +80,7 @@ export class DefinitionChangeEventInner extends React.PureComponent<Props, State
   );
 
   renderProjectChange(project: {
-    changeType: string;
+    changeType: DefinitionChangeType;
     key: string;
     name: string;
     branch?: string;
@@ -85,52 +89,56 @@ export class DefinitionChangeEventInner extends React.PureComponent<Props, State
   }) {
     const mainBranch = !this.props.branchLike || isMainBranch(this.props.branchLike);
 
-    if (project.changeType === 'ADDED') {
-      const message = mainBranch
-        ? 'event.definition_change.added'
-        : 'event.definition_change.branch_added';
-      return (
-        <div className="text-ellipsis">
-          <FormattedMessage
-            defaultMessage={translate(message)}
-            id={message}
-            values={{
-              project: this.renderProjectLink(project, project.branch),
-              branch: this.renderBranch(project.branch),
-            }}
-          />
-        </div>
-      );
-    } else if (project.changeType === 'REMOVED') {
-      const message = mainBranch
-        ? 'event.definition_change.removed'
-        : 'event.definition_change.branch_removed';
-      return (
-        <div className="text-ellipsis">
+    switch (project.changeType) {
+      case DefinitionChangeType.Added: {
+        const message = mainBranch
+          ? 'event.definition_change.added'
+          : 'event.definition_change.branch_added';
+        return (
+          <div className="text-ellipsis">
+            <FormattedMessage
+              defaultMessage={translate(message)}
+              id={message}
+              values={{
+                project: this.renderProjectLink(project, project.branch),
+                branch: this.renderBranch(project.branch),
+              }}
+            />
+          </div>
+        );
+      }
+
+      case DefinitionChangeType.Removed: {
+        const message = mainBranch
+          ? 'event.definition_change.removed'
+          : 'event.definition_change.branch_removed';
+        return (
+          <div className="text-ellipsis">
+            <FormattedMessage
+              defaultMessage={translate(message)}
+              id={message}
+              values={{
+                project: this.renderProjectLink(project, project.branch),
+                branch: this.renderBranch(project.branch),
+              }}
+            />
+          </div>
+        );
+      }
+
+      case DefinitionChangeType.BranchChanged:
+        return (
           <FormattedMessage
-            defaultMessage={translate(message)}
-            id={message}
+            defaultMessage={translate('event.definition_change.branch_replaced')}
+            id="event.definition_change.branch_replaced"
             values={{
-              project: this.renderProjectLink(project, project.branch),
-              branch: this.renderBranch(project.branch),
+              project: this.renderProjectLink(project, project.newBranch),
+              oldBranch: this.renderBranch(project.oldBranch),
+              newBranch: this.renderBranch(project.newBranch),
             }}
           />
-        </div>
-      );
-    } else if (project.changeType === 'BRANCH_CHANGED') {
-      return (
-        <FormattedMessage
-          defaultMessage={translate('event.definition_change.branch_replaced')}
-          id="event.definition_change.branch_replaced"
-          values={{
-            project: this.renderProjectLink(project, project.newBranch),
-            oldBranch: this.renderBranch(project.oldBranch),
-            newBranch: this.renderBranch(project.newBranch),
-          }}
-        />
-      );
+        );
     }
-    return null;
   }
 
   render() {
index 7b26effe7a4560dff299baf82dea3d0996f1d2bb..c792a7348747a98f65281264870f0c2d3de9cdda 100644 (file)
@@ -42,9 +42,8 @@ export default function EventInner({ event, readonly }: EventInnerProps) {
       </ComponentContext.Consumer>
     );
   }
-
   return (
-    <Tooltip overlay={event.description || null}>
+    <Tooltip overlay={event.description}>
       <span className="text-middle">
         <span className="note little-spacer-right">
           {translate('event.category', event.category)}:
index eb32a50ad72e9c6413b39aa2eab5fb1b84728051..7065b9c17b04e190e5ce2986e50dd2c4656666e9 100644 (file)
@@ -28,35 +28,29 @@ import './styles.css';
 import { getGraphTypes, isCustomGraph } from './utils';
 
 interface Props {
-  addCustomMetric?: (metric: string) => void;
+  onAddCustomMetric?: (metric: string) => void;
   className?: string;
-  removeCustomMetric?: (metric: string) => void;
+  onRemoveCustomMetric?: (metric: string) => void;
   graph: GraphType;
   metrics: Metric[];
   metricsTypeFilter?: string[];
   selectedMetrics?: string[];
-  updateGraph: (graphType: string) => void;
+  onUpdateGraph: (graphType: string) => void;
 }
 
 export default class GraphsHeader extends React.PureComponent<Props> {
   handleGraphChange = (option: { value: string }) => {
     if (option.value !== this.props.graph) {
-      this.props.updateGraph(option.value);
+      this.props.onUpdateGraph(option.value);
     }
   };
 
   render() {
-    const {
-      addCustomMetric,
-      className,
-      graph,
-      metrics,
-      metricsTypeFilter,
-      removeCustomMetric,
-      selectedMetrics = [],
-    } = this.props;
+    const { className, graph, metrics, metricsTypeFilter, selectedMetrics = [] } = this.props;
 
-    const types = getGraphTypes(addCustomMetric === undefined || removeCustomMetric === undefined);
+    const types = getGraphTypes(
+      this.props.onAddCustomMetric === undefined || this.props.onRemoveCustomMetric === undefined
+    );
 
     const selectOptions = types.map((type) => ({
       label: translate('project_activity.graphs', type),
@@ -80,13 +74,13 @@ export default class GraphsHeader extends React.PureComponent<Props> {
             />
           </div>
           {isCustomGraph(graph) &&
-            addCustomMetric !== undefined &&
-            removeCustomMetric !== undefined && (
+            this.props.onAddCustomMetric !== undefined &&
+            this.props.onRemoveCustomMetric !== undefined && (
               <AddGraphMetric
-                addMetric={addCustomMetric}
+                onAddMetric={this.props.onAddCustomMetric}
                 metrics={metrics}
                 metricsTypeFilter={metricsTypeFilter}
-                removeMetric={removeCustomMetric}
+                onRemoveMetric={this.props.onRemoveCustomMetric}
                 selectedMetrics={selectedMetrics}
               />
             )}
index 53bdf13d621a46c98e1e7b7b76d90236c9613729..72213529e3b5f68acbe312b2fbfc065d9347d2b8 100644 (file)
@@ -31,11 +31,20 @@ interface GraphsZoomProps {
   metricsType: string;
   series: Serie[];
   showAreas?: boolean;
-  updateGraphZoom: (from?: Date, to?: Date) => void;
+  onUpdateGraphZoom: (from?: Date, to?: Date) => void;
 }
 
+const ZOOM_TIMELINE_PADDING_TOP = 0;
+const ZOOM_TIMELINE_PADDING_RIGHT = 10;
+const ZOOM_TIMELINE_PADDING_BOTTOM = 18;
+const ZOOM_TIMELINE_PADDING_LEFT = 60;
+const ZOOM_TIMELINE_HEIGHT = 64;
+
 export default function GraphsZoom(props: GraphsZoomProps) {
-  if (props.loading || !hasHistoryData(props.series)) {
+  const { loading, series, graphEndDate, leakPeriodDate, metricsType, showAreas, graphStartDate } =
+    props;
+
+  if (loading || !hasHistoryData(series)) {
     return null;
   }
 
@@ -45,15 +54,20 @@ export default function GraphsZoom(props: GraphsZoomProps) {
       <AutoSizer disableHeight={true}>
         {({ width }) => (
           <ZoomTimeLine
-            endDate={props.graphEndDate}
-            height={64}
-            leakPeriodDate={props.leakPeriodDate}
-            metricType={props.metricsType}
-            padding={[0, 10, 18, 60]}
-            series={props.series}
-            showAreas={props.showAreas}
-            startDate={props.graphStartDate}
-            updateZoom={props.updateGraphZoom}
+            endDate={graphEndDate}
+            height={ZOOM_TIMELINE_HEIGHT}
+            leakPeriodDate={leakPeriodDate}
+            metricType={metricsType}
+            padding={[
+              ZOOM_TIMELINE_PADDING_TOP,
+              ZOOM_TIMELINE_PADDING_RIGHT,
+              ZOOM_TIMELINE_PADDING_BOTTOM,
+              ZOOM_TIMELINE_PADDING_LEFT,
+            ]}
+            series={series}
+            showAreas={showAreas}
+            startDate={graphStartDate}
+            updateZoom={props.onUpdateGraphZoom}
             width={width}
           />
         )}
index 907a5dc3a2fb152cd4570febd19d843a02762f46..919005598fefa4fdad83a682fa60c78e68f2733a 100644 (file)
@@ -24,6 +24,7 @@ import { getProjectUrl } from '../../helpers/urls';
 import { AnalysisEvent } from '../../types/project-activity';
 import Link from '../common/Link';
 import { ResetButtonLink } from '../controls/buttons';
+import ClickEventBoundary from '../controls/ClickEventBoundary';
 import DropdownIcon from '../icons/DropdownIcon';
 import Level from '../ui/Level';
 
@@ -45,10 +46,6 @@ interface State {
 export class RichQualityGateEventInner extends React.PureComponent<Props, State> {
   state: State = { expanded: false };
 
-  stopPropagation = (event: React.MouseEvent<HTMLAnchorElement>) => {
-    event.stopPropagation();
-  };
-
   toggleProjectsList = () => {
     this.setState((state) => ({ expanded: !state.expanded }));
   };
@@ -93,15 +90,13 @@ export class RichQualityGateEventInner extends React.PureComponent<Props, State>
                   small={true}
                 />
                 <div className="flex-1 text-ellipsis">
-                  <Link
-                    onClick={this.stopPropagation}
-                    title={project.name}
-                    to={getProjectUrl(project.key, project.branch)}
-                  >
-                    <span aria-label={translateWithParameters('project_x', project.name)}>
-                      {project.name}
-                    </span>
-                  </Link>
+                  <ClickEventBoundary>
+                    <Link title={project.name} to={getProjectUrl(project.key, project.branch)}>
+                      <span aria-label={translateWithParameters('project_x', project.name)}>
+                        {project.name}
+                      </span>
+                    </Link>
+                  </ClickEventBoundary>
                 </div>
               </li>
             ))}
index 4b9ac8eb46859f3f027e1574ecd4b28b3eeb8b77..c967173eb9977600e7a10244e633e7d40f9cd7e1 100644 (file)
@@ -19,7 +19,6 @@
  */
 import { screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
-import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
 import { times } from 'lodash';
 import * as React from 'react';
 import selectEvent from 'react-select-event';
@@ -40,42 +39,77 @@ const MAX_SERIES_PER_GRAPH = 3;
 const HISTORY_COUNT = 10;
 const START_DATE = '2016-01-01T00:00:00+0200';
 
-it('should render correctly when loading', async () => {
-  renderActivityGraph({ loading: true });
-  expect(await screen.findByText('loading')).toBeInTheDocument();
+describe('rendering', () => {
+  it('should render correctly when loading', async () => {
+    renderActivityGraph({ loading: true });
+    expect(await screen.findByText('loading')).toBeInTheDocument();
+  });
+
+  it('should show the correct legend items', async () => {
+    const { ui, user } = getPageObject();
+    renderActivityGraph();
+
+    // Static legend items, which aren't interactive.
+    expect(ui.legendRemoveMetricBtn(MetricKey.bugs).query()).not.toBeInTheDocument();
+    expect(ui.getLegendItem(MetricKey.bugs)).toBeInTheDocument();
+
+    // Switch to custom graph.
+    await ui.changeGraphType(GraphType.custom);
+    await ui.openAddMetrics();
+    await ui.clickOnMetric(MetricKey.bugs);
+    await ui.clickOnMetric(MetricKey.test_failures);
+    await user.keyboard('{Escape}');
+
+    // These legend items are interactive (interaction tested below).
+    expect(ui.legendRemoveMetricBtn(MetricKey.bugs).get()).toBeInTheDocument();
+    expect(ui.legendRemoveMetricBtn(MetricKey.test_failures).get()).toBeInTheDocument();
+
+    // Shows warning for metrics with no data.
+    const li = ui.getLegendItem(MetricKey.test_failures);
+    // eslint-disable-next-line jest/no-conditional-in-test
+    if (li) {
+      li.focus();
+    }
+    expect(ui.noDataWarningTooltip.get()).toBeInTheDocument();
+  });
 });
 
-it('should show the correct legend items', async () => {
-  const user = userEvent.setup();
-  const ui = getPageObject(user);
-  renderActivityGraph();
+describe('data table modal', () => {
+  it('shows the same data in a table', async () => {
+    const { ui } = getPageObject();
+    renderActivityGraph();
+
+    await ui.openDataTable();
+    expect(ui.dataTable.get()).toBeInTheDocument();
+    expect(ui.dataTableColHeaders.getAll()).toHaveLength(5);
+    expect(ui.dataTableRows.getAll()).toHaveLength(HISTORY_COUNT + 1);
 
-  // Static legend items, which aren't interactive.
-  expect(ui.legendRemoveMetricBtn(MetricKey.bugs).query()).not.toBeInTheDocument();
-  expect(ui.getLegendItem(MetricKey.bugs)).toBeInTheDocument();
+    // Change graph type and dates, check table updates correctly.
+    await ui.closeDataTable();
+    await ui.changeGraphType(GraphType.coverage);
 
-  // Switch to custom graph.
-  await ui.changeGraphType(GraphType.custom);
-  await ui.openAddMetrics();
-  await ui.clickOnMetric(MetricKey.bugs);
-  await ui.clickOnMetric(MetricKey.test_failures);
-  await user.keyboard('{Escape}');
-
-  // These legend items are interactive (interaction tested below).
-  expect(ui.legendRemoveMetricBtn(MetricKey.bugs).get()).toBeInTheDocument();
-  expect(ui.legendRemoveMetricBtn(MetricKey.test_failures).get()).toBeInTheDocument();
-
-  // Shows warning for metrics with no data.
-  const li = ui.getLegendItem(MetricKey.test_failures);
-  // eslint-disable-next-line jest/no-conditional-in-test
-  if (li) {
-    li.focus();
-  }
-  expect(ui.noDataWarningTooltip.get()).toBeInTheDocument();
+    await ui.openDataTable();
+    expect(ui.dataTable.get()).toBeInTheDocument();
+    expect(ui.dataTableColHeaders.getAll()).toHaveLength(4);
+    expect(ui.dataTableRows.getAll()).toHaveLength(HISTORY_COUNT + 1);
+  });
+
+  it('shows the same data in a table when filtered by date', async () => {
+    const { ui } = getPageObject();
+    renderActivityGraph({
+      graphStartDate: parseDate('2017-01-01'),
+      graphEndDate: parseDate('2019-01-01'),
+    });
+
+    await ui.openDataTable();
+    expect(ui.dataTable.get()).toBeInTheDocument();
+    expect(ui.dataTableColHeaders.getAll()).toHaveLength(5);
+    expect(ui.dataTableRows.getAll()).toHaveLength(2);
+  });
 });
 
 it('should correctly handle adding/removing custom metrics', async () => {
-  const ui = getPageObject(userEvent.setup());
+  const { ui } = getPageObject();
   renderActivityGraph();
 
   // Change graph type to "Custom".
@@ -132,41 +166,8 @@ it('should correctly handle adding/removing custom metrics', async () => {
   expect(ui.noDataText.get()).toBeInTheDocument();
 });
 
-describe('data table modal', () => {
-  it('shows the same data in a table', async () => {
-    const ui = getPageObject(userEvent.setup());
-    renderActivityGraph();
-
-    await ui.openDataTable();
-    expect(ui.dataTable.get()).toBeInTheDocument();
-    expect(ui.dataTableColHeaders.getAll()).toHaveLength(5);
-    expect(ui.dataTableRows.getAll()).toHaveLength(HISTORY_COUNT + 1);
-
-    // Change graph type and dates, check table updates correctly.
-    await ui.closeDataTable();
-    await ui.changeGraphType(GraphType.coverage);
-
-    await ui.openDataTable();
-    expect(ui.dataTable.get()).toBeInTheDocument();
-    expect(ui.dataTableColHeaders.getAll()).toHaveLength(4);
-    expect(ui.dataTableRows.getAll()).toHaveLength(HISTORY_COUNT + 1);
-  });
-
-  it('shows the same data in a table when filtered by date', async () => {
-    const ui = getPageObject(userEvent.setup());
-    renderActivityGraph({
-      graphStartDate: parseDate('2017-01-01'),
-      graphEndDate: parseDate('2019-01-01'),
-    });
-
-    await ui.openDataTable();
-    expect(ui.dataTable.get()).toBeInTheDocument();
-    expect(ui.dataTableColHeaders.getAll()).toHaveLength(5);
-    expect(ui.dataTableRows.getAll()).toHaveLength(2);
-  });
-});
-
-function getPageObject(user: UserEvent) {
+function getPageObject() {
+  const user = userEvent.setup();
   const ui = {
     // Graph types.
     graphTypeSelect: byLabelText('project_activity.graphs.choose_type'),
@@ -195,11 +196,6 @@ function getPageObject(user: UserEvent) {
     graphs: byLabelText('project_activity.graphs.explanation_x', { exact: false }),
     noDataText: byText('project_activity.graphs.custom.no_history'),
 
-    // Date filters.
-    fromDateInput: byLabelText('from_date'),
-    toDateInput: byLabelText('to_date'),
-    submitDatesBtn: byRole('button', { name: 'Submit dates' }),
-
     // Data in table.
     openInTableBtn: byRole('button', { name: 'project_activity.graphs.open_in_table' }),
     closeDataTableBtn: byRole('button', { name: 'close' }),
@@ -212,27 +208,30 @@ function getPageObject(user: UserEvent) {
   };
 
   return {
-    ...ui,
-    async changeGraphType(type: GraphType) {
-      await selectEvent.select(ui.graphTypeSelect.get(), [`project_activity.graphs.${type}`]);
-    },
-    async openAddMetrics() {
-      await user.click(ui.addMetricBtn.get());
-    },
-    async searchForMetric(text: string) {
-      await user.type(ui.filterMetrics.get(), text);
-    },
-    async clickOnMetric(name: MetricKey) {
-      await user.click(screen.getByRole('checkbox', { name }));
-    },
-    async removeMetric(metric: MetricKey) {
-      await user.click(ui.legendRemoveMetricBtn(metric).get());
-    },
-    async openDataTable() {
-      await user.click(ui.openInTableBtn.get());
-    },
-    async closeDataTable() {
-      await user.click(ui.closeDataTableBtn.get());
+    user,
+    ui: {
+      ...ui,
+      async changeGraphType(type: GraphType) {
+        await selectEvent.select(ui.graphTypeSelect.get(), [`project_activity.graphs.${type}`]);
+      },
+      async openAddMetrics() {
+        await user.click(ui.addMetricBtn.get());
+      },
+      async searchForMetric(text: string) {
+        await user.type(ui.filterMetrics.get(), text);
+      },
+      async clickOnMetric(name: MetricKey) {
+        await user.click(screen.getByRole('checkbox', { name }));
+      },
+      async removeMetric(metric: MetricKey) {
+        await user.click(ui.legendRemoveMetricBtn(metric).get());
+      },
+      async openDataTable() {
+        await user.click(ui.openInTableBtn.get());
+      },
+      async closeDataTable() {
+        await user.click(ui.closeDataTableBtn.get());
+      },
     },
   };
 }
@@ -244,11 +243,6 @@ function renderActivityGraph(
   function ActivityGraph() {
     const [selectedMetrics, setSelectedMetrics] = React.useState<string[]>([]);
     const [graph, setGraph] = React.useState(graphsHistoryProps.graph || GraphType.issues);
-    const [selectedDate, setSelectedDate] = React.useState<Date | undefined>(
-      graphsHistoryProps.selectedDate
-    );
-    const [fromDate, setFromDate] = React.useState<Date | undefined>(undefined);
-    const [toDate, setToDate] = React.useState<Date | undefined>(undefined);
 
     const measuresHistory: MeasureHistory[] = [];
     const metrics: Metric[] = [];
@@ -327,40 +321,26 @@ function renderActivityGraph(
       setGraph(graphType as GraphType);
     };
 
-    const updateSelectedDate = (date?: Date) => {
-      setSelectedDate(date);
-    };
-
-    const updateFromToDates = (from?: Date, to?: Date) => {
-      setFromDate(from);
-      setToDate(to);
-    };
-
     return (
       <>
         <GraphsHeader
-          addCustomMetric={addCustomMetric}
+          onAddCustomMetric={addCustomMetric}
           graph={graph}
           metrics={metrics}
           metricsTypeFilter={metricsTypeFilter}
-          removeCustomMetric={removeCustomMetric}
+          onRemoveCustomMetric={removeCustomMetric}
           selectedMetrics={selectedMetrics}
-          updateGraph={updateGraph}
+          onUpdateGraph={updateGraph}
           {...graphsHeaderProps}
         />
         <GraphsHistory
           analyses={[]}
           graph={graph}
-          graphEndDate={toDate}
-          graphStartDate={fromDate}
           graphs={graphs}
           loading={false}
           measuresHistory={[]}
           removeCustomMetric={removeCustomMetric}
-          selectedDate={selectedDate}
           series={series}
-          updateGraphZoom={updateFromToDates}
-          updateSelectedDate={updateSelectedDate}
           {...graphsHistoryProps}
         />
       </>
index 424fd9227811fea14542e6e57962c167e32565e5..b5b234a9275c750190ac27e34b2bf9044a597ed4 100644 (file)
@@ -30,7 +30,11 @@ import {
 import { mockMetric } from '../../../helpers/testMocks';
 import { renderComponent } from '../../../helpers/testReactTestingUtils';
 import { MetricKey } from '../../../types/metrics';
-import { GraphType, MeasureHistory } from '../../../types/project-activity';
+import {
+  GraphType,
+  MeasureHistory,
+  ProjectAnalysisEventCategory,
+} from '../../../types/project-activity';
 import { Metric } from '../../../types/types';
 import DataTableModal, { DataTableModalProps, MAX_DATA_TABLE_ROWS } from '../DataTableModal';
 import { generateSeries, getDisplayedHistoryMetrics } from '../utils';
@@ -47,7 +51,9 @@ it('should render correctly if there are events', () => {
     analyses: [
       mockParsedAnalysis({
         date: parseDate('2016-01-01T00:00:00+0200'),
-        events: [mockAnalysisEvent({ key: '1', category: 'QUALITY_GATE' })],
+        events: [
+          mockAnalysisEvent({ key: '1', category: ProjectAnalysisEventCategory.QualityGate }),
+        ],
       }),
     ],
   });
index 899768d86d8ea28d4e92e2cd12f3344182e731bb..daa09bcfc8f74f1855113b3044755efa64e34f4b 100644 (file)
 import { screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import * as React from 'react';
+import { Route } from 'react-router-dom';
 import { byRole, byText } from 'testing-library-selector';
+import { isMainBranch } from '../../../helpers/branch-like';
+import { mockBranch, mockMainBranch } from '../../../helpers/mocks/branch-like';
 import { mockAnalysisEvent } from '../../../helpers/mocks/project-activity';
-import { renderComponent } from '../../../helpers/testReactTestingUtils';
+import { renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils';
+import { BranchLike } from '../../../types/branch-like';
+import { ComponentContextShape } from '../../../types/component';
+import {
+  ApplicationAnalysisEventCategory,
+  DefinitionChangeType,
+  ProjectAnalysisEventCategory,
+} from '../../../types/project-activity';
 import EventInner, { EventInnerProps } from '../EventInner';
 
 const ui = {
@@ -31,8 +41,14 @@ const ui = {
   projectLink: (name: string) => byRole('link', { name }),
 
   definitionChangeLabel: byText('event.category.DEFINITION_CHANGE', { exact: false }),
-  projectAddedTxt: byText('event.definition_change.added'),
-  projectRemovedTxt: byText('event.definition_change.removed'),
+  projectAddedTxt: (branch: BranchLike) =>
+    isMainBranch(branch)
+      ? byText('event.definition_change.added')
+      : byText('event.definition_change.branch_added'),
+  projectRemovedTxt: (branch: BranchLike) =>
+    isMainBranch(branch)
+      ? byText('event.definition_change.removed')
+      : byText('event.definition_change.branch_removed'),
   branchReplacedTxt: byText('event.definition_change.branch_replaced'),
 
   qualityGateLabel: byText('event.category.QUALITY_GATE', { exact: false }),
@@ -42,17 +58,81 @@ const ui = {
 };
 
 describe('DEFINITION_CHANGE events', () => {
-  it('should render correctly for "DEFINITION_CHANGE" events', async () => {
+  it.each([mockMainBranch(), mockBranch()])(
+    'should render correctly for "ADDED" events',
+    async (branchLike: BranchLike) => {
+      const user = userEvent.setup();
+      renderEventInner(
+        {
+          event: mockAnalysisEvent({
+            category: ApplicationAnalysisEventCategory.DefinitionChange,
+            definitionChange: {
+              projects: [
+                {
+                  changeType: DefinitionChangeType.Added,
+                  key: 'foo',
+                  name: 'Foo',
+                  branch: 'master-foo',
+                },
+              ],
+            },
+          }),
+        },
+        { branchLike }
+      );
+
+      expect(ui.definitionChangeLabel.get()).toBeInTheDocument();
+
+      await user.click(ui.showMoreBtn.get());
+
+      expect(ui.projectAddedTxt(branchLike).get()).toBeInTheDocument();
+      expect(ui.projectLink('Foo').get()).toBeInTheDocument();
+      expect(screen.getByText('master-foo')).toBeInTheDocument();
+    }
+  );
+
+  it.each([mockMainBranch(), mockBranch()])(
+    'should render correctly for "REMOVED" events',
+    async (branchLike: BranchLike) => {
+      const user = userEvent.setup();
+      renderEventInner(
+        {
+          event: mockAnalysisEvent({
+            category: ApplicationAnalysisEventCategory.DefinitionChange,
+            definitionChange: {
+              projects: [
+                {
+                  changeType: DefinitionChangeType.Removed,
+                  key: 'bar',
+                  name: 'Bar',
+                  branch: 'master-bar',
+                },
+              ],
+            },
+          }),
+        },
+        { branchLike }
+      );
+
+      expect(ui.definitionChangeLabel.get()).toBeInTheDocument();
+
+      await user.click(ui.showMoreBtn.get());
+
+      expect(ui.projectRemovedTxt(branchLike).get()).toBeInTheDocument();
+      expect(ui.projectLink('Bar').get()).toBeInTheDocument();
+      expect(screen.getByText('master-bar')).toBeInTheDocument();
+    }
+  );
+
+  it('should render correctly for "BRANCH_CHANGED" events', async () => {
     const user = userEvent.setup();
     renderEventInner({
       event: mockAnalysisEvent({
-        category: 'DEFINITION_CHANGE',
+        category: ApplicationAnalysisEventCategory.DefinitionChange,
         definitionChange: {
           projects: [
-            { changeType: 'ADDED', key: 'foo', name: 'Foo', branch: 'master-foo' },
-            { changeType: 'REMOVED', key: 'bar', name: 'Bar', branch: 'master-bar' },
             {
-              changeType: 'BRANCH_CHANGED',
+              changeType: DefinitionChangeType.BranchChanged,
               key: 'baz',
               name: 'Baz',
               oldBranch: 'old-branch',
@@ -67,17 +147,6 @@ describe('DEFINITION_CHANGE events', () => {
 
     await user.click(ui.showMoreBtn.get());
 
-    // ADDED.
-    expect(ui.projectAddedTxt.get()).toBeInTheDocument();
-    expect(ui.projectLink('Foo').get()).toBeInTheDocument();
-    expect(screen.getByText('master-foo')).toBeInTheDocument();
-
-    // REMOVED.
-    expect(ui.projectRemovedTxt.get()).toBeInTheDocument();
-    expect(ui.projectLink('Bar').get()).toBeInTheDocument();
-    expect(screen.getByText('master-bar')).toBeInTheDocument();
-
-    // BRANCH_CHANGED
     expect(ui.branchReplacedTxt.get()).toBeInTheDocument();
     expect(ui.projectLink('Baz').get()).toBeInTheDocument();
     expect(screen.getByText('old-branch')).toBeInTheDocument();
@@ -89,7 +158,7 @@ describe('QUALITY_GATE events', () => {
   it('should render correctly for simple "QUALITY_GATE" events', () => {
     renderEventInner({
       event: mockAnalysisEvent({
-        category: 'QUALITY_GATE',
+        category: ProjectAnalysisEventCategory.QualityGate,
         qualityGate: { status: 'ERROR', stillFailing: false, failing: [] },
       }),
     });
@@ -100,7 +169,7 @@ describe('QUALITY_GATE events', () => {
   it('should render correctly for "still failing" "QUALITY_GATE" events', () => {
     renderEventInner({
       event: mockAnalysisEvent({
-        category: 'QUALITY_GATE',
+        category: ProjectAnalysisEventCategory.QualityGate,
         qualityGate: { status: 'ERROR', stillFailing: true, failing: [] },
       }),
     });
@@ -113,7 +182,7 @@ describe('QUALITY_GATE events', () => {
     const user = userEvent.setup();
     renderEventInner({
       event: mockAnalysisEvent({
-        category: 'QUALITY_GATE',
+        category: ProjectAnalysisEventCategory.QualityGate,
         qualityGate: {
           status: 'ERROR',
           stillFailing: true,
@@ -149,7 +218,7 @@ describe('VERSION events', () => {
   it('should render correctly', () => {
     renderEventInner({
       event: mockAnalysisEvent({
-        category: 'VERSION',
+        category: ProjectAnalysisEventCategory.Version,
         name: '1.0',
       }),
     });
@@ -159,6 +228,14 @@ describe('VERSION events', () => {
   });
 });
 
-function renderEventInner(props: Partial<EventInnerProps> = {}) {
-  return renderComponent(<EventInner event={mockAnalysisEvent()} {...props} />);
+function renderEventInner(
+  props: Partial<EventInnerProps> = {},
+  componentContext: Partial<ComponentContextShape> = {}
+) {
+  return renderAppWithComponentContext(
+    '/',
+    () => <Route path="*" element={<EventInner event={mockAnalysisEvent()} {...props} />} />,
+    {},
+    componentContext
+  );
 }
index dbd75cd496b2c65bf8258463248127099e1cd6bb..0c670cffbc54db0d0e19fed64c556e15ca966f3a 100644 (file)
@@ -56,7 +56,7 @@ exports[`generateSeries should correctly generate the series 1`] = `
       },
     ],
     "name": "lines_to_cover",
-    "translatedName": "Line to Cover",
+    "translatedName": "lines_to_cover",
     "type": "PERCENT",
   },
 ]
index a9d311461eeb7e336861d53c22c41a9207071a77..22e86d7bdde6d7976007c2f920e82ad366ac5050 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as dates from '../../../helpers/dates';
-import { MetricKey } from '../../../types/metrics';
-import { GraphType, Serie } from '../../../types/project-activity';
+import { mockMeasureHistory, mockSerie } from '../../../helpers/mocks/project-activity';
+import { get, save } from '../../../helpers/storage';
+import { mockMetric } from '../../../helpers/testMocks';
+import { MetricKey, MetricType } from '../../../types/metrics';
+import { GraphType } from '../../../types/project-activity';
 import * as utils from '../utils';
 
 jest.mock('date-fns', () => {
@@ -34,37 +37,36 @@ jest.mock('date-fns', () => {
   };
 });
 
+jest.mock('../../../helpers/storage', () => ({
+  save: jest.fn(),
+  get: jest.fn(),
+}));
+
 const HISTORY = [
-  {
+  mockMeasureHistory({
     metric: MetricKey.lines_to_cover,
     history: [
       { date: dates.parseDate('2017-04-27T08:21:32.000Z'), value: '100' },
       { date: dates.parseDate('2017-04-30T23:06:24.000Z'), value: '100' },
     ],
-  },
-  {
+  }),
+  mockMeasureHistory({
     metric: MetricKey.uncovered_lines,
     history: [
       { date: dates.parseDate('2017-04-27T08:21:32.000Z'), value: '12' },
       { date: dates.parseDate('2017-04-30T23:06:24.000Z'), value: '50' },
     ],
-  },
+  }),
 ];
 
 const METRICS = [
-  { id: '1', key: MetricKey.uncovered_lines, name: 'Uncovered Lines', type: 'INT' },
-  { id: '2', key: MetricKey.lines_to_cover, name: 'Line to Cover', type: 'PERCENT' },
+  mockMetric({ key: MetricKey.uncovered_lines, type: MetricType.Integer }),
+  mockMetric({ key: MetricKey.lines_to_cover, type: MetricType.Percent }),
 ];
 
-const SERIE: Serie = {
-  data: [
-    { x: dates.parseDate('2017-04-27T08:21:32.000Z'), y: 2 },
-    { x: dates.parseDate('2017-04-28T08:21:32.000Z'), y: 2 },
-  ],
-  name: 'foo',
-  translatedName: 'Foo',
-  type: 'PERCENT',
-};
+const SERIE = mockSerie({
+  type: MetricType.Percent,
+});
 
 describe('generateCoveredLinesMetric', () => {
   it('should correctly generate covered lines metric', () => {
@@ -129,46 +131,25 @@ describe('getHistoryMetrics', () => {
 
 describe('hasHistoryData', () => {
   it('should correctly detect if there is history data', () => {
+    expect(utils.hasHistoryData([mockSerie()])).toBe(true);
     expect(
       utils.hasHistoryData([
-        {
-          name: 'foo',
-          translatedName: 'foo',
-          type: 'INT',
-          data: [
-            { x: dates.parseDate('2017-04-27T08:21:32.000Z'), y: 2 },
-            { x: dates.parseDate('2017-04-30T23:06:24.000Z'), y: 2 },
-          ],
-        },
-      ])
-    ).toBe(true);
-    expect(
-      utils.hasHistoryData([
-        {
-          name: 'foo',
-          translatedName: 'foo',
-          type: 'INT',
+        mockSerie({
           data: [],
-        },
-        {
+        }),
+        mockSerie({
           name: 'bar',
           translatedName: 'bar',
-          type: 'INT',
-          data: [
-            { x: dates.parseDate('2017-04-27T08:21:32.000Z'), y: 2 },
-            { x: dates.parseDate('2017-04-30T23:06:24.000Z'), y: 2 },
-          ],
-        },
+        }),
       ])
     ).toBe(true);
     expect(
       utils.hasHistoryData([
-        {
+        mockSerie({
           name: 'bar',
           translatedName: 'bar',
-          type: 'INT',
           data: [{ x: dates.parseDate('2017-04-27T08:21:32.000Z'), y: 2 }],
-        },
+        }),
       ])
     ).toBe(false);
   });
@@ -190,8 +171,8 @@ describe('hasDataValues', () => {
 
 describe('getSeriesMetricType', () => {
   it('should return the correct type', () => {
-    expect(utils.getSeriesMetricType([SERIE])).toBe('PERCENT');
-    expect(utils.getSeriesMetricType([])).toBe('INT');
+    expect(utils.getSeriesMetricType([SERIE])).toBe(MetricType.Percent);
+    expect(utils.getSeriesMetricType([])).toBe(MetricType.Integer);
   });
 });
 
@@ -201,3 +182,65 @@ describe('hasHistoryDataValue', () => {
     expect(utils.hasHistoryDataValue([])).toBe(false);
   });
 });
+
+describe('saveActivityGraph', () => {
+  it('should correctly store data for standard graph types', () => {
+    utils.saveActivityGraph('foo', 'bar', GraphType.issues);
+    expect(save).toHaveBeenCalledWith('foo', GraphType.issues, 'bar');
+  });
+
+  it.each([undefined, [MetricKey.bugs, MetricKey.alert_status]])(
+    'should correctly store data for custom graph types',
+    (metrics) => {
+      utils.saveActivityGraph('foo', 'bar', GraphType.custom, metrics);
+      expect(save).toHaveBeenCalledWith('foo', GraphType.custom, 'bar');
+      // eslint-disable-next-line jest/no-conditional-in-test
+      expect(save).toHaveBeenCalledWith('foo.custom', metrics ? metrics.join(',') : '', 'bar');
+    }
+  );
+});
+
+describe('getActivityGraph', () => {
+  it('should correctly retrieve data for standard graph types', () => {
+    jest.mocked(get).mockImplementation((key) => {
+      // eslint-disable-next-line jest/no-conditional-in-test
+      if (key.includes('.custom')) {
+        return null;
+      }
+      return GraphType.coverage;
+    });
+
+    expect(utils.getActivityGraph('foo', 'bar')).toEqual({
+      graph: GraphType.coverage,
+      customGraphs: [],
+    });
+  });
+
+  it.each([null, 'bugs,code_smells'])(
+    'should correctly retrieve data for custom graph types',
+    (data) => {
+      jest.mocked(get).mockImplementation((key) => {
+        // eslint-disable-next-line jest/no-conditional-in-test
+        if (key.includes('.custom')) {
+          return data;
+        }
+        return GraphType.custom;
+      });
+
+      expect(utils.getActivityGraph('foo', 'bar')).toEqual({
+        graph: GraphType.custom,
+        // eslint-disable-next-line jest/no-conditional-in-test
+        customGraphs: data ? [MetricKey.bugs, MetricKey.code_smells] : [],
+      });
+    }
+  );
+
+  it('should correctly retrieve data for unknown graphs', () => {
+    jest.mocked(get).mockReturnValue(null);
+
+    expect(utils.getActivityGraph('foo', 'bar')).toEqual({
+      graph: GraphType.issues,
+      customGraphs: [],
+    });
+  });
+});
index 5821dd7c3e41a4107b7ba7a0d3987e25dfc97f6b..2938a44f927dab22489d4abfc7e2e935fde560ee 100644 (file)
@@ -21,7 +21,7 @@ import { chunk, flatMap, groupBy, sortBy } from 'lodash';
 import { getLocalizedMetricName, translate } from '../../helpers/l10n';
 import { localizeMetric } from '../../helpers/measures';
 import { get, save } from '../../helpers/storage';
-import { MetricKey } from '../../types/metrics';
+import { MetricKey, MetricType } from '../../types/metrics';
 import { GraphType, MeasureHistory, ParsedAnalysis, Serie } from '../../types/project-activity';
 import { Dict, Metric } from '../../types/types';
 
@@ -64,7 +64,7 @@ export function hasHistoryData(series: Serie[]) {
 }
 
 export function getSeriesMetricType(series: Serie[]) {
-  return series.length > 0 ? series[0].type : 'INT';
+  return series.length > 0 ? series[0].type : MetricType.Integer;
 }
 
 export function getDisplayedHistoryMetrics(graph: GraphType, customMetrics: string[]) {
@@ -89,7 +89,7 @@ export function splitSeriesInGraphs(series: Serie[], maxGraph: number, maxSeries
 export function generateCoveredLinesMetric(
   uncoveredLines: MeasureHistory,
   measuresHistory: MeasureHistory[]
-) {
+): Serie {
   const linesToCover = measuresHistory.find(
     (measure) => measure.metric === MetricKey.lines_to_cover
   );
@@ -102,14 +102,14 @@ export function generateCoveredLinesMetric(
       : [],
     name: 'covered_lines',
     translatedName: translate('project_activity.custom_metric.covered_lines'),
-    type: 'INT',
+    type: MetricType.Integer,
   };
 }
 
 export function generateSeries(
   measuresHistory: MeasureHistory[],
   graph: GraphType,
-  metrics: Metric[] | Dict<Metric>,
+  metrics: Metric[],
   displayedMetrics: string[]
 ): Serie[] {
   if (displayedMetrics.length <= 0 || measuresHistory === undefined) {
@@ -126,11 +126,11 @@ export function generateSeries(
         return {
           data: measure.history.map((analysis) => ({
             x: analysis.date,
-            y: metric && metric.type === 'LEVEL' ? analysis.value : Number(analysis.value),
+            y: metric && metric.type === MetricType.Level ? analysis.value : Number(analysis.value),
           })),
           name: measure.metric,
           translatedName: metric ? getLocalizedMetricName(metric) : localizeMetric(measure.metric),
-          type: metric ? metric.type : 'INT',
+          type: metric ? metric.type : MetricType.Integer,
         };
       }),
     (serie) =>
@@ -171,9 +171,6 @@ export function getAnalysisEventsForDate(analyses: ParsedAnalysis[], date?: Date
   return [];
 }
 
-function findMetric(key: string, metrics: Metric[] | Dict<Metric>) {
-  if (Array.isArray(metrics)) {
-    return metrics.find((metric) => metric.key === key);
-  }
-  return metrics[key];
+function findMetric(key: string, metrics: Metric[]) {
+  return metrics.find((metric) => metric.key === key);
 }
index f129754140b8881d2bf95b456405e4e88b2cf055..2cc3432dfac9dd65fd1ebbc59457eababfe5b168 100644 (file)
@@ -54,9 +54,16 @@ export default class ConfirmModal<T = string> extends React.PureComponent<Props<
   handleSubmit = () => {
     const result = this.props.onConfirm(this.props.confirmData);
     if (result) {
-      return result.then(this.props.onClose, () => {
-        /* noop */
-      });
+      return result.then(
+        () => {
+          if (this.mounted) {
+            this.props.onClose();
+          }
+        },
+        () => {
+          /* noop */
+        }
+      );
     }
     this.props.onClose();
     return undefined;
index 98d47a05bb5f093fa260cc05e942d9e6335fb2f1..c9faf55b54b4e9f01e930344bebe599ee38a71ff 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { MetricKey, MetricType } from '../../types/metrics';
 import {
   Analysis,
   AnalysisEvent,
   HistoryItem,
   MeasureHistory,
   ParsedAnalysis,
+  ProjectAnalysisEventCategory,
+  Serie,
 } from '../../types/project-activity';
 import { parseDate } from '../dates';
 
@@ -38,7 +41,7 @@ export function mockAnalysis(overrides: Partial<Analysis> = {}): Analysis {
 
 export function mockParsedAnalysis(overrides: Partial<ParsedAnalysis> = {}): ParsedAnalysis {
   return {
-    date: new Date('2017-03-01T09:37:01+0100'),
+    date: parseDate('2017-03-01T09:37:01+0100'),
     events: [],
     key: 'foo',
     projectVersion: '1.0',
@@ -48,7 +51,7 @@ export function mockParsedAnalysis(overrides: Partial<ParsedAnalysis> = {}): Par
 
 export function mockAnalysisEvent(overrides: Partial<AnalysisEvent> = {}): AnalysisEvent {
   return {
-    category: 'QUALITY_GATE',
+    category: ProjectAnalysisEventCategory.QualityGate,
     key: 'E11',
     description: 'Lorem ipsum dolor sit amet',
     name: 'Lorem ipsum',
@@ -74,7 +77,7 @@ export function mockAnalysisEvent(overrides: Partial<AnalysisEvent> = {}): Analy
 
 export function mockMeasureHistory(overrides: Partial<MeasureHistory> = {}): MeasureHistory {
   return {
-    metric: 'code_smells',
+    metric: MetricKey.code_smells,
     history: [
       mockHistoryItem(),
       mockHistoryItem({ date: parseDate('2018-10-27T12:21:15+0200'), value: '1749' }),
@@ -91,3 +94,16 @@ export function mockHistoryItem(overrides: Partial<HistoryItem> = {}): HistoryIt
     ...overrides,
   };
 }
+
+export function mockSerie(overrides: Partial<Serie> = {}): Serie {
+  return {
+    data: [
+      { x: parseDate('2017-04-27T08:21:32.000Z'), y: 2 },
+      { x: parseDate('2017-04-30T23:06:24.000Z'), y: 2 },
+    ],
+    name: 'foo',
+    translatedName: 'foo',
+    type: MetricType.Integer,
+    ...overrides,
+  };
+}
index d3a32551d7b7fce80a482bcecece0247d325f722..1e132aaf84088e5702a26296ff8773a060bf7806 100644 (file)
@@ -17,7 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { render, RenderResult } from '@testing-library/react';
+import { fireEvent, render, RenderResult } from '@testing-library/react';
+import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
 import { omit } from 'lodash';
 import * as React from 'react';
 import { HelmetProvider } from 'react-helmet-async';
@@ -202,3 +203,45 @@ function renderRoutedApp(
     </HelmetProvider>
   );
 }
+
+/* eslint-disable testing-library/no-node-access */
+export function dateInputEvent(user: UserEvent) {
+  return {
+    async openDatePicker(element: HTMLElement) {
+      await user.click(element);
+    },
+    async pickDate(element: HTMLElement, date: Date) {
+      await user.click(element);
+
+      const monthSelect =
+        element.parentNode?.querySelector<HTMLSelectElement>('select[name="months"]');
+      if (!monthSelect) {
+        throw new Error('Could not find the month selector of the date picker element');
+      }
+
+      const yearSelect =
+        element.parentNode?.querySelector<HTMLSelectElement>('select[name="years"]');
+      if (!yearSelect) {
+        throw new Error('Could not find the year selector of the date picker element');
+      }
+
+      fireEvent.change(monthSelect, { target: { value: date.getMonth() } });
+      fireEvent.change(yearSelect, { target: { value: date.getFullYear() } });
+
+      const dayButtons =
+        element.parentNode?.querySelectorAll<HTMLSelectElement>('button[name="day"]');
+      if (!dayButtons) {
+        throw new Error('Could not find the day buttons of the date picker element');
+      }
+      const dayButton = Array.from(dayButtons).find(
+        (button) => Number(button.textContent) === date.getDate()
+      );
+      if (!dayButton) {
+        throw new Error(`Could not find the button for day ${date.getDate()}`);
+      }
+
+      await user.click(dayButton);
+    },
+  };
+}
+/* eslint-enable testing-library/no-node-access */
index a71d16fb2120b5eca1a7a2f1f2623a684fbaea71..f3255f3d86147293ca2a698f1579b39f301b57f4 100644 (file)
@@ -17,6 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { Status } from './types';
+
 interface BaseAnalysis {
   buildString?: string;
   detectedCI?: string;
@@ -35,19 +37,19 @@ export interface ParsedAnalysis extends BaseAnalysis {
 }
 
 export interface AnalysisEvent {
-  category: string;
+  category: ProjectAnalysisEventCategory | ApplicationAnalysisEventCategory;
   description?: string;
   key: string;
   name: string;
   qualityGate?: {
     failing: Array<{ branch: string; key: string; name: string }>;
-    status: string;
+    status: Status;
     stillFailing: boolean;
   };
   definitionChange?: {
     projects: Array<{
       branch?: string;
-      changeType: string;
+      changeType: DefinitionChangeType;
       key: string;
       name: string;
       newBranch?: string;
@@ -63,6 +65,25 @@ export enum GraphType {
   custom = 'custom',
 }
 
+export enum ProjectAnalysisEventCategory {
+  Version = 'VERSION',
+  QualityGate = 'QUALITY_GATE',
+  QualityProfile = 'QUALITY_PROFILE',
+  Other = 'OTHER',
+}
+
+export enum ApplicationAnalysisEventCategory {
+  QualityGate = 'QUALITY_GATE',
+  DefinitionChange = 'DEFINITION_CHANGE',
+  Other = 'OTHER',
+}
+
+export enum DefinitionChangeType {
+  Added = 'ADDED',
+  Removed = 'REMOVED',
+  BranchChanged = 'BRANCH_CHANGED',
+}
+
 export interface HistoryItem {
   date: Date;
   value?: string;