瀏覽代碼

SONAR-11489 drop listing of tests that cover a line

tags/7.5
Stas Vilchik 5 年之前
父節點
當前提交
f316b94c37

+ 3
- 1
server/sonar-web/src/main/js/app/types.d.ts 查看文件

@@ -752,7 +752,7 @@ declare namespace T {
export interface SourceLine {
code?: string;
conditions?: number;
coverageStatus?: string;
coverageStatus?: SourceLineCoverageStatus;
coveredConditions?: number;
duplicated?: boolean;
isNew?: boolean;
@@ -763,6 +763,8 @@ declare namespace T {
scmRevision?: string;
}

export type SourceLineCoverageStatus = 'uncovered' | 'partially-covered' | 'covered';

export interface SourceViewerFile {
canMarkAsFavorite?: boolean;
fav?: boolean;

+ 0
- 184
server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx 查看文件

@@ -1,184 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { groupBy } from 'lodash';
import * as PropTypes from 'prop-types';
import { getTests } from '../../../api/components';
import { DropdownOverlay } from '../../controls/Dropdown';
import TestStatusIcon from '../../icons-components/TestStatusIcon';
import { PopupPlacement } from '../../ui/popups';
import { WorkspaceContext } from '../../workspace/context';
import {
isSameBranchLike,
getBranchLikeQuery,
isShortLivingBranch,
isPullRequest
} from '../../../helpers/branches';
import { translate } from '../../../helpers/l10n';
import { collapsePath } from '../../../helpers/path';

interface Props {
branchLike: T.BranchLike | undefined;
componentKey: string;
line: T.SourceLine;
onClose: () => void;
}

interface State {
loading: boolean;
testCases: T.TestCase[];
}

export default class CoveragePopup extends React.PureComponent<Props, State> {
context!: { workspace: WorkspaceContext };
mounted = false;

static contextTypes = {
workspace: PropTypes.object.isRequired
};

state: State = { loading: true, testCases: [] };

componentDidMount() {
this.mounted = true;
this.fetchTests();
}

componentDidUpdate(prevProps: Props) {
if (
!isSameBranchLike(prevProps.branchLike, this.props.branchLike) ||
prevProps.componentKey !== this.props.componentKey ||
prevProps.line.line !== this.props.line.line
) {
this.fetchTests();
}
}

componentWillUnmount() {
this.mounted = false;
}

shouldLink() {
const { branchLike } = this.props;
return !isShortLivingBranch(branchLike) && !isPullRequest(branchLike);
}

fetchTests = () => {
this.setState({ loading: true });
getTests({
sourceFileKey: this.props.componentKey,
sourceFileLineNumber: this.props.line.line,
...getBranchLikeQuery(this.props.branchLike)
}).then(
testCases => {
if (this.mounted) {
this.setState({ loading: false, testCases });
}
},
() => {
if (this.mounted) {
this.setState({ loading: false });
}
}
);
};

handleTestClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
const { key } = event.currentTarget.dataset;
if (this.shouldLink() && key) {
this.context.workspace.openComponent({ branchLike: this.props.branchLike, key });
}
this.props.onClose();
};

renderFile(file: { key: string; longName: string }) {
return this.shouldLink() ? (
<a data-key={file.key} href="#" onClick={this.handleTestClick} title={file.longName}>
<span>{collapsePath(file.longName)}</span>
</a>
) : (
<span>{collapsePath(file.longName)}</span>
);
}

render() {
const { line } = this.props;
const testCasesByFile = groupBy(this.state.testCases || [], 'fileKey');
const testFiles = Object.keys(testCasesByFile).map(fileKey => {
const testSet = testCasesByFile[fileKey];
const test = testSet[0];
return {
file: { key: test.fileKey, longName: test.fileName },
tests: testSet
};
});

return (
<DropdownOverlay placement={PopupPlacement.RightTop}>
<div className="source-viewer-bubble-popup abs-width-400">
<h6 className="spacer-bottom">
{translate('source_viewer.covered')}
{!!line.conditions && (
<div>
{'('}
{line.coveredConditions || '0'}
{' of '}
{line.conditions} {translate('source_viewer.conditions')}
{')'}
</div>
)}
</h6>
{this.state.loading ? (
<i className="spinner" />
) : (
<>
{testFiles.length === 0 &&
translate('source_viewer.tooltip.no_information_about_tests')}
{testFiles.map(testFile => (
<div className="spacer-top text-ellipsis" key={testFile.file.key}>
{this.renderFile(testFile.file)}
<ul>
{testFile.tests.map(testCase => (
<li
className="display-flex-center little-spacer-top"
key={testCase.id}
title={testCase.name}>
<TestStatusIcon className="spacer-right" status={testCase.status} />
<div className="display-inline-block text-ellipsis">{testCase.name}</div>
{testCase.status !== 'SKIPPED' && (
<span className="spacer-left note">
{testCase.durationInMs}
ms
</span>
)}
</li>
))}
</ul>
</div>
))}
</>
)}
</div>
</DropdownOverlay>
);
}
}

+ 1
- 9
server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx 查看文件

@@ -126,15 +126,7 @@ export default class Line extends React.PureComponent<Props> {
previousLine={this.props.previousLine}
/>

{this.props.displayCoverage && (
<LineCoverage
branchLike={this.props.branchLike}
componentKey={this.props.componentKey}
line={line}
onPopupToggle={this.props.onLinePopupToggle}
popupOpen={this.isPopupOpen('coverage')}
/>
)}
{this.props.displayCoverage && <LineCoverage line={line} />}

{this.props.displayDuplications && (
<LineDuplications line={line} onClick={this.props.loadDuplications} />

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

@@ -18,85 +18,49 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import CoveragePopup from './CoveragePopup';
import Tooltip from '../../controls/Tooltip';
import Toggler from '../../controls/Toggler';
import { translate } from '../../../helpers/l10n';
import { translate, translateWithParameters } from '../../../helpers/l10n';

interface Props {
branchLike: T.BranchLike | undefined;
componentKey: string;
line: T.SourceLine;
onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void;
popupOpen: boolean;
}

export default class LineCoverage extends React.PureComponent<Props> {
handleClick = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
event.stopPropagation();
event.currentTarget.blur();
this.props.onPopupToggle({ line: this.props.line.line, name: 'coverage' });
};

handleTogglePopup = (open: boolean) => {
this.props.onPopupToggle({ line: this.props.line.line, name: 'coverage', open });
};

closePopup = () => {
this.props.onPopupToggle({ line: this.props.line.line, name: 'coverage', open: false });
};

render() {
const { branchLike, componentKey, line, popupOpen } = this.props;

const className =
'source-meta source-line-coverage' +
(line.coverageStatus != null ? ` source-line-${line.coverageStatus}` : '');

const hasPopup =
line.coverageStatus === 'covered' || line.coverageStatus === 'partially-covered';

const bar = hasPopup ? (
<div className="source-line-bar" onClick={this.handleClick} role="button" tabIndex={0} />
) : (
<div className="source-line-bar" />
);

const cell = line.coverageStatus ? (
<Tooltip
overlay={popupOpen ? undefined : translate('source_viewer.tooltip', line.coverageStatus)}
placement="right">
{bar}
export default function LineCoverage({ line }: Props) {
const className =
'source-meta source-line-coverage' +
(line.coverageStatus != null ? ` source-line-${line.coverageStatus}` : '');
return (
<td className={className} data-line-number={line.line}>
<Tooltip overlay={getStatusTooltip(line)} placement="right">
<div className="source-line-bar" />
</Tooltip>
) : (
bar
);
</td>
);
}

if (hasPopup) {
return (
<td className={className} data-line-number={line.line}>
<Toggler
onRequestClose={this.closePopup}
open={popupOpen}
overlay={
<CoveragePopup
branchLike={branchLike}
componentKey={componentKey}
line={line}
onClose={this.closePopup}
/>
}>
{cell}
</Toggler>
</td>
function getStatusTooltip(line: T.SourceLine) {
if (line.coverageStatus === 'uncovered') {
if (line.conditions) {
return translateWithParameters('source_viewer.tooltip.uncovered.conditions', line.conditions);
} else {
return translate('source_viewer.tooltip.uncovered');
}
} else if (line.coverageStatus === 'covered') {
if (line.conditions) {
return translateWithParameters('source_viewer.tooltip.covered.conditions', line.conditions);
} else {
return translate('source_viewer.tooltip.covered');
}
} else if (line.coverageStatus === 'partially-covered') {
if (line.conditions) {
return translateWithParameters(
'source_viewer.tooltip.partially-covered.conditions',
line.coveredConditions || 0,
line.conditions
);
} else {
return translate('source_viewer.tooltip.partially-covered');
}

return (
<td className={className} data-line-number={line.line}>
{cell}
</td>
);
}
return undefined;
}

+ 3
- 45
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx 查看文件

@@ -20,63 +20,21 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import LineCoverage from '../LineCoverage';
import { click } from '../../../../helpers/testUtils';

it('render covered line', () => {
const line: T.SourceLine = { line: 3, coverageStatus: 'covered' };
const wrapper = shallow(
<LineCoverage
branchLike={undefined}
componentKey="foo"
line={line}
onPopupToggle={jest.fn()}
popupOpen={false}
/>
);
const wrapper = shallow(<LineCoverage line={line} />);
expect(wrapper).toMatchSnapshot();
click(wrapper.find('[tabIndex]'));
});

it('render uncovered line', () => {
const line: T.SourceLine = { line: 3, coverageStatus: 'uncovered' };
const wrapper = shallow(
<LineCoverage
branchLike={undefined}
componentKey="foo"
line={line}
onPopupToggle={jest.fn()}
popupOpen={false}
/>
);
const wrapper = shallow(<LineCoverage line={line} />);
expect(wrapper).toMatchSnapshot();
});

it('render line with unknown coverage', () => {
const line: T.SourceLine = { line: 3 };
const wrapper = shallow(
<LineCoverage
branchLike={undefined}
componentKey="foo"
line={line}
onPopupToggle={jest.fn()}
popupOpen={false}
/>
);
const wrapper = shallow(<LineCoverage line={line} />);
expect(wrapper).toMatchSnapshot();
});

it('should open coverage popup', () => {
const line: T.SourceLine = { line: 3, coverageStatus: 'covered' };
const onPopupToggle = jest.fn();
const wrapper = shallow(
<LineCoverage
branchLike={undefined}
componentKey="foo"
line={line}
onPopupToggle={onPopupToggle}
popupOpen={false}
/>
);
click(wrapper.find('[role="button"]'));
expect(onPopupToggle).toBeCalledWith({ line: 3, name: 'coverage' });
});

+ 14
- 30
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap 查看文件

@@ -5,34 +5,14 @@ exports[`render covered line 1`] = `
className="source-meta source-line-coverage source-line-covered"
data-line-number={3}
>
<Toggler
onRequestClose={[Function]}
open={false}
overlay={
<CoveragePopup
componentKey="foo"
line={
Object {
"coverageStatus": "covered",
"line": 3,
}
}
onClose={[Function]}
/>
}
<Tooltip
overlay="source_viewer.tooltip.covered"
placement="right"
>
<Tooltip
overlay="source_viewer.tooltip.covered"
placement="right"
>
<div
className="source-line-bar"
onClick={[Function]}
role="button"
tabIndex={0}
/>
</Tooltip>
</Toggler>
<div
className="source-line-bar"
/>
</Tooltip>
</td>
`;

@@ -41,9 +21,13 @@ exports[`render line with unknown coverage 1`] = `
className="source-meta source-line-coverage"
data-line-number={3}
>
<div
className="source-line-bar"
/>
<Tooltip
placement="right"
>
<div
className="source-line-bar"
/>
</Tooltip>
</td>
`;


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

@@ -18,8 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

export default function getCoverageStatus(s: T.SourceLine): string | undefined {
let status: string | undefined;
export default function getCoverageStatus(s: T.SourceLine): T.SourceLineCoverageStatus | undefined {
let status: T.SourceLineCoverageStatus | undefined;
if (s.lineHits != null && s.lineHits > 0) {
status = 'partially-covered';
}

+ 3
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties 查看文件

@@ -2211,8 +2211,11 @@ source_viewer.conditions=conditions
source_viewer.tooltip.duplicated_line=This line is duplicated. Click to see duplicated blocks.
source_viewer.tooltip.duplicated_block=Duplicated block. Click for details.
source_viewer.tooltip.covered=Fully covered by tests.
source_viewer.tooltip.covered.conditions=Fully covered by tests ({0} conditions).
source_viewer.tooltip.partially-covered=Partially covered by tests.
source_viewer.tooltip.partially-covered.conditions=Partially covered by tests ({0} of {1} conditions).
source_viewer.tooltip.uncovered=Not covered by tests.
source_viewer.tooltip.uncovered.conditions=Not covered by tests ({0} conditions).
source_viewer.tooltip.no_information_about_tests=There is no extra information about test files.

source_viewer.load_more_code=Load More Code

Loading…
取消
儲存