* 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,
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);
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) {
};
findAnalysis = (analysisKey: string) => {
- const analysis = this.analysisList.find(({ key }) => key === analysisKey);
+ const analysis = this.#analysisList.find(({ key }) => key === analysisKey);
if (analysis !== undefined) {
return analysis;
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));
}
}
--- /dev/null
+/*
+ * 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));
+ }
+}
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 {
analysis: string;
key: string;
name: string;
- category: string;
+ category: ProjectAnalysisEventCategory | ApplicationAnalysisEventCategory;
description?: string;
}
<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(
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', () => {
<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}
*/
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: {
},
]
}
- updateGraph={[MockFunction]}
+ onUpdateGraph={[MockFunction]}
/>
<GraphsHistory
analyses={[]}
},
]
}
- updateGraph={[MockFunction]}
+ onUpdateGraph={[MockFunction]}
/>
<GraphsHistory
analyses={[]}
"date": 2016-10-27T10:21:15.000Z,
"events": [
{
- "category": "Custom",
+ "category": "OTHER",
"key": "Enew",
"name": "Foo",
},
*/
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 = [
events: [
{
key: 'E1',
- category: 'VERSION',
+ category: ProjectAnalysisEventCategory.Version,
name: '6.5-SNAPSHOT',
},
],
events: [
{
key: 'E2',
- category: 'OTHER',
+ category: ProjectAnalysisEventCategory.Other,
name: 'foo',
},
{
key: 'E3',
- category: 'OTHER',
+ category: ProjectAnalysisEventCategory.Other,
name: 'foo',
},
],
const newEvent = {
key: 'Enew',
name: 'Foo',
- category: 'Custom',
+ category: ProjectAnalysisEventCategory.Other,
};
const emptyState = {
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);
});
});
*/
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', () => {
{
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',
events: [
{
key: 'AVxZtC-N7841nF4RNEMJ',
- category: 'QUALITY_PROFILE',
+ category: ProjectAnalysisEventCategory.QualityProfile,
name: 'Changes in "Default - SonarSource conventions" (Java)',
},
],
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)',
},
],
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';
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);
*/
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 {
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'
);
*/
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';
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 {
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}
/>
);
}
return (
<ul
className="project-activity-versions-list"
- onScroll={this.handleScroll}
ref={(element) => (this.scrollContainer = element)}
style={{
marginTop:
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 {
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">
'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}
{addVersionForm && (
<AddEventForm
- addEvent={props.addVersion}
+ addEvent={props.onAddVersion}
addEventButtonText="project_activity.add_version"
analysis={analysis}
onClose={() => setAddVersionForm(false)}
{addEventForm && (
<AddEventForm
- addEvent={props.addCustomEvent}
+ addEvent={props.onAddCustomEvent}
addEventButtonText="project_activity.add_custom_event"
analysis={analysis}
onClose={() => setAddEventForm(false)}
{removeAnalysisForm && (
<RemoveAnalysisForm
analysis={analysis}
- deleteAnalysis={props.deleteAnalysis}
+ deleteAnalysis={props.onDeleteAnalysis}
onClose={() => setRemoveAnalysisForm(false)}
/>
)}
canAdmin={canAdmin}
events={analysis.events}
isFirst={isFirst}
- onChange={props.changeEvent}
- onDelete={props.deleteEvent}
+ onChange={props.onChangeEvent}
+ onDelete={props.onDeleteEvent}
/>
)}
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 {
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) {
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));
});
};
- 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));
});
};
- deleteAnalysis = (analysis: string) => {
+ handleDeleteAnalysis = (analysis: string) => {
return deleteAnalysis(analysis).then(() => {
if (this.mounted) {
this.updateGraphData(
});
};
- deleteEvent = (analysis: string, event: string) => {
+ handleDeleteEvent = (analysis: string, event: string) => {
return deleteEvent(event).then(() => {
if (this.mounted) {
this.setState(actions.deleteEvent(analysis, event));
);
};
- updateQuery = (newQuery: Query) => {
+ handleUpdateQuery = (newQuery: Query) => {
const query = serializeUrlQuery({
...this.state.query,
...newQuery,
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}
/>
);
}
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';
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 (
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">
metrics={props.metrics}
project={props.project.key}
query={query}
- updateQuery={props.updateQuery}
+ updateQuery={props.onUpdateQuery}
/>
</div>
</div>
.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
);
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);
}
};
- 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
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) {
};
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}
metricsType={getSeriesMetricType(series)}
series={series}
showAreas={[GraphType.coverage, GraphType.duplications].includes(query.graph)}
- updateGraphZoom={this.updateGraphZoom}
+ onUpdateGraphZoom={this.handleUpdateGraphZoom}
/>
</div>
);
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 {
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,
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}
+++ /dev/null
-/*
- * 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} />;
-}
+++ /dev/null
-/*
- * 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}
- />
- );
-}
*/
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: [
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(
{
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'
),
+++ /dev/null
-/*
- * 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();
-});
+++ /dev/null
-/*
- * 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();
-});
+++ /dev/null
-/*
- * 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();
-});
+++ /dev/null
-// 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>
-`;
+++ /dev/null
-// 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>
-`;
+++ /dev/null
-// 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",
- },
- ],
-}
-`;
+++ /dev/null
-// 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>
-`;
+++ /dev/null
-/*
- * 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();
-});
+++ /dev/null
-/*
- * 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();
-});
+++ /dev/null
-/*
- * 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}
- />
- );
-}
+++ /dev/null
-// 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>
-`;
+++ /dev/null
-// 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>
-`;
+++ /dev/null
-// 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>
-`;
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 {
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);
}
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;
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', () => {
date: '2017-03-02T08:36:01',
events: [
mockAnalysisEvent(),
- mockAnalysisEvent({ category: 'VERSION', qualityGate: undefined }),
+ mockAnalysisEvent({
+ category: ProjectAnalysisEventCategory.Version,
+ qualityGate: undefined,
+ }),
],
projectVersion: '4.1',
}),
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';
date: new Date('2017-03-02T08:36:01Z'),
events: [
mockAnalysisEvent(),
- mockAnalysisEvent({ category: 'VERSION', qualityGate: undefined }),
+ mockAnalysisEvent({ category: ProjectAnalysisEventCategory.Version, qualityGate: undefined }),
],
projectVersion: '4.1',
}),
* 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';
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[];
}
.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);
};
};
onSelect = (metric: string) => {
- this.props.addMetric(metric);
+ this.props.onAddMetric(metric);
this.setState((state) => {
return {
selectedMetrics: sortBy([...state.selectedMetrics, metric]),
};
onUnselect = (metric: string) => {
- this.props.removeMetric(metric);
+ this.props.onRemoveMetric(metric);
this.setState((state) => {
return {
metrics: sortBy([...state.metrics, metric]),
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';
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 {
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')) => (
);
renderProjectChange(project: {
- changeType: string;
+ changeType: DefinitionChangeType;
key: string;
name: string;
branch?: string;
}) {
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() {
</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)}:
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),
/>
</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}
/>
)}
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;
}
<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}
/>
)}
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';
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 }));
};
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>
))}
*/
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';
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".
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'),
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' }),
};
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());
+ },
},
};
}
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[] = [];
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}
/>
</>
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';
analyses: [
mockParsedAnalysis({
date: parseDate('2016-01-01T00:00:00+0200'),
- events: [mockAnalysisEvent({ key: '1', category: 'QUALITY_GATE' })],
+ events: [
+ mockAnalysisEvent({ key: '1', category: ProjectAnalysisEventCategory.QualityGate }),
+ ],
}),
],
});
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 = {
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 }),
};
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',
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();
it('should render correctly for simple "QUALITY_GATE" events', () => {
renderEventInner({
event: mockAnalysisEvent({
- category: 'QUALITY_GATE',
+ category: ProjectAnalysisEventCategory.QualityGate,
qualityGate: { status: 'ERROR', stillFailing: false, failing: [] },
}),
});
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: [] },
}),
});
const user = userEvent.setup();
renderEventInner({
event: mockAnalysisEvent({
- category: 'QUALITY_GATE',
+ category: ProjectAnalysisEventCategory.QualityGate,
qualityGate: {
status: 'ERROR',
stillFailing: true,
it('should render correctly', () => {
renderEventInner({
event: mockAnalysisEvent({
- category: 'VERSION',
+ category: ProjectAnalysisEventCategory.Version,
name: '1.0',
}),
});
});
});
-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
+ );
}
},
],
"name": "lines_to_cover",
- "translatedName": "Line to Cover",
+ "translatedName": "lines_to_cover",
"type": "PERCENT",
},
]
* 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', () => {
};
});
+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', () => {
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);
});
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);
});
});
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: [],
+ });
+ });
+});
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';
}
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[]) {
export function generateCoveredLinesMetric(
uncoveredLines: MeasureHistory,
measuresHistory: MeasureHistory[]
-) {
+): Serie {
const linesToCover = measuresHistory.find(
(measure) => measure.metric === MetricKey.lines_to_cover
);
: [],
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) {
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) =>
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);
}
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;
* 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';
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',
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',
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' }),
...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,
+ };
+}
* 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';
</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 */
* 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;
}
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;
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;