*/
import * as React from 'react';
import * as classNames from 'classnames';
-import {
- createSnippets,
- expandSnippet,
- inSnippet,
- EXPAND_BY_LINES,
- LINES_BELOW_LAST,
- MERGE_DISTANCE
-} from './utils';
-import ExpandSnippetIcon from '../../../components/icons-components/ExpandSnippetIcon';
-import Line from '../../../components/SourceViewer/components/Line';
+import { createSnippets, expandSnippet, EXPAND_BY_LINES, MERGE_DISTANCE } from './utils';
+import SnippetViewer from './SnippetViewer';
import SourceViewerHeaderSlim from '../../../components/SourceViewer/SourceViewerHeaderSlim';
import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus';
import { getSources } from '../../../api/components';
-import { symbolsByLine, locationsByLine } from '../../../components/SourceViewer/helpers/indexing';
-import { getSecondaryIssueLocationsForLine } from '../../../components/SourceViewer/helpers/issueLocations';
-import {
- optimizeLocationMessage,
- optimizeHighlightedSymbols,
- optimizeSelectedIssue
-} from '../../../components/SourceViewer/helpers/lines';
-import { translate } from '../../../helpers/l10n';
+import { locationsByLine } from '../../../components/SourceViewer/helpers/indexing';
import { getBranchLikeQuery } from '../../../helpers/branches';
interface Props {
return this.props.renderDuplicationPopup(this.props.snippetGroup.component, index, line);
};
- renderLine({
- displayDuplications,
+ renderSnippet({
index,
- issuesForLine,
- issueLocations,
- line,
- snippet,
- symbols,
- verticalBuffer
+ issuesByLine,
+ last,
+ locationsByLine,
+ snippet
}: {
- displayDuplications: boolean;
index: number;
- issuesForLine: T.Issue[];
- issueLocations: T.LinearIssueLocation[];
- line: T.SourceLine;
+ issuesByLine: T.IssuesByLine;
+ last: boolean;
+ locationsByLine: { [line: number]: T.LinearIssueLocation[] };
snippet: T.SourceLine[];
- symbols: string[];
- verticalBuffer: number;
}) {
- const { openIssuesByLine } = this.state;
- const secondaryIssueLocations = getSecondaryIssueLocationsForLine(line, this.props.locations);
-
- const { duplications, duplicationsByLine } = this.props;
- const duplicationsCount = duplications ? duplications.length : 0;
- const lineDuplications =
- (duplicationsCount && duplicationsByLine && duplicationsByLine[line.line]) || [];
-
- const isSinkLine = issuesForLine.some(i => i.key === this.props.issue.key);
-
return (
- <Line
+ <SnippetViewer
branchLike={this.props.branchLike}
- displayAllIssues={false}
- displayCoverage={true}
- displayDuplications={displayDuplications}
- displayIssues={!isSinkLine || issuesForLine.length > 1}
- displayLocationMarkers={true}
- duplications={lineDuplications}
- duplicationsCount={duplicationsCount}
- highlighted={false}
- highlightedLocationMessage={optimizeLocationMessage(
- this.props.highlightedLocationMessage,
- secondaryIssueLocations
- )}
- highlightedSymbols={optimizeHighlightedSymbols(symbols, this.state.highlightedSymbols)}
- issueLocations={issueLocations}
- issuePopup={this.props.issuePopup}
- issues={issuesForLine}
- key={line.line}
- last={false}
- line={line}
- linePopup={this.props.linePopup}
+ component={this.props.snippetGroup.component}
+ expandBlock={this.expandBlock}
+ handleCloseIssues={this.handleCloseIssues}
+ handleLinePopupToggle={this.handleLinePopupToggle}
+ handleOpenIssues={this.handleOpenIssues}
+ handleSymbolClick={this.handleSymbolClick}
+ highlightedLocationMessage={this.props.highlightedLocationMessage}
+ highlightedSymbols={this.state.highlightedSymbols}
+ index={index}
+ issue={this.props.issue}
+ issuesByLine={issuesByLine}
+ key={index}
+ last={last}
loadDuplications={this.loadDuplications}
+ locations={this.props.locations}
+ locationsByLine={locationsByLine}
onIssueChange={this.props.onIssueChange}
onIssuePopupToggle={this.props.onIssuePopupToggle}
- onIssueSelect={() => {}}
- onIssueUnselect={() => {}}
- onIssuesClose={this.handleCloseIssues}
- onIssuesOpen={this.handleOpenIssues}
- onLinePopupToggle={this.handleLinePopupToggle}
onLocationSelect={this.props.onLocationSelect}
- onSymbolClick={this.handleSymbolClick}
- openIssues={openIssuesByLine[line.line]}
- previousLine={index > 0 ? snippet[index - 1] : undefined}
+ openIssuesByLine={this.state.openIssuesByLine}
renderDuplicationPopup={this.renderDuplicationPopup}
scroll={this.props.scroll}
- secondaryIssueLocations={secondaryIssueLocations}
- selectedIssue={optimizeSelectedIssue(this.props.issue.key, issuesForLine)}
- verticalBuffer={verticalBuffer}
+ snippet={snippet}
/>
);
}
- renderSnippet({
- snippet,
- index,
- issue,
- issuesByLine = {},
- locationsByLine,
- last
- }: {
- snippet: T.SourceLine[];
- index: number;
- issue: T.Issue;
- issuesByLine: T.IssuesByLine;
- locationsByLine: { [line: number]: T.LinearIssueLocation[] };
- last: boolean;
- }) {
- const { component } = this.props.snippetGroup;
- const lastLine =
- component.measures && component.measures.lines && parseInt(component.measures.lines, 10);
-
- const symbols = symbolsByLine(snippet);
-
- const expandBlock = (direction: T.ExpandDirection) => () => this.expandBlock(index, direction);
-
- const bottomLine = snippet[snippet.length - 1].line;
- const issueLine = issue.textRange ? issue.textRange.endLine : issue.line;
- const lowestVisibleIssue = Math.max(
- ...Object.keys(issuesByLine)
- .map(k => parseInt(k, 10))
- .filter(l => inSnippet(l, snippet) && (l === issueLine || this.state.openIssuesByLine[l]))
- );
- const verticalBuffer = last
- ? Math.max(0, LINES_BELOW_LAST - (bottomLine - lowestVisibleIssue))
- : 0;
-
- const displayDuplications = snippet.some(s => !!s.duplicated);
-
- return (
- <div className="source-viewer-code snippet" key={index}>
- {snippet[0].line > 1 && (
- <button
- aria-label={translate('source_viewer.expand_above')}
- className="expand-block expand-block-above"
- onClick={expandBlock('up')}
- type="button">
- <ExpandSnippetIcon />
- </button>
- )}
- <table className="source-table">
- <tbody>
- {snippet.map((line, index) =>
- this.renderLine({
- displayDuplications,
- index,
- issuesForLine: issuesByLine[line.line] || [],
- issueLocations: locationsByLine[line.line] || [],
- line,
- snippet,
- symbols: symbols[line.line],
- verticalBuffer: index === snippet.length - 1 ? verticalBuffer : 0
- })
- )}
- </tbody>
- </table>
- {(!lastLine || snippet[snippet.length - 1].line < lastLine) && (
- <button
- aria-label={translate('source_viewer.expand_below')}
- className="expand-block expand-block-below"
- onClick={expandBlock('down')}
- type="button">
- <ExpandSnippetIcon />
- </button>
- )}
- </div>
- );
- }
-
render() {
const { branchLike, duplications, issue, issuesByLine, last, snippetGroup } = this.props;
const { loading, snippets } = this.state;
this.renderSnippet({
snippet,
index,
- issue,
issuesByLine: last ? issuesByLine : {},
locationsByLine: last && index === snippets.length - 1 ? locations : {},
last: last && index === snippets.length - 1
--- /dev/null
+/*
+ * 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 { inSnippet, LINES_BELOW_LAST } from './utils';
+import ExpandSnippetIcon from '../../../components/icons-components/ExpandSnippetIcon';
+import Line from '../../../components/SourceViewer/components/Line';
+import { symbolsByLine } from '../../../components/SourceViewer/helpers/indexing';
+import { getSecondaryIssueLocationsForLine } from '../../../components/SourceViewer/helpers/issueLocations';
+import {
+ optimizeLocationMessage,
+ optimizeHighlightedSymbols,
+ optimizeSelectedIssue
+} from '../../../components/SourceViewer/helpers/lines';
+import { translate } from '../../../helpers/l10n';
+import { scrollHorizontally } from '../../../helpers/scrolling';
+
+interface Props {
+ branchLike: T.BranchLike | undefined;
+ component: T.SourceViewerFile;
+ duplications?: T.Duplication[];
+ duplicationsByLine?: { [line: number]: number[] };
+ expandBlock: (snippetIndex: number, direction: T.ExpandDirection) => void;
+ handleCloseIssues: (line: T.SourceLine) => void;
+ handleLinePopupToggle: (line: T.SourceLine) => void;
+ handleOpenIssues: (line: T.SourceLine) => void;
+ handleSymbolClick: (symbols: string[]) => void;
+ highlightedLocationMessage: { index: number; text: string | undefined } | undefined;
+ highlightedSymbols: string[];
+ index: number;
+ issue: T.Issue;
+ issuePopup?: { issue: string; name: string };
+ issuesByLine: T.IssuesByLine;
+ last: boolean;
+ linePopup?: T.LinePopup;
+ loadDuplications: (line: T.SourceLine) => void;
+ locations: T.FlowLocation[];
+ locationsByLine: { [line: number]: T.LinearIssueLocation[] };
+ onIssueChange: (issue: T.Issue) => void;
+ onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void;
+ onLocationSelect: (index: number) => void;
+ openIssuesByLine: T.Dict<boolean>;
+ renderDuplicationPopup: (index: number, line: number) => React.ReactNode;
+ scroll?: (element: HTMLElement) => void;
+ snippet: T.SourceLine[];
+}
+
+const SCROLL_LEFT_OFFSET = 32;
+
+export default class SnippetViewer extends React.PureComponent<Props> {
+ node: React.RefObject<HTMLDivElement>;
+
+ constructor(props: Props) {
+ super(props);
+ this.node = React.createRef();
+ }
+
+ doScroll = (element: HTMLElement) => {
+ if (this.props.scroll) {
+ this.props.scroll(element);
+ }
+ const parent = this.node.current as Element;
+
+ if (parent) {
+ scrollHorizontally(element, {
+ leftOffset: SCROLL_LEFT_OFFSET,
+ rightOffset: parent.getBoundingClientRect().width - SCROLL_LEFT_OFFSET,
+ parent
+ });
+ }
+ };
+
+ expandBlock = (direction: T.ExpandDirection) => () =>
+ this.props.expandBlock(this.props.index, direction);
+
+ renderLine({
+ displayDuplications,
+ index,
+ issuesForLine,
+ issueLocations,
+ line,
+ snippet,
+ symbols,
+ verticalBuffer
+ }: {
+ displayDuplications: boolean;
+ index: number;
+ issuesForLine: T.Issue[];
+ issueLocations: T.LinearIssueLocation[];
+ line: T.SourceLine;
+ snippet: T.SourceLine[];
+ symbols: string[];
+ verticalBuffer: number;
+ }) {
+ const secondaryIssueLocations = getSecondaryIssueLocationsForLine(line, this.props.locations);
+
+ const { duplications, duplicationsByLine } = this.props;
+ const duplicationsCount = duplications ? duplications.length : 0;
+ const lineDuplications =
+ (duplicationsCount && duplicationsByLine && duplicationsByLine[line.line]) || [];
+
+ const isSinkLine = issuesForLine.some(i => i.key === this.props.issue.key);
+
+ return (
+ <Line
+ branchLike={this.props.branchLike}
+ displayAllIssues={false}
+ displayCoverage={true}
+ displayDuplications={displayDuplications}
+ displayIssues={!isSinkLine || issuesForLine.length > 1}
+ displayLocationMarkers={true}
+ duplications={lineDuplications}
+ duplicationsCount={duplicationsCount}
+ highlighted={false}
+ highlightedLocationMessage={optimizeLocationMessage(
+ this.props.highlightedLocationMessage,
+ secondaryIssueLocations
+ )}
+ highlightedSymbols={optimizeHighlightedSymbols(symbols, this.props.highlightedSymbols)}
+ issueLocations={issueLocations}
+ issuePopup={this.props.issuePopup}
+ issues={issuesForLine}
+ key={line.line}
+ last={false}
+ line={line}
+ linePopup={this.props.linePopup}
+ loadDuplications={this.props.loadDuplications}
+ onIssueChange={this.props.onIssueChange}
+ onIssuePopupToggle={this.props.onIssuePopupToggle}
+ onIssueSelect={() => {}}
+ onIssueUnselect={() => {}}
+ onIssuesClose={this.props.handleCloseIssues}
+ onIssuesOpen={this.props.handleOpenIssues}
+ onLinePopupToggle={this.props.handleLinePopupToggle}
+ onLocationSelect={this.props.onLocationSelect}
+ onSymbolClick={this.props.handleSymbolClick}
+ openIssues={this.props.openIssuesByLine[line.line]}
+ previousLine={index > 0 ? snippet[index - 1] : undefined}
+ renderDuplicationPopup={this.props.renderDuplicationPopup}
+ scroll={this.doScroll}
+ secondaryIssueLocations={secondaryIssueLocations}
+ selectedIssue={optimizeSelectedIssue(this.props.issue.key, issuesForLine)}
+ verticalBuffer={verticalBuffer}
+ />
+ );
+ }
+
+ render() {
+ const {
+ component,
+ issue,
+ issuesByLine = {},
+ last,
+ locationsByLine,
+ openIssuesByLine,
+ snippet
+ } = this.props;
+ const lastLine =
+ component.measures && component.measures.lines && parseInt(component.measures.lines, 10);
+
+ const symbols = symbolsByLine(snippet);
+
+ const bottomLine = snippet[snippet.length - 1].line;
+ const issueLine = issue.textRange ? issue.textRange.endLine : issue.line;
+ const lowestVisibleIssue = Math.max(
+ ...Object.keys(issuesByLine)
+ .map(k => parseInt(k, 10))
+ .filter(l => inSnippet(l, snippet) && (l === issueLine || openIssuesByLine[l]))
+ );
+ const verticalBuffer = last
+ ? Math.max(0, LINES_BELOW_LAST - (bottomLine - lowestVisibleIssue))
+ : 0;
+
+ const displayDuplications = snippet.some(s => !!s.duplicated);
+
+ return (
+ <div className="source-viewer-code snippet" ref={this.node}>
+ <table className="source-table">
+ <tbody>
+ {snippet[0].line > 1 && (
+ <tr className="expand-block expand-block-above">
+ <td colSpan={5}>
+ <button
+ aria-label={translate('source_viewer.expand_above')}
+ onClick={this.expandBlock('up')}
+ type="button">
+ <ExpandSnippetIcon />
+ </button>
+ </td>
+ </tr>
+ )}
+ {snippet.map((line, index) =>
+ this.renderLine({
+ displayDuplications,
+ index,
+ issuesForLine: issuesByLine[line.line] || [],
+ issueLocations: locationsByLine[line.line] || [],
+ line,
+ snippet,
+ symbols: symbols[line.line],
+ verticalBuffer: index === snippet.length - 1 ? verticalBuffer : 0
+ })
+ )}
+ {(!lastLine || snippet[snippet.length - 1].line < lastLine) && (
+ <tr className="expand-block expand-block-below">
+ <td colSpan={5}>
+ <button
+ aria-label={translate('source_viewer.expand_below')}
+ onClick={this.expandBlock('down')}
+ type="button">
+ <ExpandSnippetIcon />
+ </button>
+ </td>
+ </tr>
+ )}
+ </tbody>
+ </table>
+ </div>
+ );
+ }
+}
expect(wrapper.state('snippets')[0]).toHaveLength(14);
});
-it.only('should get the right branch when expanding', async () => {
+it('should get the right branch when expanding', async () => {
(getSources as jest.Mock).mockResolvedValueOnce(
Object.values(
mockSnippetsByComponent('a', [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]).sources
const line = mockSourceLine();
wrapper
- .find('Line')
+ .find('SnippetViewer')
.first()
.prop<Function>('loadDuplications')(line);
expect(loadDuplications).toHaveBeenCalledWith('a', line);
wrapper
- .find('Line')
+ .find('SnippetViewer')
.first()
- .prop<Function>('onLinePopupToggle')({ line: 13, name: 'foo' });
+ .prop<Function>('handleLinePopupToggle')({ line: 13, name: 'foo' });
expect(onLinePopupToggle).toHaveBeenCalledWith({ component: 'a', line: 13, name: 'foo' });
wrapper
- .find('Line')
+ .find('SnippetViewer')
.first()
.prop<Function>('renderDuplicationPopup')(1, 13);
expect(renderDuplicationPopup).toHaveBeenCalledWith(
--- /dev/null
+/*
+ * 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 { range } from 'lodash';
+import { shallow } from 'enzyme';
+import SnippetViewer from '../SnippetViewer';
+import {
+ mockSourceViewerFile,
+ mockMainBranch,
+ mockIssue,
+ mockSourceLine
+} from '../../../../helpers/testMocks';
+
+it('should render correctly', () => {
+ const snippet = range(5, 8).map(line => mockSourceLine({ line }));
+ const wrapper = shallowRender({
+ snippet
+ });
+
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should render correctly when at the top of the file', () => {
+ const snippet = range(1, 8).map(line => mockSourceLine({ line }));
+ const wrapper = shallowRender({
+ snippet
+ });
+
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should render correctly when at the bottom of the file', () => {
+ const component = mockSourceViewerFile({ measures: { lines: '14' } });
+ const snippet = range(10, 14).map(line => mockSourceLine({ line }));
+ const wrapper = shallowRender({
+ component,
+ snippet
+ });
+
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should correctly handle expansion', () => {
+ const snippet = range(5, 8).map(line => mockSourceLine({ line }));
+ const expandBlock = jest.fn();
+
+ const wrapper = shallowRender({
+ expandBlock,
+ index: 2,
+ snippet
+ });
+
+ wrapper
+ .find('.expand-block-above button')
+ .first()
+ .simulate('click');
+ expect(expandBlock).toHaveBeenCalledWith(2, 'up');
+
+ wrapper
+ .find('.expand-block-below button')
+ .first()
+ .simulate('click');
+ expect(expandBlock).toHaveBeenCalledWith(2, 'down');
+});
+
+function shallowRender(props: Partial<SnippetViewer['props']> = {}) {
+ return shallow<SnippetViewer>(
+ <SnippetViewer
+ branchLike={mockMainBranch()}
+ component={mockSourceViewerFile()}
+ duplications={undefined}
+ duplicationsByLine={undefined}
+ expandBlock={jest.fn()}
+ handleCloseIssues={jest.fn()}
+ handleLinePopupToggle={jest.fn()}
+ handleOpenIssues={jest.fn()}
+ handleSymbolClick={jest.fn()}
+ highlightedLocationMessage={{ index: 0, text: '' }}
+ highlightedSymbols={[]}
+ index={0}
+ issue={mockIssue()}
+ issuesByLine={{}}
+ last={false}
+ linePopup={undefined}
+ loadDuplications={jest.fn()}
+ locations={[]}
+ locationsByLine={{}}
+ onIssueChange={jest.fn()}
+ onIssuePopupToggle={jest.fn()}
+ onLocationSelect={jest.fn()}
+ openIssuesByLine={{}}
+ renderDuplicationPopup={jest.fn()}
+ scroll={jest.fn()}
+ snippet={[]}
+ {...props}
+ />
+ );
+}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+ className="source-viewer-code snippet"
+>
+ <table
+ className="source-table"
+ >
+ <tbody>
+ <tr
+ className="expand-block expand-block-above"
+ >
+ <td
+ colSpan={5}
+ >
+ <button
+ aria-label="source_viewer.expand_above"
+ onClick={[Function]}
+ type="button"
+ >
+ <ExpandSnippetIcon />
+ </button>
+ </td>
+ </tr>
+ <Line
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ displayAllIssues={false}
+ displayCoverage={true}
+ displayDuplications={false}
+ displayIssues={true}
+ displayLocationMarkers={true}
+ duplications={Array []}
+ duplicationsCount={0}
+ highlighted={false}
+ issueLocations={Array []}
+ issues={Array []}
+ key="5"
+ 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": 5,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ loadDuplications={[MockFunction]}
+ onIssueChange={[MockFunction]}
+ onIssuePopupToggle={[MockFunction]}
+ onIssueSelect={[Function]}
+ onIssueUnselect={[Function]}
+ onIssuesClose={[MockFunction]}
+ onIssuesOpen={[MockFunction]}
+ onLinePopupToggle={[MockFunction]}
+ onLocationSelect={[MockFunction]}
+ onSymbolClick={[MockFunction]}
+ renderDuplicationPopup={[MockFunction]}
+ scroll={[Function]}
+ secondaryIssueLocations={Array []}
+ verticalBuffer={0}
+ />
+ <Line
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ displayAllIssues={false}
+ displayCoverage={true}
+ displayDuplications={false}
+ displayIssues={true}
+ displayLocationMarkers={true}
+ duplications={Array []}
+ duplicationsCount={0}
+ highlighted={false}
+ issueLocations={Array []}
+ issues={Array []}
+ key="6"
+ 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": 6,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ loadDuplications={[MockFunction]}
+ onIssueChange={[MockFunction]}
+ onIssuePopupToggle={[MockFunction]}
+ onIssueSelect={[Function]}
+ onIssueUnselect={[Function]}
+ onIssuesClose={[MockFunction]}
+ onIssuesOpen={[MockFunction]}
+ onLinePopupToggle={[MockFunction]}
+ onLocationSelect={[MockFunction]}
+ onSymbolClick={[MockFunction]}
+ 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": 5,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ renderDuplicationPopup={[MockFunction]}
+ scroll={[Function]}
+ secondaryIssueLocations={Array []}
+ verticalBuffer={0}
+ />
+ <Line
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ displayAllIssues={false}
+ displayCoverage={true}
+ displayDuplications={false}
+ displayIssues={true}
+ displayLocationMarkers={true}
+ duplications={Array []}
+ duplicationsCount={0}
+ highlighted={false}
+ issueLocations={Array []}
+ issues={Array []}
+ key="7"
+ 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": 7,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ loadDuplications={[MockFunction]}
+ onIssueChange={[MockFunction]}
+ onIssuePopupToggle={[MockFunction]}
+ onIssueSelect={[Function]}
+ onIssueUnselect={[Function]}
+ onIssuesClose={[MockFunction]}
+ onIssuesOpen={[MockFunction]}
+ onLinePopupToggle={[MockFunction]}
+ onLocationSelect={[MockFunction]}
+ onSymbolClick={[MockFunction]}
+ 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": 6,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ renderDuplicationPopup={[MockFunction]}
+ scroll={[Function]}
+ secondaryIssueLocations={Array []}
+ verticalBuffer={0}
+ />
+ <tr
+ className="expand-block expand-block-below"
+ >
+ <td
+ colSpan={5}
+ >
+ <button
+ aria-label="source_viewer.expand_below"
+ onClick={[Function]}
+ type="button"
+ >
+ <ExpandSnippetIcon />
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+`;
+
+exports[`should render correctly when at the bottom of the file 1`] = `
+<div
+ className="source-viewer-code snippet"
+>
+ <table
+ className="source-table"
+ >
+ <tbody>
+ <tr
+ className="expand-block expand-block-above"
+ >
+ <td
+ colSpan={5}
+ >
+ <button
+ aria-label="source_viewer.expand_above"
+ onClick={[Function]}
+ type="button"
+ >
+ <ExpandSnippetIcon />
+ </button>
+ </td>
+ </tr>
+ <Line
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ displayAllIssues={false}
+ displayCoverage={true}
+ displayDuplications={false}
+ displayIssues={true}
+ displayLocationMarkers={true}
+ duplications={Array []}
+ duplicationsCount={0}
+ highlighted={false}
+ issueLocations={Array []}
+ issues={Array []}
+ key="10"
+ 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": 10,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ loadDuplications={[MockFunction]}
+ onIssueChange={[MockFunction]}
+ onIssuePopupToggle={[MockFunction]}
+ onIssueSelect={[Function]}
+ onIssueUnselect={[Function]}
+ onIssuesClose={[MockFunction]}
+ onIssuesOpen={[MockFunction]}
+ onLinePopupToggle={[MockFunction]}
+ onLocationSelect={[MockFunction]}
+ onSymbolClick={[MockFunction]}
+ renderDuplicationPopup={[MockFunction]}
+ scroll={[Function]}
+ secondaryIssueLocations={Array []}
+ verticalBuffer={0}
+ />
+ <Line
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ displayAllIssues={false}
+ displayCoverage={true}
+ displayDuplications={false}
+ displayIssues={true}
+ displayLocationMarkers={true}
+ duplications={Array []}
+ duplicationsCount={0}
+ highlighted={false}
+ issueLocations={Array []}
+ issues={Array []}
+ key="11"
+ 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": 11,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ loadDuplications={[MockFunction]}
+ onIssueChange={[MockFunction]}
+ onIssuePopupToggle={[MockFunction]}
+ onIssueSelect={[Function]}
+ onIssueUnselect={[Function]}
+ onIssuesClose={[MockFunction]}
+ onIssuesOpen={[MockFunction]}
+ onLinePopupToggle={[MockFunction]}
+ onLocationSelect={[MockFunction]}
+ onSymbolClick={[MockFunction]}
+ 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": 10,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ renderDuplicationPopup={[MockFunction]}
+ scroll={[Function]}
+ secondaryIssueLocations={Array []}
+ verticalBuffer={0}
+ />
+ <Line
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ displayAllIssues={false}
+ displayCoverage={true}
+ displayDuplications={false}
+ displayIssues={true}
+ displayLocationMarkers={true}
+ duplications={Array []}
+ duplicationsCount={0}
+ highlighted={false}
+ issueLocations={Array []}
+ issues={Array []}
+ key="12"
+ 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": 12,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ loadDuplications={[MockFunction]}
+ onIssueChange={[MockFunction]}
+ onIssuePopupToggle={[MockFunction]}
+ onIssueSelect={[Function]}
+ onIssueUnselect={[Function]}
+ onIssuesClose={[MockFunction]}
+ onIssuesOpen={[MockFunction]}
+ onLinePopupToggle={[MockFunction]}
+ onLocationSelect={[MockFunction]}
+ onSymbolClick={[MockFunction]}
+ 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": 11,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ renderDuplicationPopup={[MockFunction]}
+ scroll={[Function]}
+ secondaryIssueLocations={Array []}
+ verticalBuffer={0}
+ />
+ <Line
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ displayAllIssues={false}
+ displayCoverage={true}
+ displayDuplications={false}
+ displayIssues={true}
+ displayLocationMarkers={true}
+ duplications={Array []}
+ duplicationsCount={0}
+ highlighted={false}
+ issueLocations={Array []}
+ issues={Array []}
+ key="13"
+ 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": 13,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ loadDuplications={[MockFunction]}
+ onIssueChange={[MockFunction]}
+ onIssuePopupToggle={[MockFunction]}
+ onIssueSelect={[Function]}
+ onIssueUnselect={[Function]}
+ onIssuesClose={[MockFunction]}
+ onIssuesOpen={[MockFunction]}
+ onLinePopupToggle={[MockFunction]}
+ onLocationSelect={[MockFunction]}
+ onSymbolClick={[MockFunction]}
+ 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": 12,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ renderDuplicationPopup={[MockFunction]}
+ scroll={[Function]}
+ secondaryIssueLocations={Array []}
+ verticalBuffer={0}
+ />
+ <tr
+ className="expand-block expand-block-below"
+ >
+ <td
+ colSpan={5}
+ >
+ <button
+ aria-label="source_viewer.expand_below"
+ onClick={[Function]}
+ type="button"
+ >
+ <ExpandSnippetIcon />
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+`;
+
+exports[`should render correctly when at the top of the file 1`] = `
+<div
+ className="source-viewer-code snippet"
+>
+ <table
+ className="source-table"
+ >
+ <tbody>
+ <Line
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ displayAllIssues={false}
+ displayCoverage={true}
+ displayDuplications={false}
+ displayIssues={true}
+ displayLocationMarkers={true}
+ duplications={Array []}
+ duplicationsCount={0}
+ highlighted={false}
+ issueLocations={Array []}
+ issues={Array []}
+ key="1"
+ 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": 1,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ loadDuplications={[MockFunction]}
+ onIssueChange={[MockFunction]}
+ onIssuePopupToggle={[MockFunction]}
+ onIssueSelect={[Function]}
+ onIssueUnselect={[Function]}
+ onIssuesClose={[MockFunction]}
+ onIssuesOpen={[MockFunction]}
+ onLinePopupToggle={[MockFunction]}
+ onLocationSelect={[MockFunction]}
+ onSymbolClick={[MockFunction]}
+ renderDuplicationPopup={[MockFunction]}
+ scroll={[Function]}
+ secondaryIssueLocations={Array []}
+ verticalBuffer={0}
+ />
+ <Line
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ displayAllIssues={false}
+ displayCoverage={true}
+ displayDuplications={false}
+ displayIssues={true}
+ displayLocationMarkers={true}
+ duplications={Array []}
+ duplicationsCount={0}
+ highlighted={false}
+ issueLocations={Array []}
+ issues={Array []}
+ key="2"
+ 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": 2,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ loadDuplications={[MockFunction]}
+ onIssueChange={[MockFunction]}
+ onIssuePopupToggle={[MockFunction]}
+ onIssueSelect={[Function]}
+ onIssueUnselect={[Function]}
+ onIssuesClose={[MockFunction]}
+ onIssuesOpen={[MockFunction]}
+ onLinePopupToggle={[MockFunction]}
+ onLocationSelect={[MockFunction]}
+ onSymbolClick={[MockFunction]}
+ 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": 1,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ renderDuplicationPopup={[MockFunction]}
+ scroll={[Function]}
+ secondaryIssueLocations={Array []}
+ verticalBuffer={0}
+ />
+ <Line
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ displayAllIssues={false}
+ displayCoverage={true}
+ displayDuplications={false}
+ displayIssues={true}
+ displayLocationMarkers={true}
+ duplications={Array []}
+ duplicationsCount={0}
+ highlighted={false}
+ issueLocations={Array []}
+ issues={Array []}
+ key="3"
+ 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": 3,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ loadDuplications={[MockFunction]}
+ onIssueChange={[MockFunction]}
+ onIssuePopupToggle={[MockFunction]}
+ onIssueSelect={[Function]}
+ onIssueUnselect={[Function]}
+ onIssuesClose={[MockFunction]}
+ onIssuesOpen={[MockFunction]}
+ onLinePopupToggle={[MockFunction]}
+ onLocationSelect={[MockFunction]}
+ onSymbolClick={[MockFunction]}
+ 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": 2,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ renderDuplicationPopup={[MockFunction]}
+ scroll={[Function]}
+ secondaryIssueLocations={Array []}
+ verticalBuffer={0}
+ />
+ <Line
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ displayAllIssues={false}
+ displayCoverage={true}
+ displayDuplications={false}
+ displayIssues={true}
+ displayLocationMarkers={true}
+ duplications={Array []}
+ duplicationsCount={0}
+ highlighted={false}
+ issueLocations={Array []}
+ issues={Array []}
+ key="4"
+ 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": 4,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ loadDuplications={[MockFunction]}
+ onIssueChange={[MockFunction]}
+ onIssuePopupToggle={[MockFunction]}
+ onIssueSelect={[Function]}
+ onIssueUnselect={[Function]}
+ onIssuesClose={[MockFunction]}
+ onIssuesOpen={[MockFunction]}
+ onLinePopupToggle={[MockFunction]}
+ onLocationSelect={[MockFunction]}
+ onSymbolClick={[MockFunction]}
+ 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": 3,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ renderDuplicationPopup={[MockFunction]}
+ scroll={[Function]}
+ secondaryIssueLocations={Array []}
+ verticalBuffer={0}
+ />
+ <Line
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ displayAllIssues={false}
+ displayCoverage={true}
+ displayDuplications={false}
+ displayIssues={true}
+ displayLocationMarkers={true}
+ duplications={Array []}
+ duplicationsCount={0}
+ highlighted={false}
+ issueLocations={Array []}
+ issues={Array []}
+ key="5"
+ 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": 5,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ loadDuplications={[MockFunction]}
+ onIssueChange={[MockFunction]}
+ onIssuePopupToggle={[MockFunction]}
+ onIssueSelect={[Function]}
+ onIssueUnselect={[Function]}
+ onIssuesClose={[MockFunction]}
+ onIssuesOpen={[MockFunction]}
+ onLinePopupToggle={[MockFunction]}
+ onLocationSelect={[MockFunction]}
+ onSymbolClick={[MockFunction]}
+ 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": 4,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ renderDuplicationPopup={[MockFunction]}
+ scroll={[Function]}
+ secondaryIssueLocations={Array []}
+ verticalBuffer={0}
+ />
+ <Line
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ displayAllIssues={false}
+ displayCoverage={true}
+ displayDuplications={false}
+ displayIssues={true}
+ displayLocationMarkers={true}
+ duplications={Array []}
+ duplicationsCount={0}
+ highlighted={false}
+ issueLocations={Array []}
+ issues={Array []}
+ key="6"
+ 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": 6,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ loadDuplications={[MockFunction]}
+ onIssueChange={[MockFunction]}
+ onIssuePopupToggle={[MockFunction]}
+ onIssueSelect={[Function]}
+ onIssueUnselect={[Function]}
+ onIssuesClose={[MockFunction]}
+ onIssuesOpen={[MockFunction]}
+ onLinePopupToggle={[MockFunction]}
+ onLocationSelect={[MockFunction]}
+ onSymbolClick={[MockFunction]}
+ 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": 5,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ renderDuplicationPopup={[MockFunction]}
+ scroll={[Function]}
+ secondaryIssueLocations={Array []}
+ verticalBuffer={0}
+ />
+ <Line
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ displayAllIssues={false}
+ displayCoverage={true}
+ displayDuplications={false}
+ displayIssues={true}
+ displayLocationMarkers={true}
+ duplications={Array []}
+ duplicationsCount={0}
+ highlighted={false}
+ issueLocations={Array []}
+ issues={Array []}
+ key="7"
+ 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": 7,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ loadDuplications={[MockFunction]}
+ onIssueChange={[MockFunction]}
+ onIssuePopupToggle={[MockFunction]}
+ onIssueSelect={[Function]}
+ onIssueUnselect={[Function]}
+ onIssuesClose={[MockFunction]}
+ onIssuesOpen={[MockFunction]}
+ onLinePopupToggle={[MockFunction]}
+ onLocationSelect={[MockFunction]}
+ onSymbolClick={[MockFunction]}
+ 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": 6,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ }
+ }
+ renderDuplicationPopup={[MockFunction]}
+ scroll={[Function]}
+ secondaryIssueLocations={Array []}
+ verticalBuffer={0}
+ />
+ <tr
+ className="expand-block expand-block-below"
+ >
+ <td
+ colSpan={5}
+ >
+ <button
+ aria-label="source_viewer.expand_below"
+ onClick={[Function]}
+ type="button"
+ >
+ <ExpandSnippetIcon />
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+`;
overflow-x: auto;
}
-.snippet > .expand-block {
+.snippet > table {
+ width: 100%;
+}
+
+.expand-block > td > button {
+ background: transparent;
box-sizing: border-box;
color: var(--secondFontColor);
height: 20px;
text-align: left;
cursor: pointer;
}
-.snippet > .expand-block:hover,
-.snippet > .expand-block:focus,
-.snippet > .expand-block:active {
+.expand-block > td > button:hover,
+.expand-block > td > button:focus,
+.expand-block > td > button:active {
color: var(--darkBlue);
outline: none;
}
-.snippet > .expand-block-above {
+.expand-block-above {
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAAXNSR0IArs4c6QAAADdJREFUCB1dzMEKADAIAlBd1v9/bcc2YgRjHh8qq2qTxCQzsX4wM6y30RARF3sy0Es1SIK7Y64OpCES1W69JS4AAAAASUVORK5CYII=');
}
-.snippet > .expand-block-below {
+.expand-block-below {
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wQQBjQEQVd5jwAAADhJREFUCNddyTEKADEMA8GVA/7/Z+PGwUp1cGTaYe/tv5lxrLWoKj6SiMzkjZDEG7JtANt0N+ccLrB/KZxXTt7fAAAAAElFTkSuQmCC');
}
--- /dev/null
+/*
+ * 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 { scrollToElement, scrollHorizontally } from '../scrolling';
+
+jest.useFakeTimers();
+
+describe('scrollToElement', () => {
+ it('should scroll parent up to element', () => {
+ const element = document.createElement('a');
+ element.getBoundingClientRect = mockGetBoundingClientRect({ top: 5, bottom: 20 });
+
+ const parent = document.createElement('div');
+ parent.getBoundingClientRect = mockGetBoundingClientRect({ height: 30, top: 15 });
+ parent.scrollTop = 10;
+ parent.scrollLeft = 12;
+ parent.appendChild(element);
+
+ document.body.appendChild(parent);
+ scrollToElement(element, { parent, smooth: false });
+
+ expect(parent.scrollTop).toEqual(0);
+ expect(parent.scrollLeft).toEqual(12);
+ });
+
+ it('should scroll parent down to element', () => {
+ const element = document.createElement('a');
+ element.getBoundingClientRect = mockGetBoundingClientRect({ top: 25, bottom: 50 });
+
+ const parent = document.createElement('div');
+ parent.getBoundingClientRect = mockGetBoundingClientRect({ height: 30, top: 15 });
+ parent.scrollTop = 10;
+ parent.scrollLeft = 12;
+ parent.appendChild(element);
+
+ document.body.appendChild(parent);
+ scrollToElement(element, { parent, smooth: false });
+
+ expect(parent.scrollTop).toEqual(15);
+ expect(parent.scrollLeft).toEqual(12);
+ });
+
+ it('should scroll window down to element', () => {
+ const element = document.createElement('a');
+ element.getBoundingClientRect = mockGetBoundingClientRect({ top: 840, bottom: 845 });
+
+ Object.defineProperty(window, 'innerHeight', { value: 400 });
+ window.scrollTo = jest.fn();
+
+ document.body.appendChild(element);
+
+ scrollToElement(element, { smooth: false });
+
+ expect(window.scrollTo).toBeCalledWith(0, 445);
+ });
+
+ it('should scroll window up to element', () => {
+ const element = document.createElement('a');
+ element.getBoundingClientRect = mockGetBoundingClientRect({ top: -10, bottom: 10 });
+
+ Object.defineProperty(window, 'innerHeight', { value: 50 });
+ window.scrollTo = jest.fn();
+
+ document.body.appendChild(element);
+
+ scrollToElement(element, { smooth: false });
+
+ expect(window.scrollTo).toBeCalledWith(0, -10);
+ });
+
+ it('should scroll window down to element smoothly', () => {
+ const element = document.createElement('a');
+ element.getBoundingClientRect = mockGetBoundingClientRect({ top: 840, bottom: 845 });
+
+ Object.defineProperty(window, 'innerHeight', { value: 400 });
+ window.scrollTo = jest.fn();
+
+ document.body.appendChild(element);
+
+ scrollToElement(element, {});
+
+ jest.runAllTimers();
+
+ expect(window.scrollTo).toBeCalledTimes(10);
+ });
+});
+
+describe('scrollHorizontally', () => {
+ it('should scroll parent left to element', () => {
+ const element = document.createElement('a');
+ element.getBoundingClientRect = mockGetBoundingClientRect({ left: 25, right: 42 });
+
+ const parent = document.createElement('div');
+ parent.getBoundingClientRect = mockGetBoundingClientRect({ width: 67, left: 46 });
+ parent.scrollTop = 12;
+ parent.scrollLeft = 38;
+ parent.appendChild(element);
+
+ document.body.appendChild(parent);
+
+ scrollHorizontally(element, { parent, smooth: false });
+
+ expect(parent.scrollTop).toEqual(12);
+ expect(parent.scrollLeft).toEqual(17);
+ });
+
+ it('should scroll parent right to element', () => {
+ const element = document.createElement('a');
+ element.getBoundingClientRect = mockGetBoundingClientRect({ left: 25, right: 99 });
+
+ const parent = document.createElement('div');
+ parent.getBoundingClientRect = mockGetBoundingClientRect({ width: 67, left: 20 });
+ parent.scrollTop = 12;
+ parent.scrollLeft = 20;
+ parent.appendChild(element);
+
+ document.body.appendChild(parent);
+
+ scrollHorizontally(element, { parent, smooth: false });
+
+ expect(parent.scrollTop).toEqual(12);
+ expect(parent.scrollLeft).toEqual(32);
+ });
+
+ it('should scroll window right to element', () => {
+ const element = document.createElement('a');
+ element.getBoundingClientRect = mockGetBoundingClientRect({ left: 840, right: 845 });
+
+ Object.defineProperty(window, 'innerWidth', { value: 400 });
+ window.scrollTo = jest.fn();
+
+ document.body.appendChild(element);
+
+ scrollHorizontally(element, { smooth: false });
+
+ expect(window.scrollTo).toBeCalledWith(445, 0);
+ });
+
+ it('should scroll window left to element', () => {
+ const element = document.createElement('a');
+ element.getBoundingClientRect = mockGetBoundingClientRect({ left: -10, right: 10 });
+
+ Object.defineProperty(window, 'innerWidth', { value: 50 });
+ window.scrollTo = jest.fn();
+
+ document.body.appendChild(element);
+
+ scrollHorizontally(element, { smooth: false });
+
+ expect(window.scrollTo).toBeCalledWith(-10, 0);
+ });
+
+ it('should scroll window right to element smoothly', () => {
+ const element = document.createElement('a');
+ element.getBoundingClientRect = mockGetBoundingClientRect({ left: 840, right: 845 });
+
+ Object.defineProperty(window, 'innerWidth', { value: 400 });
+ window.scrollTo = jest.fn();
+
+ document.body.appendChild(element);
+
+ scrollHorizontally(element, {});
+
+ jest.runAllTimers();
+
+ expect(window.scrollTo).toBeCalledTimes(10);
+ });
+});
+
+const mockGetBoundingClientRect = (overrides: Partial<ClientRect>) => () => ({
+ bottom: 0,
+ height: 0,
+ left: 0,
+ right: 0,
+ top: 0,
+ width: 0,
+ ...overrides
+});
return element === window;
}
-function getScrollPosition(element: Element | Window): number {
- return isWindow(element) ? window.pageYOffset : element.scrollTop;
+function getScroll(element: Element | Window) {
+ return isWindow(element)
+ ? { x: window.pageXOffset, y: window.pageYOffset }
+ : { x: element.scrollLeft, y: element.scrollTop };
}
-function scrollElement(element: Element | Window, position: number): void {
+function scrollElement(element: Element | Window, x: number, y: number): void {
if (isWindow(element)) {
- window.scrollTo(0, position);
+ window.scrollTo(x, y);
} else {
- element.scrollTop = position;
+ element.scrollLeft = x;
+ element.scrollTop = y;
}
}
-let smoothScrollTop = (y: number, parent: Element | Window) => {
- let scrollTop = getScrollPosition(parent);
- const scrollingDown = y > scrollTop;
- const step = Math.ceil(Math.abs(y - scrollTop) / SCROLLING_STEPS);
+let smoothScroll = (target: number, current: number, scroll: (position: number) => void) => {
+ const positiveDirection = target > current;
+ const step = Math.ceil(Math.abs(target - current) / SCROLLING_STEPS);
let stepsDone = 0;
const interval = setInterval(() => {
- if (scrollTop === y || SCROLLING_STEPS === stepsDone) {
+ if (current === target || SCROLLING_STEPS === stepsDone) {
clearInterval(interval);
} else {
let goal;
- if (scrollingDown) {
- goal = Math.min(y, scrollTop + step);
+ if (positiveDirection) {
+ goal = Math.min(target, current + step);
} else {
- goal = Math.max(y, scrollTop - step);
+ goal = Math.max(target, current - step);
}
stepsDone++;
- scrollTop = goal;
- scrollElement(parent, goal);
+ current = goal;
+ scroll(goal);
}
}, SCROLLING_INTERVAL);
};
+smoothScroll = debounce(smoothScroll, SCROLLING_DURATION, { leading: true });
-smoothScrollTop = debounce(smoothScrollTop, SCROLLING_DURATION, { leading: true });
+function smoothScrollTop(position: number, parent: Element | Window) {
+ const scroll = getScroll(parent);
+ smoothScroll(position, scroll.y, position => scrollElement(parent, scroll.x, position));
+}
+
+function smoothScrollLeft(position: number, parent: Element | Window) {
+ const scroll = getScroll(parent);
+ smoothScroll(position, scroll.x, position => scrollElement(parent, position, scroll.y));
+}
export function scrollToElement(
element: Element,
const { top, bottom } = element.getBoundingClientRect();
- const scrollTop = getScrollPosition(parent);
+ const scroll = getScroll(parent);
const height: number = isWindow(parent)
? window.innerHeight
const parentTop = isWindow(parent) ? 0 : parent.getBoundingClientRect().top;
if (top - parentTop < opts.topOffset) {
- const goal = scrollTop - opts.topOffset + top - parentTop;
+ const goal = scroll.y - opts.topOffset + top - parentTop;
if (opts.smooth) {
smoothScrollTop(goal, parent);
} else {
- scrollElement(parent, goal);
+ scrollElement(parent, scroll.x, goal);
}
}
if (bottom - parentTop > height - opts.bottomOffset) {
- const goal = scrollTop + bottom - parentTop - height + opts.bottomOffset;
+ const goal = scroll.y + bottom - parentTop - height + opts.bottomOffset;
if (opts.smooth) {
smoothScrollTop(goal, parent);
} else {
- scrollElement(parent, goal);
+ scrollElement(parent, scroll.x, goal);
+ }
+ }
+}
+
+export function scrollHorizontally(
+ element: Element,
+ options: {
+ leftOffset?: number;
+ rightOffset?: number;
+ parent?: Element;
+ smooth?: boolean;
+ }
+): void {
+ const opts = { leftOffset: 0, rightOffset: 0, parent: window, smooth: true, ...options };
+ const { parent } = opts;
+
+ const { left, right } = element.getBoundingClientRect();
+
+ const scroll = getScroll(parent);
+
+ const { left: parentLeft, width } = isWindow(parent)
+ ? { left: 0, width: window.innerWidth }
+ : parent.getBoundingClientRect();
+
+ if (left - parentLeft < opts.leftOffset) {
+ const goal = scroll.x - opts.leftOffset + left - parentLeft;
+ if (opts.smooth) {
+ smoothScrollLeft(goal, parent);
+ } else {
+ scrollElement(parent, goal, scroll.y);
+ }
+ }
+
+ if (right - parentLeft > width - opts.rightOffset) {
+ const goal = scroll.x + right - parentLeft - width + opts.rightOffset;
+ if (opts.smooth) {
+ smoothScrollLeft(goal, parent);
+ } else {
+ scrollElement(parent, goal, scroll.y);
}
}
}