From 430ffc7c4f96676636d3f03e47e90035f3c9d3e5 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Thu, 10 Mar 2022 11:35:33 +0100 Subject: SONAR-16069 Scroll to primary hotspot location --- .../apps/security-hotspots/SecurityHotspotsApp.tsx | 5 ++-- .../SecurityHotspotsAppRenderer.tsx | 2 +- .../__tests__/SecurityHotspotsApp-test.tsx | 10 +++++-- .../security-hotspots/components/HotspotList.tsx | 2 +- .../components/HotspotListItem.tsx | 9 ++++-- .../components/HotspotPrimaryLocationBox.tsx | 15 ++++++++-- .../components/HotspotSimpleList.tsx | 2 +- .../components/HotspotSnippetContainerRenderer.tsx | 5 +++- .../components/__tests__/HotspotListItem-test.tsx | 10 +++++++ .../__tests__/HotspotPrimaryLocationBox-test.tsx | 35 +++++++++++++++++++++- .../__snapshots__/HotspotListItem-test.tsx.snap | 4 ++- 11 files changed, 84 insertions(+), 15 deletions(-) (limited to 'server/sonar-web/src') 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 845dcf1e591..66c0f94b6a7 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 @@ -471,9 +471,10 @@ export class SecurityHotspotsApp extends React.PureComponent { .catch(this.handleCallFailure); }; - handleLocationClick = (locationIndex: number) => { + handleLocationClick = (locationIndex?: number) => { const { selectedHotspotLocationIndex } = this.state; - if (locationIndex === selectedHotspotLocationIndex) { + + if (locationIndex === undefined || locationIndex === selectedHotspotLocationIndex) { this.setState({ selectedHotspotLocationIndex: undefined }); 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 4de97e96492..efdbc427ba0 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,7 +56,7 @@ export interface SecurityHotspotsAppRendererProps { loadingMore: boolean; onChangeFilters: (filters: Partial) => void; onHotspotClick: (hotspot: RawHotspot) => void; - onLocationClick: (index: number) => void; + onLocationClick: (index?: number) => void; onLoadMore: () => void; onShowAllHotspots: () => void; onSwitchStatusFilter: (option: HotspotStatusFilter) => void; 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 c530c789ff6..1ab4d4ade68 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,12 +19,14 @@ */ 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 { KeyboardCodes } from '../../../helpers/keycodes'; import { mockBranch, mockPullRequest } from '../../../helpers/mocks/branch-like'; import { mockComponent } from '../../../helpers/mocks/component'; +import { mockHtmlElement } from '../../../helpers/mocks/dom'; import { mockRawHotspot, mockStandards } from '../../../helpers/mocks/security-hotspots'; +import { scrollToElement } from '../../../helpers/scrolling'; import { getStandards } from '../../../helpers/security-standard'; import { mockCurrentUser, @@ -43,8 +45,6 @@ import { } from '../../../types/security-hotspots'; import { SecurityHotspotsApp } from '../SecurityHotspotsApp'; import SecurityHotspotsAppRenderer from '../SecurityHotspotsAppRenderer'; -import { scrollToElement } from '../../../helpers/scrolling'; -import { KeyboardCodes } from '../../../helpers/keycodes'; const originalAddEventListener = window.addEventListener; const originalRemoveEventListener = window.removeEventListener; @@ -448,6 +448,10 @@ it('should handle secondary location click', () => { wrapper.instance().handleLocationClick(1); expect(wrapper.instance().state.selectedHotspotLocationIndex).toBeUndefined(); + + wrapper.setState({ selectedHotspotLocationIndex: 2 }); + wrapper.instance().handleLocationClick(); + expect(wrapper.instance().state.selectedHotspotLocationIndex).toBeUndefined(); }); it('should handle scroll properly', async () => { 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 3bac3c57ebe..4ae08aa223e 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,7 +37,7 @@ interface Props { loadingMore: boolean; onHotspotClick: (hotspot: RawHotspot) => void; onLoadMore: () => void; - onLocationClick: (index: number) => void; + onLocationClick: (index?: number) => void; onScroll: (element: Element) => void; securityCategories: StandardSecurityCategories; selectedHotspot: RawHotspot; 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 67662ca6c12..ae34502f027 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 @@ -28,7 +28,7 @@ import { getFilePath, getLocations } from '../utils'; export interface HotspotListItemProps { hotspot: RawHotspot; onClick: (hotspot: RawHotspot) => void; - onLocationClick: (index: number) => void; + onLocationClick: (index?: number) => void; onScroll: (element: Element) => void; selected: boolean; selectedHotspotLocation?: number; @@ -44,7 +44,12 @@ export default function HotspotListItem(props: HotspotListItemProps) { className={classNames('hotspot-item', { highlight: selected })} href="#" onClick={() => !selected && props.onClick(hotspot)}> -
{hotspot.message}
+
props.onLocationClick() : undefined} + role={selected ? 'button' : undefined}> + {hotspot.message} +
void; currentUser: CurrentUser; scroll: (element: HTMLElement, offset?: number) => void; + secondaryLocationSelected: boolean; } export function HotspotPrimaryLocationBox(props: HotspotPrimaryLocationBoxProps) { - const { hotspot, currentUser } = props; + const { hotspot, currentUser, secondaryLocationSelected } = props; + + const locationRef = React.useRef(null); + + React.useEffect(() => { + const { current } = locationRef; + if (current && !secondaryLocationSelected) { + props.scroll(current); + } + }); + return (
element && props.scroll(element)}> + ref={locationRef}>
{hotspot.message}
{isLoggedIn(currentUser) && ( void; - onLocationClick: (index: number) => void; + onLocationClick: (index?: number) => void; onScroll: (element: Element) => void; onLoadMore: () => void; selectedHotspot: RawHotspot; 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 bd3d2dd1ef7..49d6d804a73 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 @@ -93,6 +93,8 @@ export default function HotspotSnippetContainerRenderer( const scrollableRef = React.useRef(null); + const secondaryLocationSelected = selectedHotspotLocation !== undefined; + /* Use memo is important to not rerender and trigger additional scrolls */ const hotspotPrimaryLocationBox = React.useMemo( () => ( @@ -100,9 +102,10 @@ export default function HotspotSnippetContainerRenderer( hotspot={hotspot} onCommentClick={props.onCommentButtonClick} scroll={getScrollHandler(scrollableRef)} + secondaryLocationSelected={secondaryLocationSelected} /> ), - [hotspot, props.onCommentButtonClick] + [hotspot, secondaryLocationSelected, props.onCommentButtonClick] ); const renderHotspotBoxInLine = (lineNumber: number) => 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 09187d1f218..99cfe471fbd 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 @@ -37,6 +37,16 @@ it('should handle click', () => { expect(onClick).toBeCalledWith(hotspot); }); +it('should handle click on the title', () => { + const hotspot = mockRawHotspot({ key: 'hotspotKey' }); + const onLocationClick = jest.fn(); + const wrapper = shallowRender({ hotspot, onLocationClick, selected: true }); + + wrapper.find('div.cursor-pointer').simulate('click'); + + expect(onLocationClick).toBeCalledWith(); +}); + function shallowRender(props: Partial = {}) { return shallow( { expect(onCommentClick).toBeCalled(); }); +it('should scroll on load if no secondary locations selected', () => { + const node = document.createElement('div'); + jest.spyOn(React, 'useRef').mockImplementationOnce(() => ({ current: node })); + jest.spyOn(React, 'useEffect').mockImplementationOnce(f => f()); + + const scroll = jest.fn(); + shallowRender({ scroll }); + + expect(scroll).toBeCalled(); +}); + +it('should not scroll on load if a secondary location is selected', () => { + const node = document.createElement('div'); + jest.spyOn(React, 'useRef').mockImplementationOnce(() => ({ current: node })); + jest.spyOn(React, 'useEffect').mockImplementationOnce(f => f()); + + const scroll = jest.fn(); + shallowRender({ scroll, secondaryLocationSelected: true }); + + expect(scroll).not.toBeCalled(); +}); + +it('should not scroll on load if node is not defined', () => { + jest.spyOn(React, 'useRef').mockImplementationOnce(() => ({ current: undefined })); + jest.spyOn(React, 'useEffect').mockImplementationOnce(f => f()); + + const scroll = jest.fn(); + shallowRender({ scroll }); + + expect(scroll).not.toBeCalled(); +}); + function shallowRender(props: Partial = {}) { return shallow( = {}) { hotspot={mockHotspot()} onCommentClick={jest.fn()} scroll={jest.fn()} + secondaryLocationSelected={false} {...props} /> ); 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 41a9dbae849..12b6518b12a 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 @@ -37,7 +37,9 @@ exports[`should render correctly 2`] = ` onClick={[Function]} >
'3' is a magic number.
-- cgit v1.2.3