From 4dd729287b547b5865b179e0b4d28f3acbbfb890 Mon Sep 17 00:00:00 2001 From: Wouter Admiraal Date: Fri, 9 Apr 2021 17:38:16 +0200 Subject: [PATCH] SONAR-13217 Auto scroll to uncovered lines --- .../components/MeasureContent.tsx | 8 + .../__tests__/MeasureContent-test.tsx | 37 +- .../MeasureContent-test.tsx.snap | 2 + .../SourceViewer/SourceViewerBase.tsx | 2 + .../SourceViewer/SourceViewerCode.tsx | 32 +- .../__tests__/SourceViewerCode-test.tsx | 117 ++++ .../SourceViewerCode-test.tsx.snap | 593 ++++++++++++++++++ .../SourceViewer/components/Line.tsx | 10 +- .../SourceViewer/components/LineCoverage.tsx | 13 +- .../__tests__/LineCoverage-test.tsx | 14 + .../__snapshots__/Line-test.tsx.snap | 1 + 11 files changed, 824 insertions(+), 5 deletions(-) create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewerCode-test.tsx create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerCode-test.tsx.snap diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx index 62ca062c231..ac027905e48 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx @@ -22,6 +22,7 @@ import { InjectedRouter } from 'react-router'; import PageActions from 'sonar-ui-common/components/ui/PageActions'; import { translate } from 'sonar-ui-common/helpers/l10n'; import { RequestData } from 'sonar-ui-common/helpers/request'; +import { scrollToElement } from 'sonar-ui-common/helpers/scrolling'; import { getComponentTree } from '../../../api/components'; import { getMeasures } from '../../../api/measures'; import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget'; @@ -252,6 +253,11 @@ export default class MeasureContent extends React.PureComponent { return index !== -1 ? index : undefined; }; + handleScroll = (element: Element) => { + const offset = window.innerHeight / 2; + scrollToElement(element, { topOffset: offset - 100, bottomOffset: offset, smooth: true }); + }; + renderMeasure() { const { view } = this.props; const { metric } = this.state; @@ -365,7 +371,9 @@ export default class MeasureContent extends React.PureComponent { ) : ( diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx index 9c11be6f3b1..bc44caa8815 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx @@ -19,11 +19,16 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { scrollToElement } from 'sonar-ui-common/helpers/scrolling'; import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; import { getComponentTree } from '../../../../api/components'; import { mockComponentMeasure, mockRouter } from '../../../../helpers/testMocks'; import MeasureContent from '../MeasureContent'; +jest.mock('sonar-ui-common/helpers/scrolling', () => ({ + scrollToElement: jest.fn() +})); + jest.mock('../../../../api/components', () => { const { mockComponentMeasure } = jest.requireActual('../../../../helpers/testMocks'); return { @@ -57,10 +62,29 @@ const METRICS = { bugs: { id: '1', key: 'bugs', type: 'INT', name: 'Bugs', domain: 'Reliability' } }; +const WINDOW_HEIGHT = 800; +const originalHeight = window.innerHeight; + beforeEach(() => { jest.clearAllMocks(); }); +beforeAll(() => { + Object.defineProperty(window, 'innerHeight', { + writable: true, + configurable: true, + value: WINDOW_HEIGHT + }); +}); + +afterAll(() => { + Object.defineProperty(window, 'innerHeight', { + writable: true, + configurable: true, + value: originalHeight + }); +}); + it('should render correctly for a project', async () => { const wrapper = shallowRender(); expect(wrapper.type()).toBeNull(); @@ -81,8 +105,19 @@ it('should render correctly for a file', async () => { expect(wrapper).toMatchSnapshot(); }); +it('should correctly handle scrolling', () => { + const element = {} as Element; + const wrapper = shallowRender(); + wrapper.instance().handleScroll(element); + expect(scrollToElement).toBeCalledWith(element, { + topOffset: 300, + bottomOffset: 400, + smooth: true + }); +}); + function shallowRender(props: Partial = {}) { - return shallow( + return shallow( diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx index d300da764ec..352d2bac7ce 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx @@ -90,6 +90,7 @@ export interface Props { scroll?: (element: HTMLElement) => void; selectedIssue?: string; showMeasures?: boolean; + metricKey?: string; slimHeader?: boolean; } @@ -609,6 +610,7 @@ export default class SourceViewerBase extends React.PureComponent openIssuesByLine={this.state.openIssuesByLine} renderDuplicationPopup={this.renderDuplicationPopup} scroll={this.props.scroll} + metricKey={this.props.metricKey} selectedIssue={this.state.selectedIssue} sources={sources} symbolsByLine={this.state.symbolsByLine} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx index b505ae42442..25b05fc41c6 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import { Button } from 'sonar-ui-common/components/controls/buttons'; import { translate } from 'sonar-ui-common/helpers/l10n'; import { BranchLike } from '../../types/branch-like'; +import { MetricKey } from '../../types/metrics'; import Line from './components/Line'; import { getSecondaryIssueLocationsForLine } from './helpers/issueLocations'; import { @@ -75,12 +76,21 @@ interface Props { openIssuesByLine: { [line: number]: boolean }; renderDuplicationPopup: (index: number, line: number) => React.ReactNode; scroll?: (element: HTMLElement) => void; + metricKey?: string; selectedIssue: string | undefined; sources: T.SourceLine[]; symbolsByLine: { [line: number]: string[] }; } export default class SourceViewerCode extends React.PureComponent { + firstUncoveredLineFound = false; + + componentDidUpdate(prevProps: Props) { + if (this.props.metricKey !== prevProps.metricKey) { + this.firstUncoveredLineFound = false; + } + } + getDuplicationsForLine = (line: T.SourceLine): number[] => { return this.props.duplicationsByLine[line.line] || EMPTY_ARRAY; }; @@ -106,7 +116,13 @@ export default class SourceViewerCode extends React.PureComponent { displayDuplications: boolean; displayIssues: boolean; }) => { - const { highlightedLocationMessage, highlightedLocations, selectedIssue, sources } = this.props; + const { + highlightedLocationMessage, + highlightedLocations, + metricKey, + selectedIssue, + sources + } = this.props; const secondaryIssueLocations = getSecondaryIssueLocationsForLine(line, highlightedLocations); @@ -114,6 +130,19 @@ export default class SourceViewerCode extends React.PureComponent { const issuesForLine = this.getIssuesForLine(line); + let scrollToUncoveredLine = false; + if ( + !this.firstUncoveredLineFound && + displayCoverage && + line.coverageStatus && + ['uncovered', 'partially-covered'].includes(line.coverageStatus) + ) { + scrollToUncoveredLine = + (metricKey === MetricKey.new_uncovered_lines && line.isNew) || + metricKey === MetricKey.uncovered_lines; + this.firstUncoveredLineFound = scrollToUncoveredLine; + } + return ( { previousLine={index > 0 ? sources[index - 1] : undefined} renderDuplicationPopup={this.props.renderDuplicationPopup} scroll={this.props.scroll} + scrollToUncoveredLine={scrollToUncoveredLine} secondaryIssueLocations={secondaryIssueLocations} selectedIssue={optimizeSelectedIssue(selectedIssue, issuesForLine)} /> diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewerCode-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewerCode-test.tsx new file mode 100644 index 00000000000..16e36736263 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewerCode-test.tsx @@ -0,0 +1,117 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { mockBranch } from '../../../helpers/mocks/branch-like'; +import { mockIssue, mockSourceLine } from '../../../helpers/testMocks'; +import { MetricKey } from '../../../types/metrics'; +import Line from '../components/Line'; +import SourceViewerCode from '../SourceViewerCode'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ issues: [mockIssue(false, { textRange: undefined })] })).toMatchSnapshot( + 'has file level issues' + ); + expect(shallowRender({ hasSourcesAfter: true, hasSourcesBefore: true })).toMatchSnapshot( + 'has more sources' + ); +}); + +it('should correctly flag a line for scrolling', () => { + const sources = [ + mockSourceLine({ line: 1, coverageStatus: 'covered', isNew: false }), + mockSourceLine({ line: 2, coverageStatus: 'partially-covered', isNew: false }), + mockSourceLine({ line: 3, coverageStatus: 'uncovered', isNew: true }) + ]; + let wrapper = shallowRender({ sources, metricKey: MetricKey.uncovered_lines }); + + expect( + wrapper + .find(Line) + .at(1) + .props().scrollToUncoveredLine + ).toBe(true); + expect( + wrapper + .find(Line) + .at(2) + .props().scrollToUncoveredLine + ).toBe(false); + + wrapper = shallowRender({ + sources, + metricKey: MetricKey.new_uncovered_lines + }); + + expect( + wrapper + .find(Line) + .at(1) + .props().scrollToUncoveredLine + ).toBe(false); + expect( + wrapper + .find(Line) + .at(2) + .props().scrollToUncoveredLine + ).toBe(true); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerCode-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerCode-test.tsx.snap new file mode 100644 index 00000000000..cec54e6a9a6 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerCode-test.tsx.snap @@ -0,0 +1,593 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: default 1`] = ` +
+ + + 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", + } + } + loadDuplications={[MockFunction]} + onIssueChange={[MockFunction]} + onIssuePopupToggle={[MockFunction]} + onIssueSelect={[MockFunction]} + onIssueUnselect={[MockFunction]} + onIssuesClose={[MockFunction]} + onIssuesOpen={[MockFunction]} + onLocationSelect={[MockFunction]} + onSymbolClick={[MockFunction]} + openIssues={false} + renderDuplicationPopup={[MockFunction]} + scrollToUncoveredLine={false} + secondaryIssueLocations={Array []} + /> + 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", + } + } + loadDuplications={[MockFunction]} + onIssueChange={[MockFunction]} + onIssuePopupToggle={[MockFunction]} + onIssueSelect={[MockFunction]} + onIssueUnselect={[MockFunction]} + onIssuesClose={[MockFunction]} + onIssuesOpen={[MockFunction]} + onLocationSelect={[MockFunction]} + onSymbolClick={[MockFunction]} + openIssues={false} + previousLine={ + 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", + } + } + renderDuplicationPopup={[MockFunction]} + scrollToUncoveredLine={false} + secondaryIssueLocations={Array []} + /> + 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", + } + } + loadDuplications={[MockFunction]} + onIssueChange={[MockFunction]} + onIssuePopupToggle={[MockFunction]} + onIssueSelect={[MockFunction]} + onIssueUnselect={[MockFunction]} + onIssuesClose={[MockFunction]} + onIssuesOpen={[MockFunction]} + onLocationSelect={[MockFunction]} + onSymbolClick={[MockFunction]} + openIssues={false} + previousLine={ + 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", + } + } + renderDuplicationPopup={[MockFunction]} + scrollToUncoveredLine={false} + secondaryIssueLocations={Array []} + /> + +
+
+`; + +exports[`should render correctly: has file level issues 1`] = ` +
+ + + + 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", + } + } + loadDuplications={[MockFunction]} + onIssueChange={[MockFunction]} + onIssuePopupToggle={[MockFunction]} + onIssueSelect={[MockFunction]} + onIssueUnselect={[MockFunction]} + onIssuesClose={[MockFunction]} + onIssuesOpen={[MockFunction]} + onLocationSelect={[MockFunction]} + onSymbolClick={[MockFunction]} + openIssues={false} + renderDuplicationPopup={[MockFunction]} + scrollToUncoveredLine={false} + secondaryIssueLocations={Array []} + /> + 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", + } + } + loadDuplications={[MockFunction]} + onIssueChange={[MockFunction]} + onIssuePopupToggle={[MockFunction]} + onIssueSelect={[MockFunction]} + onIssueUnselect={[MockFunction]} + onIssuesClose={[MockFunction]} + onIssuesOpen={[MockFunction]} + onLocationSelect={[MockFunction]} + onSymbolClick={[MockFunction]} + openIssues={false} + previousLine={ + 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", + } + } + renderDuplicationPopup={[MockFunction]} + scrollToUncoveredLine={false} + secondaryIssueLocations={Array []} + /> + 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", + } + } + loadDuplications={[MockFunction]} + onIssueChange={[MockFunction]} + onIssuePopupToggle={[MockFunction]} + onIssueSelect={[MockFunction]} + onIssueUnselect={[MockFunction]} + onIssuesClose={[MockFunction]} + onIssuesOpen={[MockFunction]} + onLocationSelect={[MockFunction]} + onSymbolClick={[MockFunction]} + openIssues={false} + previousLine={ + 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", + } + } + renderDuplicationPopup={[MockFunction]} + scrollToUncoveredLine={false} + secondaryIssueLocations={Array []} + /> + +
+
+`; + +exports[`should render correctly: has more sources 1`] = ` +
+
+ +
+ + + 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", + } + } + loadDuplications={[MockFunction]} + onIssueChange={[MockFunction]} + onIssuePopupToggle={[MockFunction]} + onIssueSelect={[MockFunction]} + onIssueUnselect={[MockFunction]} + onIssuesClose={[MockFunction]} + onIssuesOpen={[MockFunction]} + onLocationSelect={[MockFunction]} + onSymbolClick={[MockFunction]} + openIssues={false} + renderDuplicationPopup={[MockFunction]} + scrollToUncoveredLine={false} + secondaryIssueLocations={Array []} + /> + 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", + } + } + loadDuplications={[MockFunction]} + onIssueChange={[MockFunction]} + onIssuePopupToggle={[MockFunction]} + onIssueSelect={[MockFunction]} + onIssueUnselect={[MockFunction]} + onIssuesClose={[MockFunction]} + onIssuesOpen={[MockFunction]} + onLocationSelect={[MockFunction]} + onSymbolClick={[MockFunction]} + openIssues={false} + previousLine={ + 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", + } + } + renderDuplicationPopup={[MockFunction]} + scrollToUncoveredLine={false} + secondaryIssueLocations={Array []} + /> + 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", + } + } + loadDuplications={[MockFunction]} + onIssueChange={[MockFunction]} + onIssuePopupToggle={[MockFunction]} + onIssueSelect={[MockFunction]} + onIssueUnselect={[MockFunction]} + onIssuesClose={[MockFunction]} + onIssuesOpen={[MockFunction]} + onLocationSelect={[MockFunction]} + onSymbolClick={[MockFunction]} + openIssues={false} + previousLine={ + 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", + } + } + renderDuplicationPopup={[MockFunction]} + scrollToUncoveredLine={false} + secondaryIssueLocations={Array []} + /> + +
+
+ +
+
+`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx index 7dcdd12a08d..40ae17c22c7 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx @@ -62,6 +62,7 @@ interface Props { previousLine: T.SourceLine | undefined; renderDuplicationPopup: (index: number, line: number) => React.ReactNode; scroll?: (element: HTMLElement) => void; + scrollToUncoveredLine?: boolean; secondaryIssueLocations: T.LinearIssueLocation[]; selectedIssue: string | undefined; verticalBuffer?: number; @@ -107,6 +108,7 @@ export default class Line extends React.PureComponent { line, openIssues, previousLine, + scrollToUncoveredLine, secondaryIssueLocations, selectedIssue, verticalBuffer @@ -163,7 +165,13 @@ export default class Line extends React.PureComponent { /> ))} - {displayCoverage && } + {displayCoverage && ( + + )} void; + scrollToUncoveredLine?: boolean; } -export function LineCoverage({ line }: LineCoverageProps) { +export function LineCoverage({ line, scroll, scrollToUncoveredLine }: LineCoverageProps) { + const coverageMarker = React.useRef(null); + React.useEffect(() => { + if (scrollToUncoveredLine && scroll && coverageMarker.current) { + scroll(coverageMarker.current); + } + }, [scrollToUncoveredLine, scroll, coverageMarker]); + const className = 'source-meta source-line-coverage' + (line.coverageStatus != null ? ` source-line-${line.coverageStatus}` : ''); const status = getStatusTooltip(line); return ( - +
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx index 41a8a4b164d..b1868569b91 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx @@ -37,6 +37,20 @@ it('should render correctly', () => { ); }); +it('should correctly trigger a scroll', () => { + const element = { current: {} }; + jest.spyOn(React, 'useEffect').mockImplementation(f => f()); + jest.spyOn(React, 'useRef').mockImplementation(() => element); + + const scroll = jest.fn(); + shallowRender({ scroll, scrollToUncoveredLine: true }); + expect(scroll).toHaveBeenCalledWith(element.current); + + scroll.mockReset(); + shallowRender({ scroll, scrollToUncoveredLine: false }); + expect(scroll).not.toHaveBeenCalled(); +}); + function shallowRender(props: Partial = {}) { return shallow(); } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/Line-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/Line-test.tsx.snap index ec5a30fd9ec..2cc3c08322d 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/Line-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/Line-test.tsx.snap @@ -328,6 +328,7 @@ exports[`should render correctly with coverage 1`] = ` "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", } } + scroll={[MockFunction]} />