aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/components/SourceViewer
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src/main/js/components/SourceViewer')
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx653
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx673
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx84
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-test.tsx (renamed from server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewerBase-test.tsx)8
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewer-test.tsx.snap (renamed from server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerBase-test.tsx.snap)0
5 files changed, 732 insertions, 686 deletions
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 87d95775bdf..3c43b8ce50b 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx
@@ -17,10 +17,651 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { lazyLoadComponent } from '../lazyLoadComponent';
+import { intersection, uniqBy } from 'lodash';
+import * as React from 'react';
+import {
+ getComponentData,
+ getComponentForSourceViewer,
+ getDuplications,
+ getSources
+} from '../../api/components';
+import { getBranchLikeQuery, isSameBranchLike } from '../../helpers/branch-like';
+import { translate } from '../../helpers/l10n';
+import { HttpStatus } from '../../helpers/request';
+import { BranchLike } from '../../types/branch-like';
+import {
+ Dict,
+ DuplicatedFile,
+ Duplication,
+ FlowLocation,
+ Issue,
+ LinearIssueLocation,
+ Measure,
+ SourceLine,
+ SourceViewerFile
+} from '../../types/types';
+import { Alert } from '../ui/Alert';
+import { WorkspaceContext } from '../workspace/context';
+import DuplicationPopup from './components/DuplicationPopup';
+import {
+ filterDuplicationBlocksByLine,
+ getDuplicationBlocksForIndex,
+ isDuplicationBlockInRemovedComponent
+} from './helpers/duplications';
+import getCoverageStatus from './helpers/getCoverageStatus';
+import {
+ duplicationsByLine,
+ issuesByLine,
+ locationsByLine,
+ symbolsByLine
+} from './helpers/indexing';
+import { LINES_TO_LOAD } from './helpers/lines';
+import defaultLoadIssues from './helpers/loadIssues';
+import SourceViewerCode from './SourceViewerCode';
+import { SourceViewerContext } from './SourceViewerContext';
+import SourceViewerHeader from './SourceViewerHeader';
+import SourceViewerHeaderSlim from './SourceViewerHeaderSlim';
+import './styles.css';
-const SourceViewer = lazyLoadComponent(
- () => import(/* webpackPrefetch: true */ './SourceViewerBase'),
- 'SourceViewer'
-);
-export default SourceViewer;
+export interface Props {
+ aroundLine?: number;
+ branchLike: BranchLike | undefined;
+ component: string;
+ componentMeasures?: Measure[];
+ displayAllIssues?: boolean;
+ displayIssueLocationsCount?: boolean;
+ displayIssueLocationsLink?: boolean;
+ displayLocationMarkers?: boolean;
+ highlightedLine?: number;
+ // `undefined` elements mean they are located in a different file,
+ // but kept to maintaint the location indexes
+ highlightedLocations?: (FlowLocation | undefined)[];
+ highlightedLocationMessage?: { index: number; text: string | undefined };
+ loadIssues?: (
+ component: string,
+ from: number,
+ to: number,
+ branchLike: BranchLike | undefined
+ ) => Promise<Issue[]>;
+ onLoaded?: (component: SourceViewerFile, sources: SourceLine[], issues: Issue[]) => void;
+ onLocationSelect?: (index: number) => void;
+ onIssueChange?: (issue: Issue) => void;
+ onIssueSelect?: (issueKey: string) => void;
+ onIssueUnselect?: () => void;
+ scroll?: (element: HTMLElement) => void;
+ selectedIssue?: string;
+ showMeasures?: boolean;
+ metricKey?: string;
+ slimHeader?: boolean;
+}
+
+interface State {
+ component?: SourceViewerFile;
+ duplicatedFiles?: Dict<DuplicatedFile>;
+ duplications?: Duplication[];
+ duplicationsByLine: { [line: number]: number[] };
+ hasSourcesAfter: boolean;
+ highlightedSymbols: string[];
+ issueLocationsByLine: { [line: number]: LinearIssueLocation[] };
+ issuePopup?: { issue: string; name: string };
+ issues?: Issue[];
+ issuesByLine: { [line: number]: Issue[] };
+ loading: boolean;
+ loadingSourcesAfter: boolean;
+ loadingSourcesBefore: boolean;
+ notAccessible: boolean;
+ notExist: boolean;
+ openIssuesByLine: { [line: number]: boolean };
+ selectedIssue?: string;
+ sourceRemoved: boolean;
+ sources?: SourceLine[];
+ symbolsByLine: { [line: number]: string[] };
+}
+
+export default class SourceViewer extends React.PureComponent<Props, State> {
+ node?: HTMLElement | null;
+ mounted = false;
+
+ static defaultProps = {
+ displayAllIssues: false,
+ displayIssueLocationsCount: true,
+ displayIssueLocationsLink: true,
+ displayLocationMarkers: true
+ };
+
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ duplicationsByLine: {},
+ hasSourcesAfter: false,
+ highlightedSymbols: [],
+ issuesByLine: {},
+ issueLocationsByLine: {},
+ loading: true,
+ loadingSourcesAfter: false,
+ loadingSourcesBefore: false,
+ notAccessible: false,
+ notExist: false,
+ openIssuesByLine: {},
+ selectedIssue: props.selectedIssue,
+ sourceRemoved: false,
+ symbolsByLine: {}
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchComponent();
+ }
+
+ async componentDidUpdate(prevProps: Props) {
+ if (
+ this.props.onIssueSelect !== undefined &&
+ this.props.selectedIssue !== prevProps.selectedIssue
+ ) {
+ this.setState({ selectedIssue: this.props.selectedIssue });
+ }
+ if (
+ prevProps.component !== this.props.component ||
+ !isSameBranchLike(prevProps.branchLike, this.props.branchLike)
+ ) {
+ this.fetchComponent();
+ } else if (
+ this.props.aroundLine !== undefined &&
+ prevProps.aroundLine !== this.props.aroundLine &&
+ 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),
+ hasSourcesAfter: sources.length > LINES_TO_LOAD
+ },
+ () => {
+ if (this.props.onLoaded && this.state.component && this.state.issues) {
+ this.props.onLoaded(this.state.component, finalSources, this.state.issues);
+ }
+ }
+ );
+ }
+ } else {
+ this.checkSelectedIssueChange();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ loadComponent(component: string, branchLike?: BranchLike) {
+ return Promise.all([
+ getComponentForSourceViewer({ component, ...getBranchLikeQuery(branchLike) }),
+ getComponentData({ component, ...getBranchLikeQuery(branchLike) })
+ ]).then(([sourceViewerComponent, { component }]) => ({
+ ...sourceViewerComponent,
+ leakPeriodDate: component.leakPeriodDate
+ }));
+ }
+
+ checkSelectedIssueChange() {
+ const { selectedIssue } = this.props;
+ const { issues } = this.state;
+ if (
+ selectedIssue !== undefined &&
+ issues !== undefined &&
+ issues.find(issue => issue.key === selectedIssue) === undefined
+ ) {
+ this.reloadIssues();
+ }
+ }
+
+ loadSources(
+ key: string,
+ from: number | undefined,
+ to: number | undefined,
+ branchLike: BranchLike | undefined
+ ) {
+ return getSources({ key, from, to, ...getBranchLikeQuery(branchLike) });
+ }
+
+ get loadIssues() {
+ return this.props.loadIssues || defaultLoadIssues;
+ }
+
+ computeCoverageStatus(lines: SourceLine[]) {
+ return lines.map(line => ({ ...line, coverageStatus: getCoverageStatus(line) }));
+ }
+
+ isLineOutsideOfRange(lineNumber: number) {
+ const { sources } = this.state;
+ if (sources && sources.length > 0) {
+ const firstLine = sources[0];
+ const lastList = sources[sources.length - 1];
+ return lineNumber < firstLine.line || lineNumber > lastList.line;
+ }
+
+ return true;
+ }
+
+ fetchComponent() {
+ this.setState({ loading: true });
+
+ const to = (this.props.aroundLine || 0) + LINES_TO_LOAD;
+ const loadIssues = (component: SourceViewerFile, sources: SourceLine[]) => {
+ this.loadIssues(this.props.component, 1, to, this.props.branchLike).then(
+ issues => {
+ if (this.mounted) {
+ const finalSources = sources.slice(0, LINES_TO_LOAD);
+ this.setState(
+ {
+ component,
+ duplicatedFiles: undefined,
+ duplications: undefined,
+ duplicationsByLine: {},
+ hasSourcesAfter: sources.length > LINES_TO_LOAD,
+ highlightedSymbols: [],
+ issueLocationsByLine: locationsByLine(issues),
+ 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))
+ },
+ () => {
+ if (this.props.onLoaded) {
+ this.props.onLoaded(component, finalSources, issues);
+ }
+ }
+ );
+ }
+ },
+ () => {
+ /* no op */
+ }
+ );
+ };
+
+ const onFailLoadComponent = (response: Response) => {
+ if (this.mounted) {
+ if (response.status === HttpStatus.Forbidden) {
+ this.setState({ loading: false, notAccessible: true });
+ } else if (response.status === HttpStatus.NotFound) {
+ this.setState({ loading: false, notExist: true });
+ }
+ }
+ };
+
+ const onFailLoadSources = (response: Response, component: SourceViewerFile) => {
+ if (this.mounted) {
+ if (response.status === HttpStatus.Forbidden) {
+ this.setState({ component, loading: false, notAccessible: true });
+ } else if (response.status === HttpStatus.NotFound) {
+ this.setState({ component, loading: false, sourceRemoved: true });
+ }
+ }
+ };
+
+ const onResolve = (component: SourceViewerFile) => {
+ const sourcesRequest =
+ component.q === 'FIL' || component.q === 'UTS' ? this.fetchSources() : Promise.resolve([]);
+ sourcesRequest.then(
+ sources => loadIssues(component, sources),
+ response => onFailLoadSources(response, component)
+ );
+ };
+
+ this.loadComponent(this.props.component, this.props.branchLike).then(
+ onResolve,
+ onFailLoadComponent
+ );
+ }
+
+ reloadIssues() {
+ if (!this.state.sources) {
+ return;
+ }
+ const firstSourceLine = this.state.sources[0];
+ const lastSourceLine = this.state.sources[this.state.sources.length - 1];
+ this.loadIssues(
+ this.props.component,
+ firstSourceLine && firstSourceLine.line,
+ lastSourceLine && lastSourceLine.line,
+ this.props.branchLike
+ ).then(
+ issues => {
+ if (this.mounted) {
+ this.setState({
+ issues,
+ issuesByLine: issuesByLine(issues),
+ issueLocationsByLine: locationsByLine(issues)
+ });
+ }
+ },
+ () => {
+ /* no op */
+ }
+ );
+ }
+
+ fetchSources = (): Promise<SourceLine[]> => {
+ return new Promise((resolve, reject) => {
+ const onFailLoadSources = (response: Response) => {
+ if (this.mounted) {
+ if ([HttpStatus.Forbidden, HttpStatus.NotFound].includes(response.status)) {
+ reject(response);
+ } else {
+ resolve([]);
+ }
+ }
+ };
+
+ const from = this.props.aroundLine
+ ? Math.max(1, this.props.aroundLine - LINES_TO_LOAD / 2 + 1)
+ : 1;
+
+ 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++;
+
+ this.loadSources(this.props.component, from, to, this.props.branchLike).then(sources => {
+ resolve(sources);
+ }, onFailLoadSources);
+ });
+ };
+
+ loadSourcesBefore = () => {
+ 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);
+ Promise.all([
+ this.loadSources(this.props.component, from, firstSourceLine.line - 1, this.props.branchLike),
+ this.loadIssues(this.props.component, from, firstSourceLine.line - 1, this.props.branchLike)
+ ]).then(
+ ([sources, issues]) => {
+ if (this.mounted) {
+ this.setState(prevState => {
+ const nextIssues = uniqBy([...issues, ...(prevState.issues || [])], issue => issue.key);
+ return {
+ issues: nextIssues,
+ issuesByLine: issuesByLine(nextIssues),
+ issueLocationsByLine: locationsByLine(nextIssues),
+ loadingSourcesBefore: false,
+ sources: [...this.computeCoverageStatus(sources), ...(prevState.sources || [])],
+ symbolsByLine: { ...prevState.symbolsByLine, ...symbolsByLine(sources) }
+ };
+ });
+ }
+ },
+ () => {
+ /* no op */
+ }
+ );
+ };
+
+ loadSourcesAfter = () => {
+ if (!this.state.sources) {
+ return;
+ }
+ const lastSourceLine = this.state.sources[this.state.sources.length - 1];
+ this.setState({ loadingSourcesAfter: true });
+ const fromLine = lastSourceLine.line + 1;
+ // request one additional line to define `hasSourcesAfter`
+ const toLine = lastSourceLine.line + LINES_TO_LOAD + 1;
+ Promise.all([
+ this.loadSources(this.props.component, fromLine, toLine, this.props.branchLike),
+ this.loadIssues(this.props.component, fromLine, toLine, this.props.branchLike)
+ ]).then(
+ ([sources, issues]) => {
+ if (this.mounted) {
+ this.setState(prevState => {
+ const nextIssues = uniqBy([...(prevState.issues || []), ...issues], issue => issue.key);
+ return {
+ issues: nextIssues,
+ issuesByLine: issuesByLine(nextIssues),
+ issueLocationsByLine: locationsByLine(nextIssues),
+ hasSourcesAfter: sources.length > LINES_TO_LOAD,
+ loadingSourcesAfter: false,
+ sources: [
+ ...(prevState.sources || []),
+ ...this.computeCoverageStatus(sources.slice(0, LINES_TO_LOAD))
+ ],
+ symbolsByLine: {
+ ...prevState.symbolsByLine,
+ ...symbolsByLine(sources.slice(0, LINES_TO_LOAD))
+ }
+ };
+ });
+ }
+ },
+ () => {
+ /* no op */
+ }
+ );
+ };
+
+ loadDuplications = () => {
+ getDuplications({
+ key: this.props.component,
+ ...getBranchLikeQuery(this.props.branchLike)
+ }).then(
+ r => {
+ if (this.mounted) {
+ this.setState({
+ duplications: r.duplications,
+ duplicationsByLine: duplicationsByLine(r.duplications),
+ duplicatedFiles: r.files
+ });
+ }
+ },
+ () => {
+ /* no op */
+ }
+ );
+ };
+
+ handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => {
+ 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;
+ });
+ };
+
+ handleSymbolClick = (symbols: string[]) => {
+ this.setState(state => {
+ const shouldDisable = intersection(state.highlightedSymbols, symbols).length > 0;
+ const highlightedSymbols = shouldDisable ? [] : symbols;
+ return { highlightedSymbols };
+ });
+ };
+
+ handleIssueSelect = (issue: string) => {
+ if (this.props.onIssueSelect) {
+ this.props.onIssueSelect(issue);
+ } else {
+ this.setState({ selectedIssue: issue });
+ }
+ };
+
+ handleIssueUnselect = () => {
+ if (this.props.onIssueUnselect) {
+ this.props.onIssueUnselect();
+ } else {
+ this.setState({ selectedIssue: undefined });
+ }
+ };
+
+ handleOpenIssues = (line: SourceLine) => {
+ this.setState(state => ({
+ openIssuesByLine: { ...state.openIssuesByLine, [line.line]: true }
+ }));
+ };
+
+ handleCloseIssues = (line: SourceLine) => {
+ this.setState(state => ({
+ openIssuesByLine: { ...state.openIssuesByLine, [line.line]: false }
+ }));
+ };
+
+ handleIssueChange = (issue: Issue) => {
+ this.setState(({ issues = [] }) => {
+ const newIssues = issues.map(candidate => (candidate.key === issue.key ? issue : candidate));
+ return { issues: newIssues, issuesByLine: issuesByLine(newIssues) };
+ });
+ if (this.props.onIssueChange) {
+ this.props.onIssueChange(issue);
+ }
+ };
+
+ renderDuplicationPopup = (index: number, line: number) => {
+ const { component, 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)}
+ openComponent={openComponent}
+ sourceViewerFile={component}
+ />
+ )}
+ </WorkspaceContext.Consumer>
+ );
+ };
+
+ renderCode(sources: SourceLine[]) {
+ const hasSourcesBefore = sources.length > 0 && sources[0].line > 1;
+ return (
+ <SourceViewerCode
+ branchLike={this.props.branchLike}
+ displayAllIssues={this.props.displayAllIssues}
+ displayIssueLocationsCount={this.props.displayIssueLocationsCount}
+ displayIssueLocationsLink={this.props.displayIssueLocationsLink}
+ displayLocationMarkers={this.props.displayLocationMarkers}
+ duplications={this.state.duplications}
+ duplicationsByLine={this.state.duplicationsByLine}
+ hasSourcesAfter={this.state.hasSourcesAfter}
+ hasSourcesBefore={hasSourcesBefore}
+ highlightedLine={this.props.highlightedLine}
+ highlightedLocationMessage={this.props.highlightedLocationMessage}
+ highlightedLocations={this.props.highlightedLocations}
+ highlightedSymbols={this.state.highlightedSymbols}
+ issueLocationsByLine={this.state.issueLocationsByLine}
+ issuePopup={this.state.issuePopup}
+ 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}
+ onIssueChange={this.handleIssueChange}
+ onIssuePopupToggle={this.handleIssuePopupToggle}
+ onIssueSelect={this.handleIssueSelect}
+ onIssueUnselect={this.handleIssueUnselect}
+ onIssuesClose={this.handleCloseIssues}
+ onIssuesOpen={this.handleOpenIssues}
+ onLocationSelect={this.props.onLocationSelect}
+ onSymbolClick={this.handleSymbolClick}
+ openIssuesByLine={this.state.openIssuesByLine}
+ renderDuplicationPopup={this.renderDuplicationPopup}
+ scroll={this.props.scroll}
+ metricKey={this.props.metricKey}
+ selectedIssue={this.state.selectedIssue}
+ sources={sources}
+ symbolsByLine={this.state.symbolsByLine}
+ />
+ );
+ }
+
+ renderHeader(branchLike: BranchLike | undefined, sourceViewerFile: SourceViewerFile) {
+ return this.props.slimHeader ? (
+ <SourceViewerHeaderSlim branchLike={branchLike} sourceViewerFile={sourceViewerFile} />
+ ) : (
+ <WorkspaceContext.Consumer>
+ {({ openComponent }) => (
+ <SourceViewerHeader
+ branchLike={this.props.branchLike}
+ componentMeasures={this.props.componentMeasures}
+ openComponent={openComponent}
+ showMeasures={this.props.showMeasures}
+ sourceViewerFile={sourceViewerFile}
+ />
+ )}
+ </WorkspaceContext.Consumer>
+ );
+ }
+
+ render() {
+ const { component, loading, sources, notAccessible, sourceRemoved } = this.state;
+
+ if (loading) {
+ return null;
+ }
+
+ if (this.state.notExist) {
+ return (
+ <Alert className="spacer-top" variant="warning">
+ {translate('component_viewer.no_component')}
+ </Alert>
+ );
+ }
+
+ if (notAccessible) {
+ return (
+ <Alert className="spacer-top" variant="warning">
+ {translate('code_viewer.no_source_code_displayed_due_to_security')}
+ </Alert>
+ );
+ }
+
+ if (!component) {
+ return null;
+ }
+
+ return (
+ <SourceViewerContext.Provider value={{ branchLike: this.props.branchLike, file: component }}>
+ <div className="source-viewer" ref={node => (this.node = node)}>
+ {this.renderHeader(this.props.branchLike, 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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx
deleted file mode 100644
index 93950d7fff7..00000000000
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx
+++ /dev/null
@@ -1,673 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { intersection, uniqBy } from 'lodash';
-import * as React from 'react';
-import {
- getComponentData,
- getComponentForSourceViewer,
- getDuplications,
- getSources
-} from '../../api/components';
-import { Alert } from '../../components/ui/Alert';
-import { getBranchLikeQuery, isSameBranchLike } from '../../helpers/branch-like';
-import { translate } from '../../helpers/l10n';
-import { BranchLike } from '../../types/branch-like';
-import {
- Dict,
- DuplicatedFile,
- Duplication,
- FlowLocation,
- Issue,
- LinearIssueLocation,
- Measure,
- SourceLine,
- SourceViewerFile
-} from '../../types/types';
-import { WorkspaceContext } from '../workspace/context';
-import DuplicationPopup from './components/DuplicationPopup';
-import {
- filterDuplicationBlocksByLine,
- getDuplicationBlocksForIndex,
- isDuplicationBlockInRemovedComponent
-} from './helpers/duplications';
-import getCoverageStatus from './helpers/getCoverageStatus';
-import {
- duplicationsByLine,
- issuesByLine,
- locationsByLine,
- symbolsByLine
-} from './helpers/indexing';
-import { LINES_TO_LOAD } from './helpers/lines';
-import defaultLoadIssues from './helpers/loadIssues';
-import SourceViewerCode from './SourceViewerCode';
-import { SourceViewerContext } from './SourceViewerContext';
-import SourceViewerHeader from './SourceViewerHeader';
-import SourceViewerHeaderSlim from './SourceViewerHeaderSlim';
-import './styles.css';
-
-// TODO react-virtualized
-
-export interface Props {
- aroundLine?: number;
- branchLike: BranchLike | undefined;
- component: string;
- componentMeasures?: Measure[];
- displayAllIssues?: boolean;
- displayIssueLocationsCount?: boolean;
- displayIssueLocationsLink?: boolean;
- displayLocationMarkers?: boolean;
- highlightedLine?: number;
- // `undefined` elements mean they are located in a different file,
- // but kept to maintaint the location indexes
- highlightedLocations?: (FlowLocation | undefined)[];
- highlightedLocationMessage?: { index: number; text: string | undefined };
- loadIssues?: (
- component: string,
- from: number,
- to: number,
- branchLike: BranchLike | undefined
- ) => Promise<Issue[]>;
- onLoaded?: (component: SourceViewerFile, sources: SourceLine[], issues: Issue[]) => void;
- onLocationSelect?: (index: number) => void;
- onIssueChange?: (issue: Issue) => void;
- onIssueSelect?: (issueKey: string) => void;
- onIssueUnselect?: () => void;
- scroll?: (element: HTMLElement) => void;
- selectedIssue?: string;
- showMeasures?: boolean;
- metricKey?: string;
- slimHeader?: boolean;
-}
-
-interface State {
- component?: SourceViewerFile;
- duplicatedFiles?: Dict<DuplicatedFile>;
- duplications?: Duplication[];
- duplicationsByLine: { [line: number]: number[] };
- hasSourcesAfter: boolean;
- highlightedSymbols: string[];
- issueLocationsByLine: { [line: number]: LinearIssueLocation[] };
- issuePopup?: { issue: string; name: string };
- issues?: Issue[];
- issuesByLine: { [line: number]: Issue[] };
- loading: boolean;
- loadingSourcesAfter: boolean;
- loadingSourcesBefore: boolean;
- notAccessible: boolean;
- notExist: boolean;
- openIssuesByLine: { [line: number]: boolean };
- selectedIssue?: string;
- sourceRemoved: boolean;
- sources?: SourceLine[];
- symbolsByLine: { [line: number]: string[] };
-}
-
-export default class SourceViewerBase extends React.PureComponent<Props, State> {
- node?: HTMLElement | null;
- mounted = false;
-
- static defaultProps = {
- displayAllIssues: false,
- displayIssueLocationsCount: true,
- displayIssueLocationsLink: true,
- displayLocationMarkers: true
- };
-
- constructor(props: Props) {
- super(props);
-
- this.state = {
- duplicationsByLine: {},
- hasSourcesAfter: false,
- highlightedSymbols: [],
- issuesByLine: {},
- issueLocationsByLine: {},
- loading: true,
- loadingSourcesAfter: false,
- loadingSourcesBefore: false,
- notAccessible: false,
- notExist: false,
- openIssuesByLine: {},
- selectedIssue: props.selectedIssue,
- sourceRemoved: false,
- symbolsByLine: {}
- };
- }
-
- componentDidMount() {
- this.mounted = true;
- this.fetchComponent();
- }
-
- componentDidUpdate(prevProps: Props) {
- if (
- this.props.onIssueSelect !== undefined &&
- this.props.selectedIssue !== prevProps.selectedIssue
- ) {
- this.setState({ selectedIssue: this.props.selectedIssue });
- }
- if (
- prevProps.component !== this.props.component ||
- !isSameBranchLike(prevProps.branchLike, this.props.branchLike)
- ) {
- this.fetchComponent();
- } else if (
- this.props.aroundLine !== undefined &&
- prevProps.aroundLine !== this.props.aroundLine &&
- this.isLineOutsideOfRange(this.props.aroundLine)
- ) {
- this.fetchSources().then(
- sources => {
- if (this.mounted) {
- const finalSources = sources.slice(0, LINES_TO_LOAD);
- this.setState(
- {
- sources: sources.slice(0, LINES_TO_LOAD),
- hasSourcesAfter: sources.length > LINES_TO_LOAD
- },
- () => {
- if (this.props.onLoaded && this.state.component && this.state.issues) {
- this.props.onLoaded(this.state.component, finalSources, this.state.issues);
- }
- }
- );
- }
- },
- () => {
- // TODO
- }
- );
- } else {
- const { selectedIssue } = this.props;
- const { issues } = this.state;
- if (
- selectedIssue !== undefined &&
- issues !== undefined &&
- issues.find(issue => issue.key === selectedIssue) === undefined
- ) {
- this.reloadIssues();
- }
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- loadComponent(component: string, branchLike?: BranchLike) {
- return Promise.all([
- getComponentForSourceViewer({ component, ...getBranchLikeQuery(branchLike) }),
- getComponentData({ component, ...getBranchLikeQuery(branchLike) })
- ]).then(([sourceViewerComponent, { component }]) => ({
- ...sourceViewerComponent,
- leakPeriodDate: component.leakPeriodDate
- }));
- }
-
- loadSources(
- key: string,
- from: number | undefined,
- to: number | undefined,
- branchLike: BranchLike | undefined
- ) {
- return getSources({ key, from, to, ...getBranchLikeQuery(branchLike) });
- }
-
- get loadIssues() {
- return this.props.loadIssues || defaultLoadIssues;
- }
-
- computeCoverageStatus(lines: SourceLine[]) {
- return lines.map(line => ({ ...line, coverageStatus: getCoverageStatus(line) }));
- }
-
- isLineOutsideOfRange(lineNumber: number) {
- const { sources } = this.state;
- if (sources && sources.length > 0) {
- const firstLine = sources[0];
- const lastList = sources[sources.length - 1];
- return lineNumber < firstLine.line || lineNumber > lastList.line;
- }
-
- return true;
- }
-
- fetchComponent() {
- this.setState({ loading: true });
-
- const to = (this.props.aroundLine || 0) + LINES_TO_LOAD;
- const loadIssues = (component: SourceViewerFile, sources: SourceLine[]) => {
- this.loadIssues(this.props.component, 1, to, this.props.branchLike).then(
- issues => {
- if (this.mounted) {
- const finalSources = sources.slice(0, LINES_TO_LOAD);
- this.setState(
- {
- component,
- duplicatedFiles: undefined,
- duplications: undefined,
- duplicationsByLine: {},
- hasSourcesAfter: sources.length > LINES_TO_LOAD,
- highlightedSymbols: [],
- issueLocationsByLine: locationsByLine(issues),
- 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))
- },
- () => {
- if (this.props.onLoaded) {
- this.props.onLoaded(component, finalSources, issues);
- }
- }
- );
- }
- },
- () => {
- // TODO
- }
- );
- };
-
- const onFailLoadComponent = (response: Response) => {
- // TODO handle other statuses
- if (this.mounted) {
- if (response.status === 403) {
- this.setState({ loading: false, notAccessible: true });
- } else if (response.status === 404) {
- this.setState({ loading: false, notExist: true });
- }
- }
- };
-
- const onFailLoadSources = (response: Response, component: SourceViewerFile) => {
- // TODO handle other statuses
- if (this.mounted) {
- if (response.status === 403) {
- this.setState({ component, loading: false, notAccessible: true });
- } else if (response.status === 404) {
- this.setState({ component, loading: false, sourceRemoved: true });
- }
- }
- };
-
- const onResolve = (component: SourceViewerFile) => {
- const sourcesRequest =
- component.q === 'FIL' || component.q === 'UTS' ? this.fetchSources() : Promise.resolve([]);
- sourcesRequest.then(
- sources => loadIssues(component, sources),
- response => onFailLoadSources(response, component)
- );
- };
-
- this.loadComponent(this.props.component, this.props.branchLike).then(
- onResolve,
- onFailLoadComponent
- );
- }
-
- reloadIssues() {
- if (!this.state.sources) {
- return;
- }
- const firstSourceLine = this.state.sources[0];
- const lastSourceLine = this.state.sources[this.state.sources.length - 1];
- this.loadIssues(
- this.props.component,
- firstSourceLine && firstSourceLine.line,
- lastSourceLine && lastSourceLine.line,
- this.props.branchLike
- ).then(
- issues => {
- if (this.mounted) {
- this.setState({
- issues,
- issuesByLine: issuesByLine(issues),
- issueLocationsByLine: locationsByLine(issues)
- });
- }
- },
- () => {
- // TODO
- }
- );
- }
-
- fetchSources = (): Promise<SourceLine[]> => {
- return new Promise((resolve, reject) => {
- const onFailLoadSources = (response: Response) => {
- // TODO handle other statuses
- if (this.mounted) {
- if ([403, 404].includes(response.status)) {
- reject(response);
- } else {
- resolve([]);
- }
- }
- };
-
- const from = this.props.aroundLine
- ? Math.max(1, this.props.aroundLine - LINES_TO_LOAD / 2 + 1)
- : 1;
-
- 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++;
-
- this.loadSources(this.props.component, from, to, this.props.branchLike).then(sources => {
- resolve(sources);
- }, onFailLoadSources);
- });
- };
-
- loadSourcesBefore = () => {
- 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);
- Promise.all([
- this.loadSources(this.props.component, from, firstSourceLine.line - 1, this.props.branchLike),
- this.loadIssues(this.props.component, from, firstSourceLine.line - 1, this.props.branchLike)
- ]).then(
- ([sources, issues]) => {
- if (this.mounted) {
- this.setState(prevState => {
- const nextIssues = uniqBy([...issues, ...(prevState.issues || [])], issue => issue.key);
- return {
- issues: nextIssues,
- issuesByLine: issuesByLine(nextIssues),
- issueLocationsByLine: locationsByLine(nextIssues),
- loadingSourcesBefore: false,
- sources: [...this.computeCoverageStatus(sources), ...(prevState.sources || [])],
- symbolsByLine: { ...prevState.symbolsByLine, ...symbolsByLine(sources) }
- };
- });
- }
- },
- () => {
- // TODO
- }
- );
- };
-
- loadSourcesAfter = () => {
- if (!this.state.sources) {
- return;
- }
- const lastSourceLine = this.state.sources[this.state.sources.length - 1];
- this.setState({ loadingSourcesAfter: true });
- const fromLine = lastSourceLine.line + 1;
- // request one additional line to define `hasSourcesAfter`
- const toLine = lastSourceLine.line + LINES_TO_LOAD + 1;
- Promise.all([
- this.loadSources(this.props.component, fromLine, toLine, this.props.branchLike),
- this.loadIssues(this.props.component, fromLine, toLine, this.props.branchLike)
- ]).then(
- ([sources, issues]) => {
- if (this.mounted) {
- this.setState(prevState => {
- const nextIssues = uniqBy([...(prevState.issues || []), ...issues], issue => issue.key);
- return {
- issues: nextIssues,
- issuesByLine: issuesByLine(nextIssues),
- issueLocationsByLine: locationsByLine(nextIssues),
- hasSourcesAfter: sources.length > LINES_TO_LOAD,
- loadingSourcesAfter: false,
- sources: [
- ...(prevState.sources || []),
- ...this.computeCoverageStatus(sources.slice(0, LINES_TO_LOAD))
- ],
- symbolsByLine: {
- ...prevState.symbolsByLine,
- ...symbolsByLine(sources.slice(0, LINES_TO_LOAD))
- }
- };
- });
- }
- },
- () => {
- // TODO
- }
- );
- };
-
- loadDuplications = () => {
- getDuplications({
- key: this.props.component,
- ...getBranchLikeQuery(this.props.branchLike)
- }).then(
- r => {
- if (this.mounted) {
- this.setState({
- duplications: r.duplications,
- duplicationsByLine: duplicationsByLine(r.duplications),
- duplicatedFiles: r.files
- });
- }
- },
- () => {
- // TODO
- }
- );
- };
-
- handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => {
- 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;
- });
- };
-
- handleSymbolClick = (symbols: string[]) => {
- this.setState(state => {
- const shouldDisable = intersection(state.highlightedSymbols, symbols).length > 0;
- const highlightedSymbols = shouldDisable ? [] : symbols;
- return { highlightedSymbols };
- });
- };
-
- handleIssueSelect = (issue: string) => {
- if (this.props.onIssueSelect) {
- this.props.onIssueSelect(issue);
- } else {
- this.setState({ selectedIssue: issue });
- }
- };
-
- handleIssueUnselect = () => {
- if (this.props.onIssueUnselect) {
- this.props.onIssueUnselect();
- } else {
- this.setState({ selectedIssue: undefined });
- }
- };
-
- handleOpenIssues = (line: SourceLine) => {
- this.setState(state => ({
- openIssuesByLine: { ...state.openIssuesByLine, [line.line]: true }
- }));
- };
-
- handleCloseIssues = (line: SourceLine) => {
- this.setState(state => ({
- openIssuesByLine: { ...state.openIssuesByLine, [line.line]: false }
- }));
- };
-
- handleIssueChange = (issue: Issue) => {
- this.setState(({ issues = [] }) => {
- const newIssues = issues.map(candidate => (candidate.key === issue.key ? issue : candidate));
- return { issues: newIssues, issuesByLine: issuesByLine(newIssues) };
- });
- if (this.props.onIssueChange) {
- this.props.onIssueChange(issue);
- }
- };
-
- renderDuplicationPopup = (index: number, line: number) => {
- const { component, 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)}
- openComponent={openComponent}
- sourceViewerFile={component}
- />
- )}
- </WorkspaceContext.Consumer>
- );
- };
-
- renderCode(sources: SourceLine[]) {
- const hasSourcesBefore = sources.length > 0 && sources[0].line > 1;
- return (
- <SourceViewerCode
- branchLike={this.props.branchLike}
- displayAllIssues={this.props.displayAllIssues}
- displayIssueLocationsCount={this.props.displayIssueLocationsCount}
- displayIssueLocationsLink={this.props.displayIssueLocationsLink}
- displayLocationMarkers={this.props.displayLocationMarkers}
- duplications={this.state.duplications}
- duplicationsByLine={this.state.duplicationsByLine}
- hasSourcesAfter={this.state.hasSourcesAfter}
- hasSourcesBefore={hasSourcesBefore}
- highlightedLine={this.props.highlightedLine}
- highlightedLocationMessage={this.props.highlightedLocationMessage}
- highlightedLocations={this.props.highlightedLocations}
- highlightedSymbols={this.state.highlightedSymbols}
- issueLocationsByLine={this.state.issueLocationsByLine}
- issuePopup={this.state.issuePopup}
- 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}
- onIssueChange={this.handleIssueChange}
- onIssuePopupToggle={this.handleIssuePopupToggle}
- onIssueSelect={this.handleIssueSelect}
- onIssueUnselect={this.handleIssueUnselect}
- onIssuesClose={this.handleCloseIssues}
- onIssuesOpen={this.handleOpenIssues}
- onLocationSelect={this.props.onLocationSelect}
- onSymbolClick={this.handleSymbolClick}
- openIssuesByLine={this.state.openIssuesByLine}
- renderDuplicationPopup={this.renderDuplicationPopup}
- scroll={this.props.scroll}
- metricKey={this.props.metricKey}
- selectedIssue={this.state.selectedIssue}
- sources={sources}
- symbolsByLine={this.state.symbolsByLine}
- />
- );
- }
-
- renderHeader(branchLike: BranchLike | undefined, sourceViewerFile: SourceViewerFile) {
- return this.props.slimHeader ? (
- <SourceViewerHeaderSlim branchLike={branchLike} sourceViewerFile={sourceViewerFile} />
- ) : (
- <WorkspaceContext.Consumer>
- {({ openComponent }) => (
- <SourceViewerHeader
- branchLike={this.props.branchLike}
- componentMeasures={this.props.componentMeasures}
- openComponent={openComponent}
- showMeasures={this.props.showMeasures}
- sourceViewerFile={sourceViewerFile}
- />
- )}
- </WorkspaceContext.Consumer>
- );
- }
-
- render() {
- const { component, loading, sources, notAccessible, sourceRemoved } = this.state;
-
- if (loading) {
- return null;
- }
-
- if (this.state.notExist) {
- return (
- <Alert className="spacer-top" variant="warning">
- {translate('component_viewer.no_component')}
- </Alert>
- );
- }
-
- if (notAccessible) {
- return (
- <Alert className="spacer-top" variant="warning">
- {translate('code_viewer.no_source_code_displayed_due_to_security')}
- </Alert>
- );
- }
-
- if (!component) {
- return null;
- }
-
- return (
- <SourceViewerContext.Provider value={{ branchLike: this.props.branchLike, file: component }}>
- <div className="source-viewer" ref={node => (this.node = node)}>
- {this.renderHeader(this.props.branchLike, 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>
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
index c51ecb927c6..e8a35ab3ad9 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
@@ -21,12 +21,13 @@ import { queryHelpers, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { SourceViewerServiceMock } from '../../../api/mocks/SourceViewerServiceMock';
+import { HttpStatus } from '../../../helpers/request';
import { mockIssue } from '../../../helpers/testMocks';
import { renderComponent } from '../../../helpers/testReactTestingUtils';
import SourceViewer from '../SourceViewer';
-import SourceViewerBase from '../SourceViewerBase';
jest.mock('../../../api/components');
+jest.mock('../../../api/issues');
jest.mock('../helpers/lines', () => {
const lines = jest.requireActual('../helpers/lines');
return {
@@ -37,6 +38,10 @@ jest.mock('../helpers/lines', () => {
const handler = new SourceViewerServiceMock();
+beforeEach(() => {
+ handler.reset();
+});
+
it('should show a permalink on line number', async () => {
const user = userEvent.setup();
renderSourceViewer();
@@ -108,6 +113,53 @@ it('should show issue on empty file', async () => {
expect(await screen.findByRole('row', { name: 'First Issue' })).toBeInTheDocument();
});
+it('should be able to interact with issue action', async () => {
+ const user = userEvent.setup();
+ renderSourceViewer({
+ loadIssues: jest.fn().mockResolvedValue([
+ mockIssue(false, {
+ actions: ['set_type', 'set_tags', 'comment', 'set_severity', 'assign'],
+ key: 'first-issue',
+ message: 'First Issue',
+ line: 1,
+ textRange: { startLine: 1, endLine: 1, startOffset: 0, endOffset: 1 }
+ })
+ ])
+ });
+
+ //Open Issue type
+ await user.click(
+ await screen.findByRole('button', { name: 'issue.type.type_x_click_to_change.issue.type.BUG' })
+ );
+ expect(screen.getByRole('link', { name: 'issue.type.CODE_SMELL' })).toBeInTheDocument();
+
+ // Open severity
+ await user.click(
+ await screen.findByRole('button', {
+ name: 'issue.severity.severity_x_click_to_change.severity.MAJOR'
+ })
+ );
+ expect(screen.getByRole('link', { name: 'severity.MINOR' })).toBeInTheDocument();
+
+ // Close
+ await user.keyboard('{Escape}');
+ expect(screen.queryByRole('link', { name: 'severity.MINOR' })).not.toBeInTheDocument();
+
+ // Change the severity
+ await user.click(
+ await screen.findByRole('button', {
+ name: 'issue.severity.severity_x_click_to_change.severity.MAJOR'
+ })
+ );
+ expect(screen.getByRole('link', { name: 'severity.MINOR' })).toBeInTheDocument();
+ await user.click(screen.getByRole('link', { name: 'severity.MINOR' }));
+ expect(
+ screen.getByRole('button', {
+ name: 'issue.severity.severity_x_click_to_change.severity.MINOR'
+ })
+ ).toBeInTheDocument();
+});
+
it('should load line when looking arround unloaded line', async () => {
const { rerender } = renderSourceViewer({
aroundLine: 50,
@@ -299,11 +351,37 @@ it('should show duplication block', async () => {
expect(duplicateLine.queryByRole('link', { name: 'test2.js' })).not.toBeInTheDocument();
});
-function renderSourceViewer(override?: Partial<SourceViewerBase['props']>) {
+it('should highlight symbol', async () => {
+ const user = userEvent.setup();
+ renderSourceViewer({ component: 'project:testSymb.tsx' });
+ const symbols = await screen.findAllByText('symbole');
+ await user.click(symbols[0]);
+
+ // For now just check the class. Maybe found a better accessible way of showing higlighted symbole
+ symbols.forEach(element => {
+ expect(element).toHaveClass('highlighted');
+ });
+});
+
+it('should show correct message when component is not asscessible', async () => {
+ handler.setFailLoadingComponentStatus(HttpStatus.Forbidden);
+ renderSourceViewer();
+ expect(
+ await screen.findByText('code_viewer.no_source_code_displayed_due_to_security')
+ ).toBeInTheDocument();
+});
+
+it('should show correct message when component does not exist', async () => {
+ handler.setFailLoadingComponentStatus(HttpStatus.NotFound);
+ renderSourceViewer();
+ expect(await screen.findByText('component_viewer.no_component')).toBeInTheDocument();
+});
+
+function renderSourceViewer(override?: Partial<SourceViewer['props']>) {
return renderComponent(getSourceViewerUi(override));
}
-function getSourceViewerUi(override?: Partial<SourceViewerBase['props']>) {
+function getSourceViewerUi(override?: Partial<SourceViewer['props']>) {
return (
<SourceViewer
aroundLine={1}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewerBase-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-test.tsx
index 66cb69fdb7e..ac622f4052e 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewerBase-test.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-test.tsx
@@ -25,7 +25,7 @@ import { mockSourceLine, mockSourceViewerFile } from '../../../helpers/mocks/sou
import { mockIssue } from '../../../helpers/testMocks';
import { waitAndUpdate } from '../../../helpers/testUtils';
import defaultLoadIssues from '../helpers/loadIssues';
-import SourceViewerBase from '../SourceViewerBase';
+import SourceViewer from '../SourceViewer';
jest.mock('../helpers/loadIssues', () => jest.fn().mockRejectedValue({}));
@@ -148,8 +148,8 @@ it('should handle no sources when checking ranges', () => {
expect(wrapper.instance().isLineOutsideOfRange(12)).toBe(true);
});
-function shallowRender(overrides: Partial<SourceViewerBase['props']> = {}) {
- return shallow<SourceViewerBase>(
- <SourceViewerBase branchLike={mockMainBranch()} component="my-component" {...overrides} />
+function shallowRender(overrides: Partial<SourceViewer['props']> = {}) {
+ return shallow<SourceViewer>(
+ <SourceViewer branchLike={mockMainBranch()} component="my-component" {...overrides} />
);
}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerBase-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewer-test.tsx.snap
index ef8cdb4d1ba..ef8cdb4d1ba 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerBase-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewer-test.tsx.snap