From 075baf29c5250bcb52634cd4f4ab678c68581808 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Wed, 11 Dec 2019 18:36:30 +0100 Subject: [PATCH] SONAR-12718 Hotspot Code Snippet --- .../SnippetViewer.css | 80 +++++ .../SnippetViewer.tsx | 3 +- .../src/main/js/apps/issues/styles.css | 59 ---- .../securityHotspots/SecurityHotspotsApp.tsx | 2 + .../SecurityHotspotsAppRenderer.tsx | 5 +- .../SecurityHotspotsApp-test.tsx.snap | 8 + .../components/HotspotSnippetContainer.tsx | 188 ++++++++++ .../HotspotSnippetContainerRenderer.tsx | 102 ++++++ .../components/HotspotViewer.tsx | 5 +- .../components/HotspotViewerRenderer.tsx | 6 +- .../HotspotSnippetContainer-test.tsx | 186 ++++++++++ .../HotspotSnippetContainerRenderer-test.tsx | 52 +++ .../HotspotSnippetContainer-test.tsx.snap | 116 +++++++ ...spotSnippetContainerRenderer-test.tsx.snap | 247 +++++++++++++ .../HotspotViewerRenderer-test.tsx.snap | 328 ++++++++++++++++++ .../main/js/apps/securityHotspots/utils.ts | 17 +- .../SourceViewer/helpers/indexing.ts | 2 +- .../sonar-web/src/main/js/helpers/issues.ts | 2 +- 18 files changed, 1342 insertions(+), 66 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.css create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotSnippetContainer.tsx create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotSnippetContainerRenderer.tsx create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotSnippetContainer-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotSnippetContainerRenderer-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotSnippetContainer-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.css b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.css new file mode 100644 index 00000000000..89fd8a2e662 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.css @@ -0,0 +1,80 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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. + */ +.snippet { + margin: var(--gridSize); + border: 1px solid var(--gray80); + overflow-x: auto; + overflow-y: hidden; + transition: max-height 0.2s; +} + +.snippet > div { + display: table; + width: 100%; + position: relative; + transition: margin-top 0.2s; +} + +.snippet table { + width: 100%; +} + +.expand-block { + position: absolute; + z-index: 2; + width: 100%; +} + +.expand-block > button { + background: transparent; + box-sizing: border-box; + color: var(--secondFontColor); + height: 20px; + width: 100%; + padding: calc(var(--gridSize) / 4); + border: 0; + text-align: left; + cursor: pointer; +} + +.expand-block > button:hover, +.expand-block > button:focus, +.expand-block > button:active { + color: var(--darkBlue); + outline: none; +} + +.expand-block-above { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAAXNSR0IArs4c6QAAADdJREFUCB1dzMEKADAIAlBd1v9/bcc2YgRjHh8qq2qTxCQzsX4wM6y30RARF3sy0Es1SIK7Y64OpCES1W69JS4AAAAASUVORK5CYII='); + top: 0; +} + +.expand-block-below { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wQQBjQEQVd5jwAAADhJREFUCNddyTEKADEMA8GVA/7/Z+PGwUp1cGTaYe/tv5lxrLWoKj6SiMzkjZDEG7JtANt0N+ccLrB/KZxXTt7fAAAAAElFTkSuQmCC'); + bottom: 0; +} + +.source-table.expand-up { + margin-top: 20px; +} + +.source-table.expand-down { + margin-bottom: 20px; +} diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx index cc39d046142..ad13be5f4b8 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx @@ -31,6 +31,7 @@ import { optimizeSelectedIssue } from '../../../components/SourceViewer/helpers/lines'; import { BranchLike } from '../../../types/branch-like'; +import './SnippetViewer.css'; import { inSnippet, LINES_BELOW_ISSUE } from './utils'; interface Props { @@ -46,7 +47,7 @@ interface Props { highlightedLocationMessage: { index: number; text: string | undefined } | undefined; highlightedSymbols: string[]; index: number; - issue: T.Issue; + issue: Pick; issuePopup?: { issue: string; name: string }; issuesByLine: T.IssuesByLine; last: boolean; diff --git a/server/sonar-web/src/main/js/apps/issues/styles.css b/server/sonar-web/src/main/js/apps/issues/styles.css index 7a28b199b22..88402d912af 100644 --- a/server/sonar-web/src/main/js/apps/issues/styles.css +++ b/server/sonar-web/src/main/js/apps/issues/styles.css @@ -238,65 +238,6 @@ padding: var(--gridSize); } -.snippet { - margin: var(--gridSize); - border: 1px solid var(--gray80); - overflow-x: auto; - overflow-y: hidden; - transition: max-height 0.2s; -} - -.snippet > div { - display: table; - width: 100%; - position: relative; - transition: margin-top 0.2s; -} - -.snippet table { - width: 100%; -} - -.expand-block { - position: absolute; - z-index: 2; - width: 100%; -} - -.expand-block > button { - background: transparent; - box-sizing: border-box; - color: var(--secondFontColor); - height: 20px; - width: 100%; - padding: calc(var(--gridSize) / 4); - border: 0; - text-align: left; - cursor: pointer; -} -.expand-block > button:hover, -.expand-block > button:focus, -.expand-block > button:active { - color: var(--darkBlue); - outline: none; -} -.expand-block-above { - background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAAXNSR0IArs4c6QAAADdJREFUCB1dzMEKADAIAlBd1v9/bcc2YgRjHh8qq2qTxCQzsX4wM6y30RARF3sy0Es1SIK7Y64OpCES1W69JS4AAAAASUVORK5CYII='); - top: 0; -} -.expand-block-below { - background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wQQBjQEQVd5jwAAADhJREFUCNddyTEKADEMA8GVA/7/Z+PGwUp1cGTaYe/tv5lxrLWoKj6SiMzkjZDEG7JtANt0N+ccLrB/KZxXTt7fAAAAAElFTkSuQmCC'); - bottom: 0; -} - -.source-table.expand-up { - margin-top: 20px; -} - -.source-table.expand-down { - margin-bottom: 20px; -} - .issues-my-issues-filter { margin-bottom: 24px; text-align: center; diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx index f960ac80185..e1d9e8a17fd 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx @@ -96,10 +96,12 @@ export default class SecurityHotspotsApp extends React.PureComponent this.setState({ selectedHotspotKey: key }); render() { + const { branchLike } = this.props; const { hotspots, loading, securityCategories, selectedHotspotKey } = this.state; return ( void; @@ -41,7 +43,7 @@ export interface SecurityHotspotsAppRendererProps { } export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRendererProps) { - const { hotspots, loading, securityCategories, selectedHotspotKey } = props; + const { branchLike, hotspots, loading, securityCategories, selectedHotspotKey } = props; return (
@@ -87,6 +89,7 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
{selectedHotspotKey && ( diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap index 4f83ea76cd3..6ad55141e38 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap @@ -2,6 +2,14 @@ exports[`should render correctly 1`] = ` { + mounted = false; + state: State = { + highlightedSymbols: [], + loading: true, + sourceLines: [] + }; + + componentWillMount() { + this.mounted = true; + this.fetchSources(); + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.hotspot.key !== this.props.hotspot.key) { + this.fetchSources(); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + checkLastLine(lines: T.SourceLine[], target: number): number | undefined { + if (lines.length < 1) { + return undefined; + } + const lastLineReceived = lines[lines.length - 1].line; + if (lastLineReceived < target) { + return lastLineReceived; + } + return undefined; + } + + fetchSources() { + const { component, textRange } = this.props.hotspot; + + const from = Math.max(1, textRange.startLine - BUFFER_LINES); + // Add 1 to check for end-of-file: + const to = textRange.endLine + BUFFER_LINES + 1; + + this.setState({ loading: true }); + return getSources({ key: component.key, from, to }) + .then(sourceLines => { + if (this.mounted) { + const lastLine = this.checkLastLine(sourceLines, to); + + // remove extra sourceline if we didn't reach the end: + sourceLines = lastLine ? sourceLines : sourceLines.slice(0, -1); + this.setState({ lastLine, loading: false, sourceLines }); + } + }) + .catch(() => this.mounted && this.setState({ loading: false })); + } + + handleExpansion = (direction: T.ExpandDirection) => { + const { branchLike, hotspot } = this.props; + const { sourceLines } = this.state; + + const range = + direction === 'up' + ? { + from: Math.max(1, sourceLines[0].line - EXPAND_BY_LINES), + to: sourceLines[0].line - 1 + } + : { + from: sourceLines[sourceLines.length - 1].line + 1, + // Add 1 to check for end-of-file: + to: sourceLines[sourceLines.length - 1].line + EXPAND_BY_LINES + 1 + }; + + return getSources({ + key: hotspot.component.key, + ...range, + ...getBranchLikeQuery(branchLike) + }).then(additionalLines => { + const lastLine = + direction === 'down' ? this.checkLastLine(additionalLines, range.to) : undefined; + + let concatSourceLines; + if (direction === 'up') { + concatSourceLines = additionalLines.concat(sourceLines); + } else { + // remove extra sourceline if we didn't reach the end: + concatSourceLines = sourceLines.concat( + lastLine ? additionalLines : additionalLines.slice(0, -1) + ); + } + + this.setState({ + lastLine, + sourceLines: concatSourceLines + }); + }); + }; + + handleLinePopupToggle = (params: T.LinePopup & { component: string }) => { + const { component, index, line, name, open } = params; + this.setState((state: State) => { + const samePopup = + state.linePopup !== undefined && + state.linePopup.line === line && + state.linePopup.name === name && + state.linePopup.component === component && + state.linePopup.index === index; + if (open !== false && !samePopup) { + return { linePopup: params }; + } else if (open !== true && samePopup) { + return { linePopup: undefined }; + } + return null; + }); + }; + + handleSymbolClick = (highlightedSymbols: string[]) => { + this.setState({ highlightedSymbols }); + }; + + render() { + const { branchLike, hotspot } = this.props; + const { highlightedSymbols, lastLine, linePopup, loading, sourceLines } = this.state; + + const locations = locationsByLine([hotspot]); + + const sourceViewerFile = constructSourceViewerFile(hotspot, lastLine); + + return ( + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotSnippetContainerRenderer.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotSnippetContainerRenderer.tsx new file mode 100644 index 00000000000..d8dd52cabea --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotSnippetContainerRenderer.tsx @@ -0,0 +1,102 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; +import { SourceViewerContext } from '../../../components/SourceViewer/SourceViewerContext'; +import SourceViewerHeaderSlim from '../../../components/SourceViewer/SourceViewerHeaderSlim'; +import { BranchLike } from '../../../types/branch-like'; +import { DetailedHotspot } from '../../../types/security-hotspots'; +import SnippetViewer from '../../issues/crossComponentSourceViewer/SnippetViewer'; + +export interface HotspotSnippetContainerRendererProps { + branchLike?: BranchLike; + highlightedSymbols: string[]; + hotspot: DetailedHotspot; + lastLine?: number; + loading: boolean; + locations: { [line: number]: T.LinearIssueLocation[] }; + linePopup?: T.LinePopup & { component: string }; + onExpandBlock: (direction: T.ExpandDirection) => Promise; + onLinePopupToggle: (line: T.SourceLine) => void; + onSymbolClick: (symbols: string[]) => void; + sourceLines: T.SourceLine[]; + sourceViewerFile: T.SourceViewerFile; +} + +const noop = () => undefined; + +export default function HotspotSnippetContainerRenderer( + props: HotspotSnippetContainerRendererProps +) { + const { + branchLike, + highlightedSymbols, + hotspot, + linePopup, + loading, + locations, + sourceLines, + sourceViewerFile + } = props; + + return ( +
+ + + {sourceLines.length > 0 && ( + + props.onExpandBlock(direction)} + handleCloseIssues={noop} + handleLinePopupToggle={props.onLinePopupToggle} + handleOpenIssues={noop} + handleSymbolClick={props.onSymbolClick} + highlightedLocationMessage={undefined} + highlightedSymbols={highlightedSymbols} + index={0} + issue={hotspot} + issuesByLine={{}} + last={false} + linePopup={linePopup} + loadDuplications={noop} + locations={[]} + locationsByLine={locations} + onIssueChange={noop} + onIssuePopupToggle={noop} + onLocationSelect={noop} + openIssuesByLine={{}} + renderDuplicationPopup={noop} + snippet={sourceLines} + /> + + )} + +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewer.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewer.tsx index 35192c96b62..9ca28887e07 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewer.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewer.tsx @@ -20,10 +20,12 @@ import * as React from 'react'; import { getSecurityHotspotDetails } from '../../../api/security-hotspots'; +import { BranchLike } from '../../../types/branch-like'; import { DetailedHotspot } from '../../../types/security-hotspots'; import HotspotViewerRenderer from './HotspotViewerRenderer'; interface Props { + branchLike?: BranchLike; hotspotKey: string; securityCategories: T.StandardSecurityCategories; } @@ -59,11 +61,12 @@ export default class HotspotViewer extends React.PureComponent { } render() { - const { securityCategories } = this.props; + const { branchLike, securityCategories } = this.props; const { hotspot, loading } = this.state; return ( @@ -68,6 +71,7 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) { )}
+
)} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotSnippetContainer-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotSnippetContainer-test.tsx new file mode 100644 index 00000000000..4000d8ed8dd --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotSnippetContainer-test.tsx @@ -0,0 +1,186 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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 { range } from 'lodash'; +import * as React from 'react'; +import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; +import { getSources } from '../../../../api/components'; +import { mockDetailledHotspot } from '../../../../helpers/mocks/security-hotspots'; +import { mockSourceLine } from '../../../../helpers/testMocks'; +import HotspotSnippetContainer from '../HotspotSnippetContainer'; +import HotspotSnippetContainerRenderer from '../HotspotSnippetContainerRenderer'; + +jest.mock('../../../../api/components', () => ({ + getSources: jest.fn().mockResolvedValue([]) +})); + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +it('should load sources on mount', async () => { + (getSources as jest.Mock).mockResolvedValueOnce( + range(5, 18).map(line => mockSourceLine({ line })) + ); + + const hotspot = mockDetailledHotspot({ + textRange: { startLine: 10, endLine: 11, startOffset: 0, endOffset: 12 } + }); + + const wrapper = shallowRender({ hotspot }); + + await waitAndUpdate(wrapper); + + expect(getSources).toBeCalled(); + expect(wrapper.state().lastLine).toBeUndefined(); + expect(wrapper.state().sourceLines).toHaveLength(12); +}); + +it('should handle end-of-file on mount', async () => { + (getSources as jest.Mock).mockResolvedValueOnce( + range(5, 15).map(line => mockSourceLine({ line })) + ); + + const hotspot = mockDetailledHotspot({ + textRange: { startLine: 10, endLine: 11, startOffset: 0, endOffset: 12 } + }); + + const wrapper = shallowRender({ hotspot }); + + await waitAndUpdate(wrapper); + + expect(getSources).toBeCalled(); + expect(wrapper.state().lastLine).toBe(14); + expect(wrapper.state().sourceLines).toHaveLength(10); +}); + +describe('Expansion', () => { + beforeEach(() => { + (getSources as jest.Mock).mockResolvedValueOnce( + range(5, 18).map(line => mockSourceLine({ line })) + ); + }); + + const hotspot = mockDetailledHotspot({ + textRange: { startLine: 10, endLine: 11, startOffset: 0, endOffset: 12 } + }); + + it('up should work', async () => { + (getSources as jest.Mock).mockResolvedValueOnce( + range(1, 5).map(line => mockSourceLine({ line })) + ); + + const wrapper = shallowRender({ hotspot }); + await waitAndUpdate(wrapper); + + wrapper + .find(HotspotSnippetContainerRenderer) + .props() + .onExpandBlock('up'); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().sourceLines).toHaveLength(16); + }); + + it('down should work', async () => { + (getSources as jest.Mock).mockResolvedValueOnce( + // lastLine + expand + extra for EOF check + range end is excluded + // 16 + 50 + 1 + 1 + range(17, 68).map(line => mockSourceLine({ line })) + ); + + const wrapper = shallowRender({ hotspot }); + await waitAndUpdate(wrapper); + + wrapper + .find(HotspotSnippetContainerRenderer) + .props() + .onExpandBlock('down'); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().lastLine).toBeUndefined(); + expect(wrapper.state().sourceLines).toHaveLength(62); + }); + + it('down should work and handle EOF', async () => { + (getSources as jest.Mock).mockResolvedValueOnce( + // lastLine + expand + extra for EOF check + range end is excluded - 1 to trigger end-of-file + // 16 + 50 + 1 + 1 - 1 + range(17, 67).map(line => mockSourceLine({ line })) + ); + + const wrapper = shallowRender({ hotspot }); + await waitAndUpdate(wrapper); + + wrapper + .find(HotspotSnippetContainerRenderer) + .props() + .onExpandBlock('down'); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().lastLine).toBe(66); + expect(wrapper.state().sourceLines).toHaveLength(62); + }); +}); + +it('should handle line popups', async () => { + (getSources as jest.Mock).mockResolvedValueOnce( + range(5, 18).map(line => mockSourceLine({ line })) + ); + + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + const params = wrapper.state().sourceLines[0]; + + wrapper + .find(HotspotSnippetContainerRenderer) + .props() + .onLinePopupToggle(params); + + expect(wrapper.state().linePopup).toEqual(params); + + // Close it + wrapper + .find(HotspotSnippetContainerRenderer) + .props() + .onLinePopupToggle(params); + + expect(wrapper.state().linePopup).toBeUndefined(); +}); + +it('should handle symbol click', () => { + const wrapper = shallowRender(); + const symbols = ['symbol']; + wrapper + .find(HotspotSnippetContainerRenderer) + .props() + .onSymbolClick(symbols); + expect(wrapper.state().highlightedSymbols).toBe(symbols); +}); + +function shallowRender(props?: Partial) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotSnippetContainerRenderer-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotSnippetContainerRenderer-test.tsx new file mode 100644 index 00000000000..9fd30ab5216 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotSnippetContainerRenderer-test.tsx @@ -0,0 +1,52 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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 { mockMainBranch } from '../../../../helpers/mocks/branch-like'; +import { mockDetailledHotspot } from '../../../../helpers/mocks/security-hotspots'; +import { mockSourceLine, mockSourceViewerFile } from '../../../../helpers/testMocks'; +import HotspotSnippetContainerRenderer, { + HotspotSnippetContainerRendererProps +} from '../HotspotSnippetContainerRenderer'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot(); + expect(shallowRender({ sourceLines: [mockSourceLine()] })).toMatchSnapshot('with sourcelines'); +}); + +function shallowRender(props?: Partial) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotSnippetContainer-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotSnippetContainer-test.tsx.snap new file mode 100644 index 00000000000..1d94193a839 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotSnippetContainer-test.tsx.snap @@ -0,0 +1,116 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +This a strong message about fixing !

", + "key": "squid:S2077", + "name": "That rule", + "riskDescription": "

This a strong message about risk !

", + "securityCategory": "sql-injection", + "vulnerabilityDescription": "

This a strong message about vulnerability !

", + "vulnerabilityProbability": "HIGH", + }, + "status": "RESOLVED", + "textRange": Object { + "endLine": 142, + "endOffset": 83, + "startLine": 142, + "startOffset": 26, + }, + "updateDate": "2013-05-13T17:55:42+0200", + } + } + loading={true} + locations={ + Object { + "142": Array [ + Object { + "from": 26, + "line": 142, + "to": 83, + }, + ], + } + } + onExpandBlock={[Function]} + onLinePopupToggle={[Function]} + onSymbolClick={[Function]} + sourceLines={Array []} + sourceViewerFile={ + Object { + "key": "my-project", + "measures": Object { + "lines": undefined, + }, + "path": "", + "project": "my-project", + "projectName": "MyProject", + "q": "FIL", + "uuid": "", + } + } +/> +`; diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap new file mode 100644 index 00000000000..883f7e10dad --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap @@ -0,0 +1,247 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +
+ + +
+`; + +exports[`should render correctly: with sourcelines 1`] = ` +
+ + + + This a strong message about fixing !

", + "key": "squid:S2077", + "name": "That rule", + "riskDescription": "

This a strong message about risk !

", + "securityCategory": "sql-injection", + "vulnerabilityDescription": "

This a strong message about vulnerability !

", + "vulnerabilityProbability": "HIGH", + }, + "status": "RESOLVED", + "textRange": Object { + "endLine": 142, + "endOffset": 83, + "startLine": 142, + "startOffset": 26, + }, + "updateDate": "2013-05-13T17:55:42+0200", + } + } + issuesByLine={Object {}} + last={false} + loadDuplications={[Function]} + locations={Array []} + locationsByLine={Object {}} + onIssueChange={[Function]} + onIssuePopupToggle={[Function]} + onLocationSelect={[Function]} + openIssuesByLine={Object {}} + renderDuplicationPopup={[Function]} + snippet={ + Array [ + Object { + "code": "import java.util.ArrayList;", + "coverageStatus": "covered", + "coveredConditions": 2, + "duplicated": false, + "isNew": true, + "line": 16, + "scmAuthor": "simon.brandhof@sonarsource.com", + "scmDate": "2018-12-11T10:48:39+0100", + "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", + }, + ] + } + /> +
+
+
+`; diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap index 4f8cbe67c88..315b57ab627 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap @@ -53,6 +53,88 @@ exports[`should render correctly 1`] = ` John Doe + This a strong message about fixing !

", + "key": "squid:S2077", + "name": "That rule", + "riskDescription": "

This a strong message about risk !

", + "securityCategory": "sql-injection", + "vulnerabilityDescription": "

This a strong message about vulnerability !

", + "vulnerabilityProbability": "HIGH", + }, + "status": "RESOLVED", + "textRange": Object { + "endLine": 142, + "endOffset": 83, + "startLine": 142, + "startOffset": 26, + }, + "updateDate": "2013-05-13T17:55:42+0200", + } + } + /> + This a strong message about fixing !

", + "key": "squid:S2077", + "name": "That rule", + "riskDescription": "

This a strong message about risk !

", + "securityCategory": "sql-injection", + "vulnerabilityDescription": "

This a strong message about vulnerability !

", + "vulnerabilityProbability": "HIGH", + }, + "status": "RESOLVED", + "textRange": Object { + "endLine": 142, + "endOffset": 83, + "startLine": 142, + "startOffset": 26, + }, + "updateDate": "2013-05-13T17:55:42+0200", + } + } + /> + This a strong message about fixing !

", + "key": "squid:S2077", + "name": "That rule", + "riskDescription": "

This a strong message about risk !

", + "securityCategory": "sql-injection", + "vulnerabilityDescription": "

This a strong message about vulnerability !

", + "vulnerabilityProbability": "HIGH", + }, + "status": "RESOLVED", + "textRange": Object { + "endLine": 142, + "endOffset": 83, + "startLine": 142, + "startOffset": 26, + }, + "updateDate": "2013-05-13T17:55:42+0200", + } + } + /> + This a strong message about fixing !

", + "key": "squid:S2077", + "name": "That rule", + "riskDescription": "

This a strong message about risk !

", + "securityCategory": "sql-injection", + "vulnerabilityDescription": "

This a strong message about vulnerability !

", + "vulnerabilityProbability": "HIGH", + }, + "status": "RESOLVED", + "textRange": Object { + "endLine": 142, + "endOffset": 83, + "startLine": 142, + "startOffset": 26, + }, + "updateDate": "2013-05-13T17:55:42+0200", + } + } + /> []) { const index: { [line: number]: T.LinearIssueLocation[] } = {}; issues.forEach(issue => { getLinearLocations(issue.textRange).forEach(location => { diff --git a/server/sonar-web/src/main/js/helpers/issues.ts b/server/sonar-web/src/main/js/helpers/issues.ts index 4f2330f7888..dc84f8d64fb 100644 --- a/server/sonar-web/src/main/js/helpers/issues.ts +++ b/server/sonar-web/src/main/js/helpers/issues.ts @@ -55,7 +55,7 @@ export interface RawIssue extends IssueBase { textRange?: T.TextRange; } -export function sortByType(issues: T.Issue[]): T.Issue[] { +export function sortByType>(issues: T[]): T[] { return sortBy(issues, issue => ISSUE_TYPES.indexOf(issue.type)); } -- 2.39.5