diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2019-04-30 15:59:32 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2019-05-06 11:01:15 +0200 |
commit | d5b454babf688705a9cd4c8fd862b0fc0a9898d2 (patch) | |
tree | a3a080a46589066c8b7fd4efd4c944f92cf252a0 /server/sonar-web | |
parent | 6c37e7a9c7ef1b048c2067250235e24b386f4fc9 (diff) | |
download | sonarqube-d5b454babf688705a9cd4c8fd862b0fc0a9898d2.tar.gz sonarqube-d5b454babf688705a9cd4c8fd862b0fc0a9898d2.zip |
SONAR-11898 Fix quality issues and improve coverage
Diffstat (limited to 'server/sonar-web')
20 files changed, 795 insertions, 121 deletions
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap index d25d9196349..762028e42a4 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap @@ -95,7 +95,7 @@ exports[`should render correctly for a file 1`] = ` <div className="measure-details-viewer" > - <LazyLoader + <SourceViewer component="foo:src/index.tsx" /> </div> diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesSourceViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesSourceViewer-test.tsx new file mode 100644 index 00000000000..47580bcf048 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesSourceViewer-test.tsx @@ -0,0 +1,51 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import IssuesSourceViewer from '../IssuesSourceViewer'; +import { mockMainBranch, mockIssue } from '../../../../helpers/testMocks'; + +it('should render SourceViewer correctly', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +it('should render CrossComponentSourceViewer correctly', () => { + expect( + shallowRender({ issues: [mockIssue(true)], openIssue: mockIssue(true) }) + ).toMatchSnapshot(); +}); + +function shallowRender(props: Partial<IssuesSourceViewer['props']> = {}) { + return shallow( + <IssuesSourceViewer + branchLike={mockMainBranch()} + issues={[mockIssue()]} + loadIssues={jest.fn()} + locationsNavigator={true} + onIssueChange={jest.fn()} + onIssueSelect={jest.fn()} + onLocationSelect={jest.fn()} + openIssue={mockIssue()} + selectedFlowIndex={undefined} + selectedLocationIndex={undefined} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap new file mode 100644 index 00000000000..416ec4a47fd --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap @@ -0,0 +1,294 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render CrossComponentSourceViewer correctly 1`] = ` +<div> + <CrossComponentSourceViewer + branchLike={ + Object { + "analysisDate": "2018-01-01", + "isMain": true, + "name": "master", + } + } + components={ + Array [ + "main.js", + ] + } + 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", + "index": 0, + "textRange": Object { + "endLine": 2, + "endOffset": 2, + "startLine": 1, + "startOffset": 1, + }, + }, + Object { + "component": "main.js", + "index": 1, + "textRange": Object { + "endLine": 2, + "endOffset": 2, + "startLine": 1, + "startOffset": 1, + }, + }, + Object { + "component": "main.js", + "index": 2, + "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": "AVsae-CQS-9G3txfbFN2", + "line": 25, + "message": "Reduce the number of conditional operators (4) used in the expression", + "organization": "myorg", + "project": "myproject", + "projectKey": "foo", + "projectName": "Foo", + "projectOrganization": "org", + "rule": "javascript:S1067", + "ruleName": "foo", + "secondaryLocations": Array [ + 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", + } + } + issues={ + Array [ + 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": "AVsae-CQS-9G3txfbFN2", + "line": 25, + "message": "Reduce the number of conditional operators (4) used in the expression", + "organization": "myorg", + "project": "myproject", + "projectKey": "foo", + "projectName": "Foo", + "projectOrganization": "org", + "rule": "javascript:S1067", + "ruleName": "foo", + "secondaryLocations": Array [ + 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", + }, + ] + } + locations={ + Array [ + Object { + "component": "main.js", + "index": 0, + "textRange": Object { + "endLine": 2, + "endOffset": 2, + "startLine": 1, + "startOffset": 1, + }, + }, + Object { + "component": "main.js", + "index": 1, + "textRange": Object { + "endLine": 2, + "endOffset": 2, + "startLine": 1, + "startOffset": 1, + }, + }, + Object { + "component": "main.js", + "index": 2, + "textRange": Object { + "endLine": 2, + "endOffset": 2, + "startLine": 1, + "startOffset": 1, + }, + }, + ] + } + onIssueChange={[MockFunction]} + onLoaded={[Function]} + onLocationSelect={[MockFunction]} + scroll={[Function]} + /> +</div> +`; + +exports[`should render SourceViewer correctly 1`] = ` +<div> + <SourceViewer + aroundLine={26} + branchLike={ + Object { + "analysisDate": "2018-01-01", + "isMain": true, + "name": "master", + } + } + component="main.js" + displayAllIssues={true} + displayLocationMarkers={false} + highlightedLocations={Array []} + loadIssues={[MockFunction]} + onIssueChange={[MockFunction]} + onIssueSelect={[MockFunction]} + onLoaded={[Function]} + onLocationSelect={[MockFunction]} + scroll={[Function]} + selectedIssue="AVsae-CQS-9G3txfbFN2" + slimHeader={true} + /> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueBox-test.tsx b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueBox-test.tsx index b7f18bfdfc5..654bbf8fa6d 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueBox-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueBox-test.tsx @@ -21,14 +21,18 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import ConciseIssueBox from '../ConciseIssueBox'; import { mockIssue } from '../../../../helpers/testMocks'; -import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { waitAndUpdate, click } from '../../../../helpers/testUtils'; it('should render correctly', async () => { - const wrapper = shallowRender(); + const onClick = jest.fn(); + const wrapper = shallowRender({ onClick }); await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); - expect(shallowRender({ issue: mockIssue(true) })).toMatchSnapshot(); + const issue = mockIssue(true); + expect(shallowRender({ issue })).toMatchSnapshot(); + click(wrapper.find('.concise-issue-box-message')); + expect(onClick).toBeCalledWith(issue.key); }); const shallowRender = (props: Partial<ConciseIssueBox['props']> = {}) => { diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetViewer.tsx index 9e5c90afc8c..3e362317db0 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetViewer.tsx @@ -59,6 +59,7 @@ 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[][]; @@ -139,6 +140,7 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr return { additionalLines: combinedLines, + linePopup: undefined, snippets: expandSnippet({ direction, lines: { ...combinedLines, ...this.props.snippetGroup.sources }, @@ -149,7 +151,7 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr }); } }, - () => null + () => {} ); }; @@ -161,7 +163,7 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr getSources({ key }).then( lines => { if (this.mounted) { - this.setState({ loading: false, snippets: [lines] }); + this.setState({ linePopup: undefined, loading: false, snippets: [lines] }); } }, () => { @@ -172,6 +174,32 @@ 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; + }); + }; + handleOpenIssues = (line: T.SourceLine) => { this.setState(state => ({ openIssuesByLine: { ...state.openIssuesByLine, [line.line]: true } @@ -184,6 +212,10 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr })); }; + handleSymbolClick = (highlightedSymbols: string[]) => { + this.setState({ highlightedSymbols }); + }; + renderLine({ index, issuesForLine, @@ -231,7 +263,7 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr key={line.line} last={false} line={line} - linePopup={undefined} + linePopup={this.state.linePopup} loadDuplications={noop} onIssueChange={this.props.onIssueChange} onIssuePopupToggle={this.props.onIssuePopupToggle} @@ -239,9 +271,9 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr onIssueUnselect={noop} onIssuesClose={this.handleCloseIssues} onIssuesOpen={this.handleOpenIssues} - onLinePopupToggle={noop} + onLinePopupToggle={this.handleLinePopupToggle} onLocationSelect={this.props.onLocationSelect} - onSymbolClick={highlightedSymbols => this.setState({ highlightedSymbols })} + onSymbolClick={this.handleSymbolClick} openIssues={openIssuesByLine[line.line]} previousLine={index > 0 ? snippet[index - 1] : undefined} renderDuplicationPopup={this.props.renderDuplicationPopup} diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx index ff48d1ea91a..6c81a5744ec 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx @@ -19,8 +19,9 @@ */ import { lazyLoad } from '../../../components/lazyLoad'; -const CrossComponentSourceViewer = lazyLoad(() => - import(/* webpackPrefetch: true */ './CrossComponentSourceViewerWrapper') +const CrossComponentSourceViewer = lazyLoad( + () => import(/* webpackPrefetch: true */ './CrossComponentSourceViewerWrapper'), + 'CrossComponentSourceViewer' ); export default CrossComponentSourceViewer; diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewerWrapper.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewerWrapper.tsx index 6f27127d2e1..92760bb3b22 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewerWrapper.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewerWrapper.tsx @@ -121,7 +121,7 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone issue={this.props.issue} issuePopup={this.state.issuePopup} issuesByLine={issuesByComponent[g.component.key] || {}} - key={this.props.issue.key + '-' + this.props.selectedFlowIndex + '-' + i} + key={`${this.props.issue.key}-${this.props.selectedFlowIndex}-${i}`} last={i === locationsByComponent.length - 1} locations={g.locations || []} onIssueChange={this.props.onIssueChange} diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetViewer-test.tsx index 6076ef40b02..980df641334 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetViewer-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetViewer-test.tsx @@ -19,28 +19,25 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; +import { times } from 'lodash'; import ComponentSourceSnippetViewer from '../ComponentSourceSnippetViewer'; import { mockMainBranch, mockIssue, mockSourceViewerFile, mockFlowLocation, - mockSnippetsByComponent + mockSnippetsByComponent, + mockSourceLine } from '../../../../helpers/testMocks'; import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { getSources } from '../../../../api/components'; -jest.mock('../../../../api/components', () => { - const { mockSnippetsByComponent } = require.requireActual('../../../../helpers/testMocks'); +jest.mock('../../../../api/components', () => ({ + getSources: jest.fn().mockResolvedValue([]) +})); - return { - getSources: jest - .fn() - .mockResolvedValue( - Object.values( - mockSnippetsByComponent('a', [22, 23, 24, 25, 26, 27, 28, 29, 30, 31]).sources - ) - ) - }; +beforeEach(() => { + jest.clearAllMocks(); }); it('should render correctly', () => { @@ -48,6 +45,9 @@ it('should render correctly', () => { }); it('should expand block', async () => { + (getSources as jest.Mock).mockResolvedValueOnce( + Object.values(mockSnippetsByComponent('a', [22, 23, 24, 25, 26, 27, 28, 29, 30, 31]).sources) + ); const snippetGroup: T.SnippetGroup = { locations: [ mockFlowLocation({ @@ -67,11 +67,57 @@ it('should expand block', async () => { wrapper.instance().expandBlock(0, 'up'); await waitAndUpdate(wrapper); + expect(getSources).toHaveBeenCalledWith({ from: 19, key: 'a', to: 31 }); expect(wrapper.state('snippets')).toHaveLength(2); expect(wrapper.state('snippets')[0]).toHaveLength(15); expect(Object.keys(wrapper.state('additionalLines'))).toHaveLength(10); }); +it('should expand full component', async () => { + (getSources as jest.Mock).mockResolvedValueOnce( + Object.values(mockSnippetsByComponent('a', times(14)).sources) + ); + const snippetGroup: T.SnippetGroup = { + locations: [ + mockFlowLocation({ + component: 'a', + textRange: { startLine: 3, endLine: 3, startOffset: 0, endOffset: 0 } + }), + mockFlowLocation({ + component: 'a', + textRange: { startLine: 12, endLine: 12, startOffset: 0, endOffset: 0 } + }) + ], + ...mockSnippetsByComponent('a', [1, 2, 3, 4, 5, 10, 11, 12, 13, 14]) + }; + + const wrapper = shallowRender({ snippetGroup }); + + wrapper.instance().expandComponent(); + await waitAndUpdate(wrapper); + + expect(getSources).toHaveBeenCalledWith({ key: 'a' }); + expect(wrapper.state('snippets')).toHaveLength(1); + expect(wrapper.state('snippets')[0]).toHaveLength(14); +}); + +it('should handle correctly open/close issue', () => { + const wrapper = shallowRender(); + const sourceLine = mockSourceLine(); + expect(wrapper.state('openIssuesByLine')).toEqual({}); + wrapper.instance().handleOpenIssues(sourceLine); + expect(wrapper.state('openIssuesByLine')).toEqual({ [sourceLine.line]: true }); + wrapper.instance().handleCloseIssues(sourceLine); + expect(wrapper.state('openIssuesByLine')).toEqual({ [sourceLine.line]: false }); +}); + +it('should handle symbol highlighting', () => { + const wrapper = shallowRender(); + expect(wrapper.state('highlightedSymbols')).toEqual([]); + wrapper.instance().handleSymbolClick(['foo']); + expect(wrapper.state('highlightedSymbols')).toEqual(['foo']); +}); + function shallowRender(props: Partial<ComponentSourceSnippetViewer['props']> = {}) { const snippetGroup: T.SnippetGroup = { component: mockSourceViewerFile(), diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewerWrapper-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewerWrapper-test.tsx index 63051e1575e..634537fd178 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewerWrapper-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewerWrapper-test.tsx @@ -22,6 +22,7 @@ import { shallow } from 'enzyme'; import CrossComponentSourceViewerWrapper from '../CrossComponentSourceViewerWrapper'; import { mockIssue, mockSourceViewerFile } from '../../../../helpers/testMocks'; import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { getIssueFlowSnippets } from '../../../../api/issues'; jest.mock('../../../../api/issues', () => { const { mockSourceViewerFile } = require.requireActual('../../../../helpers/testMocks'); @@ -30,6 +31,10 @@ jest.mock('../../../../api/issues', () => { }; }); +beforeEach(() => { + jest.clearAllMocks(); +}); + it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot(); }); @@ -38,8 +43,12 @@ it('Should fetch data', async () => { const wrapper = shallowRender(); wrapper.instance().fetchIssueFlowSnippets('124'); await waitAndUpdate(wrapper); - + expect(getIssueFlowSnippets).toBeCalled(); expect(wrapper.state('components')).toEqual([mockSourceViewerFile()]); + + (getIssueFlowSnippets as jest.Mock).mockClear(); + wrapper.setProps({ issue: mockIssue(true, { key: 'foo' }) }); + expect(getIssueFlowSnippets).toBeCalledWith('foo'); }); it('should handle issue popup', () => { diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx index 46d568bb1b8..6311df20195 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx @@ -19,5 +19,8 @@ */ import { lazyLoad } from '../lazyLoad'; -const SourceViewer = lazyLoad(() => import(/* webpackPrefetch: true */ './SourceViewerBase')); +const SourceViewer = lazyLoad( + () => import(/* webpackPrefetch: true */ './SourceViewerBase'), + 'SourceViewer' +); export default SourceViewer; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeaderSlim.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeaderSlim.tsx index 167472a692a..cfc2c65ba7f 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeaderSlim.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeaderSlim.tsx @@ -28,7 +28,7 @@ import { collapsedDirFromPath, fileFromPath } from '../../helpers/path'; import { isMainBranch } from '../../helpers/branches'; import './SourceViewerHeaderSlim.css'; -interface Props { +export interface Props { branchLike: T.BranchLike | undefined; expandable?: boolean; loading?: boolean; @@ -57,9 +57,9 @@ export default function SourceViewerHeaderSlim({ </div> {subProject !== undefined && ( - <div className=""> + <> <QualifierIcon qualifier="BRC" /> <span>{subProjectName}</span> - </div> + </> )} <div className="spacer-left"> diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewerHeaderSlim-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewerHeaderSlim-test.tsx new file mode 100644 index 00000000000..2bc8557ec28 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewerHeaderSlim-test.tsx @@ -0,0 +1,55 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import SourceViewerHeaderSlim, { Props } from '../SourceViewerHeaderSlim'; +import { mockMainBranch, mockSourceViewerFile } from '../../../helpers/testMocks'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +it('should allow to mark as favorite', () => { + expect( + shallowRender({ sourceViewerFile: mockSourceViewerFile({ canMarkAsFavorite: true }) }) + .find('Favorite') + .exists() + ).toBe(true); +}); + +it('should render correctly for subproject', () => { + expect( + shallowRender({ + sourceViewerFile: mockSourceViewerFile({ subProject: 'foo', subProjectName: 'Foo' }) + }) + ).toMatchSnapshot(); +}); + +function shallowRender(props: Partial<Props> = {}) { + return shallow( + <SourceViewerHeaderSlim + branchLike={mockMainBranch()} + expandable={true} + onExpand={jest.fn()} + sourceViewerFile={mockSourceViewerFile()} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerHeaderSlim-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerHeaderSlim-test.tsx.snap new file mode 100644 index 00000000000..20f543700bf --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerHeaderSlim-test.tsx.snap @@ -0,0 +1,120 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<div + className="source-viewer-header-slim display-flex-row display-flex-space-between" +> + <div + className="display-flex-row flex-1" + > + <div> + <a + className="link-with-icon" + href="/dashboard?id=my-project" + > + <QualifierIcon + qualifier="TRK" + /> + + <span> + MyProject + </span> + </a> + </div> + <div + className="spacer-left" + > + <QualifierIcon + qualifier="FIL" + /> + + <span> + foo/ + </span> + <span + className="component-name-file" + > + bar.ts + </span> + </div> + </div> + <DeferredSpinner + className="little-spacer-right" + timeout={100} + > + <div + className="source-viewer-header-slim-actions flex-0" + > + <ButtonIcon + className="js-actions" + onClick={[MockFunction]} + > + <ExpandSnippetIcon /> + </ButtonIcon> + </div> + </DeferredSpinner> +</div> +`; + +exports[`should render correctly for subproject 1`] = ` +<div + className="source-viewer-header-slim display-flex-row display-flex-space-between" +> + <div + className="display-flex-row flex-1" + > + <div> + <a + className="link-with-icon" + href="/dashboard?id=my-project" + > + <QualifierIcon + qualifier="TRK" + /> + + <span> + MyProject + </span> + </a> + </div> + <QualifierIcon + qualifier="BRC" + /> + + <span> + Foo + </span> + <div + className="spacer-left" + > + <QualifierIcon + qualifier="FIL" + /> + + <span> + foo/ + </span> + <span + className="component-name-file" + > + bar.ts + </span> + </div> + </div> + <DeferredSpinner + className="little-spacer-right" + timeout={100} + > + <div + className="source-viewer-header-slim-actions flex-0" + > + <ButtonIcon + className="js-actions" + onClick={[MockFunction]} + > + <ExpandSnippetIcon /> + </ButtonIcon> + </div> + </DeferredSpinner> +</div> +`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx index 485353c9626..ca5118377b9 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx @@ -217,11 +217,8 @@ export default class LineCode extends React.PureComponent<Props, State> { leadingMarker = (index === 0 ? true : leadingMarker) && !token.text.trim().length; }); - const style = padding - ? { - paddingBottom: padding + 'px' - } - : undefined; + const style = padding ? { paddingBottom: `${padding}px` } : undefined; + const filteredSelectedIssues = issues.filter(i => i.key === selectedIssue); return ( <td className={className} data-line-number={line.line} style={style}> @@ -243,7 +240,7 @@ export default class LineCode extends React.PureComponent<Props, State> { <LineIssuesList branchLike={this.props.branchLike} issuePopup={this.props.issuePopup} - issues={issues.filter(i => i.key === selectedIssue)} + issues={filteredSelectedIssues} onIssueChange={this.props.onIssueChange} onIssueClick={onIssueSelect} onIssuePopupToggle={this.props.onIssuePopupToggle} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx index 8091d30aa14..50a447bf0c7 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx @@ -20,53 +20,23 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import LineCode from '../LineCode'; - -const issueBase: T.Issue = { - actions: [], - component: '', - componentLongName: '', - componentQualifier: '', - componentUuid: '', - creationDate: '', - key: '', - flows: [], - fromHotspot: false, - message: '', - organization: '', - project: '', - projectName: '', - projectOrganization: '', - projectKey: '', - rule: '', - ruleName: '', - secondaryLocations: [], - severity: '', - status: '', - transitions: [], - type: 'BUG' -}; +import { mockShortLivingBranch, mockIssue, mockSourceLine } from '../../../../helpers/testMocks'; it('render code', () => { - const line = { - line: 3, - code: '<span class="k">class</span> <span class="sym sym-1">Foo</span> {' - }; - const issueLocations = [{ from: 0, to: 5, line: 3 }]; - const branch: T.ShortLivingBranch = { - isMain: false, - mergeBranch: 'master', - name: 'feature', - type: 'SHORT' - }; - const wrapper = shallow( + expect(shallowRender()).toMatchSnapshot(); +}); + +function shallowRender(props: Partial<LineCode['props']> = {}) { + return shallow( <LineCode - branchLike={branch} - highlightedLocationMessage={undefined} - highlightedSymbols={['sym1']} - issueLocations={issueLocations} + branchLike={mockShortLivingBranch()} + displayLocationMarkers={true} + highlightedLocationMessage={{ index: 0, text: 'location description' }} + highlightedSymbols={['sym-9']} + issueLocations={[{ from: 0, to: 5, line: 16 }]} issuePopup={undefined} - issues={[{ ...issueBase, key: 'issue-1' }, { ...issueBase, key: 'issue-2' }]} - line={line} + issues={[mockIssue(false, { key: 'issue-1' }), mockIssue(false, { key: 'issue-2' })]} + line={mockSourceLine()} onIssueChange={jest.fn()} onIssuePopupToggle={jest.fn()} onIssueSelect={jest.fn()} @@ -75,7 +45,7 @@ it('render code', () => { secondaryIssueLocations={[]} selectedIssue="issue-1" showIssues={true} + {...props} /> ); - expect(wrapper).toMatchSnapshot(); -}); +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap index df5c914a676..cffede6b404 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap @@ -3,7 +3,7 @@ exports[`render code 1`] = ` <td className="source-line-code code has-issues" - data-line-number={3} + data-line-number={16} > <div className="source-line-code-inner" @@ -13,34 +13,41 @@ exports[`render code 1`] = ` className="k source-line-code-issue" key="0" > - class + impor </span> <span - className="" + className="k" key="1" > - + t </span> <span - className="sym sym-1" + className="" key="2" > - Foo + java.util. </span> <span - className="" + className="sym-9 sym highlighted" key="3" > - { + ArrayList + </span> + <span + className="" + key="4" + > + ; </span> </pre> </div> <LineIssuesList branchLike={ Object { + "analysisDate": "2018-01-01", "isMain": false, "mergeBranch": "master", - "name": "feature", + "name": "feature/foo", "type": "SHORT", } } @@ -48,49 +55,63 @@ exports[`render code 1`] = ` Array [ Object { "actions": Array [], - "component": "", - "componentLongName": "", - "componentQualifier": "", - "componentUuid": "", - "creationDate": "", + "component": "main.js", + "componentLongName": "main.js", + "componentQualifier": "FIL", + "componentUuid": "foo1234", + "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], "fromHotspot": false, "key": "issue-1", - "message": "", - "organization": "", - "project": "", - "projectKey": "", - "projectName": "", - "projectOrganization": "", - "rule": "", - "ruleName": "", + "line": 25, + "message": "Reduce the number of conditional operators (4) used in the expression", + "organization": "myorg", + "project": "myproject", + "projectKey": "foo", + "projectName": "Foo", + "projectOrganization": "org", + "rule": "javascript:S1067", + "ruleName": "foo", "secondaryLocations": Array [], - "severity": "", - "status": "", + "severity": "MAJOR", + "status": "OPEN", + "textRange": Object { + "endLine": 26, + "endOffset": 15, + "startLine": 25, + "startOffset": 0, + }, "transitions": Array [], "type": "BUG", }, Object { "actions": Array [], - "component": "", - "componentLongName": "", - "componentQualifier": "", - "componentUuid": "", - "creationDate": "", + "component": "main.js", + "componentLongName": "main.js", + "componentQualifier": "FIL", + "componentUuid": "foo1234", + "creationDate": "2017-03-01T09:36:01+0100", "flows": Array [], "fromHotspot": false, "key": "issue-2", - "message": "", - "organization": "", - "project": "", - "projectKey": "", - "projectName": "", - "projectOrganization": "", - "rule": "", - "ruleName": "", + "line": 25, + "message": "Reduce the number of conditional operators (4) used in the expression", + "organization": "myorg", + "project": "myproject", + "projectKey": "foo", + "projectName": "Foo", + "projectOrganization": "org", + "rule": "javascript:S1067", + "ruleName": "foo", "secondaryLocations": Array [], - "severity": "", - "status": "", + "severity": "MAJOR", + "status": "OPEN", + "textRange": Object { + "endLine": 26, + "endOffset": 15, + "startLine": 25, + "startOffset": 0, + }, "transitions": Array [], "type": "BUG", }, diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.ts b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.ts index 1dbd82fac36..19aef66e7b9 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.ts +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.ts @@ -17,7 +17,32 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { symbolsByLine } from '../indexing'; +import { issuesByComponentAndLine, symbolsByLine } from '../indexing'; +import { mockIssue } from '../../../../helpers/testMocks'; + +describe('issuesByComponentAndLine', () => { + it('should map issues by lines and components', () => { + const issues = [ + mockIssue(true, { component: 'foo.js' }), + mockIssue(false, { + component: 'foo.js', + textRange: { startLine: 5, endLine: 5, startOffset: 0, endOffset: 0 } + }), + mockIssue(false, { component: 'bar.js' }), + mockIssue(), + mockIssue() + ]; + const result = issuesByComponentAndLine(issues); + expect(Object.keys(result['foo.js'])).toHaveLength(2); + expect(Object.keys(result['foo.js'])).toEqual(['5', '26']); + expect(result['foo.js'][5]).toHaveLength(1); + + expect(Object.keys(result['bar.js'])).toHaveLength(1); + + expect(Object.keys(result['main.js'])).toHaveLength(1); + expect(result['main.js'][26]).toHaveLength(2); + }); +}); describe('symbolsByLine', () => { it('should highlight symbols marked twice', () => { diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/issueLocations-test.ts b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/issueLocations-test.ts new file mode 100644 index 00000000000..4096f9208d5 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/issueLocations-test.ts @@ -0,0 +1,46 @@ +/* + * 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 { getSecondaryIssueLocationsForLine, getLinearLocations } from '../issueLocations'; +import { mockSourceLine, mockFlowLocation } from '../../../../helpers/testMocks'; + +describe('getSecondaryIssueLocationsForLine', () => { + it('should return secondary locations for a line', () => { + const sourceLine = mockSourceLine({ line: 2 }); + expect(getSecondaryIssueLocationsForLine(sourceLine, undefined)).toEqual([]); + expect(getSecondaryIssueLocationsForLine(sourceLine, [mockFlowLocation()])).toEqual([ + { from: 0, index: undefined, line: 2, startLine: 1, text: undefined, to: 2 } + ]); + }); +}); + +describe('getLinearLocations', () => { + it('should return a linear location', () => { + expect(getLinearLocations({ startLine: 6, startOffset: 3, endLine: 8, endOffset: 56 })).toEqual( + [ + { from: 3, line: 6, to: 999999 }, + { from: 0, line: 7, to: 999999 }, + { from: 0, line: 8, to: 56 } + ] + ); + expect(getLinearLocations({ startLine: 6, startOffset: 0, endLine: 6, endOffset: 42 })).toEqual( + [{ from: 0, line: 6, to: 42 }] + ); + }); +}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.tsx b/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.ts index 9218ea9168e..9218ea9168e 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.ts diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap index fb3201016c1..79e79272640 100644 --- a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap @@ -28,7 +28,7 @@ exports[`should render 1`] = ` } } > - <LazyLoader + <SourceViewer component="foo" onLoaded={[Function]} /> |