]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13217 Auto scroll to uncovered lines
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Fri, 9 Apr 2021 15:38:16 +0000 (17:38 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 12 Apr 2021 20:03:39 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx
server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewerCode-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerCode-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/Line-test.tsx.snap

index 62ca062c2318eaf35a533e868056cbd9add2ad13..ac027905e487fd3879e284477efad398918acdd5 100644 (file)
@@ -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<Props, State> {
     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<Props, State> {
               <SourceViewer
                 branchLike={branchLike}
                 component={baseComponent.key}
+                metricKey={this.state.metric?.key}
                 onIssueChange={this.props.onIssueChange}
+                scroll={this.handleScroll}
               />
             </div>
           ) : (
index 9c11be6f3b1cfdc51dd2bc7f55c345b80b5a00cd..bc44caa8815287598d150a96315dca20053e087e 100644 (file)
  */
 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<MeasureContent['props']> = {}) {
-  return shallow(
+  return shallow<MeasureContent>(
     <MeasureContent
       metrics={METRICS}
       requestedMetric={{ direction: 1, key: 'bugs' }}
index 762028e42a472d23489fca28cc726cf1768c4471..7f0e11a7aa3daa224798a56a3704f26c4eb39366 100644 (file)
@@ -97,6 +97,8 @@ exports[`should render correctly for a file 1`] = `
     >
       <SourceViewer
         component="foo:src/index.tsx"
+        metricKey="bugs"
+        scroll={[Function]}
       />
     </div>
   </div>
index d300da764ec720d912546b3eaec45a76af02b82b..352d2bac7ceb4e415aba3c7a60e011d6debc1756 100644 (file)
@@ -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<Props, State>
         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}
index b505ae424425a94ca9c9daa889ef7877dfe4ffef..25b05fc41c6a183ec30b83e67dd52bf2f0c01d3b 100644 (file)
@@ -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<Props> {
+  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<Props> {
     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<Props> {
 
     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 (
       <Line
         branchLike={this.props.branchLike}
@@ -154,6 +183,7 @@ export default class SourceViewerCode extends React.PureComponent<Props> {
         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 (file)
index 0000000..16e3673
--- /dev/null
@@ -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<SourceViewerCode['props']> = {}) {
+  return shallow<SourceViewerCode>(
+    <SourceViewerCode
+      branchLike={mockBranch()}
+      componentKey="foo"
+      duplications={[]}
+      duplicationsByLine={[]}
+      hasSourcesAfter={false}
+      hasSourcesBefore={false}
+      highlightedLine={undefined}
+      highlightedLocationMessage={undefined}
+      highlightedLocations={undefined}
+      highlightedSymbols={[]}
+      issueLocationsByLine={{}}
+      issuePopup={undefined}
+      issues={[mockIssue(), mockIssue()]}
+      issuesByLine={{}}
+      loadDuplications={jest.fn()}
+      loadingSourcesAfter={false}
+      loadingSourcesBefore={false}
+      loadSourcesAfter={jest.fn()}
+      loadSourcesBefore={jest.fn()}
+      onIssueChange={jest.fn()}
+      onIssuePopupToggle={jest.fn()}
+      onIssuesClose={jest.fn()}
+      onIssueSelect={jest.fn()}
+      onIssuesOpen={jest.fn()}
+      onIssueUnselect={jest.fn()}
+      onLocationSelect={jest.fn()}
+      onSymbolClick={jest.fn()}
+      openIssuesByLine={{}}
+      renderDuplicationPopup={jest.fn()}
+      selectedIssue={undefined}
+      sources={[mockSourceLine(), mockSourceLine(), mockSourceLine()]}
+      symbolsByLine={{}}
+      {...props}
+    />
+  );
+}
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 (file)
index 0000000..cec54e6
--- /dev/null
@@ -0,0 +1,593 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: default 1`] = `
+<div
+  className="source-viewer-code"
+>
+  <table
+    className="source-table"
+  >
+    <tbody>
+      <Line
+        branchLike={
+          Object {
+            "analysisDate": "2018-01-01",
+            "excludedFromPurge": true,
+            "isMain": false,
+            "name": "branch-6.7",
+          }
+        }
+        displayCoverage={true}
+        displayDuplications={false}
+        displayIssues={true}
+        duplications={Array []}
+        duplicationsCount={0}
+        highlighted={false}
+        issueLocations={Array []}
+        issues={Array []}
+        key="16"
+        last={false}
+        line={
+          Object {
+            "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
+            "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 []}
+      />
+      <Line
+        branchLike={
+          Object {
+            "analysisDate": "2018-01-01",
+            "excludedFromPurge": true,
+            "isMain": false,
+            "name": "branch-6.7",
+          }
+        }
+        displayCoverage={true}
+        displayDuplications={false}
+        displayIssues={true}
+        duplications={Array []}
+        duplicationsCount={0}
+        highlighted={false}
+        issueLocations={Array []}
+        issues={Array []}
+        key="16"
+        last={false}
+        line={
+          Object {
+            "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
+            "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": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
+            "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 []}
+      />
+      <Line
+        branchLike={
+          Object {
+            "analysisDate": "2018-01-01",
+            "excludedFromPurge": true,
+            "isMain": false,
+            "name": "branch-6.7",
+          }
+        }
+        displayCoverage={true}
+        displayDuplications={false}
+        displayIssues={true}
+        duplications={Array []}
+        duplicationsCount={0}
+        highlighted={false}
+        issueLocations={Array []}
+        issues={Array []}
+        key="16"
+        last={true}
+        line={
+          Object {
+            "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
+            "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": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
+            "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 []}
+      />
+    </tbody>
+  </table>
+</div>
+`;
+
+exports[`should render correctly: has file level issues 1`] = `
+<div
+  className="source-viewer-code"
+>
+  <table
+    className="source-table"
+  >
+    <tbody>
+      <Line
+        branchLike={
+          Object {
+            "analysisDate": "2018-01-01",
+            "excludedFromPurge": true,
+            "isMain": false,
+            "name": "branch-6.7",
+          }
+        }
+        displayCoverage={true}
+        displayDuplications={false}
+        displayIssues={true}
+        duplications={Array []}
+        duplicationsCount={0}
+        highlighted={false}
+        issueLocations={Array []}
+        issues={Array []}
+        key="0"
+        last={false}
+        line={
+          Object {
+            "code": "",
+            "duplicated": false,
+            "isNew": false,
+            "line": 0,
+          }
+        }
+        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 []}
+      />
+      <Line
+        branchLike={
+          Object {
+            "analysisDate": "2018-01-01",
+            "excludedFromPurge": true,
+            "isMain": false,
+            "name": "branch-6.7",
+          }
+        }
+        displayCoverage={true}
+        displayDuplications={false}
+        displayIssues={true}
+        duplications={Array []}
+        duplicationsCount={0}
+        highlighted={false}
+        issueLocations={Array []}
+        issues={Array []}
+        key="16"
+        last={false}
+        line={
+          Object {
+            "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
+            "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 []}
+      />
+      <Line
+        branchLike={
+          Object {
+            "analysisDate": "2018-01-01",
+            "excludedFromPurge": true,
+            "isMain": false,
+            "name": "branch-6.7",
+          }
+        }
+        displayCoverage={true}
+        displayDuplications={false}
+        displayIssues={true}
+        duplications={Array []}
+        duplicationsCount={0}
+        highlighted={false}
+        issueLocations={Array []}
+        issues={Array []}
+        key="16"
+        last={false}
+        line={
+          Object {
+            "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
+            "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": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
+            "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 []}
+      />
+      <Line
+        branchLike={
+          Object {
+            "analysisDate": "2018-01-01",
+            "excludedFromPurge": true,
+            "isMain": false,
+            "name": "branch-6.7",
+          }
+        }
+        displayCoverage={true}
+        displayDuplications={false}
+        displayIssues={true}
+        duplications={Array []}
+        duplicationsCount={0}
+        highlighted={false}
+        issueLocations={Array []}
+        issues={Array []}
+        key="16"
+        last={true}
+        line={
+          Object {
+            "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
+            "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": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
+            "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 []}
+      />
+    </tbody>
+  </table>
+</div>
+`;
+
+exports[`should render correctly: has more sources 1`] = `
+<div
+  className="source-viewer-code"
+>
+  <div
+    className="source-viewer-more-code"
+  >
+    <Button
+      className="js-component-viewer-source-before"
+      onClick={[MockFunction]}
+    >
+      source_viewer.load_more_code
+    </Button>
+  </div>
+  <table
+    className="source-table"
+  >
+    <tbody>
+      <Line
+        branchLike={
+          Object {
+            "analysisDate": "2018-01-01",
+            "excludedFromPurge": true,
+            "isMain": false,
+            "name": "branch-6.7",
+          }
+        }
+        displayCoverage={true}
+        displayDuplications={false}
+        displayIssues={true}
+        duplications={Array []}
+        duplicationsCount={0}
+        highlighted={false}
+        issueLocations={Array []}
+        issues={Array []}
+        key="16"
+        last={false}
+        line={
+          Object {
+            "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
+            "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 []}
+      />
+      <Line
+        branchLike={
+          Object {
+            "analysisDate": "2018-01-01",
+            "excludedFromPurge": true,
+            "isMain": false,
+            "name": "branch-6.7",
+          }
+        }
+        displayCoverage={true}
+        displayDuplications={false}
+        displayIssues={true}
+        duplications={Array []}
+        duplicationsCount={0}
+        highlighted={false}
+        issueLocations={Array []}
+        issues={Array []}
+        key="16"
+        last={false}
+        line={
+          Object {
+            "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
+            "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": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
+            "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 []}
+      />
+      <Line
+        branchLike={
+          Object {
+            "analysisDate": "2018-01-01",
+            "excludedFromPurge": true,
+            "isMain": false,
+            "name": "branch-6.7",
+          }
+        }
+        displayCoverage={true}
+        displayDuplications={false}
+        displayIssues={true}
+        duplications={Array []}
+        duplicationsCount={0}
+        highlighted={false}
+        issueLocations={Array []}
+        issues={Array []}
+        key="16"
+        last={false}
+        line={
+          Object {
+            "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
+            "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": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
+            "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 []}
+      />
+    </tbody>
+  </table>
+  <div
+    className="source-viewer-more-code"
+  >
+    <Button
+      className="js-component-viewer-source-after"
+      onClick={[MockFunction]}
+    >
+      source_viewer.load_more_code
+    </Button>
+  </div>
+</div>
+`;
index 7dcdd12a08dffd5217e7fed0d7cb6530906773d1..40ae17c22c7041c598b8b9ba6333293e785cd7e5 100644 (file)
@@ -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<Props> {
       line,
       openIssues,
       previousLine,
+      scrollToUncoveredLine,
       secondaryIssueLocations,
       selectedIssue,
       verticalBuffer
@@ -163,7 +165,13 @@ export default class Line extends React.PureComponent<Props> {
             />
           ))}
 
-        {displayCoverage && <LineCoverage line={line} />}
+        {displayCoverage && (
+          <LineCoverage
+            line={line}
+            scroll={this.props.scroll}
+            scrollToUncoveredLine={scrollToUncoveredLine}
+          />
+        )}
 
         <LineCode
           branchLike={branchLike}
index 3119c5ec589569a210f980dbca2cb99492757549..51281973fb9aaf9a0d0900a4ceedec0dc9e4793e 100644 (file)
@@ -23,16 +23,25 @@ import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n
 
 export interface LineCoverageProps {
   line: T.SourceLine;
+  scroll?: (element: HTMLElement) => void;
+  scrollToUncoveredLine?: boolean;
 }
 
-export function LineCoverage({ line }: LineCoverageProps) {
+export function LineCoverage({ line, scroll, scrollToUncoveredLine }: LineCoverageProps) {
+  const coverageMarker = React.useRef<HTMLTableDataCellElement>(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 (
-    <td className={className} data-line-number={line.line}>
+    <td className={className} data-line-number={line.line} ref={coverageMarker}>
       <Tooltip overlay={status} placement="right">
         <div aria-label={status} className="source-line-bar" />
       </Tooltip>
index 41a8a4b164d4e75e8d2d595c8c875746c680fbab..b1868569b91173ef4b5716caf5308a98b01f5615 100644 (file)
@@ -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<LineCoverageProps> = {}) {
   return shallow(<LineCoverage line={{ line: 3, coverageStatus: 'covered' }} {...props} />);
 }
index ec5a30fd9ec290c490f627aaf5b0012888eb6d7b..2cc3c08322d8644419712a3143c2802bd724dc3b 100644 (file)
@@ -328,6 +328,7 @@ exports[`should render correctly with coverage 1`] = `
         "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
       }
     }
+    scroll={[MockFunction]}
   />
   <LineCode
     branchLike={