aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>2022-02-22 09:29:32 +0100
committersonartech <sonartech@sonarsource.com>2022-02-25 20:02:54 +0000
commit50fa615acd453be581adda89e7fec784d3adcc34 (patch)
treed73debfa8a96bcaffaa0219f0816eb616443d8b9
parent1a4de4eb41d7c9a62476ff43e8360960da74dcf1 (diff)
downloadsonarqube-50fa615acd453be581adda89e7fec784d3adcc34.tar.gz
sonarqube-50fa615acd453be581adda89e7fec784d3adcc34.zip
SONAR-16007 Binding secondary location click in snippet and hotspot box
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueBox-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx30
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx41
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx16
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainer.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx16
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotCategory-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotList-test.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotListItem-test.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSnippetContainer-test.tsx19
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSnippetContainerRenderer-test.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerTabs-test.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotCategory-test.tsx.snap12
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotList-test.tsx.snap16
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap16
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainer-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap139
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/components/locations/LocationsList.tsx1
-rw-r--r--server/sonar-web/src/main/js/components/locations/__tests__/LocationsList-test.tsx1
36 files changed, 444 insertions, 25 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx
index 2411146ab9c..4cadb1885df 100644
--- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx
@@ -118,7 +118,6 @@ export default class ConciseIssueBox extends React.PureComponent<Props> {
isCrossFile={isCrossFile}
onLocationSelect={this.props.onLocationSelect}
scroll={this.props.scroll}
- selectedFlowIndex={selectedFlowIndex}
selectedLocationIndex={selectedLocationIndex}
/>
)}
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueBox-test.tsx.snap
index 5badb12ee28..a3239afee18 100644
--- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueBox-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueBox-test.tsx.snap
@@ -60,7 +60,6 @@ exports[`should render correctly 1`] = `
locations={Array []}
onLocationSelect={[MockFunction]}
scroll={[MockFunction]}
- selectedFlowIndex={0}
selectedLocationIndex={0}
uniqueKey="AVsae-CQS-9G3txfbFN2"
/>
@@ -226,7 +225,6 @@ exports[`should render correctly 2`] = `
}
onLocationSelect={[MockFunction]}
scroll={[MockFunction]}
- selectedFlowIndex={0}
selectedLocationIndex={0}
uniqueKey="AVsae-CQS-9G3txfbFN2"
/>
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
index f90fc16bf4b..975dd3c674f 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
@@ -28,6 +28,7 @@ import { withCurrentUser } from '../../components/hoc/withCurrentUser';
import { Router } from '../../components/hoc/withRouter';
import { getLeakValue } from '../../components/measure/utils';
import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../helpers/branch-like';
+import { scrollToElement } from '../../helpers/scrolling';
import { getStandards } from '../../helpers/security-standard';
import { isLoggedIn } from '../../helpers/users';
import { fetchBranchStatus } from '../../store/rootActions';
@@ -75,7 +76,7 @@ interface State {
loadingMeasure: boolean;
loadingMore: boolean;
selectedHotspot?: RawHotspot;
- selectedHotspotLocation?: number;
+ selectedHotspotLocationIndex?: number;
standards: Standards;
}
@@ -353,7 +354,8 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
this.handleChangeFilters({ status });
};
- handleHotspotClick = (selectedHotspot: RawHotspot) => this.setState({ selectedHotspot });
+ handleHotspotClick = (selectedHotspot: RawHotspot) =>
+ this.setState({ selectedHotspot, selectedHotspotLocationIndex: undefined });
handleHotspotUpdate = (hotspotKey: string) => {
const { hotspots, hotspotsPageIndex } = this.state;
@@ -419,6 +421,26 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
.catch(this.handleCallFailure);
};
+ handleLocationClick = (locationIndex: number) => {
+ const { selectedHotspotLocationIndex } = this.state;
+ if (locationIndex === selectedHotspotLocationIndex) {
+ this.setState({
+ selectedHotspotLocationIndex: undefined
+ });
+ } else {
+ this.setState({
+ selectedHotspotLocationIndex: locationIndex
+ });
+ }
+ };
+
+ handleScroll = (element: Element, bottomOffset = 100) => {
+ const scrollableElement = document.querySelector('.layout-page-side');
+ if (element && scrollableElement) {
+ scrollToElement(element, { topOffset: 150, bottomOffset, parent: scrollableElement });
+ }
+ };
+
render() {
const { branchLike, component } = this.props;
const {
@@ -434,6 +456,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
loadingMeasure,
loadingMore,
selectedHotspot,
+ selectedHotspotLocationIndex,
standards
} = this.state;
@@ -460,8 +483,11 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
onShowAllHotspots={this.handleShowAllHotspots}
onSwitchStatusFilter={this.handleChangeStatusFilter}
onUpdateHotspot={this.handleHotspotUpdate}
+ onLocationClick={this.handleLocationClick}
+ onScroll={this.handleScroll}
securityCategories={standards[SecurityStandard.SONARSOURCE]}
selectedHotspot={selectedHotspot}
+ selectedHotspotLocation={selectedHotspotLocationIndex}
standards={standards}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
index deaf72093cd..4de97e96492 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
@@ -56,10 +56,12 @@ export interface SecurityHotspotsAppRendererProps {
loadingMore: boolean;
onChangeFilters: (filters: Partial<HotspotFilters>) => void;
onHotspotClick: (hotspot: RawHotspot) => void;
+ onLocationClick: (index: number) => void;
onLoadMore: () => void;
onShowAllHotspots: () => void;
onSwitchStatusFilter: (option: HotspotStatusFilter) => void;
onUpdateHotspot: (hotspotKey: string) => Promise<void>;
+ onScroll: (element: Element) => void;
selectedHotspot?: RawHotspot;
selectedHotspotLocation?: number;
securityCategories: StandardSecurityCategories;
@@ -83,6 +85,7 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
loadingMore,
securityCategories,
selectedHotspot,
+ selectedHotspotLocation,
standards
} = props;
@@ -149,6 +152,9 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
loadingMore={loadingMore}
onHotspotClick={props.onHotspotClick}
onLoadMore={props.onLoadMore}
+ onLocationClick={props.onLocationClick}
+ onScroll={props.onScroll}
+ selectedHotspotLocation={selectedHotspotLocation}
selectedHotspot={selectedHotspot}
standards={standards}
/>
@@ -160,9 +166,12 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
loadingMore={loadingMore}
onHotspotClick={props.onHotspotClick}
onLoadMore={props.onLoadMore}
+ onLocationClick={props.onLocationClick}
securityCategories={securityCategories}
selectedHotspot={selectedHotspot}
+ selectedHotspotLocation={selectedHotspotLocation}
statusFilter={filters.status}
+ onScroll={props.onScroll}
/>
)}
</div>
@@ -177,6 +186,8 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
hotspotsReviewedMeasure={hotspotsReviewedMeasure}
onSwitchStatusFilter={props.onSwitchStatusFilter}
onUpdateHotspot={props.onUpdateHotspot}
+ onLocationClick={props.onLocationClick}
+ selectedHotspotLocation={selectedHotspotLocation}
/>
</div>
</div>
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx
index 5da406c5330..ed73077f227 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx
@@ -19,6 +19,7 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
+import { mockHtmlElement } from '../../../helpers/mocks/dom';
import { getMeasures } from '../../../api/measures';
import { getSecurityHotspotList, getSecurityHotspots } from '../../../api/security-hotspots';
import { mockBranch, mockPullRequest } from '../../../helpers/mocks/branch-like';
@@ -40,6 +41,7 @@ import {
} from '../../../types/security-hotspots';
import { SecurityHotspotsApp } from '../SecurityHotspotsApp';
import SecurityHotspotsAppRenderer from '../SecurityHotspotsAppRenderer';
+import { scrollToElement } from '../../../helpers/scrolling';
beforeEach(() => jest.clearAllMocks());
@@ -56,6 +58,10 @@ jest.mock('../../../helpers/security-standard', () => ({
getStandards: jest.fn().mockResolvedValue({ sonarsourceSecurity: { cat1: { title: 'cat 1' } } })
}));
+jest.mock('../../../helpers/scrolling', () => ({
+ scrollToElement: jest.fn()
+}));
+
const branch = mockBranch();
it('should render correctly', () => {
@@ -379,6 +385,41 @@ it('should handle leakPeriod filter change', async () => {
expect(getSecurityHotspots).toBeCalledWith(expect.objectContaining({ sinceLeakPeriod: true }));
});
+it('should handle hotspot click', () => {
+ const wrapper = shallowRender();
+ const selectedHotspot = mockRawHotspot();
+ wrapper.instance().handleHotspotClick(selectedHotspot);
+
+ expect(wrapper.instance().state.selectedHotspotLocationIndex).toBeUndefined();
+ expect(wrapper.instance().state.selectedHotspot).toEqual(selectedHotspot);
+});
+
+it('should handle secondary location click', () => {
+ const wrapper = shallowRender();
+ wrapper.instance().handleLocationClick(0);
+ expect(wrapper.instance().state.selectedHotspotLocationIndex).toEqual(0);
+
+ wrapper.instance().handleLocationClick(1);
+ expect(wrapper.instance().state.selectedHotspotLocationIndex).toEqual(1);
+
+ wrapper.instance().handleLocationClick(1);
+ expect(wrapper.instance().state.selectedHotspotLocationIndex).toBeUndefined();
+});
+
+it('should handle scroll properly', async () => {
+ const fakeElement = document.createElement('div');
+ jest.spyOn(document, 'querySelector').mockImplementationOnce(() => fakeElement);
+ const wrapper = shallowRender();
+ const element = mockHtmlElement();
+ wrapper.instance().handleScroll(element);
+ await waitAndUpdate(wrapper);
+ expect(scrollToElement).toBeCalledWith(element, {
+ bottomOffset: 100,
+ parent: fakeElement,
+ topOffset: 150
+ });
+});
+
describe('keyboard navigation', () => {
const hotspots = [
mockRawHotspot({ key: 'k1' }),
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx
index 577ed513560..6f1b477c1c1 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx
@@ -146,6 +146,8 @@ function shallowRender(props: Partial<SecurityHotspotsAppRendererProps> = {}) {
onShowAllHotspots={jest.fn()}
onSwitchStatusFilter={jest.fn()}
onUpdateHotspot={jest.fn()}
+ onLocationClick={jest.fn()}
+ onScroll={jest.fn()}
securityCategories={{}}
selectedHotspot={undefined}
standards={mockStandards()}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap
index 4775fd8a595..1762e1a557e 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap
@@ -48,6 +48,8 @@ exports[`should render correctly 1`] = `
onChangeFilters={[Function]}
onHotspotClick={[Function]}
onLoadMore={[Function]}
+ onLocationClick={[Function]}
+ onScroll={[Function]}
onShowAllHotspots={[Function]}
onSwitchStatusFilter={[Function]}
onUpdateHotspot={[Function]}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap
index ce093d196af..bd4730c4110 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap
@@ -117,6 +117,8 @@ exports[`should render correctly when filtered by category or cwe: category 1`]
loadingMore={false}
onHotspotClick={[MockFunction]}
onLoadMore={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
selectedHotspot={
Object {
"author": "Developer 1",
@@ -236,6 +238,8 @@ exports[`should render correctly when filtered by category or cwe: cwe 1`] = `
loadingMore={false}
onHotspotClick={[MockFunction]}
onLoadMore={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
selectedHotspot={
Object {
"author": "Developer 1",
@@ -415,6 +419,8 @@ exports[`should render correctly with hotspots 2`] = `
loadingMore={false}
onHotspotClick={[MockFunction]}
onLoadMore={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
securityCategories={Object {}}
selectedHotspot={
Object {
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx
index fa176838b89..5365ef9d372 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx
@@ -30,6 +30,8 @@ export interface HotspotCategoryProps {
hotspots: RawHotspot[];
onHotspotClick: (hotspot: RawHotspot) => void;
onToggleExpand?: (categoryKey: string, value: boolean) => void;
+ onLocationClick: (index: number) => void;
+ onScroll: (element: Element) => void;
selectedHotspot: RawHotspot;
selectedHotspotLocation?: number;
title: string;
@@ -37,7 +39,15 @@ export interface HotspotCategoryProps {
}
export default function HotspotCategory(props: HotspotCategoryProps) {
- const { categoryKey, expanded, hotspots, selectedHotspot, title, isLastAndIncomplete } = props;
+ const {
+ categoryKey,
+ expanded,
+ hotspots,
+ selectedHotspot,
+ title,
+ isLastAndIncomplete,
+ selectedHotspotLocation
+ } = props;
if (hotspots.length < 1) {
return null;
@@ -80,6 +90,9 @@ export default function HotspotCategory(props: HotspotCategoryProps) {
<HotspotListItem
hotspot={h}
onClick={props.onHotspotClick}
+ onLocationClick={props.onLocationClick}
+ onScroll={props.onScroll}
+ selectedHotspotLocation={selectedHotspotLocation}
selected={h.key === selectedHotspot.key}
/>
</li>
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx
index afa5194f180..610aa769544 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx
@@ -37,6 +37,8 @@ interface Props {
loadingMore: boolean;
onHotspotClick: (hotspot: RawHotspot) => void;
onLoadMore: () => void;
+ onLocationClick: (index: number) => void;
+ onScroll: (element: Element) => void;
securityCategories: StandardSecurityCategories;
selectedHotspot: RawHotspot;
selectedHotspotLocation?: number;
@@ -84,6 +86,13 @@ export default class HotspotList extends React.Component<Props, State> {
);
this.setState({ groupedHotspots });
}
+
+ if (this.props.selectedHotspotLocation !== prevProps.selectedHotspotLocation) {
+ const { selectedHotspot } = this.props;
+ this.setState({
+ expandedCategories: { [selectedHotspot.securityCategory]: true }
+ });
+ }
}
componentWillUnmount() {
@@ -112,6 +121,7 @@ export default class HotspotList extends React.Component<Props, State> {
isStaticListOfHotspots,
loadingMore,
selectedHotspot,
+ selectedHotspotLocation,
statusFilter
} = this.props;
@@ -150,7 +160,10 @@ export default class HotspotList extends React.Component<Props, State> {
hotspots={cat.hotspots}
onHotspotClick={this.props.onHotspotClick}
onToggleExpand={this.handleToggleCategory}
+ onLocationClick={this.props.onLocationClick}
+ onScroll={this.props.onScroll}
selectedHotspot={selectedHotspot}
+ selectedHotspotLocation={selectedHotspotLocation}
title={cat.title}
isLastAndIncomplete={
isLastRiskGroup && isLastCategory && hotspots.length < hotspotsTotal
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
index 7865080337c..e2e34da722e 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
@@ -22,17 +22,20 @@ import * as React from 'react';
import QualifierIcon from '../../../components/icons/QualifierIcon';
import { ComponentQualifier } from '../../../types/component';
import { getFilePath, getLocations } from '../utils';
-import LocationsList from '../../../components/locations/LocationsList';
import { RawHotspot } from '../../../types/security-hotspots';
+import LocationsList from '../../../components/locations/LocationsList';
export interface HotspotListItemProps {
hotspot: RawHotspot;
onClick: (hotspot: RawHotspot) => void;
+ onLocationClick: (index: number) => void;
+ onScroll: (element: Element) => void;
selected: boolean;
+ selectedHotspotLocation?: number;
}
export default function HotspotListItem(props: HotspotListItemProps) {
- const { hotspot, selected } = props;
+ const { hotspot, selected, selectedHotspotLocation } = props;
const locations = getLocations(hotspot.flows, undefined);
const path = getFilePath(hotspot.component, hotspot.project);
@@ -56,12 +59,9 @@ export default function HotspotListItem(props: HotspotListItemProps) {
locations={locations}
isCrossFile={false} // Currently we are not supporting cross file for security hotspot
uniqueKey={hotspot.key}
- onLocationSelect={() => {
- /* noop */
- }}
- scroll={() => {
- /* noop */
- }}
+ onLocationSelect={props.onLocationClick}
+ selectedLocationIndex={selectedHotspotLocation}
+ scroll={props.onScroll}
/>
)}
</div>
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx
index 11a96cbccc4..ca7c81cd7a3 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx
@@ -42,8 +42,11 @@ export interface HotspotSimpleListProps {
hotspotsTotal: number;
loadingMore: boolean;
onHotspotClick: (hotspot: RawHotspot) => void;
+ onLocationClick: (index: number) => void;
+ onScroll: (element: Element) => void;
onLoadMore: () => void;
selectedHotspot: RawHotspot;
+ selectedHotspotLocation?: number;
standards: Standards;
}
@@ -65,6 +68,7 @@ export default class HotspotSimpleList extends React.Component<HotspotSimpleList
hotspotsTotal,
loadingMore,
selectedHotspot,
+ selectedHotspotLocation,
standards
} = this.props;
@@ -110,7 +114,10 @@ export default class HotspotSimpleList extends React.Component<HotspotSimpleList
<HotspotListItem
hotspot={h}
onClick={this.props.onHotspotClick}
+ onLocationClick={this.props.onLocationClick}
+ onScroll={this.props.onScroll}
selected={h.key === selectedHotspot.key}
+ selectedHotspotLocation={selectedHotspotLocation}
/>
</li>
))}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainer.tsx
index 690f1047d1c..f858ee711e4 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainer.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainer.tsx
@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { scrollToElement } from '../../../helpers/scrolling';
import { getSources } from '../../../api/components';
import { locationsByLine } from '../../../components/SourceViewer/helpers/indexing';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
@@ -32,6 +33,8 @@ interface Props {
component: Component;
hotspot: Hotspot;
onCommentButtonClick: () => void;
+ onLocationSelect: (index: number) => void;
+ selectedHotspotLocation?: number;
}
interface State {
@@ -195,8 +198,12 @@ export default class HotspotSnippetContainer extends React.Component<Props, Stat
this.setState({ highlightedSymbols });
};
+ handleScroll = (element: Element, offset = window.innerHeight / 2, smooth = true) => {
+ scrollToElement(element, { topOffset: offset - 100, bottomOffset: offset, smooth });
+ };
+
render() {
- const { branchLike, component, hotspot } = this.props;
+ const { branchLike, component, hotspot, selectedHotspotLocation } = this.props;
const { highlightedSymbols, lastLine, loading, sourceLines, secondaryLocations } = this.state;
const locations = locationsByLine([hotspot]);
@@ -214,9 +221,12 @@ export default class HotspotSnippetContainer extends React.Component<Props, Stat
onCommentButtonClick={this.props.onCommentButtonClick}
onExpandBlock={this.handleExpansion}
onSymbolClick={this.handleSymbolClick}
+ onLocationSelect={this.props.onLocationSelect}
+ onScroll={this.handleScroll}
sourceLines={sourceLines}
sourceViewerFile={sourceViewerFile}
secondaryLocations={secondaryLocations}
+ selectedHotspotLocation={selectedHotspotLocation}
/>
);
}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx
index 80998643a2f..459c1610fe1 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx
@@ -40,9 +40,12 @@ export interface HotspotSnippetContainerRendererProps {
hotspot: Hotspot;
loading: boolean;
locations: { [line: number]: LinearIssueLocation[] };
+ selectedHotspotLocation?: number;
onCommentButtonClick: () => void;
onExpandBlock: (direction: ExpandDirection) => Promise<void>;
onSymbolClick: (symbols: string[]) => void;
+ onLocationSelect: (index: number) => void;
+ onScroll: (element: Element) => void;
sourceLines: SourceLine[];
sourceViewerFile: SourceViewerFile;
secondaryLocations: FlowLocation[];
@@ -63,7 +66,8 @@ export default function HotspotSnippetContainerRenderer(
sourceLines,
sourceViewerFile,
secondaryLocations,
- component
+ component,
+ selectedHotspotLocation
} = props;
const renderHotspotBoxInLine = (lineNumber: number) =>
@@ -73,6 +77,11 @@ export default function HotspotSnippetContainerRenderer(
undefined
);
+ const highlightedLocation =
+ selectedHotspotLocation !== undefined
+ ? { index: selectedHotspotLocation, text: hotspot.message }
+ : undefined;
+
return (
<>
{!loading && sourceLines.length === 0 && (
@@ -91,7 +100,7 @@ export default function HotspotSnippetContainerRenderer(
handleCloseIssues={noop}
handleOpenIssues={noop}
handleSymbolClick={props.onSymbolClick}
- highlightedLocationMessage={undefined}
+ highlightedLocationMessage={highlightedLocation}
highlightedSymbols={highlightedSymbols}
index={0}
issue={hotspot}
@@ -101,11 +110,12 @@ export default function HotspotSnippetContainerRenderer(
locationsByLine={primaryLocations}
onIssueChange={noop}
onIssuePopupToggle={noop}
- onLocationSelect={noop}
+ onLocationSelect={props.onLocationSelect}
openIssuesByLine={{}}
renderAdditionalChildInLine={renderHotspotBoxInLine}
renderDuplicationPopup={noop}
snippet={sourceLines}
+ scroll={props.onScroll}
/>
)}
</DeferredSpinner>
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx
index 860c97f2bbb..14c648765fd 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx
@@ -35,6 +35,8 @@ interface Props {
hotspotsReviewedMeasure?: string;
onSwitchStatusFilter: (option: HotspotStatusFilter) => void;
onUpdateHotspot: (hotspotKey: string) => Promise<void>;
+ onLocationClick: (index: number) => void;
+ selectedHotspotLocation?: number;
}
interface State {
@@ -116,7 +118,7 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
};
render() {
- const { component, hotspotsReviewedMeasure } = this.props;
+ const { component, hotspotsReviewedMeasure, selectedHotspotLocation } = this.props;
const { hotspot, lastStatusChangedTo, loading, showStatusUpdateSuccessModal } = this.state;
return (
@@ -131,7 +133,9 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
onSwitchFilterToStatusOfUpdatedHotspot={this.handleSwitchFilterToStatusOfUpdatedHotspot}
onShowCommentForm={this.handleScrollToCommentForm}
onUpdateHotspot={this.handleHotspotUpdate}
+ onLocationClick={this.props.onLocationClick}
showStatusUpdateSuccessModal={showStatusUpdateSuccessModal}
+ selectedHotspotLocation={selectedHotspotLocation}
/>
);
}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx
index a46ab94816b..0fc2f8540b0 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx
@@ -42,7 +42,9 @@ export interface HotspotViewerRendererProps {
onUpdateHotspot: (statusUpdate?: boolean, statusOption?: HotspotStatusOption) => Promise<void>;
onShowCommentForm: () => void;
onSwitchFilterToStatusOfUpdatedHotspot: () => void;
+ onLocationClick: (index: number) => void;
showStatusUpdateSuccessModal: boolean;
+ selectedHotspotLocation?: number;
}
export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
@@ -54,7 +56,8 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
loading,
lastStatusChangedTo,
showStatusUpdateSuccessModal,
- commentTextRef
+ commentTextRef,
+ selectedHotspotLocation
} = props;
return (
@@ -78,9 +81,12 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
component={component}
hotspot={hotspot}
onCommentButtonClick={props.onShowCommentForm}
+ onLocationSelect={props.onLocationClick}
+ selectedHotspotLocation={selectedHotspotLocation}
/>
}
hotspot={hotspot}
+ selectedHotspotLocation={selectedHotspotLocation}
/>
<HotspotReviewHistoryAndComments
commentTextRef={commentTextRef}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx
index 3b5cea5cc0a..bb4faab83bf 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx
@@ -26,6 +26,7 @@ import { Hotspot } from '../../../types/security-hotspots';
interface Props {
codeTabContent: React.ReactNode;
hotspot: Hotspot;
+ selectedHotspotLocation?: number;
}
interface State {
@@ -63,6 +64,14 @@ export default class HotspotViewerTabs extends React.PureComponent<Props, State>
currentTab: tabs[0],
tabs
});
+ } else if (
+ this.props.selectedHotspotLocation !== undefined &&
+ this.props.selectedHotspotLocation !== prevProps.selectedHotspotLocation
+ ) {
+ const { tabs } = this.state;
+ this.setState({
+ currentTab: tabs[0]
+ });
}
}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotCategory-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotCategory-test.tsx
index cc1f787a8d4..55f7f5c6dc0 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotCategory-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotCategory-test.tsx
@@ -75,6 +75,8 @@ function shallowRender(props: Partial<HotspotCategoryProps> = {}) {
selectedHotspot={mockRawHotspot()}
title="Class Injection"
isLastAndIncomplete={false}
+ onLocationClick={jest.fn()}
+ onScroll={jest.fn()}
{...props}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotList-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotList-test.tsx
index 4d21f225d7a..2d9aa8ea056 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotList-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotList-test.tsx
@@ -98,6 +98,19 @@ it('should update grouped hotspots when the list changes', () => {
expect(wrapper.state().groupedHotspots[0].categories[0].hotspots).toHaveLength(1);
});
+it('should expand the categories for which the location is selected', () => {
+ const wrapper = shallowRender({ hotspots, selectedHotspot: hotspots[0] });
+
+ expect(wrapper.state().expandedCategories).toEqual({ cat2: true });
+
+ wrapper.instance().handleToggleCategory('cat2', false);
+ expect(wrapper.state().expandedCategories).toEqual({ cat2: false });
+
+ wrapper.setProps({ selectedHotspotLocation: 1 });
+
+ expect(wrapper.state().expandedCategories).toEqual({ cat2: true });
+});
+
function shallowRender(props: Partial<HotspotList['props']> = {}) {
return shallow<HotspotList>(
<HotspotList
@@ -107,6 +120,8 @@ function shallowRender(props: Partial<HotspotList['props']> = {}) {
loadingMore={false}
onHotspotClick={jest.fn()}
onLoadMore={jest.fn()}
+ onLocationClick={jest.fn()}
+ onScroll={jest.fn()}
securityCategories={{}}
selectedHotspot={mockRawHotspot({ key: 'h2' })}
statusFilter={HotspotStatusFilter.TO_REVIEW}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotListItem-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotListItem-test.tsx
index e5090040fac..09187d1f218 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotListItem-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotListItem-test.tsx
@@ -39,6 +39,13 @@ it('should handle click', () => {
function shallowRender(props: Partial<HotspotListItemProps> = {}) {
return shallow(
- <HotspotListItem hotspot={mockRawHotspot()} onClick={jest.fn()} selected={false} {...props} />
+ <HotspotListItem
+ hotspot={mockRawHotspot()}
+ onClick={jest.fn()}
+ onScroll={jest.fn()}
+ onLocationClick={jest.fn}
+ selected={false}
+ {...props}
+ />
);
}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx
index 525156472ae..a74de8f1277 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx
@@ -64,6 +64,8 @@ function shallowRender(props: Partial<HotspotSimpleListProps> = {}) {
loadingMore={false}
onHotspotClick={jest.fn()}
onLoadMore={jest.fn()}
+ onLocationClick={jest.fn()}
+ onScroll={jest.fn()}
selectedHotspot={hotspots[0]}
standards={{
cwe: { 327: { title: 'Use of a Broken or Risky Cryptographic Algorithm' } },
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSnippetContainer-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSnippetContainer-test.tsx
index 98edc5eddf2..8a0fb0e734d 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSnippetContainer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSnippetContainer-test.tsx
@@ -20,6 +20,8 @@
import { shallow } from 'enzyme';
import { range } from 'lodash';
import * as React from 'react';
+import { mockHtmlElement } from '../../../../helpers/mocks/dom';
+import { scrollToElement } from '../../../../helpers/scrolling';
import { getSources } from '../../../../api/components';
import { mockBranch } from '../../../../helpers/mocks/branch-like';
import { mockComponent } from '../../../../helpers/mocks/component';
@@ -34,6 +36,10 @@ jest.mock('../../../../api/components', () => ({
getSources: jest.fn().mockResolvedValue([])
}));
+jest.mock('../../../../helpers/scrolling', () => ({
+ scrollToElement: jest.fn()
+}));
+
beforeEach(() => jest.clearAllMocks());
const branch = mockBranch();
@@ -214,6 +220,18 @@ it('should handle symbol click', () => {
expect(wrapper.state().highlightedSymbols).toBe(symbols);
});
+it('should handle scroll properly', async () => {
+ const wrapper = shallowRender();
+ const element = mockHtmlElement();
+ wrapper.instance().handleScroll(element, 200);
+ await waitAndUpdate(wrapper);
+ expect(scrollToElement).toBeCalledWith(element, {
+ bottomOffset: 200,
+ smooth: true,
+ topOffset: 100
+ });
+});
+
function shallowRender(props?: Partial<HotspotSnippetContainer['props']>) {
return shallow<HotspotSnippetContainer>(
<HotspotSnippetContainer
@@ -221,6 +239,7 @@ function shallowRender(props?: Partial<HotspotSnippetContainer['props']>) {
component={mockComponent()}
hotspot={mockHotspot()}
onCommentButtonClick={jest.fn()}
+ onLocationSelect={jest.fn()}
{...props}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSnippetContainerRenderer-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSnippetContainerRenderer-test.tsx
index 2b36ee19b29..5afde638a1a 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSnippetContainerRenderer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSnippetContainerRenderer-test.tsx
@@ -45,6 +45,13 @@ it('should render a HotspotPrimaryLocationBox', () => {
expect(renderAdditionalChildInLine!(42)).not.toBeUndefined();
});
+it('should render correctly when secondary location is selected', () => {
+ const wrapper = shallowRender({
+ selectedHotspotLocation: 1
+ });
+ expect(wrapper).toMatchSnapshot('with selected hotspot location');
+});
+
function shallowRender(props?: Partial<HotspotSnippetContainerRendererProps>) {
return shallow(
<HotspotSnippetContainerRenderer
@@ -60,6 +67,8 @@ function shallowRender(props?: Partial<HotspotSnippetContainerRendererProps>) {
sourceLines={[]}
sourceViewerFile={mockSourceViewerFile()}
component={mockComponent()}
+ onLocationSelect={jest.fn()}
+ onScroll={jest.fn()}
{...props}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx
index 31a07d26364..264e81a5812 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx
@@ -143,6 +143,7 @@ function shallowRender(props?: Partial<HotspotViewer['props']>) {
hotspotKey={hotspotKey}
onSwitchStatusFilter={jest.fn()}
onUpdateHotspot={jest.fn()}
+ onLocationClick={jest.fn()}
{...props}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx
index d99c07fc360..391389ef308 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx
@@ -64,6 +64,7 @@ function shallowRender(props?: Partial<HotspotViewerRendererProps>) {
onShowCommentForm={jest.fn()}
onUpdateHotspot={jest.fn()}
showStatusUpdateSuccessModal={false}
+ onLocationClick={jest.fn()}
{...props}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerTabs-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerTabs-test.tsx
index 941b24e0364..2ba96c577f8 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerTabs-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerTabs-test.tsx
@@ -96,6 +96,23 @@ it('should select first tab on hotspot update', () => {
expect(wrapper.state().currentTab.key).toBe(TabKeys.Code);
});
+it('should select first tab when hotspot location is selected and is not undefined', () => {
+ const wrapper = shallowRender();
+ const onSelect: (tab: TabKeys) => void = wrapper.find(BoxedTabs).prop('onSelect');
+
+ onSelect(TabKeys.VulnerabilityDescription);
+ expect(wrapper.state().currentTab.key).toBe(TabKeys.VulnerabilityDescription);
+
+ wrapper.setProps({ selectedHotspotLocation: 1 });
+ expect(wrapper.state().currentTab.key).toBe(TabKeys.Code);
+
+ onSelect(TabKeys.VulnerabilityDescription);
+ expect(wrapper.state().currentTab.key).toBe(TabKeys.VulnerabilityDescription);
+
+ wrapper.setProps({ selectedHotspotLocation: undefined });
+ expect(wrapper.state().currentTab.key).toBe(TabKeys.VulnerabilityDescription);
+});
+
function shallowRender(props?: Partial<HotspotViewerTabs['props']>) {
return shallow<HotspotViewerTabs>(
<HotspotViewerTabs
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotCategory-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotCategory-test.tsx.snap
index cd2664c3ad4..766b906c75a 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotCategory-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotCategory-test.tsx.snap
@@ -49,6 +49,8 @@ exports[`should render correctly with hotspots 1`] = `
}
}
onClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
selected={false}
/>
</li>
@@ -75,6 +77,8 @@ exports[`should render correctly with hotspots 1`] = `
}
}
onClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
selected={false}
/>
</li>
@@ -159,6 +163,8 @@ exports[`should render correctly with hotspots: contains selected 1`] = `
}
}
onClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
selected={true}
/>
</li>
@@ -185,6 +191,8 @@ exports[`should render correctly with hotspots: contains selected 1`] = `
}
}
onClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
selected={false}
/>
</li>
@@ -242,6 +250,8 @@ exports[`should render correctly with hotspots: lastAndIncomplete 1`] = `
}
}
onClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
selected={false}
/>
</li>
@@ -268,6 +278,8 @@ exports[`should render correctly with hotspots: lastAndIncomplete 1`] = `
}
}
onClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
selected={false}
/>
</li>
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotList-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotList-test.tsx.snap
index f506600e1ad..9fd195681f6 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotList-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotList-test.tsx.snap
@@ -131,6 +131,8 @@ exports[`should render correctly with hotspots: no pagination 1`] = `
}
isLastAndIncomplete={false}
onHotspotClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
onToggleExpand={[Function]}
selectedHotspot={
Object {
@@ -179,6 +181,8 @@ exports[`should render correctly with hotspots: no pagination 1`] = `
}
isLastAndIncomplete={false}
onHotspotClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
onToggleExpand={[Function]}
selectedHotspot={
Object {
@@ -262,6 +266,8 @@ exports[`should render correctly with hotspots: no pagination 1`] = `
}
isLastAndIncomplete={false}
onHotspotClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
onToggleExpand={[Function]}
selectedHotspot={
Object {
@@ -310,6 +316,8 @@ exports[`should render correctly with hotspots: no pagination 1`] = `
}
isLastAndIncomplete={false}
onHotspotClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
onToggleExpand={[Function]}
selectedHotspot={
Object {
@@ -403,6 +411,8 @@ exports[`should render correctly with hotspots: pagination 1`] = `
}
isLastAndIncomplete={false}
onHotspotClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
onToggleExpand={[Function]}
selectedHotspot={
Object {
@@ -451,6 +461,8 @@ exports[`should render correctly with hotspots: pagination 1`] = `
}
isLastAndIncomplete={false}
onHotspotClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
onToggleExpand={[Function]}
selectedHotspot={
Object {
@@ -534,6 +546,8 @@ exports[`should render correctly with hotspots: pagination 1`] = `
}
isLastAndIncomplete={false}
onHotspotClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
onToggleExpand={[Function]}
selectedHotspot={
Object {
@@ -582,6 +596,8 @@ exports[`should render correctly with hotspots: pagination 1`] = `
}
isLastAndIncomplete={true}
onHotspotClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
onToggleExpand={[Function]}
selectedHotspot={
Object {
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap
index 0d0f163550e..41a9dbae849 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap
@@ -61,7 +61,7 @@ exports[`should render correctly 2`] = `
isCrossFile={false}
locations={Array []}
onLocationSelect={[Function]}
- scroll={[Function]}
+ scroll={[MockFunction]}
uniqueKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123"
/>
</div>
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap
index 950f99e0f46..b7e685bd3f0 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap
@@ -53,6 +53,8 @@ exports[`should render correctly: filter by both 1`] = `
}
}
onClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
selected={true}
/>
</li>
@@ -79,6 +81,8 @@ exports[`should render correctly: filter by both 1`] = `
}
}
onClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
selected={false}
/>
</li>
@@ -145,6 +149,8 @@ exports[`should render correctly: filter by category 1`] = `
}
}
onClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
selected={true}
/>
</li>
@@ -171,6 +177,8 @@ exports[`should render correctly: filter by category 1`] = `
}
}
onClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
selected={false}
/>
</li>
@@ -237,6 +245,8 @@ exports[`should render correctly: filter by cwe 1`] = `
}
}
onClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
selected={true}
/>
</li>
@@ -263,6 +273,8 @@ exports[`should render correctly: filter by cwe 1`] = `
}
}
onClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
selected={false}
/>
</li>
@@ -339,6 +351,8 @@ exports[`should render correctly: filter by file 1`] = `
}
}
onClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
selected={true}
/>
</li>
@@ -365,6 +379,8 @@ exports[`should render correctly: filter by file 1`] = `
}
}
onClick={[MockFunction]}
+ onLocationClick={[MockFunction]}
+ onScroll={[MockFunction]}
selected={false}
/>
</li>
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainer-test.tsx.snap
index eae97ea5a08..bf4192ecb35 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainer-test.tsx.snap
@@ -133,6 +133,8 @@ exports[`should render correctly 1`] = `
}
onCommentButtonClick={[MockFunction]}
onExpandBlock={[Function]}
+ onLocationSelect={[MockFunction]}
+ onScroll={[Function]}
onSymbolClick={[Function]}
secondaryLocations={
Array [
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap
index 5a9f5912a14..71d62cac55b 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap
@@ -136,6 +136,142 @@ exports[`should render correctly 1`] = `
</Fragment>
`;
+exports[`should render correctly when secondary location is selected: with selected hotspot location 1`] = `
+<Fragment>
+ <p
+ className="spacer-bottom"
+ >
+ hotspots.no_associated_lines
+ </p>
+ <Connect(withCurrentUser(HotspotSnippetHeader))
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "excludedFromPurge": true,
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ component={
+ 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 [],
+ }
+ }
+ hotspot={
+ Object {
+ "assignee": "assignee",
+ "assigneeUser": Object {
+ "active": true,
+ "local": true,
+ "login": "assignee",
+ "name": "John Doe",
+ },
+ "author": "author",
+ "authorUser": Object {
+ "active": true,
+ "local": true,
+ "login": "author",
+ "name": "John Doe",
+ },
+ "canChangeStatus": true,
+ "changelog": Array [],
+ "comment": Array [],
+ "component": Object {
+ "key": "hotspot-component",
+ "longName": "Hotspot component long name",
+ "name": "Hotspot Component",
+ "path": "path/to/component",
+ "qualifier": "FIL",
+ },
+ "creationDate": "2013-05-13T17:55:41+0200",
+ "flows": Array [
+ Object {
+ "locations": Array [
+ Object {
+ "component": "main.js",
+ "textRange": Object {
+ "endLine": 2,
+ "endOffset": 2,
+ "startLine": 1,
+ "startOffset": 1,
+ },
+ },
+ ],
+ },
+ ],
+ "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
+ "line": 142,
+ "message": "'3' is a magic number.",
+ "project": Object {
+ "key": "hotspot-component",
+ "longName": "Hotspot component long name",
+ "name": "Hotspot Component",
+ "path": "path/to/component",
+ "qualifier": "TRK",
+ },
+ "resolution": "FIXED",
+ "rule": Object {
+ "fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
+ "key": "squid:S2077",
+ "name": "That rule",
+ "riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
+ "securityCategory": "sql-injection",
+ "vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
+ "vulnerabilityProbability": "HIGH",
+ },
+ "status": "REVIEWED",
+ "textRange": Object {
+ "endLine": 142,
+ "endOffset": 83,
+ "startLine": 142,
+ "startOffset": 26,
+ },
+ "updateDate": "2013-05-13T17:55:42+0200",
+ "users": Array [
+ Object {
+ "active": true,
+ "local": true,
+ "login": "assignee",
+ "name": "John Doe",
+ },
+ Object {
+ "active": true,
+ "local": true,
+ "login": "author",
+ "name": "John Doe",
+ },
+ ],
+ }
+ }
+ />
+ <div
+ className="bordered"
+ >
+ <DeferredSpinner
+ className="big-spacer"
+ loading={false}
+ />
+ </div>
+</Fragment>
+`;
+
exports[`should render correctly: with sourcelines 1`] = `
<Fragment>
<Connect(withCurrentUser(HotspotSnippetHeader))
@@ -380,10 +516,11 @@ exports[`should render correctly: with sourcelines 1`] = `
locationsByLine={Object {}}
onIssueChange={[Function]}
onIssuePopupToggle={[Function]}
- onLocationSelect={[Function]}
+ onLocationSelect={[MockFunction]}
openIssuesByLine={Object {}}
renderAdditionalChildInLine={[Function]}
renderDuplicationPopup={[Function]}
+ scroll={[MockFunction]}
snippet={
Array [
Object {
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap
index 0b61ad710a4..1e5149058ec 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap
@@ -31,6 +31,7 @@ exports[`should render correctly 1`] = `
}
loading={true}
onCloseStatusUpdateSuccessModal={[Function]}
+ onLocationClick={[MockFunction]}
onShowCommentForm={[Function]}
onSwitchFilterToStatusOfUpdatedHotspot={[Function]}
onUpdateHotspot={[Function]}
@@ -74,6 +75,7 @@ exports[`should render correctly 2`] = `
}
loading={false}
onCloseStatusUpdateSuccessModal={[Function]}
+ onLocationClick={[MockFunction]}
onShowCommentForm={[Function]}
onSwitchFilterToStatusOfUpdatedHotspot={[Function]}
onUpdateHotspot={[Function]}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap
index 7bf90466852..70121ce2eb5 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap
@@ -209,6 +209,7 @@ exports[`should render correctly: anonymous user 1`] = `
}
}
onCommentButtonClick={[MockFunction]}
+ onLocationSelect={[MockFunction]}
/>
}
hotspot={
@@ -610,6 +611,7 @@ exports[`should render correctly: assignee without name 1`] = `
}
}
onCommentButtonClick={[MockFunction]}
+ onLocationSelect={[MockFunction]}
/>
}
hotspot={
@@ -1011,6 +1013,7 @@ exports[`should render correctly: default 1`] = `
}
}
onCommentButtonClick={[MockFunction]}
+ onLocationSelect={[MockFunction]}
/>
}
hotspot={
@@ -1412,6 +1415,7 @@ exports[`should render correctly: deleted assignee 1`] = `
}
}
onCommentButtonClick={[MockFunction]}
+ onLocationSelect={[MockFunction]}
/>
}
hotspot={
@@ -1826,6 +1830,7 @@ exports[`should render correctly: show success modal 1`] = `
}
}
onCommentButtonClick={[MockFunction]}
+ onLocationSelect={[MockFunction]}
/>
}
hotspot={
@@ -2227,6 +2232,7 @@ exports[`should render correctly: unassigned 1`] = `
}
}
onCommentButtonClick={[MockFunction]}
+ onLocationSelect={[MockFunction]}
/>
}
hotspot={
diff --git a/server/sonar-web/src/main/js/components/locations/LocationsList.tsx b/server/sonar-web/src/main/js/components/locations/LocationsList.tsx
index 6a38b047a05..19e52f004c0 100644
--- a/server/sonar-web/src/main/js/components/locations/LocationsList.tsx
+++ b/server/sonar-web/src/main/js/components/locations/LocationsList.tsx
@@ -28,7 +28,6 @@ interface Props {
locations: FlowLocation[];
onLocationSelect: (index: number) => void;
scroll: (element: Element) => void;
- selectedFlowIndex?: number;
selectedLocationIndex?: number;
}
diff --git a/server/sonar-web/src/main/js/components/locations/__tests__/LocationsList-test.tsx b/server/sonar-web/src/main/js/components/locations/__tests__/LocationsList-test.tsx
index 48b9868be13..3f525e8845c 100644
--- a/server/sonar-web/src/main/js/components/locations/__tests__/LocationsList-test.tsx
+++ b/server/sonar-web/src/main/js/components/locations/__tests__/LocationsList-test.tsx
@@ -67,7 +67,6 @@ function shallowRender(overrides: Partial<LocationsList['props']> = {}) {
isCrossFile={true}
onLocationSelect={jest.fn()}
scroll={jest.fn()}
- selectedFlowIndex={undefined}
selectedLocationIndex={undefined}
{...overrides}
/>