瀏覽代碼

SONAR-13217 Auto scroll to uncovered lines

tags/8.9.0.43852
Wouter Admiraal 3 年之前
父節點
當前提交
4dd729287b

+ 8
- 0
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<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>
) : (

+ 36
- 1
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<MeasureContent['props']> = {}) {
return shallow(
return shallow<MeasureContent>(
<MeasureContent
metrics={METRICS}
requestedMetric={{ direction: 1, key: 'bugs' }}

+ 2
- 0
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap 查看文件

@@ -97,6 +97,8 @@ exports[`should render correctly for a file 1`] = `
>
<SourceViewer
component="foo:src/index.tsx"
metricKey="bugs"
scroll={[Function]}
/>
</div>
</div>

+ 2
- 0
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<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}

+ 31
- 1
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<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)}
/>

+ 117
- 0
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<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}
/>
);
}

+ 593
- 0
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`] = `
<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>
`;

+ 9
- 1
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<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}

+ 11
- 2
server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx 查看文件

@@ -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>

+ 14
- 0
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<LineCoverageProps> = {}) {
return shallow(<LineCoverage line={{ line: 3, coverageStatus: 'covered' }} {...props} />);
}

+ 1
- 0
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]}
/>
<LineCode
branchLike={

Loading…
取消
儲存