Browse Source

SONAR-17472 Improve accessibility of analysis navigation on Activity page

tags/9.8.0.63668
Wouter Admiraal 1 year ago
parent
commit
b4238b570e
23 changed files with 476 additions and 1274 deletions
  1. 3
    1
      server/sonar-web/config/jest/SetupTestEnvironment.ts
  2. 177
    0
      server/sonar-web/src/main/js/api/mocks/ProjectActivityServiceMock.ts
  3. 30
    6
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx
  4. 31
    15
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx
  5. 1
    1
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppRenderer.tsx
  6. 0
    90
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/Event-test.tsx
  7. 0
    43
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/Events-test.tsx
  8. 0
    128
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysis-test.tsx
  9. 207
    75
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx
  10. 0
    129
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.tsx
  11. 0
    98
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppRenderer-test.tsx
  12. 0
    85
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/Event-test.tsx.snap
  13. 0
    98
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/Events-test.tsx.snap
  14. 10
    10
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.tsx.snap
  15. 0
    232
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysis-test.tsx.snap
  16. 0
    72
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.tsx.snap
  17. 0
    185
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAppRenderer-test.tsx.snap
  18. 2
    1
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.tsx
  19. 2
    2
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeEventForm.tsx
  20. 4
    1
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/AddEventForm-test.tsx.snap
  21. 4
    1
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/ChangeEventForm-test.tsx.snap
  22. 3
    1
      server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx
  23. 2
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 3
- 1
server/sonar-web/config/jest/SetupTestEnvironment.ts View File

@@ -24,10 +24,12 @@ document.documentElement.appendChild(content);
const baseUrl = '';
(window as any).baseUrl = baseUrl;

Element.prototype.scrollIntoView = () => {};

jest.mock('../../src/main/js/helpers/l10n', () => ({
...jest.requireActual('../../src/main/js/helpers/l10n'),
hasMessage: () => true,
translate: (...keys: string[]) => keys.join('.'),
translateWithParameters: (messageKey: string, ...parameters: Array<string | number>) =>
[messageKey, ...parameters].join('.')
[messageKey, ...parameters].join('.'),
}));

+ 177
- 0
server/sonar-web/src/main/js/api/mocks/ProjectActivityServiceMock.ts View File

@@ -0,0 +1,177 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 { cloneDeep, uniqueId } from 'lodash';
import { mockAnalysis, mockAnalysisEvent } from '../../helpers/mocks/project-activity';
import { Analysis } from '../../types/project-activity';
import {
changeEvent,
createEvent,
deleteAnalysis,
deleteEvent,
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' }),
],
}),
mockAnalysis({
key: 'AXJMbIUGPAOIsUIE3eNC',
date: '2017-03-01T09:36:01+0100',
projectVersion: '1.0',
buildString: '1.0.0.1',
}),
];

this.analysisList = cloneDeep(this.readOnlyAnalysisList);

(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);
}

reset = () => {
this.analysisList = cloneDeep(this.readOnlyAnalysisList);
};

getActivityHandler = () => {
return this.reply({
analyses: this.analysisList,
paging: {
pageIndex: 1,
pageSize: 100,
total: this.analysisList.length,
},
});
};

deleteAnalysisHandler = (analysisKey: string) => {
const i = this.analysisList.findIndex(({ key }) => key === analysisKey);
if (i !== undefined) {
this.analysisList.splice(i, 1);
return this.reply();
}
throw new Error(`Could not find analysis with key: ${analysisKey}`);
};

createEventHandler = (
analysisKey: string,
name: string,
category = 'OTHER',
description?: string
) => {
const analysis = this.findAnalysis(analysisKey);

const key = uniqueId(analysisKey);
analysis.events.push({ key, name, category, description });

return this.reply({
analysis: analysisKey,
key,
name,
category,
description,
});
};

changeEventHandler = (eventKey: string, name: string, description?: string) => {
const [eventIndex, analysisKey] = this.findEvent(eventKey);
const analysis = this.findAnalysis(analysisKey);
const event = analysis.events[eventIndex];

event.name = name;
event.description = description;

return this.reply({ analysis: analysisKey, ...event });
};

deleteEventHandler = (eventKey: string) => {
const [eventIndex, analysisKey] = this.findEvent(eventKey);
const analysis = this.findAnalysis(analysisKey);

analysis.events.splice(eventIndex, 1);

return this.reply();
};

findEvent = (eventKey: string): [number, string] => {
let analysisKey;
const eventIndex = this.analysisList.reduce((acc, { key, events }) => {
if (acc === undefined) {
const i = events.findIndex(({ key }) => key === eventKey);
if (i > -1) {
analysisKey = key;
return i;
}
}

return acc;
}, undefined);

if (eventIndex !== undefined && analysisKey !== undefined) {
return [eventIndex, analysisKey];
}

throw new Error(`Could not find event with key: ${eventKey}`);
};

findAnalysis = (analysisKey: string) => {
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);
}
}

+ 30
- 6
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx View File

@@ -19,12 +19,15 @@
*/
import classNames from 'classnames';
import * as React from 'react';
import { injectIntl, WrappedComponentProps } from 'react-intl';
import ActionsDropdown, {
ActionsDropdownDivider,
ActionsDropdownItem,
} from '../../../components/controls/ActionsDropdown';
import { ButtonPlain } from '../../../components/controls/buttons';
import ClickEventBoundary from '../../../components/controls/ClickEventBoundary';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import { formatterOption } from '../../../components/intl/DateTimeFormatter';
import TimeFormatter from '../../../components/intl/TimeFormatter';
import { PopupPlacement } from '../../../components/ui/popups';
import { parseDate } from '../../../helpers/dates';
@@ -34,7 +37,7 @@ import Events from './Events';
import AddEventForm from './forms/AddEventForm';
import RemoveAnalysisForm from './forms/RemoveAnalysisForm';

export interface ProjectActivityAnalysisProps {
export interface ProjectActivityAnalysisProps extends WrappedComponentProps {
addCustomEvent: (analysis: string, name: string, category?: string) => Promise<void>;
addVersion: (analysis: string, version: string) => Promise<void>;
analysis: ParsedAnalysis;
@@ -53,7 +56,15 @@ export interface ProjectActivityAnalysisProps {
export function ProjectActivityAnalysis(props: ProjectActivityAnalysisProps) {
let node: HTMLLIElement | null = null;

const { analysis, isBaseline, isFirst, canAdmin, canCreateVersion, selected } = props;
const {
analysis,
isBaseline,
isFirst,
canAdmin,
canCreateVersion,
selected,
intl: { formatDate },
} = props;

React.useEffect(() => {
if (node && selected) {
@@ -85,9 +96,18 @@ export function ProjectActivityAnalysis(props: ProjectActivityAnalysisProps) {
<div className="project-activity-time">
<TimeFormatter date={parsedDate} long={false}>
{(formattedTime) => (
<time className="text-middle" dateTime={parsedDate.toISOString()}>
{formattedTime}
</time>
<ButtonPlain
aria-current={selected}
aria-label={translateWithParameters(
'project_activity.show_analysis_X_on_graph',
analysis.buildString || formatDate(parsedDate, formatterOption)
)}
onClick={() => props.updateSelectedDate(analysis.date)}
>
<time className="text-middle" dateTime={parsedDate.toISOString()}>
{formattedTime}
</time>
</ButtonPlain>
)}
</TimeFormatter>
</div>
@@ -105,6 +125,10 @@ export function ProjectActivityAnalysis(props: ProjectActivityAnalysisProps) {
<ClickEventBoundary>
<div className="project-activity-analysis-actions big-spacer-left">
<ActionsDropdown
ariaLabel={translateWithParameters(
'project_activity.analysis_X_actions',
analysis.buildString || formatDate(parsedDate, formatterOption)
)}
overlayPlacement={PopupPlacement.BottomRight}
small={true}
toggleClassName="js-analysis-actions"
@@ -196,4 +220,4 @@ export function ProjectActivityAnalysis(props: ProjectActivityAnalysisProps) {
);
}

export default React.memo(ProjectActivityAnalysis);
export default injectIntl(ProjectActivityAnalysis);

+ 31
- 15
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx View File

@@ -19,7 +19,6 @@
*/
import * as React from 'react';
import { useSearchParams } from 'react-router-dom';
import { getAllMetrics } from '../../../api/metrics';
import {
changeEvent,
createEvent,
@@ -30,6 +29,7 @@ import {
} from '../../../api/projectActivity';
import { getAllTimeMachineData } from '../../../api/time-machine';
import withComponentContext from '../../../app/components/componentContext/withComponentContext';
import withMetricsContext from '../../../app/components/metrics/withMetricsContext';
import {
DEFAULT_GRAPH,
getActivityGraph,
@@ -41,9 +41,10 @@ import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { parseDate } from '../../../helpers/dates';
import { serializeStringArray } from '../../../helpers/query';
import { BranchLike } from '../../../types/branch-like';
import { ComponentQualifier, isPortfolioLike } from '../../../types/component';
import { MetricKey } from '../../../types/metrics';
import { GraphType, MeasureHistory, ParsedAnalysis } from '../../../types/project-activity';
import { Component, Metric, Paging, RawQuery } from '../../../types/types';
import { Component, Dict, Metric, Paging, RawQuery } from '../../../types/types';
import * as actions from '../actions';
import {
customMetricsChanged,
@@ -58,6 +59,7 @@ interface Props {
branchLike?: BranchLike;
component: Component;
location: Location;
metrics: Dict<Metric>;
router: Router;
}

@@ -66,7 +68,6 @@ export interface State {
analysesLoading: boolean;
graphLoading: boolean;
initialized: boolean;
metrics: Metric[];
measuresHistory: MeasureHistory[];
query: Query;
}
@@ -87,7 +88,6 @@ export class ProjectActivityApp extends React.PureComponent<Props, State> {
graphLoading: true,
initialized: false,
measuresHistory: [],
metrics: [],
query: parseQuery(props.location.query),
};
}
@@ -251,18 +251,35 @@ export class ProjectActivityApp extends React.PureComponent<Props, State> {
let current = component.breadcrumbs.length - 1;
while (
current > 0 &&
!['TRK', 'VW', 'APP'].includes(component.breadcrumbs[current].qualifier)
!(
[
ComponentQualifier.Project,
ComponentQualifier.Portfolio,
ComponentQualifier.Application,
] as string[]
).includes(component.breadcrumbs[current].qualifier)
) {
current--;
}
return component.breadcrumbs[current].key;
};

filterMetrics({ qualifier }: Component, metrics: Metric[]) {
return ['VW', 'SVW'].includes(qualifier)
? metrics.filter((metric) => metric.key !== MetricKey.security_hotspots_reviewed)
: metrics.filter((metric) => metric.key !== MetricKey.security_review_rating);
}
filterMetrics = () => {
const {
component: { qualifier },
metrics,
} = this.props;

if (isPortfolioLike(qualifier)) {
return Object.values(metrics).filter(
(metric) => metric.key !== MetricKey.security_hotspots_reviewed
);
}

return Object.values(metrics).filter(
(metric) => metric.key !== MetricKey.security_review_rating
);
};

firstLoadData(query: Query, component: Component) {
const graphMetrics = getHistoryMetrics(query.graph || DEFAULT_GRAPH, query.customMetrics);
@@ -278,17 +295,15 @@ export class ProjectActivityApp extends React.PureComponent<Props, State> {
ACTIVITY_PAGE_SIZE_FIRST_BATCH,
serializeQuery(query)
),
getAllMetrics(),
this.fetchMeasuresHistory(graphMetrics),
]).then(
([{ analyses }, metrics, measuresHistory]) => {
([{ analyses }, measuresHistory]) => {
if (this.mounted) {
this.setState({
analyses,
graphLoading: false,
initialized: true,
measuresHistory,
metrics: this.filterMetrics(component, metrics),
});

this.fetchAllActivities(topLevelComponent);
@@ -335,6 +350,7 @@ export class ProjectActivityApp extends React.PureComponent<Props, State> {
};

render() {
const metrics = this.filterMetrics();
return (
<ProjectActivityAppRenderer
addCustomEvent={this.addCustomEvent}
@@ -347,7 +363,7 @@ export class ProjectActivityApp extends React.PureComponent<Props, State> {
graphLoading={!this.state.initialized || this.state.graphLoading}
initializing={!this.state.initialized}
measuresHistory={this.state.measuresHistory}
metrics={this.state.metrics}
metrics={metrics}
project={this.props.component}
query={this.state.query}
updateQuery={this.updateQuery}
@@ -393,4 +409,4 @@ function RedirectWrapper(props: Props) {
return shouldRedirect ? null : <ProjectActivityApp {...props} />;
}

export default withComponentContext(withRouter(RedirectWrapper));
export default withComponentContext(withRouter(withMetricsContext(RedirectWrapper)));

+ 1
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppRenderer.tsx View File

@@ -87,7 +87,7 @@ export default function ProjectActivityAppRenderer(props: Props) {
props.project.leakPeriodDate ? parseDate(props.project.leakPeriodDate) : undefined
}
project={props.project}
query={props.query}
query={query}
updateQuery={props.updateQuery}
/>
</div>

+ 0
- 90
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/Event-test.tsx View File

@@ -1,90 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 { DeleteButton, EditButton } from '../../../../components/controls/buttons';
import { mockAnalysisEvent } from '../../../../helpers/mocks/project-activity';
import { click } from '../../../../helpers/testUtils';
import { Event, EventProps } from '../Event';
import ChangeEventForm from '../forms/ChangeEventForm';
import RemoveEventForm from '../forms/RemoveEventForm';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
expect(shallowRender({ canAdmin: true })).toMatchSnapshot('with admin options');
});

it('should correctly allow deletion', () => {
expect(
shallowRender({
canAdmin: true,
event: mockAnalysisEvent({ category: 'VERSION' }),
isFirst: true,
})
.find(DeleteButton)
.exists()
).toBe(false);

expect(
shallowRender({ canAdmin: true, event: mockAnalysisEvent() }).find(DeleteButton).exists()
).toBe(false);

expect(shallowRender({ canAdmin: true }).find(DeleteButton).exists()).toBe(true);
});

it('should correctly allow edition', () => {
expect(shallowRender({ canAdmin: true }).find(EditButton).exists()).toBe(true);

expect(shallowRender({ canAdmin: true, isFirst: true }).find(EditButton).exists()).toBe(true);

expect(
shallowRender({ canAdmin: true, event: mockAnalysisEvent() }).find(EditButton).exists()
).toBe(false);
});

it('should correctly show edit form', () => {
const wrapper = shallowRender({ canAdmin: true });
click(wrapper.find(EditButton));
const changeEventForm = wrapper.find(ChangeEventForm);
expect(changeEventForm.exists()).toBe(true);
changeEventForm.prop('onClose')();
expect(wrapper.find(ChangeEventForm).exists()).toBe(false);
});

it('should correctly show delete form', () => {
const wrapper = shallowRender({ canAdmin: true });
click(wrapper.find(DeleteButton));
const removeEventForm = wrapper.find(RemoveEventForm);
expect(removeEventForm.exists()).toBe(true);
removeEventForm.prop('onClose')();
expect(wrapper.find(RemoveEventForm).exists()).toBe(false);
});

function shallowRender(props: Partial<EventProps> = {}) {
return shallow<Event>(
<Event
analysisKey="foo"
event={mockAnalysisEvent({ category: 'OTHER' })}
onChange={jest.fn()}
onDelete={jest.fn()}
{...props}
/>
);
}

+ 0
- 43
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/Events-test.tsx View File

@@ -1,43 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 { mockAnalysisEvent } from '../../../../helpers/mocks/project-activity';
import { Events, EventsProps } from '../Events';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
});

function shallowRender(props: Partial<EventsProps> = {}) {
return shallow(
<Events
analysisKey="foo"
events={[
mockAnalysisEvent(),
mockAnalysisEvent({ category: 'VERSION' }),
mockAnalysisEvent({ category: 'OTHER' }),
]}
onChange={jest.fn()}
onDelete={jest.fn()}
{...props}
/>
);
}

+ 0
- 128
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysis-test.tsx View File

@@ -1,128 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 TimeFormatter from '../../../../components/intl/TimeFormatter';
import { mockAnalysisEvent, mockParsedAnalysis } from '../../../../helpers/mocks/project-activity';
import { click } from '../../../../helpers/testUtils';
import AddEventForm from '../forms/AddEventForm';
import RemoveAnalysisForm from '../forms/RemoveAnalysisForm';
import { ProjectActivityAnalysis, ProjectActivityAnalysisProps } from '../ProjectActivityAnalysis';

jest.mock('../../../../helpers/dates', () => ({
parseDate: () => ({
valueOf: () => 1546333200000,
toISOString: () => '2019-01-01T09:00:00.000Z',
}),
}));

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
expect(
shallowRender({ analysis: mockParsedAnalysis({ events: [mockAnalysisEvent()] }) })
).toMatchSnapshot('with events');
expect(
shallowRender({ analysis: mockParsedAnalysis({ buildString: '1.0.234' }) })
).toMatchSnapshot('with build string');
expect(shallowRender({ isBaseline: true })).toMatchSnapshot('with baseline marker');
expect(
shallowRender({
canAdmin: true,
canCreateVersion: true,
canDeleteAnalyses: true,
})
).toMatchSnapshot('with admin options');

const timeFormatter = shallowRender().find(TimeFormatter).prop('children');
expect(timeFormatter!('formatted_time')).toMatchSnapshot('formatted time');
});

it('should show the correct admin options', () => {
const wrapper = shallowRender({
canAdmin: true,
canCreateVersion: true,
canDeleteAnalyses: true,
});

expect(wrapper.find('.js-add-version').exists()).toBe(true);
click(wrapper.find('.js-add-version'));
const addVersionForm = wrapper.find(AddEventForm);
expect(addVersionForm.exists()).toBe(true);
addVersionForm.prop('onClose')();
expect(wrapper.find(AddEventForm).exists()).toBe(false);

expect(wrapper.find('.js-add-event').exists()).toBe(true);
click(wrapper.find('.js-add-event'));
const addEventForm = wrapper.find(AddEventForm);
expect(addEventForm.exists()).toBe(true);
addEventForm.prop('onClose')();
expect(wrapper.find(AddEventForm).exists()).toBe(false);

expect(wrapper.find('.js-delete-analysis').exists()).toBe(true);
click(wrapper.find('.js-delete-analysis'));
const removeAnalysisForm = wrapper.find(RemoveAnalysisForm);
expect(removeAnalysisForm.exists()).toBe(true);
removeAnalysisForm.prop('onClose')();
expect(wrapper.find(RemoveAnalysisForm).exists()).toBe(false);
});

it('should not allow the first item to be deleted', () => {
expect(
shallowRender({
canAdmin: true,
canCreateVersion: true,
canDeleteAnalyses: true,
isFirst: true,
})
.find('.js-delete-analysis')
.exists()
).toBe(false);
});

it('should be clickable', () => {
const date = new Date('2018-03-01T09:37:01+0100');
const updateSelectedDate = jest.fn();
const wrapper = shallowRender({ analysis: mockParsedAnalysis({ date }), updateSelectedDate });
click(wrapper);
expect(updateSelectedDate).toHaveBeenCalledWith(date);
});

function shallowRender(props: Partial<ProjectActivityAnalysisProps> = {}) {
return shallow(createComponent(props));
}

function createComponent(props: Partial<ProjectActivityAnalysisProps> = {}) {
return (
<ProjectActivityAnalysis
addCustomEvent={jest.fn()}
addVersion={jest.fn()}
analysis={mockParsedAnalysis()}
canCreateVersion={false}
changeEvent={jest.fn()}
deleteAnalysis={jest.fn()}
deleteEvent={jest.fn()}
isBaseline={false}
isFirst={false}
selected={false}
updateSelectedDate={jest.fn()}
{...props}
/>
);
}

+ 207
- 75
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx View File

@@ -17,106 +17,238 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { screen } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { keyBy } from 'lodash';
import React from 'react';
import { ComponentContext } from '../../../../app/components/componentContext/ComponentContext';
import { getActivityGraph } from '../../../../components/activity-graph/utils';
import { Route } from 'react-router-dom';
import { byLabelText, byRole, byText } from 'testing-library-selector';
import { ProjectActivityServiceMock } from '../../../../api/mocks/ProjectActivityServiceMock';
import { getAllTimeMachineData } from '../../../../api/time-machine';
import { mockComponent } from '../../../../helpers/mocks/component';
import { renderApp } from '../../../../helpers/testReactTestingUtils';
import { get } from '../../../../helpers/storage';
import { mockMetric, mockPaging } from '../../../../helpers/testMocks';
import { renderAppWithComponentContext } from '../../../../helpers/testReactTestingUtils';
import { ComponentQualifier } from '../../../../types/component';
import { Component } from '../../../../types/types';
import { MetricKey } from '../../../../types/metrics';
import { GraphType } from '../../../../types/project-activity';
import ProjectActivityAppContainer from '../ProjectActivityApp';

jest.mock('../../../../api/time-machine', () => {
const { mockPaging } = jest.requireActual('../../../../helpers/testMocks');
return {
getAllTimeMachineData: jest.fn().mockResolvedValue({
measures: [
{
metric: 'bugs',
history: [{ date: '2022-01-01', value: '10' }],
},
],
paging: mockPaging({ total: 1 }),
}),
};
});
jest.mock('../../../../api/projectActivity');

jest.mock('../../../../api/metrics', () => {
const { mockMetric } = jest.requireActual('../../../../helpers/testMocks');
return {
getAllMetrics: jest.fn().mockResolvedValue([mockMetric()]),
};
});
jest.mock('../../../../api/time-machine', () => ({
getAllTimeMachineData: jest.fn(),
}));

jest.mock('../../../../api/projectActivity', () => {
const { mockPaging } = jest.requireActual('../../../../helpers/testMocks');
const { mockAnalysis } = jest.requireActual('../../../../helpers/mocks/project-activity');
return {
...jest.requireActual('../../../../api/projectActivity'),
createEvent: jest.fn(),
changeEvent: jest.fn(),
getProjectActivity: jest.fn().mockResolvedValue({
analyses: [mockAnalysis({ key: 'foo' })],
paging: mockPaging({ total: 1 }),
}),
};
});
jest.mock('../../../../helpers/storage', () => ({
...jest.requireActual('../../../../helpers/storage'),
get: jest.fn(),
}));

jest.mock('../../../../components/activity-graph/utils', () => {
const actual = jest.requireActual('../../../../components/activity-graph/utils');
return {
...actual,
getActivityGraph: jest.fn(),
};
});
let handler: ProjectActivityServiceMock;

it('should render default graph', async () => {
(getActivityGraph as jest.Mock).mockImplementation(() => {
return {
graph: 'issues',
};
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(),
});
});

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('link', { name: 'project_activity.add_custom_event' }),
addVersionEvenBtn: byRole('link', { name: 'project_activity.add_version' }),
deleteAnalysisBtn: byRole('link', { 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 screen.findByText('project_activity.graphs.issues')).toBeInTheDocument();
expect(await ui.graphTypeIssues.find()).toBeInTheDocument();
});

it('should reload custom graph from local storage', async () => {
(getActivityGraph as jest.Mock).mockImplementation(() => {
return {
graph: 'custom',
customGraphs: ['bugs', 'code_smells'],
};
});

(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(await screen.findByText('project_activity.graphs.custom')).toBeInTheDocument();
expect(await ui.graphTypeCustom.find()).toBeInTheDocument();
});

function renderProjectActivityAppContainer(
{ component, navigateTo }: { component: Component; navigateTo?: string } = {
component: mockComponent({
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();
renderProjectActivityAppContainer(
mockComponent({
breadcrumbs: [
{ key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project },
],
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());

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());

expect(screen.getAllByText(updatedValue)[0]).toBeInTheDocument();

await user.click(ui.deleteEventBtn.getAll()[0]);
await user.click(ui.deleteBtn.get());

expect(screen.queryByText(updatedValue)).not.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();

// Most recent analysis is not deletable.
await user.click(ui.cogBtn('1.1.0.2').get());
expect(ui.deleteAnalysisBtn.query()).not.toBeInTheDocument();

await user.click(ui.cogBtn('1.1.0.1').get());
await user.click(ui.deleteAnalysisBtn.get());
await user.click(ui.deleteBtn.get());

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();

expect(ui.baseline.get()).toBeInTheDocument();
});

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();
renderProjectActivityAppContainer(
mockComponent({
qualifier,
breadcrumbs: [{ key: 'breadcrumb', name: 'breadcrumb', qualifier }],
})
);

await user.click(ui.addMetricBtn.get());

expect(visible.get()).toBeInTheDocument();
expect(invisible.query()).not.toBeInTheDocument();
}
);

it('should allow analyses to be clicked', async () => {
const user = userEvent.setup();
renderProjectActivityAppContainer();
await waitOnDataLoaded();

expect(ui.bugsPopupCell.query()).not.toBeInTheDocument();

await user.click(ui.seeDetailsBtn('1.0.0.1').get());

expect(ui.bugsPopupCell.get()).toBeInTheDocument();
});

async function waitOnDataLoaded() {
await waitFor(() => {
expect(ui.loading.query()).not.toBeInTheDocument();
});
}

function renderProjectActivityAppContainer(
component = mockComponent({
breadcrumbs: [{ key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project }],
})
) {
return renderApp(
return renderAppWithComponentContext(
'project/activity',
<ComponentContext.Provider
value={{
branchLikes: [],
onBranchesChange: jest.fn(),
onComponentChange: jest.fn(),
component,
}}
>
<ProjectActivityAppContainer />
</ComponentContext.Provider>,
{ navigateTo }
() => <Route path="*" element={<ProjectActivityAppContainer />} />,
{
metrics: keyBy(
[
mockMetric({ key: MetricKey.bugs, type: 'INT' }),
mockMetric({ key: MetricKey.security_hotspots_reviewed }),
mockMetric({ key: MetricKey.security_review_rating, type: 'RATING' }),
],
'key'
),
},
{ component }
);
}

+ 0
- 129
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.tsx View File

@@ -1,129 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 { changeEvent, createEvent } from '../../../../api/projectActivity';
import { mockComponent } from '../../../../helpers/mocks/component';
import { mockAnalysisEvent } from '../../../../helpers/mocks/project-activity';
import { mockLocation, mockMetric, mockRouter } from '../../../../helpers/testMocks';
import { waitAndUpdate } from '../../../../helpers/testUtils';
import { ComponentQualifier } from '../../../../types/component';
import { MetricKey } from '../../../../types/metrics';
import { ProjectActivityApp } from '../ProjectActivityApp';

jest.mock('../../../../helpers/dates', () => ({
parseDate: jest.fn((date) => `PARSED:${date}`),
}));

jest.mock('../../../../api/time-machine', () => {
const { mockPaging } = jest.requireActual('../../../../helpers/testMocks');
return {
getAllTimeMachineData: jest.fn().mockResolvedValue({
measures: [
{
metric: 'bugs',
history: [{ date: '2022-01-01', value: '10' }],
},
],
paging: mockPaging({ total: 1 }),
}),
};
});

jest.mock('../../../../api/metrics', () => {
const { mockMetric } = jest.requireActual('../../../../helpers/testMocks');
return {
getAllMetrics: jest.fn().mockResolvedValue([mockMetric()]),
};
});

jest.mock('../../../../api/projectActivity', () => {
const { mockPaging } = jest.requireActual('../../../../helpers/testMocks');
const { mockAnalysis } = jest.requireActual('../../../../helpers/mocks/project-activity');
return {
...jest.requireActual('../../../../api/projectActivity'),
createEvent: jest.fn(),
changeEvent: jest.fn(),
getProjectActivity: jest.fn().mockResolvedValue({
analyses: [mockAnalysis({ key: 'foo' })],
paging: mockPaging({ total: 1 }),
}),
};
});

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
});

it('should filter metric correctly', () => {
const wrapper = shallowRender();
let metrics = wrapper
.instance()
.filterMetrics(mockComponent({ qualifier: ComponentQualifier.Project }), [
mockMetric({ key: MetricKey.bugs }),
mockMetric({ key: MetricKey.security_review_rating }),
]);
expect(metrics).toHaveLength(1);
metrics = wrapper
.instance()
.filterMetrics(mockComponent({ qualifier: ComponentQualifier.Portfolio }), [
mockMetric({ key: MetricKey.bugs }),
mockMetric({ key: MetricKey.security_hotspots_reviewed }),
]);
expect(metrics).toHaveLength(1);
});

it('should correctly create and update custom events', async () => {
const analysisKey = 'foo';
const name = 'bar';
const newName = 'baz';
const event = mockAnalysisEvent({ name });
(createEvent as jest.Mock).mockResolvedValueOnce({ analysis: analysisKey, ...event });
(changeEvent as jest.Mock).mockResolvedValueOnce({
analysis: analysisKey,
...event,
name: newName,
});

const wrapper = shallowRender();
await waitAndUpdate(wrapper);
const instance = wrapper.instance();

instance.addCustomEvent(analysisKey, name);
expect(createEvent).toHaveBeenCalledWith(analysisKey, name, undefined);
await waitAndUpdate(wrapper);
expect(wrapper.state().analyses[0].events[0]).toEqual(event);

instance.changeEvent(event.key, newName);
expect(changeEvent).toHaveBeenCalledWith(event.key, newName);
await waitAndUpdate(wrapper);
expect(wrapper.state().analyses[0].events[0]).toEqual({ ...event, name: newName });
});

function shallowRender(props: Partial<ProjectActivityApp['props']> = {}) {
return shallow<ProjectActivityApp>(
<ProjectActivityApp
component={mockComponent({ breadcrumbs: [mockComponent()] })}
location={mockLocation()}
router={mockRouter()}
{...props}
/>
);
}

+ 0
- 98
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppRenderer-test.tsx View File

@@ -1,98 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 ProjectActivityAppRenderer from '../ProjectActivityAppRenderer';

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 DEFAULT_PROPS = {
addCustomEvent: jest.fn().mockResolvedValue(undefined),
addVersion: jest.fn().mockResolvedValue(undefined),
analyses: ANALYSES,
analysesLoading: false,
branch: { isMain: true },
changeEvent: jest.fn().mockResolvedValue(undefined),
deleteAnalysis: jest.fn().mockResolvedValue(undefined),
deleteEvent: jest.fn().mockResolvedValue(undefined),
graphLoading: false,
initializing: false,
project: {
key: 'foo',
leakPeriodDate: '2017-05-16T13:50:02+0200',
qualifier: 'TRK',
},
metrics: [{ id: '1', key: 'code_smells', name: 'Code Smells', type: 'INT' }],
measuresHistory: [
{
metric: 'code_smells',
history: [
{ date: parseDate('Fri Mar 04 2016 10:40:12 GMT+0100 (CET)'), value: '1749' },
{ date: parseDate('Fri Mar 04 2016 18:40:16 GMT+0100 (CET)'), value: '2286' },
],
},
],
query: {
category: '',
customMetrics: [],
graph: DEFAULT_GRAPH,
project: 'org.sonarsource.sonarqube:sonarqube',
},
updateQuery: () => {},
};

it('should render correctly', () => {
expect(shallow(<ProjectActivityAppRenderer {...DEFAULT_PROPS} />)).toMatchSnapshot();
});

+ 0
- 85
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/Event-test.tsx.snap View File

@@ -1,85 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly: default 1`] = `
<div
className="project-activity-event"
>
<EventInner
event={
Object {
"category": "OTHER",
"description": "Lorem ipsum dolor sit amet",
"key": "E11",
"name": "Lorem ipsum",
"qualityGate": Object {
"failing": Array [
Object {
"branch": "master",
"key": "foo",
"name": "Foo",
},
Object {
"branch": "feature/bar",
"key": "bar",
"name": "Bar",
},
],
"status": "ERROR",
"stillFailing": true,
},
}
}
/>
</div>
`;

exports[`should render correctly: with admin options 1`] = `
<div
className="project-activity-event"
>
<EventInner
event={
Object {
"category": "OTHER",
"description": "Lorem ipsum dolor sit amet",
"key": "E11",
"name": "Lorem ipsum",
"qualityGate": Object {
"failing": Array [
Object {
"branch": "master",
"key": "foo",
"name": "Foo",
},
Object {
"branch": "feature/bar",
"key": "bar",
"name": "Bar",
},
],
"status": "ERROR",
"stillFailing": true,
},
}
}
/>
<span
className="nowrap"
>
<EditButton
aria-label="project_activity.events.tooltip.edit"
className="button-small"
data-test="project-activity__edit-event"
onClick={[Function]}
stopPropagation={true}
/>
<DeleteButton
aria-label="project_activity.events.tooltip.delete"
className="button-small"
data-test="project-activity__delete-event"
onClick={[Function]}
stopPropagation={true}
/>
</span>
</div>
`;

+ 0
- 98
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/Events-test.tsx.snap View File

@@ -1,98 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<div
className="big-spacer-top"
>
<Memo(Event)
analysisKey="foo"
event={
Object {
"category": "OTHER",
"description": "Lorem ipsum dolor sit amet",
"key": "E11",
"name": "Lorem ipsum",
"qualityGate": Object {
"failing": Array [
Object {
"branch": "master",
"key": "foo",
"name": "Foo",
},
Object {
"branch": "feature/bar",
"key": "bar",
"name": "Bar",
},
],
"status": "ERROR",
"stillFailing": true,
},
}
}
key="E11"
onChange={[MockFunction]}
onDelete={[MockFunction]}
/>
<Memo(Event)
analysisKey="foo"
event={
Object {
"category": "QUALITY_GATE",
"description": "Lorem ipsum dolor sit amet",
"key": "E11",
"name": "Lorem ipsum",
"qualityGate": Object {
"failing": Array [
Object {
"branch": "master",
"key": "foo",
"name": "Foo",
},
Object {
"branch": "feature/bar",
"key": "bar",
"name": "Bar",
},
],
"status": "ERROR",
"stillFailing": true,
},
}
}
key="E11"
onChange={[MockFunction]}
onDelete={[MockFunction]}
/>
<Memo(Event)
analysisKey="foo"
event={
Object {
"category": "VERSION",
"description": "Lorem ipsum dolor sit amet",
"key": "E11",
"name": "Lorem ipsum",
"qualityGate": Object {
"failing": Array [
Object {
"branch": "master",
"key": "foo",
"name": "Foo",
},
Object {
"branch": "feature/bar",
"key": "bar",
"name": "Bar",
},
],
"status": "ERROR",
"stillFailing": true,
},
}
}
key="E11"
onChange={[MockFunction]}
onDelete={[MockFunction]}
/>
</div>
`;

+ 10
- 10
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.tsx.snap View File

@@ -44,7 +44,7 @@ exports[`should correctly filter analyses by category 1`] = `
<ul
className="project-activity-analyses-list"
>
<Memo(ProjectActivityAnalysis)
<injectIntl(ProjectActivityAnalysis)
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analysis={
@@ -123,7 +123,7 @@ exports[`should correctly filter analyses by date range 1`] = `
<ul
className="project-activity-analyses-list"
>
<Memo(ProjectActivityAnalysis)
<injectIntl(ProjectActivityAnalysis)
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analysis={
@@ -202,7 +202,7 @@ exports[`should render correctly: application 1`] = `
<ul
className="project-activity-analyses-list"
>
<Memo(ProjectActivityAnalysis)
<injectIntl(ProjectActivityAnalysis)
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analysis={
@@ -230,7 +230,7 @@ exports[`should render correctly: application 1`] = `
selected={false}
updateSelectedDate={[Function]}
/>
<Memo(ProjectActivityAnalysis)
<injectIntl(ProjectActivityAnalysis)
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analysis={
@@ -290,7 +290,7 @@ exports[`should render correctly: application 1`] = `
<ul
className="project-activity-analyses-list"
>
<Memo(ProjectActivityAnalysis)
<injectIntl(ProjectActivityAnalysis)
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analysis={
@@ -339,7 +339,7 @@ exports[`should render correctly: application 1`] = `
<ul
className="project-activity-analyses-list"
>
<Memo(ProjectActivityAnalysis)
<injectIntl(ProjectActivityAnalysis)
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analysis={
@@ -418,7 +418,7 @@ exports[`should render correctly: default 1`] = `
<ul
className="project-activity-analyses-list"
>
<Memo(ProjectActivityAnalysis)
<injectIntl(ProjectActivityAnalysis)
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analysis={
@@ -446,7 +446,7 @@ exports[`should render correctly: default 1`] = `
selected={false}
updateSelectedDate={[Function]}
/>
<Memo(ProjectActivityAnalysis)
<injectIntl(ProjectActivityAnalysis)
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analysis={
@@ -506,7 +506,7 @@ exports[`should render correctly: default 1`] = `
<ul
className="project-activity-analyses-list"
>
<Memo(ProjectActivityAnalysis)
<injectIntl(ProjectActivityAnalysis)
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analysis={
@@ -555,7 +555,7 @@ exports[`should render correctly: default 1`] = `
<ul
className="project-activity-analyses-list"
>
<Memo(ProjectActivityAnalysis)
<injectIntl(ProjectActivityAnalysis)
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analysis={

+ 0
- 232
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysis-test.tsx.snap View File

@@ -1,232 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly: default 1`] = `
<li
className="project-activity-analysis bordered-top bordered-bottom"
onClick={[Function]}
>
<div
className="display-flex-center display-flex-space-between"
>
<div
className="project-activity-time"
>
<TimeFormatter
date={
Object {
"toISOString": [Function],
"valueOf": [Function],
}
}
long={false}
>
<Component />
</TimeFormatter>
</div>
</div>
</li>
`;

exports[`should render correctly: formatted time 1`] = `
<time
className="text-middle"
dateTime="2019-01-01T09:00:00.000Z"
>
formatted_time
</time>
`;

exports[`should render correctly: with admin options 1`] = `
<li
className="project-activity-analysis bordered-top bordered-bottom"
onClick={[Function]}
>
<div
className="display-flex-center display-flex-space-between"
>
<div
className="project-activity-time"
>
<TimeFormatter
date={
Object {
"toISOString": [Function],
"valueOf": [Function],
}
}
long={false}
>
<Component />
</TimeFormatter>
</div>
<ClickEventBoundary>
<div
className="project-activity-analysis-actions big-spacer-left"
>
<ActionsDropdown
overlayPlacement="bottom-right"
small={true}
toggleClassName="js-analysis-actions"
>
<ActionsDropdownItem
className="js-add-version"
onClick={[Function]}
>
project_activity.add_version
</ActionsDropdownItem>
<ActionsDropdownItem
className="js-add-event"
onClick={[Function]}
>
project_activity.add_custom_event
</ActionsDropdownItem>
<ActionsDropdownDivider />
<ActionsDropdownItem
className="js-delete-analysis"
destructive={true}
onClick={[Function]}
>
project_activity.delete_analysis
</ActionsDropdownItem>
</ActionsDropdown>
</div>
</ClickEventBoundary>
</div>
</li>
`;

exports[`should render correctly: with baseline marker 1`] = `
<li
className="project-activity-analysis bordered-top bordered-bottom"
onClick={[Function]}
>
<div
className="display-flex-center display-flex-space-between"
>
<div
className="project-activity-time"
>
<TimeFormatter
date={
Object {
"toISOString": [Function],
"valueOf": [Function],
}
}
long={false}
>
<Component />
</TimeFormatter>
</div>
</div>
<div
className="baseline-marker"
>
<div
className="wedge"
/>
<hr />
<div
className="label display-flex-center"
>
project_activity.new_code_period_start
<HelpTooltip
className="little-spacer-left"
overlay="project_activity.new_code_period_start.help"
placement="top"
/>
</div>
</div>
</li>
`;

exports[`should render correctly: with build string 1`] = `
<li
className="project-activity-analysis bordered-top bordered-bottom"
onClick={[Function]}
>
<div
className="display-flex-center display-flex-space-between"
>
<div
className="project-activity-time"
>
<TimeFormatter
date={
Object {
"toISOString": [Function],
"valueOf": [Function],
}
}
long={false}
>
<Component />
</TimeFormatter>
</div>
<div
className="flex-shrink small text-muted text-ellipsis"
>
project_activity.analysis_build_string_X.1.0.234
</div>
</div>
</li>
`;

exports[`should render correctly: with events 1`] = `
<li
className="project-activity-analysis bordered-top bordered-bottom"
onClick={[Function]}
>
<div
className="display-flex-center display-flex-space-between"
>
<div
className="project-activity-time"
>
<TimeFormatter
date={
Object {
"toISOString": [Function],
"valueOf": [Function],
}
}
long={false}
>
<Component />
</TimeFormatter>
</div>
</div>
<Memo(Events)
analysisKey="foo"
events={
Array [
Object {
"category": "QUALITY_GATE",
"description": "Lorem ipsum dolor sit amet",
"key": "E11",
"name": "Lorem ipsum",
"qualityGate": Object {
"failing": Array [
Object {
"branch": "master",
"key": "foo",
"name": "Foo",
},
Object {
"branch": "feature/bar",
"key": "bar",
"name": "Bar",
},
],
"status": "ERROR",
"stillFailing": true,
},
},
]
}
isFirst={false}
onChange={[MockFunction]}
onDelete={[MockFunction]}
/>
</li>
`;

+ 0
- 72
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.tsx.snap View File

@@ -1,72 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<ProjectActivityAppRenderer
addCustomEvent={[Function]}
addVersion={[Function]}
analyses={Array []}
analysesLoading={false}
changeEvent={[Function]}
deleteAnalysis={[Function]}
deleteEvent={[Function]}
graphLoading={true}
initializing={true}
measuresHistory={Array []}
metrics={Array []}
project={
Object {
"breadcrumbs": Array [
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
},
],
"key": "my-project",
"name": "MyProject",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
query={
Object {
"category": "",
"customMetrics": Array [],
"from": undefined,
"graph": "issues",
"project": "",
"selectedDate": undefined,
"to": undefined,
}
}
updateQuery={[Function]}
/>
`;

+ 0
- 185
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAppRenderer-test.tsx.snap View File

@@ -1,185 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<div
className="page page-limited"
id="project-activity"
>
<Suggestions
suggestions="project_activity"
/>
<Helmet
defer={false}
encodeSpecialCharacters={true}
prioritizeSeoTags={false}
title="project_activity.page"
/>
<A11ySkipTarget
anchor="activity_main"
/>
<ProjectActivityPageFilters
category=""
project={
Object {
"key": "foo",
"leakPeriodDate": "2017-05-16T13:50:02+0200",
"qualifier": "TRK",
}
}
updateQuery={[Function]}
/>
<div
className="layout-page project-activity-page"
>
<div
className="layout-page-side-outer project-activity-page-side-outer boxed-group"
>
<ProjectActivityAnalysesList
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analyses={
Array [
Object {
"date": 2016-10-27T14:33:50.000Z,
"events": Array [
Object {
"category": "VERSION",
"key": "E1",
"name": "6.5-SNAPSHOT",
},
],
"key": "A1",
},
Object {
"date": 2016-10-27T10:21:15.000Z,
"events": Array [],
"key": "A2",
},
Object {
"date": 2016-10-26T10:17:29.000Z,
"events": Array [
Object {
"category": "VERSION",
"key": "E2",
"name": "6.4",
},
Object {
"category": "OTHER",
"key": "E3",
"name": "foo",
},
],
"key": "A3",
},
]
}
analysesLoading={false}
canAdmin={false}
canDeleteAnalyses={false}
changeEvent={[MockFunction]}
deleteAnalysis={[MockFunction]}
deleteEvent={[MockFunction]}
initializing={false}
leakPeriodDate={2017-05-16T11:50:02.000Z}
project={
Object {
"key": "foo",
"leakPeriodDate": "2017-05-16T13:50:02+0200",
"qualifier": "TRK",
}
}
query={
Object {
"category": "",
"customMetrics": Array [],
"graph": "issues",
"project": "org.sonarsource.sonarqube:sonarqube",
}
}
updateQuery={[Function]}
/>
</div>
<div
className="project-activity-layout-page-main"
>
<ProjectActivityGraphs
analyses={
Array [
Object {
"date": 2016-10-27T14:33:50.000Z,
"events": Array [
Object {
"category": "VERSION",
"key": "E1",
"name": "6.5-SNAPSHOT",
},
],
"key": "A1",
},
Object {
"date": 2016-10-27T10:21:15.000Z,
"events": Array [],
"key": "A2",
},
Object {
"date": 2016-10-26T10:17:29.000Z,
"events": Array [
Object {
"category": "VERSION",
"key": "E2",
"name": "6.4",
},
Object {
"category": "OTHER",
"key": "E3",
"name": "foo",
},
],
"key": "A3",
},
]
}
leakPeriodDate={2017-05-16T11:50:02.000Z}
loading={false}
measuresHistory={
Array [
Object {
"history": Array [
Object {
"date": 2016-03-04T09:40:12.000Z,
"value": "1749",
},
Object {
"date": 2016-03-04T17:40:16.000Z,
"value": "2286",
},
],
"metric": "code_smells",
},
]
}
metrics={
Array [
Object {
"id": "1",
"key": "code_smells",
"name": "Code Smells",
"type": "INT",
},
]
}
project="foo"
query={
Object {
"category": "",
"customMetrics": Array [],
"graph": "issues",
"project": "org.sonarsource.sonarqube:sonarqube",
}
}
updateQuery={[Function]}
/>
</div>
</div>
</div>
`;

+ 2
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.tsx View File

@@ -55,8 +55,9 @@ export default class AddEventForm extends React.PureComponent<Props, State> {
size="small"
>
<div className="modal-field">
<label>{translate('name')}</label>
<label htmlFor="name">{translate('name')}</label>
<input
id="name"
autoFocus={true}
onChange={this.handleNameChange}
type="text"

+ 2
- 2
server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeEventForm.tsx View File

@@ -59,8 +59,8 @@ export default class ChangeEventForm extends React.PureComponent<Props, State> {
size="small"
>
<div className="modal-field">
<label>{translate('name')}</label>
<input autoFocus={true} onChange={this.changeInput} type="text" value={name} />
<label htmlFor="name">{translate('name')}</label>
<input id="name" autoFocus={true} onChange={this.changeInput} type="text" value={name} />
</div>
</ConfirmModal>
);

+ 4
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/AddEventForm-test.tsx.snap View File

@@ -12,11 +12,14 @@ exports[`should render correctly 1`] = `
<div
className="modal-field"
>
<label>
<label
htmlFor="name"
>
name
</label>
<input
autoFocus={true}
id="name"
onChange={[Function]}
type="text"
value=""

+ 4
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/ChangeEventForm-test.tsx.snap View File

@@ -12,11 +12,14 @@ exports[`should render correctly 1`] = `
<div
className="modal-field"
>
<label>
<label
htmlFor="name"
>
name
</label>
<input
autoFocus={true}
id="name"
onChange={[Function]}
type="text"
value="1.0"

+ 3
- 1
server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx View File

@@ -28,6 +28,7 @@ import { Button } from './buttons';
import Dropdown from './Dropdown';

export interface ActionsDropdownProps {
ariaLabel?: string;
className?: string;
children: React.ReactNode;
onOpen?: () => void;
@@ -37,7 +38,7 @@ export interface ActionsDropdownProps {
}

export default function ActionsDropdown(props: ActionsDropdownProps) {
const { children, className, overlayPlacement, small, toggleClassName } = props;
const { ariaLabel, children, className, overlayPlacement, small, toggleClassName } = props;
return (
<Dropdown
className={className}
@@ -46,6 +47,7 @@ export default function ActionsDropdown(props: ActionsDropdownProps) {
overlayPlacement={overlayPlacement}
>
<Button
aria-label={ariaLabel}
className={classNames('dropdown-toggle', toggleClassName, {
'button-small': small,
})}

+ 2
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -1526,6 +1526,8 @@ project_activity.analysis_build_string_X=Build string: {0}
project_activity.add_version=Create Version
project_activity.analyzed.TRK=Project Analyzed
project_activity.analyzed.APP=Application Analyzed
project_activity.analysis_X_actions=Show actions for analysis {0}
project_activity.show_analysis_X_on_graph=Show details on interactive graph for analysis {0}. Note: this data is also available as a table. Click on the button below the graph.
project_activity.remove_version=Remove Version
project_activity.remove_version.question=Are you sure you want to delete this version?
project_activity.change_version=Change Version

Loading…
Cancel
Save