@@ -276,8 +276,10 @@ export function getSources( | |||
return getJSON('/api/sources/lines', data).then(r => r.sources); | |||
} | |||
export function getDuplications(data: { key: string } & T.BranchParameters): Promise<any> { | |||
return getJSON('/api/duplications/show', data); | |||
export function getDuplications( | |||
data: { key: string } & T.BranchParameters | |||
): Promise<{ duplications: T.Duplication[]; files: T.Dict<T.DuplicatedFile> }> { | |||
return getJSON('/api/duplications/show', data).catch(throwGlobalError); | |||
} | |||
export function getTests( |
@@ -257,7 +257,7 @@ declare namespace T { | |||
} | |||
export interface DuplicationBlock { | |||
_ref: string; | |||
_ref?: string; | |||
from: number; | |||
size: number; | |||
} | |||
@@ -419,6 +419,13 @@ declare namespace T { | |||
[line: number]: SourceLine; | |||
} | |||
export interface LinePopup { | |||
index?: number; | |||
line: number; | |||
name: string; | |||
open?: boolean; | |||
} | |||
export interface LoggedInUser extends CurrentUser { | |||
avatar?: string; | |||
email?: string; |
@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import { | |||
createSnippets, | |||
expandSnippet, | |||
@@ -26,11 +27,11 @@ import { | |||
LINES_BELOW_LAST, | |||
MERGE_DISTANCE | |||
} from './utils'; | |||
import { getSources } from '../../../api/components'; | |||
import ExpandSnippetIcon from '../../../components/icons-components/ExpandSnippetIcon'; | |||
import Line from '../../../components/SourceViewer/components/Line'; | |||
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 { | |||
@@ -42,16 +43,25 @@ import { translate } from '../../../helpers/l10n'; | |||
interface Props { | |||
branchLike: T.BranchLike | undefined; | |||
duplications?: T.Duplication[]; | |||
duplicationsByLine?: { [line: number]: number[] }; | |||
highlightedLocationMessage: { index: number; text: string | undefined } | undefined; | |||
issue: T.Issue; | |||
issuePopup?: { issue: string; name: string }; | |||
issuesByLine: T.IssuesByLine; | |||
last: boolean; | |||
linePopup?: T.LinePopup; | |||
loadDuplications: (component: string, line: T.SourceLine) => void; | |||
locations: T.FlowLocation[]; | |||
onIssueChange: (issue: T.Issue) => void; | |||
onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void; | |||
onLinePopupToggle: (linePopup: T.LinePopup & { component: string }) => void; | |||
onLocationSelect: (index: number) => void; | |||
renderDuplicationPopup: (index: number, line: number) => JSX.Element; | |||
renderDuplicationPopup: ( | |||
component: T.SourceViewerFile, | |||
index: number, | |||
line: number | |||
) => React.ReactNode; | |||
scroll?: (element: HTMLElement) => void; | |||
snippetGroup: T.SnippetGroup; | |||
} | |||
@@ -59,7 +69,6 @@ interface Props { | |||
interface State { | |||
additionalLines: { [line: number]: T.SourceLine }; | |||
highlightedSymbols: string[]; | |||
linePopup?: { index?: number; line: number; name: string }; | |||
loading: boolean; | |||
openIssuesByLine: T.Dict<boolean>; | |||
snippets: T.SourceLine[][]; | |||
@@ -140,7 +149,6 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr | |||
return { | |||
additionalLines: combinedLines, | |||
linePopup: undefined, | |||
snippets: expandSnippet({ | |||
direction, | |||
lines: { ...combinedLines, ...this.props.snippetGroup.sources }, | |||
@@ -163,7 +171,7 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr | |||
getSources({ key }).then( | |||
lines => { | |||
if (this.mounted) { | |||
this.setState({ linePopup: undefined, loading: false, snippets: [lines] }); | |||
this.setState({ loading: false, snippets: [lines] }); | |||
} | |||
}, | |||
() => { | |||
@@ -174,29 +182,10 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr | |||
); | |||
}; | |||
handleLinePopupToggle = ({ | |||
index, | |||
line, | |||
name, | |||
open | |||
}: { | |||
index?: number; | |||
line: number; | |||
name: string; | |||
open?: boolean; | |||
}) => { | |||
this.setState((state: State) => { | |||
const samePopup = | |||
state.linePopup !== undefined && | |||
state.linePopup.name === name && | |||
state.linePopup.line === line && | |||
state.linePopup.index === index; | |||
if (open !== false && !samePopup) { | |||
return { linePopup: { index, line, name } }; | |||
} else if (open !== true && samePopup) { | |||
return { linePopup: undefined }; | |||
} | |||
return null; | |||
handleLinePopupToggle = (linePopup: T.LinePopup) => { | |||
this.props.onLinePopupToggle({ | |||
...linePopup, | |||
component: this.props.snippetGroup.component.key | |||
}); | |||
}; | |||
@@ -216,6 +205,14 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr | |||
this.setState({ highlightedSymbols }); | |||
}; | |||
loadDuplications = (line: T.SourceLine) => { | |||
this.props.loadDuplications(this.props.snippetGroup.component.key, line); | |||
}; | |||
renderDuplicationPopup = (index: number, line: number) => { | |||
return this.props.renderDuplicationPopup(this.props.snippetGroup.component, index, line); | |||
}; | |||
renderLine({ | |||
index, | |||
issuesForLine, | |||
@@ -234,10 +231,12 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr | |||
verticalBuffer: number; | |||
}) { | |||
const { openIssuesByLine } = this.state; | |||
const secondaryIssueLocations = getSecondaryIssueLocationsForLine(line, this.props.locations); | |||
const noop = () => {}; | |||
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); | |||
@@ -246,11 +245,11 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr | |||
branchLike={undefined} | |||
displayAllIssues={false} | |||
displayCoverage={true} | |||
displayDuplications={false} | |||
displayDuplications={!!line.duplicated} | |||
displayIssues={!isSinkLine || issuesForLine.length > 1} | |||
displayLocationMarkers={true} | |||
duplications={[]} | |||
duplicationsCount={0} | |||
duplications={lineDuplications} | |||
duplicationsCount={duplicationsCount} | |||
highlighted={false} | |||
highlightedLocationMessage={optimizeLocationMessage( | |||
this.props.highlightedLocationMessage, | |||
@@ -263,12 +262,12 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr | |||
key={line.line} | |||
last={false} | |||
line={line} | |||
linePopup={this.state.linePopup} | |||
loadDuplications={noop} | |||
linePopup={this.props.linePopup} | |||
loadDuplications={this.loadDuplications} | |||
onIssueChange={this.props.onIssueChange} | |||
onIssuePopupToggle={this.props.onIssuePopupToggle} | |||
onIssueSelect={noop} | |||
onIssueUnselect={noop} | |||
onIssueSelect={() => {}} | |||
onIssueUnselect={() => {}} | |||
onIssuesClose={this.handleCloseIssues} | |||
onIssuesOpen={this.handleOpenIssues} | |||
onLinePopupToggle={this.handleLinePopupToggle} | |||
@@ -276,7 +275,7 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr | |||
onSymbolClick={this.handleSymbolClick} | |||
openIssues={openIssuesByLine[line.line]} | |||
previousLine={index > 0 ? snippet[index - 1] : undefined} | |||
renderDuplicationPopup={this.props.renderDuplicationPopup} | |||
renderDuplicationPopup={this.renderDuplicationPopup} | |||
scroll={this.props.scroll} | |||
secondaryIssueLocations={secondaryIssueLocations} | |||
selectedIssue={optimizeSelectedIssue(this.props.issue.key, issuesForLine)} | |||
@@ -359,7 +358,7 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr | |||
} | |||
render() { | |||
const { branchLike, issue, issuesByLine, last, snippetGroup } = this.props; | |||
const { branchLike, duplications, issue, issuesByLine, last, snippetGroup } = this.props; | |||
const { loading, snippets } = this.state; | |||
const locations = locationsByLine([issue]); | |||
@@ -369,7 +368,10 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr | |||
snippets[0].length === parseInt(snippetGroup.component.measures.lines || '', 10); | |||
return ( | |||
<div className="component-source-container"> | |||
<div | |||
className={classNames('component-source-container', { | |||
'source-duplications-expanded': duplications && duplications.length > 0 | |||
})}> | |||
<SourceViewerHeaderSlim | |||
branchLike={branchLike} | |||
expandable={!fullyShown} |
@@ -21,14 +21,20 @@ import * as React from 'react'; | |||
import ComponentSourceSnippetViewer from './ComponentSourceSnippetViewer'; | |||
import { groupLocationsByComponent } from './utils'; | |||
import DeferredSpinner from '../../../components/common/DeferredSpinner'; | |||
import DuplicationPopup from '../../../components/SourceViewer/components/DuplicationPopup'; | |||
import { WorkspaceContext } from '../../../components/workspace/context'; | |||
import { getIssueFlowSnippets } from '../../../api/issues'; | |||
import { issuesByComponentAndLine } from '../../../components/SourceViewer/helpers/indexing'; | |||
interface State { | |||
components: T.Dict<T.SnippetsByComponent>; | |||
issuePopup?: { issue: string; name: string }; | |||
loading: boolean; | |||
} | |||
import { | |||
filterDuplicationBlocksByLine, | |||
isDuplicationBlockInRemovedComponent, | |||
getDuplicationBlocksForIndex | |||
} from '../../../components/SourceViewer/helpers/duplications'; | |||
import { | |||
duplicationsByLine, | |||
issuesByComponentAndLine | |||
} from '../../../components/SourceViewer/helpers/indexing'; | |||
import { getDuplications } from '../../../api/components'; | |||
import { getBranchLikeQuery } from '../../../helpers/branches'; | |||
interface Props { | |||
branchLike: T.Branch | T.PullRequest | undefined; | |||
@@ -39,15 +45,25 @@ interface Props { | |||
onIssueChange: (issue: T.Issue) => void; | |||
onLoaded?: () => void; | |||
onLocationSelect: (index: number) => void; | |||
renderDuplicationPopup: (index: number, line: number) => JSX.Element; | |||
scroll?: (element: HTMLElement) => void; | |||
selectedFlowIndex: number | undefined; | |||
} | |||
interface State { | |||
components: T.Dict<T.SnippetsByComponent>; | |||
duplicatedFiles?: T.Dict<T.DuplicatedFile>; | |||
duplications?: T.Duplication[]; | |||
duplicationsByLine: { [line: number]: number[] }; | |||
issuePopup?: { issue: string; name: string }; | |||
linePopup?: T.LinePopup & { component: string }; | |||
loading: boolean; | |||
} | |||
export default class CrossComponentSourceViewerWrapper extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { | |||
components: {}, | |||
duplicationsByLine: {}, | |||
loading: true | |||
}; | |||
@@ -66,12 +82,39 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone | |||
this.mounted = false; | |||
} | |||
fetchDuplications = (component: string, line: T.SourceLine) => { | |||
getDuplications({ | |||
key: component, | |||
...getBranchLikeQuery(this.props.branchLike) | |||
}).then( | |||
r => { | |||
if (this.mounted) { | |||
this.setState(state => ({ | |||
duplicatedFiles: r.files, | |||
duplications: r.duplications, | |||
duplicationsByLine: duplicationsByLine(r.duplications), | |||
linePopup: | |||
r.duplications.length === 1 | |||
? { component, index: 0, line: line.line, name: 'duplications' } | |||
: state.linePopup | |||
})); | |||
} | |||
}, | |||
() => {} | |||
); | |||
}; | |||
fetchIssueFlowSnippets(issueKey: string) { | |||
this.setState({ loading: true }); | |||
getIssueFlowSnippets(issueKey).then( | |||
components => { | |||
if (this.mounted) { | |||
this.setState({ components, issuePopup: undefined, loading: false }); | |||
this.setState({ | |||
components, | |||
issuePopup: undefined, | |||
linePopup: undefined, | |||
loading: false | |||
}); | |||
if (this.props.onLoaded) { | |||
this.props.onLoaded(); | |||
} | |||
@@ -98,8 +141,61 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone | |||
}); | |||
}; | |||
handleLinePopupToggle = ({ | |||
component, | |||
index, | |||
line, | |||
name, | |||
open | |||
}: T.LinePopup & { component: string }) => { | |||
this.setState((state: State) => { | |||
const samePopup = | |||
state.linePopup !== undefined && | |||
state.linePopup.line === line && | |||
state.linePopup.name === name && | |||
state.linePopup.component === component && | |||
state.linePopup.index === index; | |||
if (open !== false && !samePopup) { | |||
return { linePopup: { component, index, line, name } }; | |||
} else if (open !== true && samePopup) { | |||
return { linePopup: undefined }; | |||
} | |||
return null; | |||
}); | |||
}; | |||
handleCloseLinePopup = () => { | |||
this.setState({ linePopup: undefined }); | |||
}; | |||
renderDuplicationPopup = (component: T.SourceViewerFile, index: number, line: number) => { | |||
const { duplicatedFiles, duplications } = this.state; | |||
if (!component || !duplicatedFiles) { | |||
return null; | |||
} | |||
const blocks = getDuplicationBlocksForIndex(duplications, index); | |||
return ( | |||
<WorkspaceContext.Consumer> | |||
{({ openComponent }) => ( | |||
<DuplicationPopup | |||
blocks={filterDuplicationBlocksByLine(blocks, line)} | |||
branchLike={this.props.branchLike} | |||
duplicatedFiles={duplicatedFiles} | |||
inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)} | |||
onClose={this.handleCloseLinePopup} | |||
openComponent={openComponent} | |||
sourceViewerFile={component} | |||
/> | |||
)} | |||
</WorkspaceContext.Consumer> | |||
); | |||
}; | |||
render() { | |||
const { components, loading } = this.state; | |||
const { loading } = this.state; | |||
if (loading) { | |||
return ( | |||
@@ -109,29 +205,43 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone | |||
); | |||
} | |||
const { components, duplications, duplicationsByLine, linePopup } = this.state; | |||
const issuesByComponent = issuesByComponentAndLine(this.props.issues); | |||
const locationsByComponent = groupLocationsByComponent(this.props.locations, components); | |||
return ( | |||
<div> | |||
{locationsByComponent.map((g, i) => ( | |||
<ComponentSourceSnippetViewer | |||
branchLike={this.props.branchLike} | |||
highlightedLocationMessage={this.props.highlightedLocationMessage} | |||
issue={this.props.issue} | |||
issuePopup={this.state.issuePopup} | |||
issuesByLine={issuesByComponent[g.component.key] || {}} | |||
key={`${this.props.issue.key}-${this.props.selectedFlowIndex}-${i}`} | |||
last={i === locationsByComponent.length - 1} | |||
locations={g.locations || []} | |||
onIssueChange={this.props.onIssueChange} | |||
onIssuePopupToggle={this.handleIssuePopupToggle} | |||
onLocationSelect={this.props.onLocationSelect} | |||
renderDuplicationPopup={this.props.renderDuplicationPopup} | |||
scroll={this.props.scroll} | |||
snippetGroup={g} | |||
/> | |||
))} | |||
{locationsByComponent.map((snippetGroup, i) => { | |||
let componentProps = {}; | |||
if (linePopup && snippetGroup.component.key === linePopup.component) { | |||
componentProps = { | |||
duplications, | |||
duplicationsByLine, | |||
linePopup: { index: linePopup.index, line: linePopup.line, name: linePopup.name } | |||
}; | |||
} | |||
return ( | |||
<ComponentSourceSnippetViewer | |||
branchLike={this.props.branchLike} | |||
highlightedLocationMessage={this.props.highlightedLocationMessage} | |||
issue={this.props.issue} | |||
issuePopup={this.state.issuePopup} | |||
issuesByLine={issuesByComponent[snippetGroup.component.key] || {}} | |||
key={`${this.props.issue.key}-${this.props.selectedFlowIndex}-${i}`} | |||
last={i === locationsByComponent.length - 1} | |||
loadDuplications={this.fetchDuplications} | |||
locations={snippetGroup.locations || []} | |||
onIssueChange={this.props.onIssueChange} | |||
onIssuePopupToggle={this.handleIssuePopupToggle} | |||
onLinePopupToggle={this.handleLinePopupToggle} | |||
onLocationSelect={this.props.onLocationSelect} | |||
renderDuplicationPopup={this.renderDuplicationPopup} | |||
scroll={this.props.scroll} | |||
snippetGroup={snippetGroup} | |||
{...componentProps} | |||
/> | |||
); | |||
})} | |||
</div> | |||
); | |||
} |
@@ -118,6 +118,55 @@ it('should handle symbol highlighting', () => { | |||
expect(wrapper.state('highlightedSymbols')).toEqual(['foo']); | |||
}); | |||
it('should correctly handle lines actions', () => { | |||
const snippetGroup: T.SnippetGroup = { | |||
locations: [ | |||
mockFlowLocation({ | |||
component: 'a', | |||
textRange: { startLine: 34, endLine: 34, startOffset: 0, endOffset: 0 } | |||
}), | |||
mockFlowLocation({ | |||
component: 'a', | |||
textRange: { startLine: 54, endLine: 54, startOffset: 0, endOffset: 0 } | |||
}) | |||
], | |||
...mockSnippetsByComponent('a', [32, 33, 34, 35, 36, 52, 53, 54, 55, 56]) | |||
}; | |||
const loadDuplications = jest.fn(); | |||
const onLinePopupToggle = jest.fn(); | |||
const renderDuplicationPopup = jest.fn(); | |||
const wrapper = shallowRender({ | |||
loadDuplications, | |||
onLinePopupToggle, | |||
renderDuplicationPopup, | |||
snippetGroup | |||
}); | |||
const line = mockSourceLine(); | |||
wrapper | |||
.find('Line') | |||
.first() | |||
.prop<Function>('loadDuplications')(line); | |||
expect(loadDuplications).toHaveBeenCalledWith('a', line); | |||
wrapper | |||
.find('Line') | |||
.first() | |||
.prop<Function>('onLinePopupToggle')({ line: 13, name: 'foo' }); | |||
expect(onLinePopupToggle).toHaveBeenCalledWith({ component: 'a', line: 13, name: 'foo' }); | |||
wrapper | |||
.find('Line') | |||
.first() | |||
.prop<Function>('renderDuplicationPopup')(1, 13); | |||
expect(renderDuplicationPopup).toHaveBeenCalledWith( | |||
mockSourceViewerFile({ key: 'a', path: 'a' }), | |||
1, | |||
13 | |||
); | |||
}); | |||
function shallowRender(props: Partial<ComponentSourceSnippetViewer['props']> = {}) { | |||
const snippetGroup: T.SnippetGroup = { | |||
component: mockSourceViewerFile(), | |||
@@ -127,13 +176,18 @@ function shallowRender(props: Partial<ComponentSourceSnippetViewer['props']> = { | |||
return shallow<ComponentSourceSnippetViewer>( | |||
<ComponentSourceSnippetViewer | |||
branchLike={mockMainBranch()} | |||
duplications={undefined} | |||
duplicationsByLine={undefined} | |||
highlightedLocationMessage={{ index: 0, text: '' }} | |||
issue={mockIssue()} | |||
issuesByLine={{}} | |||
last={false} | |||
linePopup={undefined} | |||
loadDuplications={jest.fn()} | |||
locations={[]} | |||
onIssueChange={jest.fn()} | |||
onIssuePopupToggle={jest.fn()} | |||
onLinePopupToggle={jest.fn()} | |||
onLocationSelect={jest.fn()} | |||
renderDuplicationPopup={jest.fn()} | |||
scroll={jest.fn()} |
@@ -20,31 +20,46 @@ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import CrossComponentSourceViewerWrapper from '../CrossComponentSourceViewerWrapper'; | |||
import { mockIssue, mockSourceViewerFile } from '../../../../helpers/testMocks'; | |||
import { | |||
mockFlowLocation, | |||
mockIssue, | |||
mockSnippetsByComponent, | |||
mockSourceLine, | |||
mockSourceViewerFile | |||
} from '../../../../helpers/testMocks'; | |||
import { waitAndUpdate } from '../../../../helpers/testUtils'; | |||
import { getIssueFlowSnippets } from '../../../../api/issues'; | |||
import { getDuplications } from '../../../../api/components'; | |||
jest.mock('../../../../api/issues', () => { | |||
const { mockSourceViewerFile } = require.requireActual('../../../../helpers/testMocks'); | |||
const { mockSnippetsByComponent } = require.requireActual('../../../../helpers/testMocks'); | |||
return { | |||
getIssueFlowSnippets: jest.fn().mockResolvedValue([mockSourceViewerFile()]) | |||
getIssueFlowSnippets: jest.fn().mockResolvedValue({ 'main.js': mockSnippetsByComponent() }) | |||
}; | |||
}); | |||
jest.mock('../../../../api/components', () => ({ | |||
getDuplications: jest.fn().mockResolvedValue({}) | |||
})); | |||
beforeEach(() => { | |||
jest.clearAllMocks(); | |||
}); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
it('should render correctly', async () => { | |||
const wrapper = shallowRender(); | |||
expect(wrapper).toMatchSnapshot(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('Should fetch data', async () => { | |||
const wrapper = shallowRender(); | |||
wrapper.instance().fetchIssueFlowSnippets('124'); | |||
await waitAndUpdate(wrapper); | |||
expect(getIssueFlowSnippets).toBeCalled(); | |||
expect(wrapper.state('components')).toEqual([mockSourceViewerFile()]); | |||
expect(getIssueFlowSnippets).toHaveBeenCalledWith('1'); | |||
expect(wrapper.state('components')).toEqual({ 'main.js': mockSnippetsByComponent() }); | |||
(getIssueFlowSnippets as jest.Mock).mockClear(); | |||
wrapper.setProps({ issue: mockIssue(true, { key: 'foo' }) }); | |||
@@ -62,18 +77,68 @@ it('should handle issue popup', () => { | |||
expect(wrapper.state('issuePopup')).toBeUndefined(); | |||
}); | |||
it('should handle line popup', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
const linePopup = { component: 'foo', index: 0, line: 16, name: 'b.tsx' }; | |||
wrapper.find('ComponentSourceSnippetViewer').prop<Function>('onLinePopupToggle')(linePopup); | |||
expect(wrapper.state('linePopup')).toEqual(linePopup); | |||
wrapper.find('ComponentSourceSnippetViewer').prop<Function>('onLinePopupToggle')(linePopup); | |||
expect(wrapper.state('linePopup')).toEqual(undefined); | |||
const openLinePopup = { ...linePopup, open: true }; | |||
wrapper.find('ComponentSourceSnippetViewer').prop<Function>('onLinePopupToggle')(openLinePopup); | |||
wrapper.find('ComponentSourceSnippetViewer').prop<Function>('onLinePopupToggle')(openLinePopup); | |||
expect(wrapper.state('linePopup')).toEqual(linePopup); | |||
}); | |||
it('should handle duplication popup', async () => { | |||
const files = { b: { key: 'b', name: 'B.tsx', project: 'foo', projectName: 'Foo' } }; | |||
const duplications = [{ blocks: [{ _ref: '1', from: 1, size: 2 }] }]; | |||
(getDuplications as jest.Mock).mockResolvedValueOnce({ duplications, files }); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.find('ComponentSourceSnippetViewer').prop<Function>('loadDuplications')( | |||
'foo', | |||
mockSourceLine() | |||
); | |||
await waitAndUpdate(wrapper); | |||
expect(getDuplications).toHaveBeenCalledWith({ key: 'foo' }); | |||
expect(wrapper.state('duplicatedFiles')).toEqual(files); | |||
expect(wrapper.state('duplications')).toEqual(duplications); | |||
expect(wrapper.state('duplicationsByLine')).toEqual({ '1': [0], '2': [0] }); | |||
expect(wrapper.state('linePopup')).toEqual({ | |||
component: 'foo', | |||
index: 0, | |||
line: 16, | |||
name: 'duplications' | |||
}); | |||
expect( | |||
wrapper.find('ComponentSourceSnippetViewer').prop<Function>('renderDuplicationPopup')( | |||
mockSourceViewerFile(), | |||
0, | |||
16 | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<CrossComponentSourceViewerWrapper['props']> = {}) { | |||
return shallow<CrossComponentSourceViewerWrapper>( | |||
<CrossComponentSourceViewerWrapper | |||
branchLike={undefined} | |||
highlightedLocationMessage={undefined} | |||
issue={mockIssue(true)} | |||
issue={mockIssue(true, { key: '1' })} | |||
issues={[]} | |||
locations={[]} | |||
locations={[mockFlowLocation()]} | |||
onIssueChange={jest.fn()} | |||
onLoaded={jest.fn()} | |||
onLocationSelect={jest.fn()} | |||
renderDuplicationPopup={jest.fn()} | |||
scroll={jest.fn()} | |||
selectedFlowIndex={0} | |||
{...props} |
@@ -1,5 +1,11 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should handle duplication popup 1`] = ` | |||
<Context.Consumer> | |||
[Function] | |||
</Context.Consumer> | |||
`; | |||
exports[`should render correctly 1`] = ` | |||
<div> | |||
<DeferredSpinner | |||
@@ -7,3 +13,179 @@ exports[`should render correctly 1`] = ` | |||
/> | |||
</div> | |||
`; | |||
exports[`should render correctly 2`] = ` | |||
<div> | |||
<ComponentSourceSnippetViewer | |||
issue={ | |||
Object { | |||
"actions": Array [], | |||
"component": "main.js", | |||
"componentLongName": "main.js", | |||
"componentQualifier": "FIL", | |||
"componentUuid": "foo1234", | |||
"creationDate": "2017-03-01T09:36:01+0100", | |||
"flows": Array [ | |||
Array [ | |||
Object { | |||
"component": "main.js", | |||
"textRange": Object { | |||
"endLine": 2, | |||
"endOffset": 2, | |||
"startLine": 1, | |||
"startOffset": 1, | |||
}, | |||
}, | |||
Object { | |||
"component": "main.js", | |||
"textRange": Object { | |||
"endLine": 2, | |||
"endOffset": 2, | |||
"startLine": 1, | |||
"startOffset": 1, | |||
}, | |||
}, | |||
Object { | |||
"component": "main.js", | |||
"textRange": Object { | |||
"endLine": 2, | |||
"endOffset": 2, | |||
"startLine": 1, | |||
"startOffset": 1, | |||
}, | |||
}, | |||
], | |||
Array [ | |||
Object { | |||
"component": "main.js", | |||
"textRange": Object { | |||
"endLine": 2, | |||
"endOffset": 2, | |||
"startLine": 1, | |||
"startOffset": 1, | |||
}, | |||
}, | |||
Object { | |||
"component": "main.js", | |||
"textRange": Object { | |||
"endLine": 2, | |||
"endOffset": 2, | |||
"startLine": 1, | |||
"startOffset": 1, | |||
}, | |||
}, | |||
], | |||
], | |||
"fromHotspot": false, | |||
"key": "1", | |||
"line": 25, | |||
"message": "Reduce the number of conditional operators (4) used in the expression", | |||
"organization": "myorg", | |||
"project": "myproject", | |||
"projectKey": "foo", | |||
"projectName": "Foo", | |||
"projectOrganization": "org", | |||
"rule": "javascript:S1067", | |||
"ruleName": "foo", | |||
"secondaryLocations": Array [ | |||
Object { | |||
"component": "main.js", | |||
"textRange": Object { | |||
"endLine": 2, | |||
"endOffset": 2, | |||
"startLine": 1, | |||
"startOffset": 1, | |||
}, | |||
}, | |||
Object { | |||
"component": "main.js", | |||
"textRange": Object { | |||
"endLine": 2, | |||
"endOffset": 2, | |||
"startLine": 1, | |||
"startOffset": 1, | |||
}, | |||
}, | |||
], | |||
"severity": "MAJOR", | |||
"status": "OPEN", | |||
"textRange": Object { | |||
"endLine": 26, | |||
"endOffset": 15, | |||
"startLine": 25, | |||
"startOffset": 0, | |||
}, | |||
"transitions": Array [], | |||
"type": "BUG", | |||
} | |||
} | |||
issuesByLine={Object {}} | |||
key="1-0-0" | |||
last={true} | |||
loadDuplications={[Function]} | |||
locations={ | |||
Array [ | |||
Object { | |||
"component": "main.js", | |||
"index": 0, | |||
"textRange": Object { | |||
"endLine": 2, | |||
"endOffset": 2, | |||
"startLine": 1, | |||
"startOffset": 1, | |||
}, | |||
}, | |||
] | |||
} | |||
onIssueChange={[MockFunction]} | |||
onIssuePopupToggle={[Function]} | |||
onLinePopupToggle={[Function]} | |||
onLocationSelect={[MockFunction]} | |||
renderDuplicationPopup={[Function]} | |||
scroll={[MockFunction]} | |||
snippetGroup={ | |||
Object { | |||
"component": Object { | |||
"key": "main.js", | |||
"measures": Object { | |||
"coverage": "85.2", | |||
"duplicationDensity": "1.0", | |||
"issues": "12", | |||
"lines": "56", | |||
}, | |||
"path": "main.js", | |||
"project": "my-project", | |||
"projectName": "MyProject", | |||
"q": "FIL", | |||
"uuid": "foo-bar", | |||
}, | |||
"locations": Array [ | |||
Object { | |||
"component": "main.js", | |||
"index": 0, | |||
"textRange": Object { | |||
"endLine": 2, | |||
"endOffset": 2, | |||
"startLine": 1, | |||
"startOffset": 1, | |||
}, | |||
}, | |||
], | |||
"sources": Object { | |||
"16": Object { | |||
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;", | |||
"coverageStatus": "covered", | |||
"coveredConditions": 2, | |||
"duplicated": false, | |||
"isNew": true, | |||
"line": 16, | |||
"scmAuthor": "simon.brandhof@sonarsource.com", | |||
"scmDate": "2018-12-11T10:48:39+0100", | |||
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", | |||
}, | |||
}, | |||
} | |||
} | |||
/> | |||
</div> | |||
`; |
@@ -254,8 +254,11 @@ | |||
text-align: left; | |||
cursor: pointer; | |||
} | |||
.snippet > .expand-block:hover { | |||
.snippet > .expand-block:hover, | |||
.snippet > .expand-block:focus, | |||
.snippet > .expand-block:active { | |||
color: var(--darkBlue); | |||
outline: none; | |||
} | |||
.snippet > .expand-block-above { | |||
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAAXNSR0IArs4c6QAAADdJREFUCB1dzMEKADAIAlBd1v9/bcc2YgRjHh8qq2qTxCQzsX4wM6y30RARF3sy0Es1SIK7Y64OpCES1W69JS4AAAAASUVORK5CYII='); |
@@ -27,12 +27,18 @@ import { SourceViewerContext } from './SourceViewerContext'; | |||
import DuplicationPopup from './components/DuplicationPopup'; | |||
import defaultLoadIssues from './helpers/loadIssues'; | |||
import getCoverageStatus from './helpers/getCoverageStatus'; | |||
import { | |||
filterDuplicationBlocksByLine, | |||
getDuplicationBlocksForIndex, | |||
isDuplicationBlockInRemovedComponent | |||
} from './helpers/duplications'; | |||
import { | |||
duplicationsByLine, | |||
issuesByLine, | |||
locationsByLine, | |||
symbolsByLine | |||
} from './helpers/indexing'; | |||
import { Alert } from '../ui/Alert'; | |||
import { | |||
getComponentData, | |||
getComponentForSourceViewer, | |||
@@ -41,7 +47,6 @@ import { | |||
} from '../../api/components'; | |||
import { isSameBranchLike, getBranchLikeQuery } from '../../helpers/branches'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { Alert } from '../ui/Alert'; | |||
import { WorkspaceContext } from '../workspace/context'; | |||
import './styles.css'; | |||
@@ -97,7 +102,7 @@ interface State { | |||
issuePopup?: { issue: string; name: string }; | |||
issues?: T.Issue[]; | |||
issuesByLine: { [line: number]: T.Issue[] }; | |||
linePopup?: { index?: number; line: number; name: string }; | |||
linePopup?: T.LinePopup; | |||
loading: boolean; | |||
loadingSourcesAfter: boolean; | |||
loadingSourcesBefore: boolean; | |||
@@ -495,17 +500,7 @@ export default class SourceViewerBase extends React.PureComponent<Props, State> | |||
); | |||
}; | |||
handleLinePopupToggle = ({ | |||
index, | |||
line, | |||
name, | |||
open | |||
}: { | |||
index?: number; | |||
line: number; | |||
name: string; | |||
open?: boolean; | |||
}) => { | |||
handleLinePopupToggle = ({ index, line, name, open }: T.LinePopup) => { | |||
this.setState((state: State) => { | |||
const samePopup = | |||
state.linePopup !== undefined && | |||
@@ -587,34 +582,20 @@ export default class SourceViewerBase extends React.PureComponent<Props, State> | |||
renderDuplicationPopup = (index: number, line: number) => { | |||
const { component, duplicatedFiles, duplications } = this.state; | |||
if (!component || !duplicatedFiles) return <></>; | |||
const duplication = duplications && duplications[index]; | |||
let blocks = (duplication && duplication.blocks) || []; | |||
/* eslint-disable no-underscore-dangle */ | |||
const inRemovedComponent = blocks.some(b => b._ref === undefined); | |||
let foundOne = false; | |||
blocks = blocks.filter(b => { | |||
const outOfBounds = b.from > line || b.from + b.size < line; | |||
const currentFile = b._ref === '1'; | |||
const shouldDisplayForCurrentFile = outOfBounds || foundOne; | |||
const shouldDisplay = !currentFile || shouldDisplayForCurrentFile; | |||
const isOk = b._ref !== undefined && shouldDisplay; | |||
if (b._ref === '1' && !outOfBounds) { | |||
foundOne = true; | |||
} | |||
return isOk; | |||
}); | |||
/* eslint-enable no-underscore-dangle */ | |||
if (!component || !duplicatedFiles) { | |||
return null; | |||
} | |||
const blocks = getDuplicationBlocksForIndex(duplications, index); | |||
return ( | |||
<WorkspaceContext.Consumer> | |||
{({ openComponent }) => ( | |||
<DuplicationPopup | |||
blocks={blocks} | |||
blocks={filterDuplicationBlocksByLine(blocks, line)} | |||
branchLike={this.props.branchLike} | |||
duplicatedFiles={duplicatedFiles} | |||
inRemovedComponent={inRemovedComponent} | |||
inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)} | |||
onClose={this.closeLinePopup} | |||
openComponent={openComponent} | |||
sourceViewerFile={component} |
@@ -56,7 +56,7 @@ interface Props { | |||
issuePopup: { issue: string; name: string } | undefined; | |||
issues: T.Issue[] | undefined; | |||
issuesByLine: { [line: number]: T.Issue[] }; | |||
linePopup: { index?: number; line: number; name: string } | undefined; | |||
linePopup: T.LinePopup | undefined; | |||
loadDuplications: (line: T.SourceLine) => void; | |||
loadingSourcesAfter: boolean; | |||
loadingSourcesBefore: boolean; | |||
@@ -68,11 +68,11 @@ interface Props { | |||
onIssueSelect: (issueKey: string) => void; | |||
onIssuesOpen: (line: T.SourceLine) => void; | |||
onIssueUnselect: () => void; | |||
onLinePopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; | |||
onLinePopupToggle: (linePopup: T.LinePopup) => void; | |||
onLocationSelect: ((index: number) => void) | undefined; | |||
onSymbolClick: (symbols: string[]) => void; | |||
openIssuesByLine: { [line: number]: boolean }; | |||
renderDuplicationPopup: (index: number, line: number) => JSX.Element; | |||
renderDuplicationPopup: (index: number, line: number) => React.ReactNode; | |||
scroll?: (element: HTMLElement) => void; | |||
selectedIssue: string | undefined; | |||
sources: T.SourceLine[]; |
@@ -46,9 +46,9 @@ interface Props { | |||
issues: T.Issue[]; | |||
last: boolean; | |||
line: T.SourceLine; | |||
linePopup: { index?: number; line: number; name: string } | undefined; | |||
linePopup: T.LinePopup | undefined; | |||
loadDuplications: (line: T.SourceLine) => void; | |||
onLinePopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; | |||
onLinePopupToggle: (linePopup: T.LinePopup) => void; | |||
onIssueChange: (issue: T.Issue) => void; | |||
onIssuePopupToggle: (issueKey: string, popupName: string, open?: boolean) => void; | |||
onIssuesClose: (line: T.SourceLine) => void; | |||
@@ -59,7 +59,7 @@ interface Props { | |||
onSymbolClick: (symbols: string[]) => void; | |||
openIssues: boolean; | |||
previousLine: T.SourceLine | undefined; | |||
renderDuplicationPopup: (index: number, line: number) => JSX.Element; | |||
renderDuplicationPopup: (index: number, line: number) => React.ReactNode; | |||
scroll?: (element: HTMLElement) => void; | |||
secondaryIssueLocations: T.LinearIssueLocation[]; | |||
selectedIssue: string | undefined; |
@@ -27,9 +27,9 @@ interface Props { | |||
duplicated: boolean; | |||
index: number; | |||
line: T.SourceLine; | |||
onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; | |||
onPopupToggle: (linePopup: T.LinePopup) => void; | |||
popupOpen: boolean; | |||
renderDuplicationPopup: (index: number, line: number) => JSX.Element; | |||
renderDuplicationPopup: (index: number, line: number) => React.ReactNode; | |||
} | |||
export default class LineDuplicationBlock extends React.PureComponent<Props> { |
@@ -23,7 +23,7 @@ import Toggler from '../../controls/Toggler'; | |||
interface Props { | |||
line: T.SourceLine; | |||
onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; | |||
onPopupToggle: (linePopup: T.LinePopup) => void; | |||
popupOpen: boolean; | |||
} | |||
@@ -23,7 +23,7 @@ import Toggler from '../../controls/Toggler'; | |||
interface Props { | |||
line: T.SourceLine; | |||
onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; | |||
onPopupToggle: (linePopup: T.LinePopup) => void; | |||
popupOpen: boolean; | |||
previousLine: T.SourceLine | undefined; | |||
} |
@@ -0,0 +1,66 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`loadIssues should load issues 1`] = ` | |||
Array [ | |||
Object { | |||
"actions": Array [ | |||
"set_tags", | |||
"comment", | |||
"assign", | |||
], | |||
"assignee": "luke", | |||
"assigneeActive": true, | |||
"assigneeAvatar": "lukavatar", | |||
"assigneeLogin": "luke", | |||
"assigneeName": "Luke", | |||
"author": "luke@sonarsource.com", | |||
"comments": Array [], | |||
"component": "foo.java", | |||
"componentEnabled": true, | |||
"componentKey": "foo.java", | |||
"componentLongName": "Foo.java", | |||
"componentName": "foo.java", | |||
"componentOrganization": "default-organization", | |||
"componentPath": "/foo.java", | |||
"componentQualifier": "FIL", | |||
"creationDate": "2016-08-15T15:25:38+0200", | |||
"flows": Array [], | |||
"fromHotspot": true, | |||
"hash": "78417dcee7ba927b7e7c9161e29e02b8", | |||
"key": "AWaqVGl3tut9VbnJvk6M", | |||
"line": 62, | |||
"message": "Make sure this file handling is safe here.", | |||
"organization": "default-organization", | |||
"project": "org.sonarsource.java:java", | |||
"projectEnabled": true, | |||
"projectKey": "org.sonarsource.java:java", | |||
"projectLongName": "SonarJava", | |||
"projectName": "SonarJava", | |||
"projectOrganization": "default-organization", | |||
"projectQualifier": "TRK", | |||
"rule": "squid:S4797", | |||
"ruleKey": "squid:S4797", | |||
"ruleLang": "java", | |||
"ruleLangName": "Java", | |||
"ruleName": "Handling files is security-sensitive", | |||
"ruleStatus": "READY", | |||
"secondaryLocations": Array [], | |||
"status": "OPEN", | |||
"tags": Array [ | |||
"cert", | |||
"cwe", | |||
"owasp-a1", | |||
"owasp-a3", | |||
], | |||
"textRange": Object { | |||
"endLine": 62, | |||
"endOffset": 96, | |||
"startLine": 62, | |||
"startOffset": 93, | |||
}, | |||
"transitions": Array [], | |||
"type": "SECURITY_HOTSPOT", | |||
"updateDate": "2018-10-25T10:23:08+0200", | |||
}, | |||
] | |||
`; |
@@ -0,0 +1,49 @@ | |||
/* | |||
* 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 { | |||
getDuplicationBlocksForIndex, | |||
isDuplicationBlockInRemovedComponent | |||
} from '../duplications'; | |||
describe('getDuplicationBlocksForIndex', () => { | |||
it('should return duplications blocks', () => { | |||
const blocks = [{ _ref: '0', from: 2, size: 2 }]; | |||
expect(getDuplicationBlocksForIndex([{ blocks }], 0)).toBe(blocks); | |||
expect(getDuplicationBlocksForIndex([{ blocks }], 5)).toEqual([]); | |||
expect(getDuplicationBlocksForIndex(undefined, 5)).toEqual([]); | |||
}); | |||
}); | |||
describe('isDuplicationBlockInRemovedComponent', () => { | |||
it('should ', () => { | |||
expect( | |||
isDuplicationBlockInRemovedComponent([ | |||
{ _ref: '0', from: 2, size: 2 }, | |||
{ _ref: '0', from: 3, size: 1 } | |||
]) | |||
).toBe(false); | |||
expect( | |||
isDuplicationBlockInRemovedComponent([ | |||
{ _ref: undefined, from: 2, size: 2 }, | |||
{ _ref: '0', from: 3, size: 1 } | |||
]) | |||
).toBe(true); | |||
}); | |||
}); |
@@ -0,0 +1,92 @@ | |||
/* | |||
* 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 loadIssues from '../loadIssues'; | |||
import { mockMainBranch } from '../../../../helpers/testMocks'; | |||
jest.mock('../../../../api/issues', () => ({ | |||
searchIssues: jest.fn().mockResolvedValue({ | |||
paging: { pageIndex: 1, pageSize: 500, total: 1 }, | |||
effortTotal: 15, | |||
debtTotal: 15, | |||
issues: [ | |||
{ | |||
key: 'AWaqVGl3tut9VbnJvk6M', | |||
rule: 'squid:S4797', | |||
component: 'foo.java', | |||
project: 'org.sonarsource.java:java', | |||
line: 62, | |||
hash: '78417dcee7ba927b7e7c9161e29e02b8', | |||
textRange: { startLine: 62, endLine: 62, startOffset: 93, endOffset: 96 }, | |||
flows: [], | |||
status: 'OPEN', | |||
message: 'Make sure this file handling is safe here.', | |||
assignee: 'luke', | |||
author: 'luke@sonarsource.com', | |||
tags: ['cert', 'cwe', 'owasp-a1', 'owasp-a3'], | |||
transitions: [], | |||
actions: ['set_tags', 'comment', 'assign'], | |||
comments: [], | |||
creationDate: '2016-08-15T15:25:38+0200', | |||
updateDate: '2018-10-25T10:23:08+0200', | |||
type: 'SECURITY_HOTSPOT', | |||
organization: 'default-organization', | |||
fromHotspot: true | |||
} | |||
], | |||
components: [ | |||
{ | |||
organization: 'default-organization', | |||
key: 'org.sonarsource.java:java', | |||
enabled: true, | |||
qualifier: 'TRK', | |||
name: 'SonarJava', | |||
longName: 'SonarJava' | |||
}, | |||
{ | |||
organization: 'default-organization', | |||
key: 'foo.java', | |||
enabled: true, | |||
qualifier: 'FIL', | |||
name: 'foo.java', | |||
longName: 'Foo.java', | |||
path: '/foo.java' | |||
} | |||
], | |||
rules: [ | |||
{ | |||
key: 'squid:S4797', | |||
name: 'Handling files is security-sensitive', | |||
lang: 'java', | |||
status: 'READY', | |||
langName: 'Java' | |||
} | |||
], | |||
users: [{ login: 'luke', name: 'Luke', avatar: 'lukavatar', active: true }], | |||
languages: [{ key: 'java', name: 'Java' }], | |||
facets: [] | |||
}) | |||
})); | |||
describe('loadIssues', () => { | |||
it('should load issues', async () => { | |||
const result = await loadIssues('foo.java', 1, 500, mockMainBranch()); | |||
expect(result).toMatchSnapshot(); | |||
}); | |||
}); |
@@ -0,0 +1,48 @@ | |||
/* | |||
* 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. | |||
*/ | |||
// TODO Test this function, but I don't get the logic behind it | |||
export function filterDuplicationBlocksByLine(blocks: T.DuplicationBlock[], line: number) { | |||
/* eslint-disable no-underscore-dangle */ | |||
let foundOne = false; | |||
return blocks.filter(b => { | |||
const outOfBounds = b.from > line || b.from + b.size < line; | |||
const currentFile = b._ref === '1'; | |||
const shouldDisplayForCurrentFile = outOfBounds || foundOne; | |||
const shouldDisplay = !currentFile || shouldDisplayForCurrentFile; | |||
const isOk = b._ref !== undefined && shouldDisplay; | |||
if (b._ref === '1' && !outOfBounds) { | |||
foundOne = true; | |||
} | |||
return isOk; | |||
}); | |||
/* eslint-enable no-underscore-dangle */ | |||
} | |||
export function getDuplicationBlocksForIndex( | |||
duplications: T.Duplication[] | undefined, | |||
index: number | |||
) { | |||
return (duplications && duplications[index] && duplications[index].blocks) || []; | |||
} | |||
export function isDuplicationBlockInRemovedComponent(blocks: T.DuplicationBlock[]) { | |||
return blocks.some(b => b._ref === undefined); // eslint-disable-line no-underscore-dangle | |||
} |
@@ -31,6 +31,7 @@ | |||
.issue.selected { | |||
box-shadow: none; | |||
outline: none; | |||
border: 2px solid var(--blue) !important; | |||
} | |||