* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { intersection } from 'lodash';
import * as React from 'react';
import {
getDuplications,
getSources,
} from '../../api/components';
+import { ComponentContext } from '../../app/components/componentContext/ComponentContext';
import { getBranchLikeQuery, isSameBranchLike } from '../../helpers/branch-like';
import { translate } from '../../helpers/l10n';
import { HttpStatus } from '../../helpers/request';
import { BranchLike } from '../../types/branch-like';
+import { ComponentQualifier } from '../../types/component';
import {
Dict,
DuplicatedFile,
import './styles.css';
export interface Props {
- hideHeader?: boolean;
aroundLine?: number;
branchLike: BranchLike | undefined;
component: string;
componentMeasures?: Measure[];
displayAllIssues?: boolean;
displayLocationMarkers?: boolean;
+ hideHeader?: boolean;
highlightedLine?: number;
+ highlightedLocationMessage?: { index: number; text: string | undefined };
// `undefined` elements mean they are located in a different file,
- // but kept to maintaint the location indexes
+ // but kept to maintain the location indexes
highlightedLocations?: (FlowLocation | undefined)[];
- highlightedLocationMessage?: { index: number; text: string | undefined };
- onLoaded?: (component: SourceViewerFile, sources: SourceLine[], issues: Issue[]) => void;
- onLocationSelect?: (index: number) => void;
+ metricKey?: string;
+ needIssueSync?: boolean;
onIssueSelect?: (issueKey: string) => void;
onIssueUnselect?: () => void;
+ onLoaded?: (component: SourceViewerFile, sources: SourceLine[], issues: Issue[]) => void;
+ onLocationSelect?: (index: number) => void;
selectedIssue?: string;
showMeasures?: boolean;
- metricKey?: string;
}
interface State {
symbolsByLine: { [line: number]: string[] };
}
-export default class SourceViewer extends React.PureComponent<Props, State> {
+export class SourceViewerClass extends React.PureComponent<Props, State> {
mounted = false;
static defaultProps = {
) {
this.setState({ selectedIssue: this.props.selectedIssue });
}
+
if (
prevProps.component !== this.props.component ||
!isSameBranchLike(prevProps.branchLike, this.props.branchLike)
this.isLineOutsideOfRange(this.props.aroundLine)
) {
const sources = await this.fetchSources().catch(() => []);
+
if (this.mounted) {
const finalSources = sources.slice(0, LINES_TO_LOAD);
+
this.setState(
{
sources: sources.slice(0, LINES_TO_LOAD),
isLineOutsideOfRange(lineNumber: number) {
const { sources } = this.state;
+
if (sources && sources.length > 0) {
const firstLine = sources[0];
const lastList = sources[sources.length - 1];
this.setState({ loading: true });
const loadIssuesCallback = (component: SourceViewerFile, sources: SourceLine[]) => {
- loadIssues(this.props.component, this.props.branchLike).then(
+ loadIssues(this.props.component, this.props.branchLike, this.props.needIssueSync).then(
(issues) => {
if (this.mounted) {
const finalSources = sources.slice(0, LINES_TO_LOAD);
+
this.setState(
{
component,
hasSourcesAfter: sources.length > LINES_TO_LOAD,
highlightedSymbols: [],
issueLocationsByLine: locationsByLine(issues),
+ issuePopup: undefined,
issues,
issuesByLine: issuesByLine(issues),
loading: false,
notAccessible: false,
notExist: false,
openIssuesByLine: {},
- issuePopup: undefined,
sourceRemoved: false,
sources: this.computeCoverageStatus(finalSources),
symbolsByLine: symbolsByLine(sources.slice(0, LINES_TO_LOAD)),
const onResolve = (component: SourceViewerFile) => {
const sourcesRequest =
- component.q === 'FIL' || component.q === 'UTS' ? this.fetchSources() : Promise.resolve([]);
+ component.q === ComponentQualifier.File || component.q === ComponentQualifier.TestFile
+ ? this.fetchSources()
+ : Promise.resolve([]);
+
sourcesRequest.then(
(sources) => loadIssuesCallback(component, sources),
(response) => onFailLoadSources(response, component)
let to = this.props.aroundLine
? this.props.aroundLine + LINES_TO_LOAD / 2 + 1
: LINES_TO_LOAD + 1;
+
// make sure we try to download `LINES` lines
if (from === 1 && to < LINES_TO_LOAD) {
to = LINES_TO_LOAD;
}
+
// request one additional line to define `hasSourcesAfter`
to++;
if (!this.state.sources) {
return;
}
+
const firstSourceLine = this.state.sources[0];
+
this.setState({ loadingSourcesBefore: true });
+
const from = Math.max(1, firstSourceLine.line - LINES_TO_LOAD);
+
this.loadSources(
this.props.component,
from,
if (!this.state.sources) {
return;
}
+
const lastSourceLine = this.state.sources[this.state.sources.length - 1];
+
this.setState({ loadingSourcesAfter: true });
+
const fromLine = lastSourceLine.line + 1;
const toLine = lastSourceLine.line + LINES_TO_LOAD + 1;
+
this.loadSources(this.props.component, fromLine, toLine, this.props.branchLike).then(
(sources) => {
if (this.mounted) {
const hasSourcesAfter = LINES_TO_LOAD < sources.length;
+
if (hasSourcesAfter) {
sources.pop();
}
+
this.setState((prevState) => {
return {
hasSourcesAfter,
this.setState((state: State) => {
const samePopup =
state.issuePopup && state.issuePopup.name === popupName && state.issuePopup.issue === issue;
+
if (open !== false && !samePopup) {
return { issuePopup: { issue, name: popupName } };
} else if (open !== true && samePopup) {
return { issuePopup: undefined };
}
+
return null;
});
};
this.setState((state) => {
const shouldDisable = intersection(state.highlightedSymbols, symbols).length > 0;
const highlightedSymbols = shouldDisable ? [] : symbols;
+
return { highlightedSymbols };
});
};
const newIssues = issues.map((candidate) =>
candidate.key === issue.key ? issue : candidate
);
+
return { issues: newIssues, issuesByLine: issuesByLine(newIssues) };
});
};
<DuplicationPopup
blocks={filterDuplicationBlocksByLine(blocks, line)}
branchLike={this.props.branchLike}
- inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)}
duplicatedFiles={duplicatedFiles}
+ duplicationHeader={translate('component_viewer.transition.duplication')}
+ inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)}
openComponent={openComponent}
sourceViewerFile={component}
- duplicationHeader={translate('component_viewer.transition.duplication')}
/>
)}
</WorkspaceContext.Consumer>
renderCode(sources: SourceLine[]) {
const hasSourcesBefore = sources.length > 0 && sources[0].line > 1;
+
return (
<SourceViewerCode
branchLike={this.props.branchLike}
issues={this.state.issues}
issuesByLine={this.state.issuesByLine}
loadDuplications={this.loadDuplications}
- loadSourcesAfter={this.loadSourcesAfter}
- loadSourcesBefore={this.loadSourcesBefore}
loadingSourcesAfter={this.state.loadingSourcesAfter}
loadingSourcesBefore={this.state.loadingSourcesBefore}
+ loadSourcesAfter={this.loadSourcesAfter}
+ loadSourcesBefore={this.loadSourcesBefore}
+ metricKey={this.props.metricKey}
onIssueChange={this.handleIssueChange}
onIssuePopupToggle={this.handleIssuePopupToggle}
- onIssueSelect={this.handleIssueSelect}
- onIssueUnselect={this.handleIssueUnselect}
onIssuesClose={this.handleCloseIssues}
+ onIssueSelect={this.handleIssueSelect}
onIssuesOpen={this.handleOpenIssues}
+ onIssueUnselect={this.handleIssueUnselect}
onLocationSelect={this.props.onLocationSelect}
onSymbolClick={this.handleSymbolClick}
openIssuesByLine={this.state.openIssuesByLine}
renderDuplicationPopup={this.renderDuplicationPopup}
- metricKey={this.props.metricKey}
selectedIssue={this.state.selectedIssue}
sources={sources}
symbolsByLine={this.state.symbolsByLine}
<SourceViewerContext.Provider value={{ branchLike: this.props.branchLike, file: component }}>
<div className="source-viewer">
{!hideHeader && this.renderHeader(component)}
+
{sourceRemoved && (
<Alert className="spacer-top" variant="warning">
{translate('code_viewer.no_source_code_displayed_due_to_source_removed')}
</Alert>
)}
+
{!sourceRemoved && sources !== undefined && this.renderCode(sources)}
</div>
</SourceViewerContext.Provider>
);
}
}
+
+export default function SourceViewer(props: Props) {
+ return (
+ // we can't use withComponentContext as it would override the "component" prop
+ <ComponentContext.Consumer>
+ {({ component }) => <SourceViewerClass needIssueSync={component?.needIssueSync} {...props} />}
+ </ComponentContext.Consumer>
+ );
+}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { queryHelpers, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { mockIssue } from '../../../helpers/testMocks';
import { renderComponent } from '../../../helpers/testReactTestingUtils';
import { byText } from '../../../helpers/testSelector';
-import SourceViewer from '../SourceViewer';
+import SourceViewer, { Props } from '../SourceViewer';
import loadIssues from '../helpers/loadIssues';
jest.mock('../../../api/components');
const componentsHandler = new ComponentsServiceMock();
const issuesHandler = new IssuesServiceMock();
+const message = 'First Issue';
beforeEach(() => {
issuesHandler.reset();
let row = await screen.findByRole('row', { name: /\/\*$/ });
expect(row).toBeInTheDocument();
const rowScreen = within(row);
+
await user.click(
rowScreen.getByRole('button', {
name: 'source_viewer.line_X.1',
});
it('should show issue on empty file', async () => {
- (loadIssues as jest.Mock).mockResolvedValueOnce([
+ jest.mocked(loadIssues).mockResolvedValueOnce([
mockIssue(false, {
key: 'first-issue',
- message: 'First Issue',
+ message,
line: undefined,
textRange: undefined,
}),
]);
+
renderSourceViewer({
component: componentsHandler.getEmptyFileKey(),
});
+
expect(await screen.findByRole('table')).toBeInTheDocument();
expect(await screen.findByRole('row', { name: 'First Issue' })).toBeInTheDocument();
});
it('should be able to interact with issue action', async () => {
- (loadIssues as jest.Mock).mockResolvedValueOnce([
+ jest.mocked(loadIssues).mockResolvedValueOnce([
mockIssue(false, {
actions: ['set_type', 'set_tags', 'comment', 'set_severity', 'assign'],
key: 'issue1',
- message: 'First Issue',
+ message,
line: 1,
textRange: { startLine: 1, endLine: 1, startOffset: 0, endOffset: 1 },
}),
]);
+
const user = userEvent.setup();
renderSourceViewer();
await user.click(
await screen.findByLabelText('issue.type.type_x_click_to_change.issue.type.BUG')
);
+
expect(ui.codeSmellTypeButton.get()).toBeInTheDocument();
// Open severity
await user.click(
await screen.findByLabelText('issue.severity.severity_x_click_to_change.severity.MAJOR')
);
+
expect(ui.minorSeverityButton.get()).toBeInTheDocument();
// Close
await user.click(
await screen.findByLabelText('issue.severity.severity_x_click_to_change.severity.MAJOR')
);
+
expect(ui.minorSeverityButton.get()).toBeInTheDocument();
await user.click(ui.minorSeverityButton.get());
+
expect(
screen.getByLabelText('issue.severity.severity_x_click_to_change.severity.MINOR')
).toBeInTheDocument();
aroundLine: 50,
component: componentsHandler.getHugeFileKey(),
});
+
expect(await screen.findByRole('row', { name: /Line 50$/ })).toBeInTheDocument();
rerender({ aroundLine: 100, component: componentsHandler.getHugeFileKey() });
let row = await screen.findByRole('row', { name: /\/\*$/ });
expect(row).toBeInTheDocument();
const firstRowScreen = within(row);
+
expect(
firstRowScreen.getByRole('cell', { name: 'stas.vilchik@sonarsource.com' })
).toBeInTheDocument();
+
await user.click(
firstRowScreen.getByRole('button', {
name: 'source_viewer.author_X.stas.vilchik@sonarsource.com, source_viewer.click_for_scm_info.1',
row = screen.getByRole('row', { name: /\* SonarQube$/ });
expect(row).toBeInTheDocument();
const secondRowScreen = within(row);
+
expect(
secondRowScreen.queryByRole('cell', { name: 'stas.vilchik@sonarsource.com' })
).not.toBeInTheDocument();
row = await screen.findByRole('row', { name: /\* mailto:info AT sonarsource DOT com$/ });
expect(row).toBeInTheDocument();
const fourthRowScreen = within(row);
+
await act(async () => {
await user.click(
fourthRowScreen.getByRole('button', {
expect(row).toBeInTheDocument();
const fithRowScreen = within(row);
expect(fithRowScreen.getByText('…')).toBeInTheDocument();
+
await act(async () => {
await user.click(
fithRowScreen.getByRole('button', {
row = await screen.findByRole('row', {
name: /\* This program is free software; you can redistribute it and\/or$/,
});
+
expect(row).toBeInTheDocument();
expect(within(row).queryByRole('button')).not.toBeInTheDocument();
});
it('should show issue indicator', async () => {
- (loadIssues as jest.Mock).mockResolvedValueOnce([
+ jest.mocked(loadIssues).mockResolvedValueOnce([
mockIssue(false, {
key: 'first-issue',
- message: 'First Issue',
+ message,
line: 1,
textRange: { startLine: 1, endLine: 1, startOffset: 0, endOffset: 1 },
}),
textRange: { startLine: 1, endLine: 1, startOffset: 1, endOffset: 2 },
}),
]);
+
const user = userEvent.setup();
const onIssueSelect = jest.fn();
+
renderSourceViewer({
onIssueSelect,
displayAllIssues: false,
});
+
const row = await screen.findByRole('row', { name: /.*\/ \*$/ });
const issueRow = within(row);
expect(issueRow.getByText('2')).toBeInTheDocument();
+
await user.click(
issueRow.getByRole('button', {
name: 'source_viewer.issues_on_line.X_issues_of_type_Y.source_viewer.issues_on_line.show.2.issue.type.BUG.plural',
it('should show coverage information', async () => {
renderSourceViewer();
+
const coverdLine = within(
await screen.findByRole('row', { name: /\* mailto:info AT sonarsource DOT com$/ })
);
+
expect(
coverdLine.getByLabelText('source_viewer.tooltip.covered.conditions.1')
).toBeInTheDocument();
const partialyCoveredWithConditionLine = within(
await screen.findByRole('row', { name: / \* 5$/ })
);
+
expect(
partialyCoveredWithConditionLine.getByLabelText(
'source_viewer.tooltip.partially-covered.conditions.1.2'
).toBeInTheDocument();
const partialyCoveredLine = within(await screen.findByRole('row', { name: /\/\*$/ }));
+
expect(
partialyCoveredLine.getByLabelText('source_viewer.tooltip.partially-covered')
).toBeInTheDocument();
const uncoveredWithConditionLine = within(
await screen.findByRole('row', { name: / \* SonarQube$/ })
);
+
expect(
uncoveredWithConditionLine.getByLabelText('source_viewer.tooltip.uncovered.conditions.1')
).toBeInTheDocument();
const coveredWithNoCondition = within(await screen.findByRole('row', { name: /\* Copyright$/ }));
+
expect(
coveredWithNoCondition.getByLabelText('source_viewer.tooltip.covered')
).toBeInTheDocument();
const user = userEvent.setup();
renderSourceViewer();
const duplicateLine = within(await screen.findByRole('row', { name: /\* 7$/ }));
+
expect(
duplicateLine.getByLabelText('source_viewer.tooltip.duplicated_block')
).toBeInTheDocument();
it('should show correct message when component is not asscessible', async () => {
componentsHandler.setFailLoadingComponentStatus(HttpStatus.Forbidden);
renderSourceViewer();
+
expect(
await screen.findByText('code_viewer.no_source_code_displayed_due_to_security')
).toBeInTheDocument();
expect(await screen.findByText('component_viewer.no_component')).toBeInTheDocument();
});
-function renderSourceViewer(override?: Partial<SourceViewer['props']>) {
+function renderSourceViewer(override?: Partial<Props>) {
const { rerender } = renderComponent(
<SourceViewer
aroundLine={1}
{...override}
/>
);
- return function (reoverride?: Partial<SourceViewer['props']>) {
+
+ return function (reoverride?: Partial<Props>) {
rerender(
<SourceViewer
aroundLine={1}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import { getComponentData, getComponentForSourceViewer, getSources } from '../../../api/components';
-import { mockMainBranch } from '../../../helpers/mocks/branch-like';
-import { mockSourceLine, mockSourceViewerFile } from '../../../helpers/mocks/sources';
-import { mockIssue } from '../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../helpers/testUtils';
-import defaultLoadIssues from '../helpers/loadIssues';
-import SourceViewer from '../SourceViewer';
-
-jest.mock('../helpers/loadIssues', () => jest.fn().mockRejectedValue({}));
-
-jest.mock('../../../api/components', () => ({
- getComponentForSourceViewer: jest.fn().mockRejectedValue(''),
- getComponentData: jest.fn().mockRejectedValue(''),
- getSources: jest.fn().mockRejectedValue(''),
-}));
-
-beforeEach(() => {
- jest.resetAllMocks();
-});
-
-it('should render nothing from the start', () => {
- expect(shallowRender().type()).toBeNull();
-});
-
-it('should render correctly', async () => {
- (defaultLoadIssues as jest.Mock).mockResolvedValueOnce([mockIssue()]);
- (getComponentForSourceViewer as jest.Mock).mockResolvedValueOnce(mockSourceViewerFile());
- (getComponentData as jest.Mock).mockResolvedValueOnce({
- component: { leakPeriodDate: '2018-06-20T17:12:19+0200' },
- });
- (getSources as jest.Mock).mockResolvedValueOnce([]);
-
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
-
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should load sources before', async () => {
- (defaultLoadIssues as jest.Mock).mockResolvedValueOnce([
- mockIssue(false, { key: 'issue1' }),
- mockIssue(false, { key: 'issue2' }),
- ]);
- (getComponentForSourceViewer as jest.Mock).mockResolvedValueOnce(mockSourceViewerFile());
- (getComponentData as jest.Mock).mockResolvedValueOnce({
- component: { leakPeriodDate: '2018-06-20T17:12:19+0200' },
- });
- (getSources as jest.Mock)
- .mockResolvedValueOnce([mockSourceLine()])
- .mockResolvedValueOnce([mockSourceLine()]);
-
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
-
- wrapper.instance().loadSourcesBefore();
- expect(wrapper.state().loadingSourcesBefore).toBe(true);
-
- expect(defaultLoadIssues).toHaveBeenCalledTimes(1);
- expect(getSources).toHaveBeenCalledTimes(2);
-
- await waitAndUpdate(wrapper);
- expect(wrapper.state().loadingSourcesBefore).toBe(false);
- expect(wrapper.state().issues).toHaveLength(2);
-});
-
-it('should load sources after', async () => {
- (defaultLoadIssues as jest.Mock).mockResolvedValueOnce([
- mockIssue(false, { key: 'issue1' }),
- mockIssue(false, { key: 'issue2' }),
- ]);
- (getComponentForSourceViewer as jest.Mock).mockResolvedValueOnce(mockSourceViewerFile());
- (getComponentData as jest.Mock).mockResolvedValueOnce({
- component: { leakPeriodDate: '2018-06-20T17:12:19+0200' },
- });
- (getSources as jest.Mock)
- .mockResolvedValueOnce([mockSourceLine()])
- .mockResolvedValueOnce([mockSourceLine()]);
-
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
-
- wrapper.instance().loadSourcesAfter();
- expect(wrapper.state().loadingSourcesAfter).toBe(true);
-
- expect(defaultLoadIssues).toHaveBeenCalledTimes(1);
- expect(getSources).toHaveBeenCalledTimes(2);
-
- await waitAndUpdate(wrapper);
-
- expect(wrapper.state().loadingSourcesAfter).toBe(false);
- expect(wrapper.state().issues).toHaveLength(2);
-});
-
-it('should handle no sources when checking ranges', () => {
- const wrapper = shallowRender();
-
- wrapper.setState({ sources: undefined });
- expect(wrapper.instance().isLineOutsideOfRange(12)).toBe(true);
-});
-
-function shallowRender(overrides: Partial<SourceViewer['props']> = {}) {
- return shallow<SourceViewer>(
- <SourceViewer branchLike={mockMainBranch()} component="my-component" {...overrides} />
- );
-}
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<ContextProvider
- value={
- {
- "branchLike": {
- "analysisDate": "2018-01-01",
- "excludedFromPurge": true,
- "isMain": true,
- "name": "master",
- },
- "file": {
- "canMarkAsFavorite": true,
- "fav": false,
- "key": "project:foo/bar.ts",
- "leakPeriodDate": "2018-06-20T17:12:19+0200",
- "longName": "foo/bar.ts",
- "measures": {
- "coverage": "85.2",
- "duplicationDensity": "1.0",
- "issues": "12",
- "lines": "56",
- },
- "name": "foo/bar.ts",
- "path": "foo/bar.ts",
- "project": "project",
- "projectName": "MyProject",
- "q": "FIL",
- "uuid": "foo-bar",
- },
- }
- }
->
- <div
- className="source-viewer"
- >
- <ContextConsumer>
- <Component />
- </ContextConsumer>
- <SourceViewerCode
- branchLike={
- {
- "analysisDate": "2018-01-01",
- "excludedFromPurge": true,
- "isMain": true,
- "name": "master",
- }
- }
- displayAllIssues={false}
- displayLocationMarkers={true}
- duplicationsByLine={{}}
- hasSourcesAfter={false}
- hasSourcesBefore={false}
- highlightedSymbols={[]}
- issueLocationsByLine={
- {
- "25": [
- {
- "from": 0,
- "line": 25,
- "to": 999999,
- },
- ],
- "26": [
- {
- "from": 0,
- "line": 26,
- "to": 15,
- },
- ],
- }
- }
- issues={
- [
- {
- "actions": [],
- "component": "main.js",
- "componentEnabled": true,
- "componentLongName": "main.js",
- "componentQualifier": "FIL",
- "componentUuid": "foo1234",
- "creationDate": "2017-03-01T09:36:01+0100",
- "flows": [],
- "flowsWithType": [],
- "key": "AVsae-CQS-9G3txfbFN2",
- "line": 25,
- "message": "Reduce the number of conditional operators (4) used in the expression",
- "project": "myproject",
- "projectKey": "foo",
- "projectName": "Foo",
- "rule": "javascript:S1067",
- "ruleName": "foo",
- "scope": "MAIN",
- "secondaryLocations": [],
- "severity": "MAJOR",
- "status": "OPEN",
- "textRange": {
- "endLine": 26,
- "endOffset": 15,
- "startLine": 25,
- "startOffset": 0,
- },
- "transitions": [],
- "type": "BUG",
- },
- ]
- }
- issuesByLine={
- {
- "26": [
- {
- "actions": [],
- "component": "main.js",
- "componentEnabled": true,
- "componentLongName": "main.js",
- "componentQualifier": "FIL",
- "componentUuid": "foo1234",
- "creationDate": "2017-03-01T09:36:01+0100",
- "flows": [],
- "flowsWithType": [],
- "key": "AVsae-CQS-9G3txfbFN2",
- "line": 25,
- "message": "Reduce the number of conditional operators (4) used in the expression",
- "project": "myproject",
- "projectKey": "foo",
- "projectName": "Foo",
- "rule": "javascript:S1067",
- "ruleName": "foo",
- "scope": "MAIN",
- "secondaryLocations": [],
- "severity": "MAJOR",
- "status": "OPEN",
- "textRange": {
- "endLine": 26,
- "endOffset": 15,
- "startLine": 25,
- "startOffset": 0,
- },
- "transitions": [],
- "type": "BUG",
- },
- ],
- }
- }
- loadDuplications={[Function]}
- loadSourcesAfter={[Function]}
- loadSourcesBefore={[Function]}
- loadingSourcesAfter={false}
- loadingSourcesBefore={false}
- onIssueChange={[Function]}
- onIssuePopupToggle={[Function]}
- onIssueSelect={[Function]}
- onIssueUnselect={[Function]}
- onIssuesClose={[Function]}
- onIssuesOpen={[Function]}
- onSymbolClick={[Function]}
- openIssuesByLine={{}}
- renderDuplicationPopup={[Function]}
- sources={[]}
- symbolsByLine={{}}
- />
- </div>
-</ContextProvider>
-`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`loadIssues should load issues 1`] = `
+exports[`loadIssues should load issues with listIssues if re-indexing 1`] = `
+[
+ {
+ "actions": [
+ "set_tags",
+ "comment",
+ "assign",
+ ],
+ "assignee": "luke",
+ "author": "luke@sonarsource.com",
+ "comments": [],
+ "component": "foo.java",
+ "componentEnabled": true,
+ "componentKey": "foo.java",
+ "componentLongName": "Foo.java",
+ "componentName": "foo.java",
+ "componentPath": "/foo.java",
+ "componentQualifier": "FIL",
+ "creationDate": "2016-08-15T15:25:38+0200",
+ "flows": [],
+ "flowsWithType": [],
+ "hash": "78417dcee7ba927b7e7c9161e29e02b8",
+ "key": "AWaqVGl3tut9VbnJvk6M",
+ "line": 62,
+ "message": "Make sure this file handling is safe here.",
+ "project": "org.sonarsource.java:java",
+ "projectEnabled": true,
+ "projectKey": "org.sonarsource.java:java",
+ "projectLongName": "SonarJava",
+ "projectName": "SonarJava",
+ "projectQualifier": "TRK",
+ "rule": "squid:S4797",
+ "secondaryLocations": [],
+ "status": "OPEN",
+ "tags": [
+ "cert",
+ "cwe",
+ "owasp-a1",
+ "owasp-a3",
+ ],
+ "textRange": {
+ "endLine": 62,
+ "endOffset": 96,
+ "startLine": 62,
+ "startOffset": 93,
+ },
+ "transitions": [],
+ "type": "SECURITY_HOTSPOT",
+ "updateDate": "2018-10-25T10:23:08+0200",
+ },
+]
+`;
+
+exports[`loadIssues should load issues with searchIssues if not re-indexing 1`] = `
[
{
"actions": [
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { mockMainBranch } from '../../../../helpers/mocks/branch-like';
+import { ComponentQualifier } from '../../../../types/component';
import loadIssues from '../loadIssues';
+const mockListResolvedValue = {
+ components: [
+ {
+ enabled: true,
+ key: 'org.sonarsource.java:java',
+ longName: 'SonarJava',
+ name: 'SonarJava',
+ qualifier: ComponentQualifier.Project,
+ },
+ {
+ enabled: true,
+ key: 'foo.java',
+ longName: 'Foo.java',
+ name: 'foo.java',
+ path: '/foo.java',
+ qualifier: ComponentQualifier.File,
+ },
+ ],
+ issues: [
+ {
+ actions: ['set_tags', 'comment', 'assign'],
+ assignee: 'luke',
+ author: 'luke@sonarsource.com',
+ comments: [],
+ component: 'foo.java',
+ creationDate: '2016-08-15T15:25:38+0200',
+ flows: [],
+ hash: '78417dcee7ba927b7e7c9161e29e02b8',
+ key: 'AWaqVGl3tut9VbnJvk6M',
+ line: 62,
+ message: 'Make sure this file handling is safe here.',
+ project: 'org.sonarsource.java:java',
+ rule: 'squid:S4797',
+ status: 'OPEN',
+ tags: ['cert', 'cwe', 'owasp-a1', 'owasp-a3'],
+ textRange: { startLine: 62, endLine: 62, startOffset: 93, endOffset: 96 },
+ transitions: [],
+ type: 'SECURITY_HOTSPOT',
+ updateDate: '2018-10-25T10:23:08+0200',
+ },
+ ],
+ paging: { pageIndex: 1, pageSize: 500, total: 1 },
+};
+
+const mockSearchResolvedValue = {
+ ...mockListResolvedValue,
+ debtTotal: 15,
+ effortTotal: 15,
+ facets: [],
+ languages: [{ key: 'java', name: 'Java' }],
+ rules: [
+ {
+ key: 'squid:S4797',
+ lang: 'java',
+ langName: 'Java',
+ name: 'Handling files is security-sensitive',
+ status: 'READY',
+ },
+ ],
+ users: [{ active: true, avatar: 'lukavatar', login: 'luke', name: 'Luke' }],
+};
+
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',
- },
- ],
- components: [
- {
- key: 'org.sonarsource.java:java',
- enabled: true,
- qualifier: 'TRK',
- name: 'SonarJava',
- longName: 'SonarJava',
- },
- {
- 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: [],
- }),
+ listIssues: jest.fn().mockImplementation(() => Promise.resolve(mockListResolvedValue)),
+ searchIssues: jest.fn().mockImplementation(() => Promise.resolve(mockSearchResolvedValue)),
}));
describe('loadIssues', () => {
- it('should load issues', async () => {
+ it('should load issues with searchIssues if not re-indexing', async () => {
const result = await loadIssues('foo.java', mockMainBranch());
+
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should load issues with listIssues if re-indexing', async () => {
+ const result = await loadIssues('foo.java', mockMainBranch(), true);
expect(result).toMatchSnapshot();
});
});
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { searchIssues } from '../../../api/issues';
+
+import { listIssues, searchIssues } from '../../../api/issues';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { parseIssueFromResponse } from '../../../helpers/issues';
import { BranchLike } from '../../../types/branch-like';
// Maximum issues return 20*500 for the API.
const PAGE_MAX = 20;
-function buildQuery(component: string, branchLike: BranchLike | undefined) {
+function buildListQuery(component: string, branchLike: BranchLike | undefined) {
return {
- additionalFields: '_all',
+ component,
resolved: 'false',
+ ...getBranchLikeQuery(branchLike),
+ };
+}
+
+function buildSearchQuery(component: string, branchLike: BranchLike | undefined) {
+ return {
+ additionalFields: '_all',
componentKeys: component,
+ resolved: 'false',
s: 'FILE_LINE',
...getBranchLikeQuery(branchLike),
};
}
-function loadPage(query: RawQuery, page: number, pageSize = PAGE_SIZE): Promise<Issue[]> {
+function loadListPage(query: RawQuery, page: number, pageSize = PAGE_SIZE): Promise<Issue[]> {
+ return listIssues({
+ ...query,
+ p: page,
+ ps: pageSize,
+ }).then((r) => r.issues.map((issue) => parseIssueFromResponse(issue, r.components)));
+}
+
+function loadSearchPage(query: RawQuery, page: number, pageSize = PAGE_SIZE): Promise<Issue[]> {
return searchIssues({
...query,
p: page,
);
}
-async function loadPageAndNext(query: RawQuery, page = 1, pageSize = PAGE_SIZE): Promise<Issue[]> {
- const issues = await loadPage(query, page);
+async function loadPageAndNext(
+ query: RawQuery,
+ needIssueSync = false,
+ page = 1,
+ pageSize = PAGE_SIZE
+): Promise<Issue[]> {
+ const issues = needIssueSync
+ ? await loadListPage(query, page)
+ : await loadSearchPage(query, page);
if (issues.length === 0) {
return [];
return issues;
}
- const nextIssues = await loadPageAndNext(query, page + 1, pageSize);
+ const nextIssues = await loadPageAndNext(query, needIssueSync, page + 1, pageSize);
+
return [...issues, ...nextIssues];
}
export default function loadIssues(
component: string,
- branchLike: BranchLike | undefined
+ branchLike: BranchLike | undefined,
+ needIssueSync = false
): Promise<Issue[]> {
- const query = buildQuery(component, branchLike);
- return loadPageAndNext(query);
+ const query = needIssueSync
+ ? buildListQuery(component, branchLike)
+ : buildSearchQuery(component, branchLike);
+
+ return loadPageAndNext(query, needIssueSync);
}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import WorkspaceComponentViewer, { Props } from '../WorkspaceComponentViewer';
-
-jest.mock('../../../api/components', () => ({
- getParents: jest.fn().mockResolvedValue([{ key: 'bar' }]),
-}));
-
-beforeEach(() => {
- jest.clearAllMocks();
-});
-
-it('should render', () => {
- expect(shallowRender()).toMatchSnapshot();
-});
-
-it('should close', () => {
- const onClose = jest.fn();
- const wrapper = shallowRender({ onClose });
- wrapper.find('WorkspaceHeader').prop<Function>('onClose')();
- expect(onClose).toHaveBeenCalledWith('foo');
-});
-
-it('should call back after load', () => {
- const onLoad = jest.fn();
- const wrapper = shallowRender({ onLoad });
- wrapper.find('[onLoaded]').prop<Function>('onLoaded')({
- key: 'foo',
- path: 'src/foo.js',
- q: 'FIL',
- });
- expect(onLoad).toHaveBeenCalledWith({ key: 'foo', name: 'src/foo.js', qualifier: 'FIL' });
-});
-
-function shallowRender(props?: Partial<Props>) {
- return shallow<WorkspaceComponentViewer>(
- <WorkspaceComponentViewer
- component={{ branchLike: undefined, key: 'foo' }}
- height={300}
- onClose={jest.fn()}
- onCollapse={jest.fn()}
- onLoad={jest.fn()}
- onMaximize={jest.fn()}
- onMinimize={jest.fn()}
- onResize={jest.fn()}
- {...props}
- />
- );
-}
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render 1`] = `
-<div
- className="workspace-viewer"
->
- <WorkspaceHeader
- onClose={[Function]}
- onCollapse={[MockFunction]}
- onMaximize={[MockFunction]}
- onMinimize={[MockFunction]}
- onResize={[MockFunction]}
- >
- <WorkspaceComponentTitle
- component={
- {
- "branchLike": undefined,
- "key": "foo",
- }
- }
- />
- </WorkspaceHeader>
- <div
- className="workspace-viewer-container"
- style={
- {
- "height": 300,
- }
- }
- >
- <SourceViewer
- component="foo"
- displayAllIssues={false}
- displayIssueLocationsCount={true}
- displayIssueLocationsLink={true}
- displayLocationMarkers={true}
- onLoaded={[Function]}
- />
- </div>
-</div>
-`;