]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11887 Inverse issue icons and coverage/duplication information in codeviewer...
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Mon, 8 Apr 2019 07:24:06 +0000 (09:24 +0200)
committerSonarTech <sonartech@sonarsource.com>
Tue, 23 Apr 2019 18:21:10 +0000 (20:21 +0200)
* Display issues type icons instead of severity icons in codeviewer gutter

16 files changed:
server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/Line-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/Line-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/styles.css
server/sonar-web/src/main/js/components/icons-components/IssueIcon.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/icons-components/__tests__/IssueIcon-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/icons-components/__tests__/__snapshots__/IssueIcon-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/ui/IssueTypeIcon.tsx
server/sonar-web/src/main/js/components/ui/__tests__/IssueTypeIcon-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/IssueTypeIcon-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/__tests__/issues-test.ts
server/sonar-web/src/main/js/helpers/issues.ts
server/sonar-web/src/main/js/helpers/testMocks.ts

index 34da94c78b8adb9ac2eae283c7ed33b662bffa4e..27574086a84fe7612f5b23d2de15aa7d39208119 100644 (file)
@@ -123,7 +123,13 @@ export default class Line extends React.PureComponent<Props> {
           previousLine={this.props.previousLine}
         />
 
-        {this.props.displayCoverage && <LineCoverage line={line} />}
+        {this.props.displayIssues && !this.props.displayAllIssues && (
+          <LineIssuesIndicator
+            issues={this.props.issues}
+            line={line}
+            onClick={this.handleIssuesIndicatorClick}
+          />
+        )}
 
         {this.props.displayDuplications && (
           <LineDuplications line={line} onClick={this.props.loadDuplications} />
@@ -141,13 +147,7 @@ export default class Line extends React.PureComponent<Props> {
           />
         ))}
 
-        {this.props.displayIssues && !this.props.displayAllIssues && (
-          <LineIssuesIndicator
-            issues={this.props.issues}
-            line={line}
-            onClick={this.handleIssuesIndicatorClick}
-          />
-        )}
+        {this.props.displayCoverage && <LineCoverage line={line} />}
 
         <LineCode
           branchLike={this.props.branchLike}
index 05411d102d4d20e5569f937b2b64c1d62cf5639d..c8d0744a4f2d0550a6ff94cd4ddbd899782b4309 100644 (file)
@@ -19,8 +19,8 @@
  */
 import * as React from 'react';
 import * as classNames from 'classnames';
-import SeverityIcon from '../../icons-components/SeverityIcon';
-import { sortBySeverity } from '../../../helpers/issues';
+import IssueIcon from '../../icons-components/IssueIcon';
+import { sortByType } from '../../../helpers/issues';
 
 interface Props {
   issues: T.Issue[];
@@ -40,7 +40,7 @@ export default class LineIssuesIndicator extends React.PureComponent<Props> {
     const className = classNames('source-meta', 'source-line-issues', {
       'source-line-with-issues': hasIssues
     });
-    const mostImportantIssue = hasIssues ? sortBySeverity(issues)[0] : null;
+    const mostImportantIssue = hasIssues ? sortByType(issues)[0] : null;
 
     return (
       <td
@@ -49,7 +49,7 @@ export default class LineIssuesIndicator extends React.PureComponent<Props> {
         onClick={hasIssues ? this.handleClick : undefined}
         role={hasIssues ? 'button' : undefined}
         tabIndex={hasIssues ? 0 : undefined}>
-        {mostImportantIssue != null && <SeverityIcon severity={mostImportantIssue.severity} />}
+        {mostImportantIssue != null && <IssueIcon type={mostImportantIssue.type} />}
         {issues.length > 1 && <span className="source-line-issues-counter">{issues.length}</span>}
       </td>
     );
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/Line-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/Line-test.tsx
new file mode 100644 (file)
index 0000000..0281bf0
--- /dev/null
@@ -0,0 +1,129 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { shallow } from 'enzyme';
+import Line from '../Line';
+import { mockPullRequest, mockSourceLine, mockIssue } from '../../../../helpers/testMocks';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should render correctly for last, new, and highlighted lines', () => {
+  expect(
+    shallowRender({
+      highlighted: true,
+      last: true,
+      line: mockSourceLine({ isNew: true })
+    })
+  ).toMatchSnapshot();
+});
+
+it('should render correctly with coverage', () => {
+  expect(
+    shallowRender({
+      displayCoverage: true
+    })
+  ).toMatchSnapshot();
+});
+
+it('should render correctly with duplication information', () => {
+  expect(
+    shallowRender({
+      displayDuplications: true,
+      duplicationsCount: 3
+    })
+  ).toMatchSnapshot();
+});
+
+it('should render correctly with issues info', () => {
+  expect(shallowRender({ displayIssues: true })).toMatchSnapshot();
+});
+
+it('handles the opening and closing of issues', () => {
+  const line = mockSourceLine();
+  const issue = mockIssue();
+  const onIssuesClose = jest.fn();
+  const onIssueUnselect = jest.fn();
+  const onIssuesOpen = jest.fn();
+  const onIssueSelect = jest.fn();
+  const wrapper = shallowRender({
+    issues: [issue],
+    line,
+    onIssuesClose,
+    onIssueSelect,
+    onIssuesOpen,
+    onIssueUnselect,
+    openIssues: true
+  });
+  const instance = wrapper.instance();
+
+  instance.handleIssuesIndicatorClick();
+  expect(onIssuesClose).toBeCalledWith(line);
+  expect(onIssueUnselect).toBeCalled();
+
+  wrapper.setProps({ openIssues: false });
+  instance.handleIssuesIndicatorClick();
+  expect(onIssuesOpen).toBeCalledWith(line);
+  expect(onIssueSelect).toBeCalledWith(issue.key);
+});
+
+function shallowRender(props: Partial<Line['props']> = {}) {
+  return shallow<Line>(
+    <Line
+      branchLike={mockPullRequest()}
+      displayAllIssues={false}
+      displayCoverage={false}
+      displayDuplications={false}
+      displayIssueLocationsCount={false}
+      displayIssueLocationsLink={false}
+      displayIssues={false}
+      displayLocationMarkers={false}
+      duplications={[]}
+      duplicationsCount={0}
+      highlighted={false}
+      highlightedLocationMessage={undefined}
+      highlightedSymbols={undefined}
+      issueLocations={[]}
+      issuePopup={undefined}
+      issues={[mockIssue(), mockIssue(false, { type: 'VULNERABILITY' })]}
+      last={false}
+      line={mockSourceLine()}
+      linePopup={undefined}
+      loadDuplications={jest.fn()}
+      onLinePopupToggle={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()}
+      openIssues={false}
+      previousLine={undefined}
+      renderDuplicationPopup={jest.fn()}
+      scroll={jest.fn()}
+      secondaryIssueLocations={[]}
+      selectedIssue={undefined}
+      {...props}
+    />
+  );
+}
index 96cf5b251a03f31e6a276c7c2465ba2421283c1a..f3a5ac92361b2a8db787e34011729ecb9fc01a0f 100644 (file)
  */
 import * as React from 'react';
 import { shallow } from 'enzyme';
-import { click } from '../../../../helpers/testUtils';
 import LineIssuesIndicator from '../LineIssuesIndicator';
+import { click } from '../../../../helpers/testUtils';
+import { mockIssue } from '../../../../helpers/testMocks';
 
-const issueBase: T.Issue = {
-  actions: [],
-  component: '',
-  componentLongName: '',
-  componentQualifier: '',
-  componentUuid: '',
-  creationDate: '',
-  key: '',
-  flows: [],
-  fromHotspot: false,
-  message: '',
-  organization: '',
-  project: '',
-  projectName: '',
-  projectOrganization: '',
-  projectKey: '',
-  rule: '',
-  ruleName: '',
-  secondaryLocations: [],
-  severity: '',
-  status: '',
-  transitions: [],
-  type: 'BUG'
-};
-
-it('render highest severity', () => {
-  const line = { line: 3 };
-  const issues = [
-    { ...issueBase, key: 'foo', severity: 'MINOR' },
-    { ...issueBase, key: 'bar', severity: 'CRITICAL' }
-  ];
+it('should render correctly', () => {
   const onClick = jest.fn();
-  const wrapper = shallow(<LineIssuesIndicator issues={issues} line={line} onClick={onClick} />);
+  const wrapper = shallowRender({ onClick });
   expect(wrapper).toMatchSnapshot();
 
   click(wrapper);
   expect(onClick).toHaveBeenCalled();
 
-  const nextIssues = [{ severity: 'MINOR' }, { severity: 'INFO' }];
+  const nextIssues = [
+    mockIssue(false, { key: 'foo', type: 'VULNERABILITY' }),
+    mockIssue(false, { key: 'bar', type: 'SECURITY_HOTSPOT' })
+  ];
   wrapper.setProps({ issues: nextIssues });
   expect(wrapper).toMatchSnapshot();
 });
 
-it('no issues', () => {
-  const line = { line: 3 };
-  const issues: T.Issue[] = [];
-  const onClick = jest.fn();
-  const wrapper = shallow(<LineIssuesIndicator issues={issues} line={line} onClick={onClick} />);
-  expect(wrapper).toMatchSnapshot();
+it('should render correctly for no issues', () => {
+  expect(shallowRender({ issues: [] })).toMatchSnapshot();
 });
+
+function shallowRender(props: Partial<LineIssuesIndicator['props']> = {}) {
+  return shallow(
+    <LineIssuesIndicator
+      issues={[
+        mockIssue(false, { key: 'foo', type: 'CODE_SMELL' }),
+        mockIssue(false, { key: 'bar', type: 'BUG' })
+      ]}
+      line={{ line: 3 }}
+      onClick={jest.fn()}
+      {...props}
+    />
+  );
+}
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
new file mode 100644 (file)
index 0000000..cc374a6
--- /dev/null
@@ -0,0 +1,800 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<tr
+  className="source-line"
+  data-line-number={5}
+>
+  <LineNumber
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "line": 5,
+      }
+    }
+    onPopupToggle={[MockFunction]}
+    popupOpen={false}
+  />
+  <LineSCM
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "line": 5,
+      }
+    }
+    onPopupToggle={[MockFunction]}
+    popupOpen={false}
+  />
+  <LineCode
+    branchLike={
+      Object {
+        "analysisDate": "2018-01-01",
+        "base": "master",
+        "branch": "feature/foo/bar",
+        "key": "1001",
+        "target": "master",
+        "title": "Foo Bar feature",
+      }
+    }
+    displayIssueLocationsCount={false}
+    displayIssueLocationsLink={false}
+    displayLocationMarkers={false}
+    issueLocations={Array []}
+    issues={
+      Array [
+        Object {
+          "actions": Array [],
+          "component": "main.js",
+          "componentLongName": "main.js",
+          "componentQualifier": "FIL",
+          "componentUuid": "foo1234",
+          "creationDate": "2017-03-01T09:36:01+0100",
+          "flows": Array [],
+          "fromHotspot": false,
+          "key": "AVsae-CQS-9G3txfbFN2",
+          "line": 25,
+          "message": "Reduce the number of conditional operators (4) used in the expression",
+          "organization": "myorg",
+          "project": "myproject",
+          "projectKey": "foo",
+          "projectName": "Foo",
+          "projectOrganization": "org",
+          "rule": "javascript:S1067",
+          "ruleName": "foo",
+          "secondaryLocations": Array [],
+          "severity": "MAJOR",
+          "status": "OPEN",
+          "textRange": Object {
+            "endLine": 26,
+            "endOffset": 15,
+            "startLine": 25,
+            "startOffset": 0,
+          },
+          "transitions": Array [],
+          "type": "BUG",
+        },
+        Object {
+          "actions": Array [],
+          "component": "main.js",
+          "componentLongName": "main.js",
+          "componentQualifier": "FIL",
+          "componentUuid": "foo1234",
+          "creationDate": "2017-03-01T09:36:01+0100",
+          "flows": Array [],
+          "fromHotspot": false,
+          "key": "AVsae-CQS-9G3txfbFN2",
+          "line": 25,
+          "message": "Reduce the number of conditional operators (4) used in the expression",
+          "organization": "myorg",
+          "project": "myproject",
+          "projectKey": "foo",
+          "projectName": "Foo",
+          "projectOrganization": "org",
+          "rule": "javascript:S1067",
+          "ruleName": "foo",
+          "secondaryLocations": Array [],
+          "severity": "MAJOR",
+          "status": "OPEN",
+          "textRange": Object {
+            "endLine": 26,
+            "endOffset": 15,
+            "startLine": 25,
+            "startOffset": 0,
+          },
+          "transitions": Array [],
+          "type": "VULNERABILITY",
+        },
+      ]
+    }
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "line": 5,
+      }
+    }
+    onIssueChange={[MockFunction]}
+    onIssuePopupToggle={[MockFunction]}
+    onIssueSelect={[MockFunction]}
+    onLocationSelect={[MockFunction]}
+    onSymbolClick={[MockFunction]}
+    scroll={[MockFunction]}
+    secondaryIssueLocations={Array []}
+    showIssues={false}
+  />
+</tr>
+`;
+
+exports[`should render correctly for last, new, and highlighted lines 1`] = `
+<tr
+  className="source-line source-line-highlighted source-line-filtered source-line-last"
+  data-line-number={5}
+>
+  <LineNumber
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "isNew": true,
+        "line": 5,
+      }
+    }
+    onPopupToggle={[MockFunction]}
+    popupOpen={false}
+  />
+  <LineSCM
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "isNew": true,
+        "line": 5,
+      }
+    }
+    onPopupToggle={[MockFunction]}
+    popupOpen={false}
+  />
+  <LineCode
+    branchLike={
+      Object {
+        "analysisDate": "2018-01-01",
+        "base": "master",
+        "branch": "feature/foo/bar",
+        "key": "1001",
+        "target": "master",
+        "title": "Foo Bar feature",
+      }
+    }
+    displayIssueLocationsCount={false}
+    displayIssueLocationsLink={false}
+    displayLocationMarkers={false}
+    issueLocations={Array []}
+    issues={
+      Array [
+        Object {
+          "actions": Array [],
+          "component": "main.js",
+          "componentLongName": "main.js",
+          "componentQualifier": "FIL",
+          "componentUuid": "foo1234",
+          "creationDate": "2017-03-01T09:36:01+0100",
+          "flows": Array [],
+          "fromHotspot": false,
+          "key": "AVsae-CQS-9G3txfbFN2",
+          "line": 25,
+          "message": "Reduce the number of conditional operators (4) used in the expression",
+          "organization": "myorg",
+          "project": "myproject",
+          "projectKey": "foo",
+          "projectName": "Foo",
+          "projectOrganization": "org",
+          "rule": "javascript:S1067",
+          "ruleName": "foo",
+          "secondaryLocations": Array [],
+          "severity": "MAJOR",
+          "status": "OPEN",
+          "textRange": Object {
+            "endLine": 26,
+            "endOffset": 15,
+            "startLine": 25,
+            "startOffset": 0,
+          },
+          "transitions": Array [],
+          "type": "BUG",
+        },
+        Object {
+          "actions": Array [],
+          "component": "main.js",
+          "componentLongName": "main.js",
+          "componentQualifier": "FIL",
+          "componentUuid": "foo1234",
+          "creationDate": "2017-03-01T09:36:01+0100",
+          "flows": Array [],
+          "fromHotspot": false,
+          "key": "AVsae-CQS-9G3txfbFN2",
+          "line": 25,
+          "message": "Reduce the number of conditional operators (4) used in the expression",
+          "organization": "myorg",
+          "project": "myproject",
+          "projectKey": "foo",
+          "projectName": "Foo",
+          "projectOrganization": "org",
+          "rule": "javascript:S1067",
+          "ruleName": "foo",
+          "secondaryLocations": Array [],
+          "severity": "MAJOR",
+          "status": "OPEN",
+          "textRange": Object {
+            "endLine": 26,
+            "endOffset": 15,
+            "startLine": 25,
+            "startOffset": 0,
+          },
+          "transitions": Array [],
+          "type": "VULNERABILITY",
+        },
+      ]
+    }
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "isNew": true,
+        "line": 5,
+      }
+    }
+    onIssueChange={[MockFunction]}
+    onIssuePopupToggle={[MockFunction]}
+    onIssueSelect={[MockFunction]}
+    onLocationSelect={[MockFunction]}
+    onSymbolClick={[MockFunction]}
+    scroll={[MockFunction]}
+    secondaryIssueLocations={Array []}
+    showIssues={false}
+  />
+</tr>
+`;
+
+exports[`should render correctly with coverage 1`] = `
+<tr
+  className="source-line"
+  data-line-number={5}
+>
+  <LineNumber
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "line": 5,
+      }
+    }
+    onPopupToggle={[MockFunction]}
+    popupOpen={false}
+  />
+  <LineSCM
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "line": 5,
+      }
+    }
+    onPopupToggle={[MockFunction]}
+    popupOpen={false}
+  />
+  <LineCoverage
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "line": 5,
+      }
+    }
+  />
+  <LineCode
+    branchLike={
+      Object {
+        "analysisDate": "2018-01-01",
+        "base": "master",
+        "branch": "feature/foo/bar",
+        "key": "1001",
+        "target": "master",
+        "title": "Foo Bar feature",
+      }
+    }
+    displayIssueLocationsCount={false}
+    displayIssueLocationsLink={false}
+    displayLocationMarkers={false}
+    issueLocations={Array []}
+    issues={
+      Array [
+        Object {
+          "actions": Array [],
+          "component": "main.js",
+          "componentLongName": "main.js",
+          "componentQualifier": "FIL",
+          "componentUuid": "foo1234",
+          "creationDate": "2017-03-01T09:36:01+0100",
+          "flows": Array [],
+          "fromHotspot": false,
+          "key": "AVsae-CQS-9G3txfbFN2",
+          "line": 25,
+          "message": "Reduce the number of conditional operators (4) used in the expression",
+          "organization": "myorg",
+          "project": "myproject",
+          "projectKey": "foo",
+          "projectName": "Foo",
+          "projectOrganization": "org",
+          "rule": "javascript:S1067",
+          "ruleName": "foo",
+          "secondaryLocations": Array [],
+          "severity": "MAJOR",
+          "status": "OPEN",
+          "textRange": Object {
+            "endLine": 26,
+            "endOffset": 15,
+            "startLine": 25,
+            "startOffset": 0,
+          },
+          "transitions": Array [],
+          "type": "BUG",
+        },
+        Object {
+          "actions": Array [],
+          "component": "main.js",
+          "componentLongName": "main.js",
+          "componentQualifier": "FIL",
+          "componentUuid": "foo1234",
+          "creationDate": "2017-03-01T09:36:01+0100",
+          "flows": Array [],
+          "fromHotspot": false,
+          "key": "AVsae-CQS-9G3txfbFN2",
+          "line": 25,
+          "message": "Reduce the number of conditional operators (4) used in the expression",
+          "organization": "myorg",
+          "project": "myproject",
+          "projectKey": "foo",
+          "projectName": "Foo",
+          "projectOrganization": "org",
+          "rule": "javascript:S1067",
+          "ruleName": "foo",
+          "secondaryLocations": Array [],
+          "severity": "MAJOR",
+          "status": "OPEN",
+          "textRange": Object {
+            "endLine": 26,
+            "endOffset": 15,
+            "startLine": 25,
+            "startOffset": 0,
+          },
+          "transitions": Array [],
+          "type": "VULNERABILITY",
+        },
+      ]
+    }
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "line": 5,
+      }
+    }
+    onIssueChange={[MockFunction]}
+    onIssuePopupToggle={[MockFunction]}
+    onIssueSelect={[MockFunction]}
+    onLocationSelect={[MockFunction]}
+    onSymbolClick={[MockFunction]}
+    scroll={[MockFunction]}
+    secondaryIssueLocations={Array []}
+    showIssues={false}
+  />
+</tr>
+`;
+
+exports[`should render correctly with duplication information 1`] = `
+<tr
+  className="source-line"
+  data-line-number={5}
+>
+  <LineNumber
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "line": 5,
+      }
+    }
+    onPopupToggle={[MockFunction]}
+    popupOpen={false}
+  />
+  <LineSCM
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "line": 5,
+      }
+    }
+    onPopupToggle={[MockFunction]}
+    popupOpen={false}
+  />
+  <LineDuplications
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "line": 5,
+      }
+    }
+    onClick={[MockFunction]}
+  />
+  <LineDuplicationBlock
+    duplicated={false}
+    index={0}
+    key="0"
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "line": 5,
+      }
+    }
+    onPopupToggle={[MockFunction]}
+    popupOpen={false}
+    renderDuplicationPopup={[MockFunction]}
+  />
+  <LineDuplicationBlock
+    duplicated={false}
+    index={1}
+    key="1"
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "line": 5,
+      }
+    }
+    onPopupToggle={[MockFunction]}
+    popupOpen={false}
+    renderDuplicationPopup={[MockFunction]}
+  />
+  <LineDuplicationBlock
+    duplicated={false}
+    index={2}
+    key="2"
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "line": 5,
+      }
+    }
+    onPopupToggle={[MockFunction]}
+    popupOpen={false}
+    renderDuplicationPopup={[MockFunction]}
+  />
+  <LineCode
+    branchLike={
+      Object {
+        "analysisDate": "2018-01-01",
+        "base": "master",
+        "branch": "feature/foo/bar",
+        "key": "1001",
+        "target": "master",
+        "title": "Foo Bar feature",
+      }
+    }
+    displayIssueLocationsCount={false}
+    displayIssueLocationsLink={false}
+    displayLocationMarkers={false}
+    issueLocations={Array []}
+    issues={
+      Array [
+        Object {
+          "actions": Array [],
+          "component": "main.js",
+          "componentLongName": "main.js",
+          "componentQualifier": "FIL",
+          "componentUuid": "foo1234",
+          "creationDate": "2017-03-01T09:36:01+0100",
+          "flows": Array [],
+          "fromHotspot": false,
+          "key": "AVsae-CQS-9G3txfbFN2",
+          "line": 25,
+          "message": "Reduce the number of conditional operators (4) used in the expression",
+          "organization": "myorg",
+          "project": "myproject",
+          "projectKey": "foo",
+          "projectName": "Foo",
+          "projectOrganization": "org",
+          "rule": "javascript:S1067",
+          "ruleName": "foo",
+          "secondaryLocations": Array [],
+          "severity": "MAJOR",
+          "status": "OPEN",
+          "textRange": Object {
+            "endLine": 26,
+            "endOffset": 15,
+            "startLine": 25,
+            "startOffset": 0,
+          },
+          "transitions": Array [],
+          "type": "BUG",
+        },
+        Object {
+          "actions": Array [],
+          "component": "main.js",
+          "componentLongName": "main.js",
+          "componentQualifier": "FIL",
+          "componentUuid": "foo1234",
+          "creationDate": "2017-03-01T09:36:01+0100",
+          "flows": Array [],
+          "fromHotspot": false,
+          "key": "AVsae-CQS-9G3txfbFN2",
+          "line": 25,
+          "message": "Reduce the number of conditional operators (4) used in the expression",
+          "organization": "myorg",
+          "project": "myproject",
+          "projectKey": "foo",
+          "projectName": "Foo",
+          "projectOrganization": "org",
+          "rule": "javascript:S1067",
+          "ruleName": "foo",
+          "secondaryLocations": Array [],
+          "severity": "MAJOR",
+          "status": "OPEN",
+          "textRange": Object {
+            "endLine": 26,
+            "endOffset": 15,
+            "startLine": 25,
+            "startOffset": 0,
+          },
+          "transitions": Array [],
+          "type": "VULNERABILITY",
+        },
+      ]
+    }
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "line": 5,
+      }
+    }
+    onIssueChange={[MockFunction]}
+    onIssuePopupToggle={[MockFunction]}
+    onIssueSelect={[MockFunction]}
+    onLocationSelect={[MockFunction]}
+    onSymbolClick={[MockFunction]}
+    scroll={[MockFunction]}
+    secondaryIssueLocations={Array []}
+    showIssues={false}
+  />
+</tr>
+`;
+
+exports[`should render correctly with issues info 1`] = `
+<tr
+  className="source-line"
+  data-line-number={5}
+>
+  <LineNumber
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "line": 5,
+      }
+    }
+    onPopupToggle={[MockFunction]}
+    popupOpen={false}
+  />
+  <LineSCM
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "line": 5,
+      }
+    }
+    onPopupToggle={[MockFunction]}
+    popupOpen={false}
+  />
+  <LineIssuesIndicator
+    issues={
+      Array [
+        Object {
+          "actions": Array [],
+          "component": "main.js",
+          "componentLongName": "main.js",
+          "componentQualifier": "FIL",
+          "componentUuid": "foo1234",
+          "creationDate": "2017-03-01T09:36:01+0100",
+          "flows": Array [],
+          "fromHotspot": false,
+          "key": "AVsae-CQS-9G3txfbFN2",
+          "line": 25,
+          "message": "Reduce the number of conditional operators (4) used in the expression",
+          "organization": "myorg",
+          "project": "myproject",
+          "projectKey": "foo",
+          "projectName": "Foo",
+          "projectOrganization": "org",
+          "rule": "javascript:S1067",
+          "ruleName": "foo",
+          "secondaryLocations": Array [],
+          "severity": "MAJOR",
+          "status": "OPEN",
+          "textRange": Object {
+            "endLine": 26,
+            "endOffset": 15,
+            "startLine": 25,
+            "startOffset": 0,
+          },
+          "transitions": Array [],
+          "type": "BUG",
+        },
+        Object {
+          "actions": Array [],
+          "component": "main.js",
+          "componentLongName": "main.js",
+          "componentQualifier": "FIL",
+          "componentUuid": "foo1234",
+          "creationDate": "2017-03-01T09:36:01+0100",
+          "flows": Array [],
+          "fromHotspot": false,
+          "key": "AVsae-CQS-9G3txfbFN2",
+          "line": 25,
+          "message": "Reduce the number of conditional operators (4) used in the expression",
+          "organization": "myorg",
+          "project": "myproject",
+          "projectKey": "foo",
+          "projectName": "Foo",
+          "projectOrganization": "org",
+          "rule": "javascript:S1067",
+          "ruleName": "foo",
+          "secondaryLocations": Array [],
+          "severity": "MAJOR",
+          "status": "OPEN",
+          "textRange": Object {
+            "endLine": 26,
+            "endOffset": 15,
+            "startLine": 25,
+            "startOffset": 0,
+          },
+          "transitions": Array [],
+          "type": "VULNERABILITY",
+        },
+      ]
+    }
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "line": 5,
+      }
+    }
+    onClick={[Function]}
+  />
+  <LineCode
+    branchLike={
+      Object {
+        "analysisDate": "2018-01-01",
+        "base": "master",
+        "branch": "feature/foo/bar",
+        "key": "1001",
+        "target": "master",
+        "title": "Foo Bar feature",
+      }
+    }
+    displayIssueLocationsCount={false}
+    displayIssueLocationsLink={false}
+    displayLocationMarkers={false}
+    issueLocations={Array []}
+    issues={
+      Array [
+        Object {
+          "actions": Array [],
+          "component": "main.js",
+          "componentLongName": "main.js",
+          "componentQualifier": "FIL",
+          "componentUuid": "foo1234",
+          "creationDate": "2017-03-01T09:36:01+0100",
+          "flows": Array [],
+          "fromHotspot": false,
+          "key": "AVsae-CQS-9G3txfbFN2",
+          "line": 25,
+          "message": "Reduce the number of conditional operators (4) used in the expression",
+          "organization": "myorg",
+          "project": "myproject",
+          "projectKey": "foo",
+          "projectName": "Foo",
+          "projectOrganization": "org",
+          "rule": "javascript:S1067",
+          "ruleName": "foo",
+          "secondaryLocations": Array [],
+          "severity": "MAJOR",
+          "status": "OPEN",
+          "textRange": Object {
+            "endLine": 26,
+            "endOffset": 15,
+            "startLine": 25,
+            "startOffset": 0,
+          },
+          "transitions": Array [],
+          "type": "BUG",
+        },
+        Object {
+          "actions": Array [],
+          "component": "main.js",
+          "componentLongName": "main.js",
+          "componentQualifier": "FIL",
+          "componentUuid": "foo1234",
+          "creationDate": "2017-03-01T09:36:01+0100",
+          "flows": Array [],
+          "fromHotspot": false,
+          "key": "AVsae-CQS-9G3txfbFN2",
+          "line": 25,
+          "message": "Reduce the number of conditional operators (4) used in the expression",
+          "organization": "myorg",
+          "project": "myproject",
+          "projectKey": "foo",
+          "projectName": "Foo",
+          "projectOrganization": "org",
+          "rule": "javascript:S1067",
+          "ruleName": "foo",
+          "secondaryLocations": Array [],
+          "severity": "MAJOR",
+          "status": "OPEN",
+          "textRange": Object {
+            "endLine": 26,
+            "endOffset": 15,
+            "startLine": 25,
+            "startOffset": 0,
+          },
+          "transitions": Array [],
+          "type": "VULNERABILITY",
+        },
+      ]
+    }
+    line={
+      Object {
+        "code": "function fooBar() {",
+        "coverageStatus": "covered",
+        "coveredConditions": 2,
+        "line": 5,
+      }
+    }
+    onIssueChange={[MockFunction]}
+    onIssuePopupToggle={[MockFunction]}
+    onIssueSelect={[MockFunction]}
+    onLocationSelect={[MockFunction]}
+    onSymbolClick={[MockFunction]}
+    scroll={[MockFunction]}
+    secondaryIssueLocations={Array []}
+    showIssues={false}
+  />
+</tr>
+`;
index e941c781cacf49b9378e89ef7daa656de290d927..fbb8d6eb661016eeefee548464378aa3764ff993 100644 (file)
@@ -1,13 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`no issues 1`] = `
-<td
-  className="source-meta source-line-issues"
-  data-line-number={3}
-/>
-`;
-
-exports[`render highest severity 1`] = `
+exports[`should render correctly 1`] = `
 <td
   className="source-meta source-line-issues source-line-with-issues"
   data-line-number={3}
@@ -15,8 +8,8 @@ exports[`render highest severity 1`] = `
   role="button"
   tabIndex={0}
 >
-  <SeverityIcon
-    severity="CRITICAL"
+  <IssueIcon
+    type="BUG"
   />
   <span
     className="source-line-issues-counter"
@@ -26,7 +19,7 @@ exports[`render highest severity 1`] = `
 </td>
 `;
 
-exports[`render highest severity 2`] = `
+exports[`should render correctly 2`] = `
 <td
   className="source-meta source-line-issues source-line-with-issues"
   data-line-number={3}
@@ -34,8 +27,8 @@ exports[`render highest severity 2`] = `
   role="button"
   tabIndex={0}
 >
-  <SeverityIcon
-    severity="MINOR"
+  <IssueIcon
+    type="VULNERABILITY"
   />
   <span
     className="source-line-issues-counter"
@@ -44,3 +37,10 @@ exports[`render highest severity 2`] = `
   </span>
 </td>
 `;
+
+exports[`should render correctly for no issues 1`] = `
+<td
+  className="source-meta source-line-issues"
+  data-line-number={3}
+/>
+`;
index beb73998c2ebd62bed2ad82d93006969132f14c8..84bd8a63a3ab26585aa6fbea65f63c37193239b5 100644 (file)
   position: relative;
   padding: 0 2px;
   background-color: var(--barBackgroundColor);
+  white-space: nowrap;
+}
+
+.source-line-with-issues {
+  padding-right: 4px;
 }
 
 .source-line-issues-counter {
   position: absolute;
-  top: -1px;
-  right: -1px;
+  left: 17px;
   line-height: 8px;
   font-size: 8px;
+  z-index: 900;
 }
 
 .source-line-coverage {
diff --git a/server/sonar-web/src/main/js/components/icons-components/IssueIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/IssueIcon.tsx
new file mode 100644 (file)
index 0000000..d408777
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 BugIcon from '../icons-components/BugIcon';
+import VulnerabilityIcon from '../icons-components/VulnerabilityIcon';
+import CodeSmellIcon from '../icons-components/CodeSmellIcon';
+import SecurityHotspotIcon from '../icons-components/SecurityHotspotIcon';
+
+interface Props {
+  className?: string;
+  type: T.IssueType;
+  size?: number;
+}
+
+export default function IssueIcon({ className, type, size }: Props) {
+  switch (type) {
+    case 'BUG':
+      return <BugIcon className={className} size={size} />;
+    case 'VULNERABILITY':
+      return <VulnerabilityIcon className={className} size={size} />;
+    case 'CODE_SMELL':
+      return <CodeSmellIcon className={className} size={size} />;
+    case 'SECURITY_HOTSPOT':
+      return <SecurityHotspotIcon className={className} size={size} />;
+    default:
+      return null;
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/icons-components/__tests__/IssueIcon-test.tsx b/server/sonar-web/src/main/js/components/icons-components/__tests__/IssueIcon-test.tsx
new file mode 100644 (file)
index 0000000..59f2d13
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { shallow } from 'enzyme';
+import IssueIcon from '../IssueIcon';
+
+it('should render correctly', () => {
+  expect(shallowRender('BUG')).toMatchSnapshot();
+  expect(shallowRender('VULNERABILITY')).toMatchSnapshot();
+  expect(shallowRender('CODE_SMELL')).toMatchSnapshot();
+  expect(shallowRender('SECURITY_HOTSPOT')).toMatchSnapshot();
+});
+
+function shallowRender(type: T.IssueType) {
+  return shallow(<IssueIcon type={type} />);
+}
diff --git a/server/sonar-web/src/main/js/components/icons-components/__tests__/__snapshots__/IssueIcon-test.tsx.snap b/server/sonar-web/src/main/js/components/icons-components/__tests__/__snapshots__/IssueIcon-test.tsx.snap
new file mode 100644 (file)
index 0000000..f3f5fcf
--- /dev/null
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `<BugIcon />`;
+
+exports[`should render correctly 2`] = `<VulnerabilityIcon />`;
+
+exports[`should render correctly 3`] = `<CodeSmellIcon />`;
+
+exports[`should render correctly 4`] = `<SecurityHotspotIcon />`;
index ac28aeabbd3f18405840c553bf38460b23843301..2ee9eba61171221dac0bba531112be992508dccc 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import BugIcon from '../icons-components/BugIcon';
-import VulnerabilityIcon from '../icons-components/VulnerabilityIcon';
-import CodeSmellIcon from '../icons-components/CodeSmellIcon';
-import SecurityHotspotIcon from '../icons-components/SecurityHotspotIcon';
+import IssueIcon from '../icons-components/IssueIcon';
 
-interface Props {
+export interface Props {
   className?: string;
   query: string;
   size?: number;
 }
 
 export default function IssueTypeIcon({ className, query, size }: Props) {
-  let icon;
+  let type: T.IssueType;
 
   switch (query.toLowerCase()) {
     case 'bug':
     case 'bugs':
     case 'new_bugs':
-      icon = <BugIcon size={size} />;
+      type = 'BUG';
       break;
     case 'vulnerability':
     case 'vulnerabilities':
     case 'new_vulnerabilities':
-      icon = <VulnerabilityIcon size={size} />;
+      type = 'VULNERABILITY';
       break;
     case 'code_smell':
     case 'code_smells':
     case 'new_code_smells':
-      icon = <CodeSmellIcon size={size} />;
+      type = 'CODE_SMELL';
       break;
     case 'security_hotspot':
     case 'security_hotspots':
-      icon = <SecurityHotspotIcon size={size} />;
+      type = 'SECURITY_HOTSPOT';
       break;
+    default:
+      return null;
   }
 
-  if (!icon) {
-    return null;
-  }
-
+  const icon = <IssueIcon size={size} type={type} />;
   return className ? <span className={className}>{icon}</span> : icon;
 }
diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/IssueTypeIcon-test.tsx b/server/sonar-web/src/main/js/components/ui/__tests__/IssueTypeIcon-test.tsx
new file mode 100644 (file)
index 0000000..a84350b
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { shallow } from 'enzyme';
+import IssueTypeIcon, { Props } from '../IssueTypeIcon';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot();
+  expect(shallowRender({ className: 'my-class', query: 'security_hotspots' })).toMatchSnapshot();
+  expect(shallowRender({ query: 'new_code_smells' })).toMatchSnapshot();
+  expect(shallowRender({ query: 'vulnerability' })).toMatchSnapshot();
+  expect(shallowRender({ query: 'unknown' }).type()).toBe(null);
+});
+
+function shallowRender(props: Partial<Props> = {}) {
+  return shallow(<IssueTypeIcon query="bugs" {...props} />);
+}
diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/IssueTypeIcon-test.tsx.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/IssueTypeIcon-test.tsx.snap
new file mode 100644 (file)
index 0000000..88faea5
--- /dev/null
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<IssueIcon
+  type="BUG"
+/>
+`;
+
+exports[`should render correctly 2`] = `
+<span
+  className="my-class"
+>
+  <IssueIcon
+    type="SECURITY_HOTSPOT"
+  />
+</span>
+`;
+
+exports[`should render correctly 3`] = `
+<IssueIcon
+  type="CODE_SMELL"
+/>
+`;
+
+exports[`should render correctly 4`] = `
+<IssueIcon
+  type="VULNERABILITY"
+/>
+`;
index 490da4ec17fb97d9d0454ab7ad53cf2e57757d81..34fb6fd865c4d755d91d6bde46859bfb1035d427 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { parseIssueFromResponse } from '../issues';
+import { parseIssueFromResponse, sortByType } from '../issues';
+import { mockIssue } from '../testMocks';
+
+it('should sort issues correctly by type', () => {
+  const bug1 = mockIssue(false, { type: 'BUG', key: 'bug1' });
+  const bug2 = mockIssue(false, { type: 'BUG', key: 'bug2' });
+  const codeSmell = mockIssue(false, { type: 'CODE_SMELL', key: 'code_smell' });
+  const vulnerability1 = mockIssue(false, { type: 'VULNERABILITY', key: 'vulnerability1' });
+  const vulnerability2 = mockIssue(false, { type: 'VULNERABILITY', key: 'vulnerability2' });
+  const securityHotspot = mockIssue(false, { type: 'SECURITY_HOTSPOT', key: 'security_hotspot' });
+
+  expect(
+    sortByType([bug1, codeSmell, bug2, securityHotspot, vulnerability1, vulnerability2])
+  ).toEqual([bug1, bug2, vulnerability1, vulnerability2, codeSmell, securityHotspot]);
+});
 
 it('should populate comments data', () => {
   const users = [
index 3a2fe6696a2db3114cfc6dbba6d33f3b18de4e8b..d0f90ebeb030e06a0bff627f6dc2d9aacfa79fc6 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { flatten, sortBy } from 'lodash';
-import { SEVERITIES } from './constants';
+import { ISSUE_TYPES } from './constants';
 
 interface Comment {
   login: string;
@@ -59,8 +59,8 @@ export interface RawIssue extends IssueBase {
   textRange?: T.TextRange;
 }
 
-export function sortBySeverity(issues: T.Issue[]): T.Issue[] {
-  return sortBy(issues, issue => SEVERITIES.indexOf(issue.severity));
+export function sortByType(issues: T.Issue[]): T.Issue[] {
+  return sortBy(issues, issue => ISSUE_TYPES.indexOf(issue.type));
 }
 
 function injectRelational(
index 1cc767c2b09ed00efcd6bed02e858009d8796b0e..dd21ec1fec25826b799d0b292b600c87122ab1b9 100644 (file)
@@ -477,6 +477,16 @@ export function mockStore(state: any = {}, reducer = (state: any) => state): Sto
   return createStore(reducer, state);
 }
 
+export function mockSourceLine(overrides: Partial<T.SourceLine> = {}): T.SourceLine {
+  return {
+    code: 'function fooBar() {',
+    coverageStatus: 'covered',
+    coveredConditions: 2,
+    line: 5,
+    ...overrides
+  };
+}
+
 export function mockDocumentationEntry(
   overrides: Partial<DocumentationEntry> = {}
 ): DocumentationEntry {