@@ -19,27 +19,12 @@ | |||
*/ | |||
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 { | |||
@@ -209,155 +194,49 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr | |||
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; | |||
@@ -384,7 +263,6 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr | |||
this.renderSnippet({ | |||
snippet, | |||
index, | |||
issue, | |||
issuesByLine: last ? issuesByLine : {}, | |||
locationsByLine: last && index === snippets.length - 1 ? locations : {}, | |||
last: last && index === snippets.length - 1 |
@@ -0,0 +1,237 @@ | |||
/* | |||
* 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> | |||
); | |||
} | |||
} |
@@ -102,7 +102,7 @@ it('should expand full component', async () => { | |||
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 | |||
@@ -168,19 +168,19 @@ it('should correctly handle lines actions', () => { | |||
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( |
@@ -0,0 +1,115 @@ | |||
/* | |||
* 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} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,930 @@ | |||
// 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> | |||
`; |
@@ -244,7 +244,12 @@ | |||
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; | |||
@@ -254,16 +259,16 @@ | |||
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'); | |||
} | |||
@@ -0,0 +1,194 @@ | |||
/* | |||
* 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 | |||
}); |
@@ -27,42 +27,53 @@ function isWindow(element: Element | Window): element is Window { | |||
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, | |||
@@ -78,7 +89,7 @@ export function scrollToElement( | |||
const { top, bottom } = element.getBoundingClientRect(); | |||
const scrollTop = getScrollPosition(parent); | |||
const scroll = getScroll(parent); | |||
const height: number = isWindow(parent) | |||
? window.innerHeight | |||
@@ -87,20 +98,59 @@ export function scrollToElement( | |||
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); | |||
} | |||
} | |||
} |