]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12911 Code viewer is not accessible to blind users
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Tue, 28 Jan 2020 07:41:21 +0000 (08:41 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 8 May 2020 20:03:26 +0000 (20:03 +0000)
47 files changed:
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewerWrapper.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewerWrapper-test.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewerWrapper-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/SnippetViewer-test.tsx.snap
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainer.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSnippetContainer-test.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSnippetContainerRenderer-test.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainer-test.tsx.snap
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx
server/sonar-web/src/main/js/components/SourceViewer/__tests__/__snapshots__/SourceViewerBase-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/Line.css
server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.tsx [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/Line-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.tsx [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineOptionsPopup-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/SCMPopup-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/Line-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 016b7ddae896b5668dd2cf4972f9c3f6106a6f13..2cc7de55fe971c008287f1255635b08092866044 100644 (file)
@@ -17,7 +17,6 @@
  * 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 classNames from 'classnames';
 import * as React from 'react';
 import { getSources } from '../../../api/components';
 import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus';
@@ -43,12 +42,10 @@ interface Props {
   issuePopup?: { issue: string; name: string };
   issuesByLine: T.IssuesByLine;
   lastSnippetGroup: boolean;
-  linePopup?: T.LinePopup;
   loadDuplications: (component: string, line: T.SourceLine) => void;
   locations: T.FlowLocation[];
   onIssueChange: (issue: T.Issue) => void;
   onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void;
-  onLinePopupToggle: (linePopup: T.LinePopup & { component: string }) => void;
   onLocationSelect: (index: number) => void;
   renderDuplicationPopup: (
     component: T.SourceViewerFile,
@@ -273,13 +270,6 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone
     );
   };
 
-  handleLinePopupToggle = (linePopup: T.LinePopup) => {
-    this.props.onLinePopupToggle({
-      ...linePopup,
-      component: this.props.snippetGroup.component.key
-    });
-  };
-
   handleOpenIssues = (line: T.SourceLine) => {
     this.setState(state => ({
       openIssuesByLine: { ...state.openIssuesByLine, [line.line]: true }
@@ -326,9 +316,10 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone
       <SnippetViewer
         branchLike={this.props.branchLike}
         component={this.props.snippetGroup.component}
+        duplications={this.props.duplications}
+        duplicationsByLine={this.props.duplicationsByLine}
         expandBlock={this.expandBlock}
         handleCloseIssues={this.handleCloseIssues}
-        handleLinePopupToggle={this.handleLinePopupToggle}
         handleOpenIssues={this.handleOpenIssues}
         handleSymbolClick={this.handleSymbolClick}
         highlightedLocationMessage={this.props.highlightedLocationMessage}
@@ -338,7 +329,6 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone
         issuePopup={this.props.issuePopup}
         issuesByLine={issuesByLine}
         lastSnippetOfLastGroup={lastSnippetOfLastGroup}
-        linePopup={this.props.linePopup}
         loadDuplications={this.loadDuplications}
         locations={this.props.locations}
         locationsByLine={locationsByLine}
@@ -354,14 +344,7 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone
   }
 
   render() {
-    const {
-      branchLike,
-      duplications,
-      issue,
-      issuesByLine,
-      lastSnippetGroup,
-      snippetGroup
-    } = this.props;
+    const { branchLike, issue, issuesByLine, lastSnippetGroup, snippetGroup } = this.props;
     const { additionalLines, loading, snippets } = this.state;
     const locations =
       issue.component === snippetGroup.component.key ? locationsByLine([issue]) : {};
@@ -382,11 +365,7 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone
       isFlow ? lastSnippetGroup && snippetIndex === snippets.length - 1 : snippetIndex === 0;
 
     return (
-      <div
-        className={classNames('component-source-container', {
-          'source-duplications-expanded': duplications && duplications.length > 0
-        })}
-        ref={this.rootNodeRef}>
+      <div className="component-source-container" ref={this.rootNodeRef}>
         <SourceViewerHeaderSlim
           branchLike={branchLike}
           expandable={!fullyShown}
index aa0c2434930fe025ed0dfc4f9e61d16f507782c1..be54678e2ff3595e9e53d4dcf6a72cfbc0f4b325 100644 (file)
@@ -60,7 +60,6 @@ interface State {
   duplications?: T.Duplication[];
   duplicationsByLine: { [line: number]: number[] };
   issuePopup?: { issue: string; name: string };
-  linePopup?: T.LinePopup & { component: string };
   loading: boolean;
   notAccessible: boolean;
 }
@@ -89,22 +88,18 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone
     this.mounted = false;
   }
 
-  fetchDuplications = (component: string, line: T.SourceLine) => {
+  fetchDuplications = (component: string) => {
     getDuplications({
       key: component,
       ...getBranchLikeQuery(this.props.branchLike)
     }).then(
       r => {
         if (this.mounted) {
-          this.setState(state => ({
+          this.setState({
             duplicatedFiles: r.files,
             duplications: r.duplications,
-            duplicationsByLine: getDuplicationsByLine(r.duplications),
-            linePopup:
-              r.duplications.length === 1
-                ? { component, index: 0, line: line.line, name: 'duplications' }
-                : state.linePopup
-          }));
+            duplicationsByLine: getDuplicationsByLine(r.duplications)
+          });
         }
       },
       () => {}
@@ -119,7 +114,6 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone
           this.setState({
             components,
             issuePopup: undefined,
-            linePopup: undefined,
             loading: false
           });
           if (this.props.onLoaded) {
@@ -151,33 +145,6 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone
     });
   };
 
-  handleLinePopupToggle = ({
-    component,
-    index,
-    line,
-    name,
-    open
-  }: T.LinePopup & { component: string }) => {
-    this.setState((state: State) => {
-      const samePopup =
-        state.linePopup !== undefined &&
-        state.linePopup.line === line &&
-        state.linePopup.name === name &&
-        state.linePopup.component === component &&
-        state.linePopup.index === index;
-      if (open !== false && !samePopup) {
-        return { linePopup: { component, index, line, name } };
-      } else if (open !== true && samePopup) {
-        return { linePopup: undefined };
-      }
-      return null;
-    });
-  };
-
-  handleCloseLinePopup = () => {
-    this.setState({ linePopup: undefined });
-  };
-
   renderDuplicationPopup = (component: T.SourceViewerFile, index: number, line: number) => {
     const { duplicatedFiles, duplications } = this.state;
 
@@ -195,7 +162,6 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone
             branchLike={this.props.branchLike}
             duplicatedFiles={duplicatedFiles}
             inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)}
-            onClose={this.handleCloseLinePopup}
             openComponent={openComponent}
             sourceViewerFile={component}
           />
@@ -224,21 +190,13 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone
     }
 
     const { issue, locations } = this.props;
-    const { components, duplications, duplicationsByLine, linePopup } = this.state;
+    const { components, duplications, duplicationsByLine } = this.state;
     const issuesByComponent = issuesByComponentAndLine(this.props.issues);
     const locationsByComponent = groupLocationsByComponent(issue, locations, components);
 
     return (
       <div>
         {locationsByComponent.map((snippetGroup, i) => {
-          let componentProps = {};
-          if (linePopup && snippetGroup.component.key === linePopup.component) {
-            componentProps = {
-              duplications,
-              duplicationsByLine,
-              linePopup: { index: linePopup.index, line: linePopup.line, name: linePopup.name }
-            };
-          }
           return (
             <SourceViewerContext.Provider
               // eslint-disable-next-line react/no-array-index-key
@@ -246,6 +204,8 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone
               value={{ branchLike: this.props.branchLike, file: snippetGroup.component }}>
               <ComponentSourceSnippetGroupViewer
                 branchLike={this.props.branchLike}
+                duplications={duplications}
+                duplicationsByLine={duplicationsByLine}
                 highlightedLocationMessage={this.props.highlightedLocationMessage}
                 issue={issue}
                 issuePopup={this.state.issuePopup}
@@ -255,12 +215,10 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone
                 locations={snippetGroup.locations || []}
                 onIssueChange={this.props.onIssueChange}
                 onIssuePopupToggle={this.handleIssuePopupToggle}
-                onLinePopupToggle={this.handleLinePopupToggle}
                 onLocationSelect={this.props.onLocationSelect}
                 renderDuplicationPopup={this.renderDuplicationPopup}
                 scroll={this.props.scroll}
                 snippetGroup={snippetGroup}
-                {...componentProps}
               />
             </SourceViewerContext.Provider>
           );
index 995ce6bb18cefc4427fc18800b55c8b0f4bf6573..c3931a058434e85bf5adf78f8a672aa2282ce587 100644 (file)
@@ -42,7 +42,6 @@ interface Props {
   duplicationsByLine?: { [line: number]: number[] };
   expandBlock: (snippetIndex: number, direction: T.ExpandDirection) => Promise<void>;
   handleCloseIssues: (line: T.SourceLine) => void;
-  handleLinePopupToggle: (line: T.SourceLine) => void;
   handleOpenIssues: (line: T.SourceLine) => void;
   handleSymbolClick: (symbols: string[]) => void;
   highlightedLocationMessage: { index: number; text: string | undefined } | undefined;
@@ -52,8 +51,7 @@ interface Props {
   issuePopup?: { issue: string; name: string };
   issuesByLine: T.IssuesByLine;
   lastSnippetOfLastGroup: boolean;
-  linePopup?: T.LinePopup;
-  loadDuplications: (line: T.SourceLine) => void;
+  loadDuplications?: (line: T.SourceLine) => void;
   locations: T.FlowLocation[];
   locationsByLine: { [line: number]: T.LinearIssueLocation[] };
   onIssueChange: (issue: T.Issue) => void;
@@ -138,6 +136,7 @@ export default class SnippetViewer extends React.PureComponent<Props> {
       (duplicationsCount && duplicationsByLine && duplicationsByLine[line.line]) || [];
 
     const isSinkLine = issuesForLine.some(i => i.key === this.props.issue.key);
+    const noop = () => {};
 
     return (
       <Line
@@ -162,15 +161,13 @@ export default class SnippetViewer extends React.PureComponent<Props> {
         key={line.line}
         last={false}
         line={line}
-        linePopup={this.props.linePopup}
-        loadDuplications={this.props.loadDuplications}
+        loadDuplications={this.props.loadDuplications || noop}
         onIssueChange={this.props.onIssueChange}
         onIssuePopupToggle={this.props.onIssuePopupToggle}
-        onIssueSelect={() => {}}
-        onIssueUnselect={() => {}}
+        onIssueSelect={noop}
+        onIssueUnselect={noop}
         onIssuesClose={this.props.handleCloseIssues}
         onIssuesOpen={this.props.handleOpenIssues}
-        onLinePopupToggle={this.props.handleLinePopupToggle}
         onLocationSelect={this.props.onLocationSelect}
         onSymbolClick={this.props.handleSymbolClick}
         openIssues={this.props.openIssuesByLine[line.line]}
@@ -211,7 +208,8 @@ export default class SnippetViewer extends React.PureComponent<Props> {
       ? Math.max(0, LINES_BELOW_ISSUE - (bottomLine - lowestVisibleIssue))
       : 0;
 
-    const displayDuplications = snippet.some(s => !!s.duplicated);
+    const displayDuplications =
+      Boolean(this.props.loadDuplications) && snippet.some(s => !!s.duplicated);
 
     return (
       <div className="source-viewer-code snippet" ref={this.snippetNodeRef}>
index f68a22f601b0f185353ee881952471f950f4bb28..6132a637b9f55f1d65248023d383618ee6866ed3 100644 (file)
@@ -221,12 +221,10 @@ it('should correctly handle lines actions', () => {
     ...mockSnippetsByComponent('a', [32, 33, 34, 35, 36, 52, 53, 54, 55, 56])
   };
   const loadDuplications = jest.fn();
-  const onLinePopupToggle = jest.fn();
   const renderDuplicationPopup = jest.fn();
 
   const wrapper = shallowRender({
     loadDuplications,
-    onLinePopupToggle,
     renderDuplicationPopup,
     snippetGroup
   });
@@ -238,12 +236,6 @@ it('should correctly handle lines actions', () => {
     .prop<Function>('loadDuplications')(line);
   expect(loadDuplications).toHaveBeenCalledWith('a', line);
 
-  wrapper
-    .find('SnippetViewer')
-    .first()
-    .prop<Function>('handleLinePopupToggle')({ line: 13, name: 'foo' });
-  expect(onLinePopupToggle).toHaveBeenCalledWith({ component: 'a', line: 13, name: 'foo' });
-
   wrapper
     .find('SnippetViewer')
     .first()
@@ -264,18 +256,14 @@ describe('getNodes', () => {
   const wrapper = mount<ComponentSourceSnippetGroupViewer>(
     <ComponentSourceSnippetGroupViewer
       branchLike={mockMainBranch()}
-      duplications={undefined}
-      duplicationsByLine={undefined}
       highlightedLocationMessage={{ index: 0, text: '' }}
       issue={mockIssue()}
       issuesByLine={{}}
       lastSnippetGroup={false}
-      linePopup={undefined}
       loadDuplications={jest.fn()}
       locations={[]}
       onIssueChange={jest.fn()}
       onIssuePopupToggle={jest.fn()}
-      onLinePopupToggle={jest.fn()}
       onLocationSelect={jest.fn()}
       renderDuplicationPopup={jest.fn()}
       scroll={jest.fn()}
@@ -322,18 +310,14 @@ describe('getHeight', () => {
   const wrapper = mount<ComponentSourceSnippetGroupViewer>(
     <ComponentSourceSnippetGroupViewer
       branchLike={mockMainBranch()}
-      duplications={undefined}
-      duplicationsByLine={undefined}
       highlightedLocationMessage={{ index: 0, text: '' }}
       issue={mockIssue()}
       issuesByLine={{}}
       lastSnippetGroup={false}
-      linePopup={undefined}
       loadDuplications={jest.fn()}
       locations={[]}
       onIssueChange={jest.fn()}
       onIssuePopupToggle={jest.fn()}
-      onLinePopupToggle={jest.fn()}
       onLocationSelect={jest.fn()}
       renderDuplicationPopup={jest.fn()}
       scroll={jest.fn()}
@@ -376,18 +360,14 @@ function shallowRender(props: Partial<ComponentSourceSnippetGroupViewer['props']
   return shallow<ComponentSourceSnippetGroupViewer>(
     <ComponentSourceSnippetGroupViewer
       branchLike={mockMainBranch()}
-      duplications={undefined}
-      duplicationsByLine={undefined}
       highlightedLocationMessage={{ index: 0, text: '' }}
       issue={mockIssue()}
       issuesByLine={{}}
       lastSnippetGroup={false}
-      linePopup={undefined}
       loadDuplications={jest.fn()}
       locations={[]}
       onIssueChange={jest.fn()}
       onIssuePopupToggle={jest.fn()}
-      onLinePopupToggle={jest.fn()}
       onLocationSelect={jest.fn()}
       renderDuplicationPopup={jest.fn()}
       scroll={jest.fn()}
index 6f6c489f5756a9000d6aeaa7f680001d0210d4fb..ff8e9d84e1b772203114c117ca3fd81a244a30be 100644 (file)
@@ -87,27 +87,6 @@ it('should handle issue popup', () => {
   expect(wrapper.state('issuePopup')).toBeUndefined();
 });
 
-it('should handle line popup', async () => {
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-
-  const linePopup = { component: 'foo', index: 0, line: 16, name: 'b.tsx' };
-  wrapper.find('ComponentSourceSnippetGroupViewer').prop<Function>('onLinePopupToggle')(linePopup);
-  expect(wrapper.state('linePopup')).toEqual(linePopup);
-
-  wrapper.find('ComponentSourceSnippetGroupViewer').prop<Function>('onLinePopupToggle')(linePopup);
-  expect(wrapper.state('linePopup')).toBeUndefined();
-
-  const openLinePopup = { ...linePopup, open: true };
-  wrapper.find('ComponentSourceSnippetGroupViewer').prop<Function>('onLinePopupToggle')(
-    openLinePopup
-  );
-  wrapper.find('ComponentSourceSnippetGroupViewer').prop<Function>('onLinePopupToggle')(
-    openLinePopup
-  );
-  expect(wrapper.state('linePopup')).toEqual(linePopup);
-});
-
 it('should handle duplication popup', async () => {
   const files = { b: { key: 'b', name: 'B.tsx', project: 'foo', projectName: 'Foo' } };
   const duplications = [{ blocks: [{ _ref: '1', from: 1, size: 2 }] }];
@@ -126,12 +105,6 @@ it('should handle duplication popup', async () => {
   expect(wrapper.state('duplicatedFiles')).toEqual(files);
   expect(wrapper.state('duplications')).toEqual(duplications);
   expect(wrapper.state('duplicationsByLine')).toEqual({ '1': [0], '2': [0] });
-  expect(wrapper.state('linePopup')).toEqual({
-    component: 'foo',
-    index: 0,
-    line: 16,
-    name: 'duplications'
-  });
 
   expect(
     wrapper.find('ComponentSourceSnippetGroupViewer').prop<Function>('renderDuplicationPopup')(
index 24bc147e95c239954191a330a3eb5743becd02ce..7f9475c4709b396891129c84fa9c1fb0413e54b2 100644 (file)
@@ -127,7 +127,6 @@ function shallowRender(props: Partial<SnippetViewer['props']> = {}) {
       duplicationsByLine={undefined}
       expandBlock={jest.fn()}
       handleCloseIssues={jest.fn()}
-      handleLinePopupToggle={jest.fn()}
       handleOpenIssues={jest.fn()}
       handleSymbolClick={jest.fn()}
       highlightedLocationMessage={{ index: 0, text: '' }}
@@ -136,7 +135,6 @@ function shallowRender(props: Partial<SnippetViewer['props']> = {}) {
       issue={mockIssue()}
       issuesByLine={{}}
       lastSnippetOfLastGroup={false}
-      linePopup={undefined}
       loadDuplications={jest.fn()}
       locations={[]}
       locationsByLine={{}}
@@ -161,7 +159,6 @@ function mountRender(props: Partial<SnippetViewer['props']> = {}) {
       duplicationsByLine={undefined}
       expandBlock={jest.fn()}
       handleCloseIssues={jest.fn()}
-      handleLinePopupToggle={jest.fn()}
       handleOpenIssues={jest.fn()}
       handleSymbolClick={jest.fn()}
       highlightedLocationMessage={{ index: 0, text: '' }}
@@ -170,7 +167,6 @@ function mountRender(props: Partial<SnippetViewer['props']> = {}) {
       issue={mockIssue()}
       issuesByLine={{}}
       lastSnippetOfLastGroup={false}
-      linePopup={undefined}
       loadDuplications={jest.fn()}
       locations={[]}
       locationsByLine={{}}
index a4c84ff6a85043a630c9c100dee459a7719636ed..f4e8122cdf2d0747ed8e9618095057e3291258f3 100644 (file)
@@ -48,6 +48,7 @@ exports[`should render correctly 2`] = `
     }
   >
     <ComponentSourceSnippetGroupViewer
+      duplicationsByLine={Object {}}
       issue={
         Object {
           "actions": Array [],
@@ -169,7 +170,6 @@ exports[`should render correctly 2`] = `
       }
       onIssueChange={[MockFunction]}
       onIssuePopupToggle={[Function]}
-      onLinePopupToggle={[Function]}
       onLocationSelect={[MockFunction]}
       renderDuplicationPopup={[Function]}
       scroll={[MockFunction]}
index f77a840ba37f364bf789c4540b91b25fc82b7bba..1279baf33946065dd1fd83683d96a24f99d5e52b 100644 (file)
@@ -61,7 +61,6 @@ exports[`should render correctly 1`] = `
           onIssueUnselect={[Function]}
           onIssuesClose={[MockFunction]}
           onIssuesOpen={[MockFunction]}
-          onLinePopupToggle={[MockFunction]}
           onLocationSelect={[MockFunction]}
           onSymbolClick={[MockFunction]}
           renderDuplicationPopup={[MockFunction]}
@@ -110,7 +109,6 @@ exports[`should render correctly 1`] = `
           onIssueUnselect={[Function]}
           onIssuesClose={[MockFunction]}
           onIssuesOpen={[MockFunction]}
-          onLinePopupToggle={[MockFunction]}
           onLocationSelect={[MockFunction]}
           onSymbolClick={[MockFunction]}
           previousLine={
@@ -172,7 +170,6 @@ exports[`should render correctly 1`] = `
           onIssueUnselect={[Function]}
           onIssuesClose={[MockFunction]}
           onIssuesOpen={[MockFunction]}
-          onLinePopupToggle={[MockFunction]}
           onLocationSelect={[MockFunction]}
           onSymbolClick={[MockFunction]}
           previousLine={
@@ -271,7 +268,6 @@ exports[`should render correctly when at the bottom of the file 1`] = `
           onIssueUnselect={[Function]}
           onIssuesClose={[MockFunction]}
           onIssuesOpen={[MockFunction]}
-          onLinePopupToggle={[MockFunction]}
           onLocationSelect={[MockFunction]}
           onSymbolClick={[MockFunction]}
           renderDuplicationPopup={[MockFunction]}
@@ -320,7 +316,6 @@ exports[`should render correctly when at the bottom of the file 1`] = `
           onIssueUnselect={[Function]}
           onIssuesClose={[MockFunction]}
           onIssuesOpen={[MockFunction]}
-          onLinePopupToggle={[MockFunction]}
           onLocationSelect={[MockFunction]}
           onSymbolClick={[MockFunction]}
           previousLine={
@@ -382,7 +377,6 @@ exports[`should render correctly when at the bottom of the file 1`] = `
           onIssueUnselect={[Function]}
           onIssuesClose={[MockFunction]}
           onIssuesOpen={[MockFunction]}
-          onLinePopupToggle={[MockFunction]}
           onLocationSelect={[MockFunction]}
           onSymbolClick={[MockFunction]}
           previousLine={
@@ -444,7 +438,6 @@ exports[`should render correctly when at the bottom of the file 1`] = `
           onIssueUnselect={[Function]}
           onIssuesClose={[MockFunction]}
           onIssuesOpen={[MockFunction]}
-          onLinePopupToggle={[MockFunction]}
           onLocationSelect={[MockFunction]}
           onSymbolClick={[MockFunction]}
           previousLine={
@@ -532,7 +525,6 @@ exports[`should render correctly when at the top of the file 1`] = `
           onIssueUnselect={[Function]}
           onIssuesClose={[MockFunction]}
           onIssuesOpen={[MockFunction]}
-          onLinePopupToggle={[MockFunction]}
           onLocationSelect={[MockFunction]}
           onSymbolClick={[MockFunction]}
           renderDuplicationPopup={[MockFunction]}
@@ -581,7 +573,6 @@ exports[`should render correctly when at the top of the file 1`] = `
           onIssueUnselect={[Function]}
           onIssuesClose={[MockFunction]}
           onIssuesOpen={[MockFunction]}
-          onLinePopupToggle={[MockFunction]}
           onLocationSelect={[MockFunction]}
           onSymbolClick={[MockFunction]}
           previousLine={
@@ -643,7 +634,6 @@ exports[`should render correctly when at the top of the file 1`] = `
           onIssueUnselect={[Function]}
           onIssuesClose={[MockFunction]}
           onIssuesOpen={[MockFunction]}
-          onLinePopupToggle={[MockFunction]}
           onLocationSelect={[MockFunction]}
           onSymbolClick={[MockFunction]}
           previousLine={
@@ -705,7 +695,6 @@ exports[`should render correctly when at the top of the file 1`] = `
           onIssueUnselect={[Function]}
           onIssuesClose={[MockFunction]}
           onIssuesOpen={[MockFunction]}
-          onLinePopupToggle={[MockFunction]}
           onLocationSelect={[MockFunction]}
           onSymbolClick={[MockFunction]}
           previousLine={
@@ -767,7 +756,6 @@ exports[`should render correctly when at the top of the file 1`] = `
           onIssueUnselect={[Function]}
           onIssuesClose={[MockFunction]}
           onIssuesOpen={[MockFunction]}
-          onLinePopupToggle={[MockFunction]}
           onLocationSelect={[MockFunction]}
           onSymbolClick={[MockFunction]}
           previousLine={
@@ -829,7 +817,6 @@ exports[`should render correctly when at the top of the file 1`] = `
           onIssueUnselect={[Function]}
           onIssuesClose={[MockFunction]}
           onIssuesOpen={[MockFunction]}
-          onLinePopupToggle={[MockFunction]}
           onLocationSelect={[MockFunction]}
           onSymbolClick={[MockFunction]}
           previousLine={
@@ -891,7 +878,6 @@ exports[`should render correctly when at the top of the file 1`] = `
           onIssueUnselect={[Function]}
           onIssuesClose={[MockFunction]}
           onIssuesOpen={[MockFunction]}
-          onLinePopupToggle={[MockFunction]}
           onLocationSelect={[MockFunction]}
           onSymbolClick={[MockFunction]}
           previousLine={
@@ -991,7 +977,6 @@ exports[`should render correctly with no SCM 1`] = `
           onIssueUnselect={[Function]}
           onIssuesClose={[MockFunction]}
           onIssuesOpen={[MockFunction]}
-          onLinePopupToggle={[MockFunction]}
           onLocationSelect={[MockFunction]}
           onSymbolClick={[MockFunction]}
           renderDuplicationPopup={[MockFunction]}
@@ -1041,7 +1026,6 @@ exports[`should render correctly with no SCM 1`] = `
           onIssueUnselect={[Function]}
           onIssuesClose={[MockFunction]}
           onIssuesOpen={[MockFunction]}
-          onLinePopupToggle={[MockFunction]}
           onLocationSelect={[MockFunction]}
           onSymbolClick={[MockFunction]}
           previousLine={
@@ -1104,7 +1088,6 @@ exports[`should render correctly with no SCM 1`] = `
           onIssueUnselect={[Function]}
           onIssuesClose={[MockFunction]}
           onIssuesOpen={[MockFunction]}
-          onLinePopupToggle={[MockFunction]}
           onLocationSelect={[MockFunction]}
           onSymbolClick={[MockFunction]}
           previousLine={
index 5a46219bd71f44ff4c380d17fcd3e45469d85b57..15e7c14b9de9b3f67630ef777dbd85ac66c801fe 100644 (file)
@@ -37,7 +37,6 @@ interface State {
   highlightedSymbols: string[];
   lastLine?: number;
   loading: boolean;
-  linePopup?: T.LinePopup & { component: string };
   sourceLines: T.SourceLine[];
 }
 
@@ -143,31 +142,13 @@ export default class HotspotSnippetContainer extends React.Component<Props, Stat
     });
   };
 
-  handleLinePopupToggle = (params: T.LinePopup & { component: string }) => {
-    const { component, index, line, name, open } = params;
-    this.setState((state: State) => {
-      const samePopup =
-        state.linePopup !== undefined &&
-        state.linePopup.line === line &&
-        state.linePopup.name === name &&
-        state.linePopup.component === component &&
-        state.linePopup.index === index;
-      if (open !== false && !samePopup) {
-        return { linePopup: params };
-      } else if (open !== true && samePopup) {
-        return { linePopup: undefined };
-      }
-      return null;
-    });
-  };
-
   handleSymbolClick = (highlightedSymbols: string[]) => {
     this.setState({ highlightedSymbols });
   };
 
   render() {
     const { branchLike, component, hotspot } = this.props;
-    const { highlightedSymbols, lastLine, linePopup, loading, sourceLines } = this.state;
+    const { highlightedSymbols, lastLine, loading, sourceLines } = this.state;
 
     const locations = locationsByLine([hotspot]);
 
@@ -180,11 +161,9 @@ export default class HotspotSnippetContainer extends React.Component<Props, Stat
         highlightedSymbols={highlightedSymbols}
         hotspot={hotspot}
         lastLine={lastLine}
-        linePopup={linePopup}
         loading={loading}
         locations={locations}
         onExpandBlock={this.handleExpansion}
-        onLinePopupToggle={this.handleLinePopupToggle}
         onSymbolClick={this.handleSymbolClick}
         sourceLines={sourceLines}
         sourceViewerFile={sourceViewerFile}
index 00e253830880d6c98cbbd065764fa401a138321d..414efd56fbef32bc14aa548c9695dfc7dad57afb 100644 (file)
@@ -33,9 +33,7 @@ export interface HotspotSnippetContainerRendererProps {
   lastLine?: number;
   loading: boolean;
   locations: { [line: number]: T.LinearIssueLocation[] };
-  linePopup?: T.LinePopup & { component: string };
   onExpandBlock: (direction: T.ExpandDirection) => Promise<void>;
-  onLinePopupToggle: (line: T.SourceLine) => void;
   onSymbolClick: (symbols: string[]) => void;
   sourceLines: T.SourceLine[];
   sourceViewerFile: T.SourceViewerFile;
@@ -51,7 +49,6 @@ export default function HotspotSnippetContainerRenderer(
     displayProjectName,
     highlightedSymbols,
     hotspot,
-    linePopup,
     loading,
     locations,
     sourceLines,
@@ -79,7 +76,6 @@ export default function HotspotSnippetContainerRenderer(
               displaySCM={false}
               expandBlock={(_i, direction) => props.onExpandBlock(direction)}
               handleCloseIssues={noop}
-              handleLinePopupToggle={props.onLinePopupToggle}
               handleOpenIssues={noop}
               handleSymbolClick={props.onSymbolClick}
               highlightedLocationMessage={undefined}
@@ -88,8 +84,6 @@ export default function HotspotSnippetContainerRenderer(
               issue={hotspot}
               issuesByLine={{}}
               lastSnippetOfLastGroup={false}
-              linePopup={linePopup}
-              loadDuplications={noop}
               locations={[]}
               locationsByLine={locations}
               onIssueChange={noop}
index 112ab0766defcbe880c8c9529bb693c1fb789ebd..69445d2298b4d50d546783f8d8f27ced3db39bb0 100644 (file)
@@ -155,32 +155,6 @@ describe('Expansion', () => {
   });
 });
 
-it('should handle line popups', async () => {
-  (getSources as jest.Mock).mockResolvedValueOnce(
-    range(5, 18).map(line => mockSourceLine({ line }))
-  );
-
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-
-  const params = wrapper.state().sourceLines[0];
-
-  wrapper
-    .find(HotspotSnippetContainerRenderer)
-    .props()
-    .onLinePopupToggle(params);
-
-  expect(wrapper.state().linePopup).toEqual(params);
-
-  // Close it
-  wrapper
-    .find(HotspotSnippetContainerRenderer)
-    .props()
-    .onLinePopupToggle(params);
-
-  expect(wrapper.state().linePopup).toBeUndefined();
-});
-
 it('should handle symbol click', () => {
   const wrapper = shallowRender();
   const symbols = ['symbol'];
index c016e8b5f9fd77deabbf10902c5754fc5bc4fdea..3ae268d10c30a2c19d3adc0a4f04e19bf3eb1976 100644 (file)
@@ -39,11 +39,9 @@ function shallowRender(props?: Partial<HotspotSnippetContainerRendererProps>) {
       highlightedSymbols={[]}
       hotspot={mockHotspot()}
       lastLine={undefined}
-      linePopup={undefined}
       loading={false}
       locations={{}}
       onExpandBlock={jest.fn()}
-      onLinePopupToggle={jest.fn()}
       onSymbolClick={jest.fn()}
       sourceLines={[]}
       sourceViewerFile={mockSourceViewerFile()}
index 75e4dce0d967bc3bbddef288e7888550c6fed3d9..c0e3e1f862a4b7452b8eb364b11fbe9f1b8cadb5 100644 (file)
@@ -124,7 +124,6 @@ exports[`should render correctly 1`] = `
     }
   }
   onExpandBlock={[Function]}
-  onLinePopupToggle={[Function]}
   onSymbolClick={[Function]}
   sourceLines={Array []}
   sourceViewerFile={
index 772a3ad35f35606447a3cc7d129a31d0164811bd..f1808647b9bf53fa8ee423a4ba3b6d4f69d16a3b 100644 (file)
@@ -135,7 +135,6 @@ exports[`should render correctly: with sourcelines 1`] = `
         displaySCM={false}
         expandBlock={[Function]}
         handleCloseIssues={[Function]}
-        handleLinePopupToggle={[MockFunction]}
         handleOpenIssues={[Function]}
         handleSymbolClick={[MockFunction]}
         highlightedSymbols={Array []}
@@ -241,7 +240,6 @@ exports[`should render correctly: with sourcelines 1`] = `
         }
         issuesByLine={Object {}}
         lastSnippetOfLastGroup={false}
-        loadDuplications={[Function]}
         locations={Array []}
         locationsByLine={Object {}}
         onIssueChange={[Function]}
index e4588216300c062aa194415ef11f5e34786d0bcf..abce4a8a4e51c2c8651044e34f7fa11954acd6db 100644 (file)
@@ -17,7 +17,6 @@
  * 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 classNames from 'classnames';
 import { intersection, uniqBy } from 'lodash';
 import * as React from 'react';
 import { Alert } from 'sonar-ui-common/components/ui/Alert';
@@ -96,7 +95,6 @@ export interface Props {
 
 interface State {
   component?: T.SourceViewerFile;
-  displayDuplications: boolean;
   duplicatedFiles?: T.Dict<T.DuplicatedFile>;
   duplications?: T.Duplication[];
   duplicationsByLine: { [line: number]: number[] };
@@ -106,7 +104,6 @@ interface State {
   issuePopup?: { issue: string; name: string };
   issues?: T.Issue[];
   issuesByLine: { [line: number]: T.Issue[] };
-  linePopup?: T.LinePopup;
   loading: boolean;
   loadingSourcesAfter: boolean;
   loadingSourcesBefore: boolean;
@@ -136,7 +133,6 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
     super(props);
 
     this.state = {
-      displayDuplications: false,
       duplicationsByLine: {},
       hasSourcesAfter: false,
       highlightedSymbols: [],
@@ -245,7 +241,6 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
             this.setState(
               {
                 component,
-                displayDuplications: false,
                 duplicatedFiles: undefined,
                 duplications: undefined,
                 duplicationsByLine: {},
@@ -254,7 +249,6 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
                 issueLocationsByLine: locationsByLine(issues),
                 issues,
                 issuesByLine: issuesByLine(issues),
-                linePopup: undefined,
                 loading: false,
                 notAccessible: false,
                 notExist: false,
@@ -474,23 +468,18 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
     );
   };
 
-  loadDuplications = (line: T.SourceLine) => {
+  loadDuplications = () => {
     getDuplications({
       key: this.props.component,
       ...getBranchLikeQuery(this.props.branchLike)
     }).then(
       r => {
         if (this.mounted) {
-          this.setState(state => ({
-            displayDuplications: true,
+          this.setState({
             duplications: r.duplications,
             duplicationsByLine: duplicationsByLine(r.duplications),
-            duplicatedFiles: r.files,
-            linePopup:
-              r.duplications.length === 1
-                ? { index: 0, line: line.line, name: 'duplications' }
-                : state.linePopup
-          }));
+            duplicatedFiles: r.files
+          });
         }
       },
       () => {
@@ -499,26 +488,6 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
     );
   };
 
-  handleLinePopupToggle = ({ index, line, name, open }: T.LinePopup) => {
-    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;
-    });
-  };
-
-  closeLinePopup = () => {
-    this.setState({ linePopup: undefined });
-  };
-
   handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => {
     this.setState((state: State) => {
       const samePopup =
@@ -595,7 +564,6 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
             branchLike={this.props.branchLike}
             duplicatedFiles={duplicatedFiles}
             inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)}
-            onClose={this.closeLinePopup}
             openComponent={openComponent}
             sourceViewerFile={component}
           />
@@ -626,7 +594,6 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
         issuePopup={this.state.issuePopup}
         issues={this.state.issues}
         issuesByLine={this.state.issuesByLine}
-        linePopup={this.state.linePopup}
         loadDuplications={this.loadDuplications}
         loadSourcesAfter={this.loadSourcesAfter}
         loadSourcesBefore={this.loadSourcesBefore}
@@ -638,7 +605,6 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
         onIssueUnselect={this.handleIssueUnselect}
         onIssuesClose={this.handleCloseIssues}
         onIssuesOpen={this.handleOpenIssues}
-        onLinePopupToggle={this.handleLinePopupToggle}
         onLocationSelect={this.props.onLocationSelect}
         onSymbolClick={this.handleSymbolClick}
         openIssuesByLine={this.state.openIssuesByLine}
@@ -696,13 +662,9 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
       return null;
     }
 
-    const className = classNames('source-viewer', {
-      'source-duplications-expanded': this.state.displayDuplications
-    });
-
     return (
       <SourceViewerContext.Provider value={{ branchLike: this.props.branchLike, file: component }}>
-        <div className={className} ref={node => (this.node = node)}>
+        <div className="source-viewer" ref={node => (this.node = node)}>
           {this.renderHeader(this.props.branchLike, component)}
           {sourceRemoved && (
             <Alert className="spacer-top" variant="warning">
index 03f68cd82856de1ca00d7e01eb9cce8ede3c8567..85cea2f0defd3aa282bf4fc1c10fb9a347032df1 100644 (file)
@@ -59,7 +59,6 @@ interface Props {
   issuePopup: { issue: string; name: string } | undefined;
   issues: T.Issue[] | undefined;
   issuesByLine: { [line: number]: T.Issue[] };
-  linePopup: T.LinePopup | undefined;
   loadDuplications: (line: T.SourceLine) => void;
   loadingSourcesAfter: boolean;
   loadingSourcesBefore: boolean;
@@ -71,7 +70,6 @@ interface Props {
   onIssueSelect: (issueKey: string) => void;
   onIssuesOpen: (line: T.SourceLine) => void;
   onIssueUnselect: () => void;
-  onLinePopupToggle: (linePopup: T.LinePopup) => void;
   onLocationSelect: ((index: number) => void) | undefined;
   onSymbolClick: (symbols: string[]) => void;
   openIssuesByLine: { [line: number]: boolean };
@@ -143,7 +141,6 @@ export default class SourceViewerCode extends React.PureComponent<Props> {
         key={line.line}
         last={index === this.props.sources.length - 1 && !this.props.hasSourcesAfter}
         line={line}
-        linePopup={this.props.linePopup}
         loadDuplications={this.props.loadDuplications}
         onIssueChange={this.props.onIssueChange}
         onIssuePopupToggle={this.props.onIssuePopupToggle}
@@ -151,7 +148,6 @@ export default class SourceViewerCode extends React.PureComponent<Props> {
         onIssueUnselect={this.props.onIssueUnselect}
         onIssuesClose={this.props.onIssuesClose}
         onIssuesOpen={this.props.onIssuesOpen}
-        onLinePopupToggle={this.props.onLinePopupToggle}
         onLocationSelect={this.props.onLocationSelect}
         onSymbolClick={this.props.onSymbolClick}
         openIssues={this.props.openIssuesByLine[line.line] || false}
index 1c54b6275983ef20761aad43f45434823a41d996..6aa4a3556eb04ab1a8eb353d8347cec7b82209a0 100644 (file)
@@ -153,7 +153,6 @@ exports[`should render correctly 1`] = `
       onIssueUnselect={[Function]}
       onIssuesClose={[Function]}
       onIssuesOpen={[Function]}
-      onLinePopupToggle={[Function]}
       onSymbolClick={[Function]}
       openIssuesByLine={Object {}}
       renderDuplicationPopup={[Function]}
index 6ccabc9ca16107cd4c81e294632d80b51d915cf7..caa47b2b49baf0b6ab3c2fda2f9c0eef7dcbd276 100644 (file)
 import { groupBy, sortBy } from 'lodash';
 import * as React from 'react';
 import { Link } from 'react-router';
-import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
 import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon';
 import { Alert } from 'sonar-ui-common/components/ui/Alert';
-import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
 import { translate } from 'sonar-ui-common/helpers/l10n';
 import { collapsedDirFromPath, fileFromPath } from 'sonar-ui-common/helpers/path';
 import { isPullRequest } from '../../../helpers/branch-like';
@@ -36,7 +34,6 @@ interface Props {
   branchLike: BranchLike | undefined;
   duplicatedFiles?: T.Dict<T.DuplicatedFile>;
   inRemovedComponent: boolean;
-  onClose: () => void;
   openComponent: WorkspaceContextShape['openComponent'];
   sourceViewerFile: T.SourceViewerFile;
 }
@@ -65,7 +62,6 @@ export default class DuplicationPopup extends React.PureComponent<Props> {
         line: line ? Number(line) : undefined
       });
     }
-    this.props.onClose();
   };
 
   renderDuplication(file: T.DuplicatedFile, children: React.ReactNode, line?: number) {
@@ -106,76 +102,74 @@ export default class DuplicationPopup extends React.PureComponent<Props> {
     );
 
     return (
-      <DropdownOverlay placement={PopupPlacement.RightTop}>
-        <div className="source-viewer-bubble-popup abs-width-400">
-          {this.props.inRemovedComponent && (
-            <Alert variant="warning">
-              {translate('duplications.dups_found_on_deleted_resource')}
-            </Alert>
-          )}
-          {duplications.length > 0 && (
-            <>
-              <h6 className="spacer-bottom">
-                {translate('component_viewer.transition.duplication')}
-              </h6>
-              {duplications.map(duplication => (
-                <div className="spacer-top text-ellipsis" key={duplication.file.key}>
-                  <div className="component-name">
-                    {this.isDifferentComponent(duplication.file, this.props.sourceViewerFile) && (
-                      <>
+      <div className="source-viewer-bubble-popup abs-width-400">
+        {this.props.inRemovedComponent && (
+          <Alert variant="warning">
+            {translate('duplications.dups_found_on_deleted_resource')}
+          </Alert>
+        )}
+        {duplications.length > 0 && (
+          <>
+            <h6 className="spacer-bottom">
+              {translate('component_viewer.transition.duplication')}
+            </h6>
+            {duplications.map(duplication => (
+              <div className="spacer-top text-ellipsis" key={duplication.file.key}>
+                <div className="component-name">
+                  {this.isDifferentComponent(duplication.file, this.props.sourceViewerFile) && (
+                    <>
+                      <div className="component-name-parent">
+                        <QualifierIcon className="little-spacer-right" qualifier="TRK" />
+                        <Link to={getProjectUrl(duplication.file.project)}>
+                          {duplication.file.projectName}
+                        </Link>
+                      </div>
+                      {duplication.file.subProject && duplication.file.subProjectName && (
                         <div className="component-name-parent">
-                          <QualifierIcon className="little-spacer-right" qualifier="TRK" />
-                          <Link to={getProjectUrl(duplication.file.project)}>
-                            {duplication.file.projectName}
-                          </Link>
+                          <QualifierIcon className="little-spacer-right" qualifier="BRC" />
+                          {duplication.file.subProjectName}
                         </div>
-                        {duplication.file.subProject && duplication.file.subProjectName && (
-                          <div className="component-name-parent">
-                            <QualifierIcon className="little-spacer-right" qualifier="BRC" />
-                            {duplication.file.subProjectName}
-                          </div>
-                        )}
-                      </>
-                    )}
+                      )}
+                    </>
+                  )}
+
+                  {duplication.file.key !== this.props.sourceViewerFile.key && (
+                    <div className="component-name-path">
+                      {this.renderDuplication(
+                        duplication.file,
+                        <>
+                          <span>{collapsedDirFromPath(duplication.file.name)}</span>
+                          <span className="component-name-file">
+                            {fileFromPath(duplication.file.name)}
+                          </span>
+                        </>
+                      )}
+                    </div>
+                  )}
 
-                    {duplication.file.key !== this.props.sourceViewerFile.key && (
-                      <div className="component-name-path">
+                  <div className="component-name-path">
+                    {'Lines: '}
+                    {duplication.blocks.map((block, index) => (
+                      <React.Fragment key={index}>
                         {this.renderDuplication(
                           duplication.file,
                           <>
-                            <span>{collapsedDirFromPath(duplication.file.name)}</span>
-                            <span className="component-name-file">
-                              {fileFromPath(duplication.file.name)}
-                            </span>
-                          </>
+                            {block.from}
+                            {' – '}
+                            {block.from + block.size - 1}
+                          </>,
+                          block.from
                         )}
-                      </div>
-                    )}
-
-                    <div className="component-name-path">
-                      {'Lines: '}
-                      {duplication.blocks.map((block, index) => (
-                        <React.Fragment key={index}>
-                          {this.renderDuplication(
-                            duplication.file,
-                            <>
-                              {block.from}
-                              {' – '}
-                              {block.from + block.size - 1}
-                            </>,
-                            block.from
-                          )}
-                          {index < duplication.blocks.length - 1 && ', '}
-                        </React.Fragment>
-                      ))}
-                    </div>
+                        {index < duplication.blocks.length - 1 && ', '}
+                      </React.Fragment>
+                    ))}
                   </div>
                 </div>
-              ))}
-            </>
-          )}
-        </div>
-      </DropdownOverlay>
+              </div>
+            ))}
+          </>
+        )}
+      </div>
     );
   }
 }
index 0e9531b4f24319aa9f67c62cede9fb732e3a3d61..50c2c866258dd966917b71deab84218bf1d67101 100644 (file)
   background-color: #f5f5f5;
 }
 
+.source-line [role='button'] {
+  cursor: pointer;
+}
+
 .source-line-highlighted .source-line-number,
 .source-line-highlighted:hover .source-line-number,
 .source-line-highlighted .source-line-issues,
   outline: none;
 }
 
-.source-meta[role='button'] {
-  cursor: pointer;
-}
-
 .source-line-number {
   min-width: 18px;
   padding: 0 10px;
   text-align: right;
 }
 
-.source-line-number:before {
-  content: attr(data-line-number);
-}
-
 .source-line-issues {
   position: relative;
   padding: 0 2px;
   display: none;
 }
 
-.source-duplications-expanded .source-line-duplications {
-  display: none;
+.source-line-scm {
+  padding: 0 5px;
+  background-color: var(--barBackgroundColor);
 }
 
-.source-duplications-expanded .source-line-duplications-extra {
-  display: table-cell;
+.source-line-scm .dropdown {
+  display: block;
 }
 
-.source-line-scm {
-  padding: 0 5px;
-  background-color: var(--barBackgroundColor);
+.source-line-scm [role='button'] {
+  height: 18px;
 }
 
 .source-line-scm-inner {
   white-space: nowrap;
 }
 
-.source-line-scm-inner:before {
-  content: attr(data-author);
-}
-
 .source-line-bar {
   width: 5px;
   height: 18px;
 }
 
-.source-line-bar[role='button'] {
-  cursor: pointer;
-}
-
 .source-line-bar:focus {
   outline: none;
 }
index 203321d80a3f624f6dc4e23067230d844ad99560..b2f63a2e9de390e24ad68e03702e7f8e5fac5ffb 100644 (file)
@@ -25,7 +25,6 @@ import './Line.css';
 import LineCode from './LineCode';
 import LineCoverage from './LineCoverage';
 import LineDuplicationBlock from './LineDuplicationBlock';
-import LineDuplications from './LineDuplications';
 import LineIssuesIndicator from './LineIssuesIndicator';
 import LineNumber from './LineNumber';
 import LineSCM from './LineSCM';
@@ -50,9 +49,7 @@ interface Props {
   issues: T.Issue[];
   last: boolean;
   line: T.SourceLine;
-  linePopup: T.LinePopup | undefined;
   loadDuplications: (line: T.SourceLine) => void;
-  onLinePopupToggle: (linePopup: T.LinePopup) => void;
   onIssueChange: (issue: T.Issue) => void;
   onIssuePopupToggle: (issueKey: string, popupName: string, open?: boolean) => void;
   onIssuesClose: (line: T.SourceLine) => void;
@@ -73,16 +70,6 @@ interface Props {
 const LINE_HEIGHT = 18;
 
 export default class Line extends React.PureComponent<Props> {
-  isPopupOpen = (name: string, index?: number) => {
-    const { line, linePopup } = this.props;
-    return (
-      linePopup !== undefined &&
-      linePopup.index === index &&
-      linePopup.line === line.line &&
-      linePopup.name === name
-    );
-  };
-
   handleIssuesIndicatorClick = () => {
     if (this.props.openIssues) {
       this.props.onIssuesClose(this.props.line);
@@ -99,45 +86,52 @@ export default class Line extends React.PureComponent<Props> {
 
   render() {
     const {
+      branchLike,
+      displayAllIssues,
       displayCoverage,
+      displayDuplications,
+      displayIssueLocationsCount,
+      displayIssueLocationsLink,
+      displayLocationMarkers,
+      highlightedLocationMessage,
+      displayIssues,
       displaySCM = true,
       duplications,
       duplicationsCount,
+      highlighted,
+      highlightedSymbols,
+      issueLocations,
       issuePopup,
-      line
+      issues,
+      last,
+      line,
+      openIssues,
+      previousLine,
+      secondaryIssueLocations,
+      selectedIssue,
+      verticalBuffer
     } = this.props;
+
     const className = classNames('source-line', {
-      'source-line-highlighted': this.props.highlighted,
+      'source-line-highlighted': highlighted,
       'source-line-filtered': line.isNew,
       'source-line-filtered-dark':
         displayCoverage &&
         (line.coverageStatus === 'uncovered' || line.coverageStatus === 'partially-covered'),
-      'source-line-last': this.props.last === true
+      'source-line-last': last === true
     });
 
-    const bottomPadding = this.props.verticalBuffer
-      ? this.props.verticalBuffer * LINE_HEIGHT
-      : undefined;
+    const bottomPadding = verticalBuffer ? verticalBuffer * LINE_HEIGHT : undefined;
 
     return (
       <tr className={className} data-line-number={line.line}>
-        <LineNumber
-          line={line}
-          onPopupToggle={this.props.onLinePopupToggle}
-          popupOpen={this.isPopupOpen('line-number')}
-        />
+        <LineNumber line={line} />
 
-        {displaySCM && (
-          <LineSCM
-            line={line}
-            onPopupToggle={this.props.onLinePopupToggle}
-            popupOpen={this.isPopupOpen('scm')}
-            previousLine={this.props.previousLine}
-          />
-        )}
-        {this.props.displayIssues && !this.props.displayAllIssues ? (
+        {displaySCM && <LineSCM line={line} previousLine={previousLine} />}
+        {displayIssues && !displayAllIssues ? (
           <LineIssuesIndicator
-            issues={this.props.issues}
+            issues={issues}
+            issuesOpen={openIssues}
             line={line}
             onClick={this.handleIssuesIndicatorClick}
           />
@@ -145,34 +139,42 @@ export default class Line extends React.PureComponent<Props> {
           <td className="source-meta source-line-issues" />
         )}
 
-        {this.props.displayDuplications && (
-          <LineDuplications line={line} onClick={this.props.loadDuplications} />
-        )}
-
-        {times(duplicationsCount, index => (
+        {displayDuplications && (
           <LineDuplicationBlock
-            duplicated={duplications.includes(index)}
-            index={index}
-            key={index}
+            blocksLoaded={duplicationsCount > 0}
+            duplicated={Boolean(line.duplicated)}
+            index={0}
+            key={0}
             line={this.props.line}
-            onPopupToggle={this.props.onLinePopupToggle}
-            popupOpen={this.isPopupOpen('duplications', index)}
+            onClick={this.props.loadDuplications}
             renderDuplicationPopup={this.props.renderDuplicationPopup}
           />
-        ))}
+        )}
+
+        {duplicationsCount > 1 &&
+          times(duplicationsCount - 1, index => (
+            <LineDuplicationBlock
+              blocksLoaded={true}
+              duplicated={duplications.includes(index + 1)}
+              index={index + 1}
+              key={index + 1}
+              line={this.props.line}
+              renderDuplicationPopup={this.props.renderDuplicationPopup}
+            />
+          ))}
 
-        {this.props.displayCoverage && <LineCoverage line={line} />}
+        {displayCoverage && <LineCoverage line={line} />}
 
         <LineCode
-          branchLike={this.props.branchLike}
-          displayIssueLocationsCount={this.props.displayIssueLocationsCount}
-          displayIssueLocationsLink={this.props.displayIssueLocationsLink}
-          displayLocationMarkers={this.props.displayLocationMarkers}
-          highlightedLocationMessage={this.props.highlightedLocationMessage}
-          highlightedSymbols={this.props.highlightedSymbols}
-          issueLocations={this.props.issueLocations}
+          branchLike={branchLike}
+          displayIssueLocationsCount={displayIssueLocationsCount}
+          displayIssueLocationsLink={displayIssueLocationsLink}
+          displayLocationMarkers={displayLocationMarkers}
+          highlightedLocationMessage={highlightedLocationMessage}
+          highlightedSymbols={highlightedSymbols}
+          issueLocations={issueLocations}
           issuePopup={issuePopup}
-          issues={this.props.issues}
+          issues={issues}
           line={line}
           onIssueChange={this.props.onIssueChange}
           onIssuePopupToggle={this.props.onIssuePopupToggle}
@@ -181,9 +183,9 @@ export default class Line extends React.PureComponent<Props> {
           onSymbolClick={this.props.onSymbolClick}
           padding={bottomPadding}
           scroll={this.props.scroll}
-          secondaryIssueLocations={this.props.secondaryIssueLocations}
-          selectedIssue={this.props.selectedIssue}
-          showIssues={this.props.openIssues || this.props.displayAllIssues}
+          secondaryIssueLocations={secondaryIssueLocations}
+          selectedIssue={selectedIssue}
+          showIssues={openIssues || displayAllIssues}
         />
       </tr>
     );
index bb50e7edd279d26e74cb722ac535cc94d31e776d..0f398fc05efe870c8770d9ccac9f948ff5eba2bf 100644 (file)
@@ -21,18 +21,20 @@ import * as React from 'react';
 import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
 import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
 
-interface Props {
+export interface LineCoverageProps {
   line: T.SourceLine;
 }
 
-export default function LineCoverage({ line }: Props) {
+export function LineCoverage({ line }: LineCoverageProps) {
   const className =
     'source-meta source-line-coverage' +
     (line.coverageStatus != null ? ` source-line-${line.coverageStatus}` : '');
+  const status = getStatusTooltip(line);
+
   return (
     <td className={className} data-line-number={line.line}>
-      <Tooltip overlay={getStatusTooltip(line)} placement="right">
-        <div className="source-line-bar" />
+      <Tooltip overlay={status} placement="right">
+        <div aria-label={status} className="source-line-bar" />
       </Tooltip>
     </td>
   );
@@ -64,3 +66,5 @@ function getStatusTooltip(line: T.SourceLine) {
   }
   return undefined;
 }
+
+export default React.memo(LineCoverage);
index b62c69f854651dbbc93c1d4c60afafee0427a4ed..fa56de50b629043bd7d47bc3e0f32d8e1e82bdd6 100644 (file)
  */
 import * as classNames from 'classnames';
 import * as React from 'react';
+import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
 import Toggler from 'sonar-ui-common/components/controls/Toggler';
 import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
+import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
 import { translate } from 'sonar-ui-common/helpers/l10n';
 
-interface Props {
+export interface LineDuplicationBlockProps {
+  blocksLoaded: boolean;
   duplicated: boolean;
   index: number;
   line: T.SourceLine;
-  onPopupToggle: (linePopup: T.LinePopup) => void;
-  popupOpen: boolean;
+  onClick?: (line: T.SourceLine) => void;
   renderDuplicationPopup: (index: number, line: number) => React.ReactNode;
 }
 
-export default class LineDuplicationBlock extends React.PureComponent<Props> {
-  handleClick = (event: React.MouseEvent<HTMLElement>) => {
-    event.preventDefault();
-    event.stopPropagation();
-    event.currentTarget.blur();
-    this.props.onPopupToggle({
-      index: this.props.index,
-      line: this.props.line.line,
-      name: 'duplications'
-    });
-  };
+export function LineDuplicationBlock(props: LineDuplicationBlockProps) {
+  const { blocksLoaded, duplicated, index, line } = props;
+  const [dropdownOpen, setDropdownOpen] = React.useState(false);
 
-  handleTogglePopup = (open: boolean) => {
-    this.props.onPopupToggle({
-      index: this.props.index,
-      line: this.props.line.line,
-      name: 'duplications',
-      open
-    });
-  };
+  const className = classNames('source-meta', 'source-line-duplications', {
+    'source-line-duplicated': duplicated
+  });
 
-  closePopup = () => {
-    this.handleTogglePopup(false);
-  };
-
-  render() {
-    const { duplicated, index, line, popupOpen } = this.props;
-    const className = classNames('source-meta', 'source-line-duplications-extra', {
-      'source-line-duplicated': duplicated
-    });
-
-    return duplicated ? (
-      <td className={className} data-index={index} data-line-number={line.line}>
-        <Toggler
-          onRequestClose={this.closePopup}
-          open={popupOpen}
-          overlay={this.props.renderDuplicationPopup(index, line.line)}>
-          <Tooltip
-            overlay={popupOpen ? undefined : translate('source_viewer.tooltip.duplicated_block')}
-            placement="right">
+  return duplicated ? (
+    <td className={className} data-index={index} data-line-number={line.line}>
+      <Tooltip
+        overlay={dropdownOpen ? undefined : translate('source_viewer.tooltip.duplicated_block')}
+        placement="right">
+        <div>
+          <Toggler
+            onRequestClose={() => setDropdownOpen(false)}
+            open={dropdownOpen}
+            overlay={
+              <DropdownOverlay placement={PopupPlacement.RightTop}>
+                {props.renderDuplicationPopup(index, line.line)}
+              </DropdownOverlay>
+            }>
             <div
+              aria-label={translate('source_viewer.tooltip.duplicated_block')}
               className="source-line-bar"
-              onClick={this.handleClick}
+              onClick={() => {
+                setDropdownOpen(true);
+                if (!blocksLoaded && line.duplicated && props.onClick) {
+                  props.onClick(line);
+                }
+              }}
               role="button"
               tabIndex={0}
             />
-          </Tooltip>
-        </Toggler>
-      </td>
-    ) : (
-      <td className={className} data-index={index} data-line-number={line.line}>
-        <div className="source-line-bar" />
-      </td>
-    );
-  }
+          </Toggler>
+        </div>
+      </Tooltip>
+    </td>
+  ) : (
+    <td className={className} data-index={index} data-line-number={line.line}>
+      <div className="source-line-bar" />
+    </td>
+  );
 }
+
+export default React.memo(LineDuplicationBlock);
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.tsx
deleted file mode 100644 (file)
index 4cffc13..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2020 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 classNames from 'classnames';
-import * as React from 'react';
-import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-
-interface Props {
-  line: T.SourceLine;
-  onClick: (line: T.SourceLine) => void;
-}
-
-export default class LineDuplications extends React.PureComponent<Props> {
-  handleClick = (event: React.MouseEvent<HTMLElement>) => {
-    event.preventDefault();
-    this.props.onClick(this.props.line);
-  };
-
-  render() {
-    const { line } = this.props;
-    const className = classNames('source-meta', 'source-line-duplications', {
-      'source-line-duplicated': line.duplicated
-    });
-
-    const cell = (
-      <td
-        className={className}
-        onClick={line.duplicated ? this.handleClick : undefined}
-        role={line.duplicated ? 'button' : undefined}
-        tabIndex={line.duplicated ? 0 : undefined}>
-        <div className="source-line-bar" />
-      </td>
-    );
-
-    return line.duplicated ? (
-      <Tooltip overlay={translate('source_viewer.tooltip.duplicated_line')} placement="right">
-        {cell}
-      </Tooltip>
-    ) : (
-      cell
-    );
-  }
-}
index 9a86486ff746516eae1a78c04f9183b42e67776f..534e73a56193a1f207b3dc1600df72db33cac49b 100644 (file)
 import * as classNames from 'classnames';
 import * as React from 'react';
 import IssueIcon from 'sonar-ui-common/components/icons/IssueIcon';
+import { translate } from 'sonar-ui-common/helpers/l10n';
 import { sortByType } from '../../../helpers/issues';
 
-interface Props {
+export interface LineIssuesIndicatorProps {
   issues: T.Issue[];
+  issuesOpen?: boolean;
   line: T.SourceLine;
   onClick: () => void;
 }
 
-export default class LineIssuesIndicator extends React.PureComponent<Props> {
-  handleClick = (event: React.MouseEvent<HTMLElement>) => {
-    event.preventDefault();
-    this.props.onClick();
-  };
+export function LineIssuesIndicator(props: LineIssuesIndicatorProps) {
+  const { issues, issuesOpen, line } = props;
+  const hasIssues = issues.length > 0;
+  const className = classNames('source-meta', 'source-line-issues', {
+    'source-line-with-issues': hasIssues
+  });
+  const mostImportantIssue = hasIssues ? sortByType(issues)[0] : null;
 
-  render() {
-    const { issues, line } = this.props;
-    const hasIssues = issues.length > 0;
-    const className = classNames('source-meta', 'source-line-issues', {
-      'source-line-with-issues': hasIssues
-    });
-    const mostImportantIssue = hasIssues ? sortByType(issues)[0] : null;
+  const handleClick = (e: React.MouseEvent<HTMLElement>) => {
+    e.preventDefault();
+    e.currentTarget.blur();
+    props.onClick();
+  };
 
-    return (
-      <td
-        className={className}
-        data-line-number={line.line}
-        onClick={hasIssues ? this.handleClick : undefined}
-        role={hasIssues ? 'button' : undefined}
-        tabIndex={hasIssues ? 0 : undefined}>
-        {mostImportantIssue != null && <IssueIcon type={mostImportantIssue.type} />}
-        {issues.length > 1 && <span className="source-line-issues-counter">{issues.length}</span>}
-      </td>
-    );
-  }
+  return (
+    <td className={className} data-line-number={line.line}>
+      {hasIssues && (
+        <span
+          aria-label={translate('source_viewer.issues_on_line', issuesOpen ? 'hide' : 'show')}
+          onClick={handleClick}
+          role="button"
+          tabIndex={0}>
+          {mostImportantIssue != null && <IssueIcon type={mostImportantIssue.type} />}
+          {issues.length > 1 && <span className="source-line-issues-counter">{issues.length}</span>}
+        </span>
+      )}
+    </td>
+  );
 }
+
+export default React.memo(LineIssuesIndicator);
index 1b9de7e2c658d187b1911f01e2b0796b142c72bc..6b59a02dd2c79f237b3cd2316ac221616e36bea9 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import Toggler from 'sonar-ui-common/components/controls/Toggler';
+import Dropdown from 'sonar-ui-common/components/controls/Dropdown';
+import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
+import { translateWithParameters } from 'sonar-ui-common/helpers/l10n';
 import LineOptionsPopup from './LineOptionsPopup';
 
-interface Props {
+export interface LineNumberProps {
   line: T.SourceLine;
-  onPopupToggle: (linePopup: T.LinePopup) => void;
-  popupOpen: boolean;
 }
 
-export default class LineNumber extends React.PureComponent<Props> {
-  handleClick = (event: React.MouseEvent<HTMLElement>) => {
-    event.preventDefault();
-    event.stopPropagation();
-    event.currentTarget.blur();
-    this.props.onPopupToggle({ line: this.props.line.line, name: 'line-number' });
-  };
-
-  handleTogglePopup = (open: boolean) => {
-    this.props.onPopupToggle({ line: this.props.line.line, name: 'line-number', open });
-  };
-
-  closePopup = () => {
-    this.handleTogglePopup(false);
-  };
-
-  render() {
-    const { line, popupOpen } = this.props;
-    const { line: lineNumber } = line;
-    const hasLineNumber = !!lineNumber;
-    return hasLineNumber ? (
-      <td
-        className="source-meta source-line-number"
-        data-line-number={lineNumber}
-        onClick={this.handleClick}
-        // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
-        role="button"
-        tabIndex={0}>
-        <Toggler
-          onRequestClose={this.closePopup}
-          open={popupOpen}
-          overlay={<LineOptionsPopup line={line} />}
-        />
-      </td>
-    ) : (
-      <td className="source-meta source-line-number" />
-    );
-  }
+export function LineNumber({ line }: LineNumberProps) {
+  const { line: lineNumber } = line;
+  const hasLineNumber = !!lineNumber;
+  return hasLineNumber ? (
+    <td className="source-meta source-line-number" data-line-number={lineNumber}>
+      <Dropdown
+        overlay={<LineOptionsPopup line={line} />}
+        overlayPlacement={PopupPlacement.RightTop}>
+        <span
+          aria-label={translateWithParameters('source_viewer.line_X', lineNumber)}
+          role="button">
+          {lineNumber}
+        </span>
+      </Dropdown>
+    </td>
+  ) : (
+    <td className="source-meta source-line-number" />
+  );
 }
+
+export default React.memo(LineNumber);
index 4bb7abd573339068a197a6a116b86c1d14faf60e..2665c73b04412589d72a0145924ee816291987c1 100644 (file)
  */
 import * as React from 'react';
 import { Link } from 'react-router';
-import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
-import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
 import { translate } from 'sonar-ui-common/helpers/l10n';
 import { getCodeUrl } from '../../../helpers/urls';
 import { SourceViewerContext } from '../SourceViewerContext';
 
-interface Props {
+interface LineOptionsPopupProps {
   line: T.SourceLine;
 }
 
-export default function LineOptionsPopup({ line }: Props) {
+export function LineOptionsPopup({ line }: LineOptionsPopupProps) {
   return (
     <SourceViewerContext.Consumer>
       {({ branchLike, file }) => (
-        <DropdownOverlay placement={PopupPlacement.RightTop}>
-          <div className="source-viewer-bubble-popup nowrap">
-            <Link
-              className="js-get-permalink"
-              onClick={event => {
-                event.stopPropagation();
-              }}
-              rel="noopener noreferrer"
-              target="_blank"
-              to={getCodeUrl(file.project, branchLike, file.key, line.line)}>
-              {translate('component_viewer.get_permalink')}
-            </Link>
-          </div>
-        </DropdownOverlay>
+        <div className="source-viewer-bubble-popup nowrap">
+          <Link
+            className="js-get-permalink"
+            rel="noopener noreferrer"
+            target="_blank"
+            to={getCodeUrl(file.project, branchLike, file.key, line.line)}>
+            {translate('component_viewer.get_permalink')}
+          </Link>
+        </div>
       )}
     </SourceViewerContext.Consumer>
   );
 }
+
+export default React.memo(LineOptionsPopup);
index 5d43f7719c3d6cab3ddca4828d29833aee69c4a3..b837748e804461d6ccfd861ef81d7e0db6a44d37 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import Toggler from 'sonar-ui-common/components/controls/Toggler';
+import Dropdown from 'sonar-ui-common/components/controls/Dropdown';
+import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
+import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
 import SCMPopup from './SCMPopup';
 
-interface Props {
+export interface LineSCMProps {
   line: T.SourceLine;
-  onPopupToggle: (linePopup: T.LinePopup) => void;
-  popupOpen: boolean;
   previousLine: T.SourceLine | undefined;
 }
 
-export default class LineSCM extends React.PureComponent<Props> {
-  handleClick = (event: React.MouseEvent<HTMLElement>) => {
-    event.preventDefault();
-    event.stopPropagation();
-    event.currentTarget.blur();
-    this.props.onPopupToggle({ line: this.props.line.line, name: 'scm' });
-  };
+export function LineSCM({ line, previousLine }: LineSCMProps) {
+  const hasPopup = !!line.line;
+  const cell = (
+    <div className="source-line-scm-inner">
+      {isSCMChanged(line, previousLine) ? line.scmAuthor || '…' : ' '}
+    </div>
+  );
 
-  handleTogglePopup = (open: boolean) => {
-    this.props.onPopupToggle({ line: this.props.line.line, name: 'scm', open });
-  };
+  if (hasPopup) {
+    let ariaLabel = translate('source_viewer.click_for_scm_info');
+    if (line.scmAuthor) {
+      ariaLabel = `${translateWithParameters(
+        'source_viewer.author_X',
+        line.scmAuthor
+      )}, ${ariaLabel}`;
+    }
 
-  closePopup = () => {
-    this.handleTogglePopup(false);
-  };
-
-  render() {
-    const { line, popupOpen, previousLine } = this.props;
-    const hasPopup = !!line.line;
-    const cell = isSCMChanged(line, previousLine) && (
-      <div className="source-line-scm-inner" data-author={line.scmAuthor || '…'} />
-    );
-    return hasPopup ? (
-      <td
-        className="source-meta source-line-scm"
-        data-line-number={line.line}
-        onClick={this.handleClick}
-        // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
-        role="button"
-        tabIndex={0}>
-        <Toggler
-          onRequestClose={this.closePopup}
-          open={popupOpen}
-          overlay={<SCMPopup line={line} />}>
-          {cell}
-        </Toggler>
+    return (
+      <td className="source-meta source-line-scm" data-line-number={line.line}>
+        <Dropdown overlay={<SCMPopup line={line} />} overlayPlacement={PopupPlacement.RightTop}>
+          <div aria-label={ariaLabel} role="button">
+            {cell}
+          </div>
+        </Dropdown>
       </td>
-    ) : (
+    );
+  } else {
+    return (
       <td className="source-meta source-line-scm" data-line-number={line.line}>
         {cell}
       </td>
@@ -80,3 +70,5 @@ function isSCMChanged(s: T.SourceLine, p: T.SourceLine | undefined) {
   }
   return changed;
 }
+
+export default React.memo(LineSCM);
index 7f66c5963bdefb011ebd4e5da7dbbee1bb6ee016..408a331f07ea56725aef6947e00244a9713a4365 100644 (file)
  */
 import * as classNames from 'classnames';
 import * as React from 'react';
-import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
 import DateFormatter from 'sonar-ui-common/components/intl/DateFormatter';
-import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
+import { translate } from 'sonar-ui-common/helpers/l10n';
 
-interface Props {
+export interface SCMPopupProps {
   line: T.SourceLine;
 }
 
-export default function SCMPopup({ line }: Props) {
-  const hasAuthor = line.scmAuthor !== '';
+export function SCMPopup({ line }: SCMPopupProps) {
+  const hasAuthor = line.scmAuthor !== undefined && line.scmAuthor !== '';
+  const hasDate = line.scmDate !== undefined;
   return (
-    <DropdownOverlay placement={PopupPlacement.RightTop}>
-      <div className="source-viewer-bubble-popup abs-width-400">
-        {hasAuthor && <div>{line.scmAuthor}</div>}
-        {line.scmDate && (
-          <div className={classNames({ 'spacer-top': hasAuthor })}>
-            <DateFormatter date={line.scmDate} />
-          </div>
-        )}
-        {line.scmRevision && <div className="spacer-top">{line.scmRevision}</div>}
-      </div>
-    </DropdownOverlay>
+    <div className="source-viewer-bubble-popup abs-width-400">
+      {hasAuthor && (
+        <div>
+          <h4>{translate('author')}</h4>
+          {line.scmAuthor}
+        </div>
+      )}
+      {hasDate && (
+        <div className={classNames({ 'spacer-top': hasAuthor })}>
+          <h4>{translate('source_viewer.tooltip.scm.commited_on')}</h4>
+          <DateFormatter date={line.scmDate!} />
+        </div>
+      )}
+      {line.scmRevision && (
+        <div className={classNames({ 'spacer-top': hasAuthor || hasDate })}>
+          <h4>{translate('source_viewer.tooltip.scm.revision')}</h4>
+          {line.scmRevision}
+        </div>
+      )}
+    </div>
   );
 }
+
+export default React.memo(SCMPopup);
index 4bff33578fc360cff231a9c11403944b3f89f317..6fbe865517eda637ef3879d6dd5c788a798e0986 100644 (file)
@@ -106,9 +106,7 @@ function shallowRender(props: Partial<Line['props']> = {}) {
       issues={[mockIssue(), mockIssue(false, { type: 'VULNERABILITY' })]}
       last={false}
       line={mockSourceLine()}
-      linePopup={undefined}
       loadDuplications={jest.fn()}
-      onLinePopupToggle={jest.fn()}
       onIssueChange={jest.fn()}
       onIssuePopupToggle={jest.fn()}
       onIssuesClose={jest.fn()}
index 446700629b6c2e496bab02516eb8efe78757068b..a458bc3fbeaa08b1934e12efe32c10db0ea3654e 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import LineCoverage from '../LineCoverage';
+import { LineCoverage, LineCoverageProps } from '../LineCoverage';
 
-it('render covered line', () => {
-  const line: T.SourceLine = { line: 3, coverageStatus: 'covered' };
-  const wrapper = shallow(<LineCoverage line={line} />);
-  expect(wrapper).toMatchSnapshot();
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('covered');
+  expect(shallowRender({ line: { line: 3, coverageStatus: 'uncovered' } })).toMatchSnapshot(
+    'uncovered'
+  );
+  expect(shallowRender({ line: { line: 3, coverageStatus: 'partially-covered' } })).toMatchSnapshot(
+    'partially covered, 0 conditions'
+  );
+  expect(
+    shallowRender({ line: { line: 3, coverageStatus: 'partially-covered', coveredConditions: 10 } })
+  ).toMatchSnapshot('partially covered, 10 conditions');
+  expect(shallowRender({ line: { line: 3, coverageStatus: undefined } })).toMatchSnapshot(
+    'no data'
+  );
 });
 
-it('render uncovered line', () => {
-  const line: T.SourceLine = { line: 3, coverageStatus: 'uncovered' };
-  const wrapper = shallow(<LineCoverage line={line} />);
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('render line with unknown coverage', () => {
-  const line: T.SourceLine = { line: 3 };
-  const wrapper = shallow(<LineCoverage line={line} />);
-  expect(wrapper).toMatchSnapshot();
-});
+function shallowRender(props: Partial<LineCoverageProps> = {}) {
+  return shallow(<LineCoverage line={{ line: 3, coverageStatus: 'covered' }} {...props} />);
+}
index 325c027d988e5f179a6061be9e214eefb9f355a8..b837d1a429a06101fa104b5e037641eff30f4240 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import Toggler from 'sonar-ui-common/components/controls/Toggler';
 import { click } from 'sonar-ui-common/helpers/testUtils';
-import LineDuplicationBlock from '../LineDuplicationBlock';
+import { LineDuplicationBlock, LineDuplicationBlockProps } from '../LineDuplicationBlock';
 
-it('render duplicated line', () => {
-  const line = { line: 3, duplicated: true };
-  const onPopupToggle = jest.fn();
-  const wrapper = shallow(
-    <LineDuplicationBlock
-      duplicated={true}
-      index={1}
-      line={line}
-      onPopupToggle={onPopupToggle}
-      popupOpen={false}
-      renderDuplicationPopup={jest.fn()}
-    />
-  );
-  expect(wrapper).toMatchSnapshot();
-  click(wrapper.find('[tabIndex]'));
-  expect(onPopupToggle).toHaveBeenCalled();
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('default');
+  expect(
+    shallowRender({ line: { line: 3, duplicated: false }, duplicated: false })
+  ).toMatchSnapshot('not duplicated');
+});
+
+it('should correctly open/close the dropdown', () => {
+  const wrapper = shallowRender();
+  click(wrapper.find('div[role="button"]'));
+  expect(wrapper.find(Toggler).prop('open')).toBe(true);
+  wrapper.find(Toggler).prop('onRequestClose')();
+  expect(wrapper.find(Toggler).prop('open')).toBe(false);
 });
 
-it('render not duplicated line', () => {
-  const line = { line: 3, duplicated: false };
-  const wrapper = shallow(
+it('should correctly call the onCick prop', () => {
+  const line = { line: 1, duplicated: true };
+  const onClick = jest.fn();
+  const wrapper = shallowRender({ line, onClick });
+
+  // Propagate if blocks aren't loaded.
+  click(wrapper.find('div[role="button"]'));
+  expect(onClick).toBeCalledWith(line);
+
+  // Don't propagate if blocks were loaded.
+  onClick.mockClear();
+  wrapper.setProps({ blocksLoaded: true });
+  click(wrapper.find('div[role="button"]'));
+  expect(onClick).not.toBeCalled();
+});
+
+function shallowRender(props: Partial<LineDuplicationBlockProps> = {}) {
+  return shallow<LineDuplicationBlockProps>(
     <LineDuplicationBlock
-      duplicated={false}
+      blocksLoaded={false}
+      duplicated={true}
       index={1}
-      line={line}
-      onPopupToggle={jest.fn()}
-      popupOpen={false}
+      line={{ line: 3, duplicated: true }}
       renderDuplicationPopup={jest.fn()}
+      {...props}
     />
   );
-  expect(wrapper).toMatchSnapshot();
-});
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.tsx
deleted file mode 100644 (file)
index 1a52277..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2020 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 { click } from 'sonar-ui-common/helpers/testUtils';
-import LineDuplications from '../LineDuplications';
-
-it('render duplicated line', () => {
-  const line = { line: 3, duplicated: true };
-  const onClick = jest.fn();
-  const wrapper = shallow(<LineDuplications line={line} onClick={onClick} />);
-  expect(wrapper).toMatchSnapshot();
-  click(wrapper.find('[tabIndex]'));
-  expect(onClick).toHaveBeenCalled();
-});
-
-it('render not duplicated line', () => {
-  const line = { line: 3, duplicated: false };
-  const onClick = jest.fn();
-  const wrapper = shallow(<LineDuplications line={line} onClick={onClick} />);
-  expect(wrapper).toMatchSnapshot();
-});
index 9d83642e2bd9657c32c30bc3d6508e15e33be767..955d74d72e91b972fe7ea7fac8fe14c754123211 100644 (file)
@@ -21,29 +21,30 @@ import { shallow } from 'enzyme';
 import * as React from 'react';
 import { click } from 'sonar-ui-common/helpers/testUtils';
 import { mockIssue } from '../../../../helpers/testMocks';
-import LineIssuesIndicator from '../LineIssuesIndicator';
+import { LineIssuesIndicator, LineIssuesIndicatorProps } from '../LineIssuesIndicator';
 
 it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('default');
+  expect(
+    shallowRender({
+      issues: [
+        mockIssue(false, { key: 'foo', type: 'VULNERABILITY' }),
+        mockIssue(false, { key: 'bar', type: 'SECURITY_HOTSPOT' })
+      ]
+    })
+  ).toMatchSnapshot('diff issue types');
+  expect(shallowRender({ issues: [] })).toMatchSnapshot('no issues');
+});
+
+it('should correctly handle click', () => {
   const onClick = jest.fn();
   const wrapper = shallowRender({ onClick });
-  expect(wrapper).toMatchSnapshot();
 
-  click(wrapper);
+  click(wrapper.find('span[role="button"]'));
   expect(onClick).toHaveBeenCalled();
-
-  const nextIssues = [
-    mockIssue(false, { key: 'foo', type: 'VULNERABILITY' }),
-    mockIssue(false, { key: 'bar', type: 'SECURITY_HOTSPOT' })
-  ];
-  wrapper.setProps({ issues: nextIssues });
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should render correctly for no issues', () => {
-  expect(shallowRender({ issues: [] })).toMatchSnapshot();
 });
 
-function shallowRender(props: Partial<LineIssuesIndicator['props']> = {}) {
+function shallowRender(props: Partial<LineIssuesIndicatorProps> = {}) {
   return shallow(
     <LineIssuesIndicator
       issues={[
index 1174c8623649b84ead98a3f0deb8b252776dffaf..40da8c2730cc95e418b6933787886ac3e369b5ca 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { click } from 'sonar-ui-common/helpers/testUtils';
-import LineNumber from '../LineNumber';
+import { LineNumber, LineNumberProps } from '../LineNumber';
 
-it('render line 3', () => {
-  const line = { line: 3 };
-  const wrapper = shallow(<LineNumber line={line} onPopupToggle={jest.fn()} popupOpen={false} />);
-  expect(wrapper).toMatchSnapshot();
-  click(wrapper);
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('default');
+  expect(shallowRender({ line: { line: 0 } })).toMatchSnapshot('no line number');
 });
 
-it('render line 0', () => {
-  const line = { line: 0 };
-  const wrapper = shallow(<LineNumber line={line} onPopupToggle={jest.fn()} popupOpen={false} />);
-  expect(wrapper).toMatchSnapshot();
-});
+function shallowRender(props: Partial<LineNumberProps> = {}) {
+  return shallow(<LineNumber line={{ line: 3 }} {...props} />);
+}
index be4112fa40cef9d226ad9e78330c6c4c1f497714..c6fae4d5176309e540535b5a34f59b75e0e70813 100644 (file)
@@ -20,7 +20,7 @@
 import { shallow } from 'enzyme';
 import * as React from 'react';
 import { mockBranch } from '../../../../helpers/mocks/branch-like';
-import LineOptionsPopup from '../LineOptionsPopup';
+import { LineOptionsPopup } from '../LineOptionsPopup';
 
 jest.mock('../../SourceViewerContext', () => ({
   SourceViewerContext: {
@@ -32,7 +32,7 @@ jest.mock('../../SourceViewerContext', () => ({
   }
 }));
 
-it('should render', () => {
+it('should render correctly', () => {
   const line = { line: 3 };
   const wrapper = shallow(<LineOptionsPopup line={line} />).dive();
   expect(wrapper).toMatchSnapshot();
index fa007db2e62417fbeaf984002a7b185f25bb8445..bd6fce7f69f2b894013ff45448d51525806715b4 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+/* eslint-disable sonarjs/no-duplicate-string */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { click } from 'sonar-ui-common/helpers/testUtils';
-import LineSCM from '../LineSCM';
+import { LineSCM, LineSCMProps } from '../LineSCM';
 
-it('render scm details', () => {
-  const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
-  const previousLine = { line: 2, scmAuthor: 'bar', scmDate: '2017-01-02' };
-  const wrapper = shallow(
-    <LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={previousLine} />
-  );
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('render scm details for the first line', () => {
-  const line = { line: 3, scmRevision: 'foo', scmAuthor: 'foo', scmDate: '2017-01-01' };
-  const wrapper = shallow(
-    <LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={undefined} />
-  );
-  expect(wrapper).toMatchSnapshot();
-});
+it('should render correctly', () => {
+  const scmInfo = { scmRevision: 'foo', scmAuthor: 'foo', scmDate: '2017-01-01' };
 
-it('does not render scm details', () => {
-  const line = { line: 3, scmRevision: 'foo', scmAuthor: 'foo', scmDate: '2017-01-01' };
-  const previousLine = { line: 2, scmRevision: 'foo', scmAuthor: 'foo', scmDate: '2017-01-01' };
-  const wrapper = shallow(
-    <LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={previousLine} />
-  );
-  expect(wrapper).toMatchSnapshot();
+  expect(shallowRender()).toMatchSnapshot('default');
+  expect(
+    shallowRender({ line: { line: 3, ...scmInfo }, previousLine: { line: 2, ...scmInfo } })
+  ).toMatchSnapshot('same commit');
+  expect(shallowRender({ line: { line: 3, scmDate: '2017-01-01' } })).toMatchSnapshot('no author');
 });
 
-it('renders ellipsis when no author info', () => {
-  const line = { line: 3, scmRevision: 'foo', scmDate: '2017-01-01' };
-  const previousLine = { line: 2, scmRevision: 'bar', scmDate: '2017-01-01' };
-  const wrapper = shallow(
-    <LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={previousLine} />
+function shallowRender(props: Partial<LineSCMProps> = {}) {
+  return shallow(
+    <LineSCM
+      line={{ line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' }}
+      previousLine={{ line: 2, scmAuthor: 'bar', scmDate: '2017-01-02' }}
+      {...props}
+    />
   );
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should open popup', () => {
-  const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
-  const onPopupToggle = jest.fn();
-  const wrapper = shallow(
-    <LineSCM line={line} onPopupToggle={onPopupToggle} popupOpen={false} previousLine={undefined} />
-  );
-  click(wrapper.find('[role="button"]'));
-  expect(onPopupToggle).toBeCalledWith({ line: 3, name: 'scm' });
-});
+}
index f68afd655ad9023d3f9ff06b8f2dd00fdc149a2e..69fe6f4bdf5f2940a6d6384590840d23133ca357 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+/* eslint-disable sonarjs/no-duplicate-string */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import SCMPopup from '../SCMPopup';
+import { SCMPopup, SCMPopupProps } from '../SCMPopup';
 
-it('should render', () => {
-  const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
-  expect(shallow(<SCMPopup line={line} />)).toMatchSnapshot();
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('default');
+  expect(
+    shallowRender({ line: { line: 3, scmDate: '2017-01-01', scmRevision: 'bar' } })
+  ).toMatchSnapshot('no author');
+  expect(
+    shallowRender({ line: { line: 3, scmAuthor: 'foo', scmRevision: 'bar' } })
+  ).toMatchSnapshot('no date');
+  expect(
+    shallowRender({ line: { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' } })
+  ).toMatchSnapshot('no revision');
 });
+
+function shallowRender(props: Partial<SCMPopupProps> = {}) {
+  return shallow(
+    <SCMPopup
+      line={{ line: 3, scmAuthor: 'foo', scmDate: '2017-01-01', scmRevision: 'bar' }}
+      {...props}
+    />
+  );
+}
index 931708c40711dc6c8d1015566c724d173e214f28..864377427da7cdc6014332340575b5e7a30b29c0 100644 (file)
@@ -5,7 +5,7 @@ exports[`should render correctly 1`] = `
   className="source-line source-line-filtered"
   data-line-number={16}
 >
-  <LineNumber
+  <Memo(LineNumber)
     line={
       Object {
         "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
@@ -19,10 +19,8 @@ exports[`should render correctly 1`] = `
         "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
       }
     }
-    onPopupToggle={[MockFunction]}
-    popupOpen={false}
   />
-  <LineSCM
+  <Memo(LineSCM)
     line={
       Object {
         "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
@@ -36,8 +34,6 @@ exports[`should render correctly 1`] = `
         "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
       }
     }
-    onPopupToggle={[MockFunction]}
-    popupOpen={false}
   />
   <td
     className="source-meta source-line-issues"
@@ -151,7 +147,7 @@ exports[`should render correctly for last, new, and highlighted lines 1`] = `
   className="source-line source-line-highlighted source-line-filtered source-line-last"
   data-line-number={16}
 >
-  <LineNumber
+  <Memo(LineNumber)
     line={
       Object {
         "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
@@ -165,10 +161,8 @@ exports[`should render correctly for last, new, and highlighted lines 1`] = `
         "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
       }
     }
-    onPopupToggle={[MockFunction]}
-    popupOpen={false}
   />
-  <LineSCM
+  <Memo(LineSCM)
     line={
       Object {
         "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
@@ -182,8 +176,6 @@ exports[`should render correctly for last, new, and highlighted lines 1`] = `
         "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
       }
     }
-    onPopupToggle={[MockFunction]}
-    popupOpen={false}
   />
   <td
     className="source-meta source-line-issues"
@@ -297,7 +289,7 @@ exports[`should render correctly with coverage 1`] = `
   className="source-line source-line-filtered"
   data-line-number={16}
 >
-  <LineNumber
+  <Memo(LineNumber)
     line={
       Object {
         "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
@@ -311,10 +303,8 @@ exports[`should render correctly with coverage 1`] = `
         "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
       }
     }
-    onPopupToggle={[MockFunction]}
-    popupOpen={false}
   />
-  <LineSCM
+  <Memo(LineSCM)
     line={
       Object {
         "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
@@ -328,13 +318,11 @@ exports[`should render correctly with coverage 1`] = `
         "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
       }
     }
-    onPopupToggle={[MockFunction]}
-    popupOpen={false}
   />
   <td
     className="source-meta source-line-issues"
   />
-  <LineCoverage
+  <Memo(LineCoverage)
     line={
       Object {
         "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
@@ -458,7 +446,7 @@ exports[`should render correctly with duplication information 1`] = `
   className="source-line source-line-filtered"
   data-line-number={16}
 >
-  <LineNumber
+  <Memo(LineNumber)
     line={
       Object {
         "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
@@ -472,10 +460,8 @@ exports[`should render correctly with duplication information 1`] = `
         "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
       }
     }
-    onPopupToggle={[MockFunction]}
-    popupOpen={false}
   />
-  <LineSCM
+  <Memo(LineSCM)
     line={
       Object {
         "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
@@ -489,29 +475,12 @@ exports[`should render correctly with duplication information 1`] = `
         "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
       }
     }
-    onPopupToggle={[MockFunction]}
-    popupOpen={false}
   />
   <td
     className="source-meta source-line-issues"
   />
-  <LineDuplications
-    line={
-      Object {
-        "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
-        "coverageStatus": "covered",
-        "coveredConditions": 2,
-        "duplicated": false,
-        "isNew": true,
-        "line": 16,
-        "scmAuthor": "simon.brandhof@sonarsource.com",
-        "scmDate": "2018-12-11T10:48:39+0100",
-        "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-      }
-    }
-    onClick={[MockFunction]}
-  />
-  <LineDuplicationBlock
+  <Memo(LineDuplicationBlock)
+    blocksLoaded={true}
     duplicated={false}
     index={0}
     key="0"
@@ -528,11 +497,11 @@ exports[`should render correctly with duplication information 1`] = `
         "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
       }
     }
-    onPopupToggle={[MockFunction]}
-    popupOpen={false}
+    onClick={[MockFunction]}
     renderDuplicationPopup={[MockFunction]}
   />
-  <LineDuplicationBlock
+  <Memo(LineDuplicationBlock)
+    blocksLoaded={true}
     duplicated={false}
     index={1}
     key="1"
@@ -549,11 +518,10 @@ exports[`should render correctly with duplication information 1`] = `
         "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
       }
     }
-    onPopupToggle={[MockFunction]}
-    popupOpen={false}
     renderDuplicationPopup={[MockFunction]}
   />
-  <LineDuplicationBlock
+  <Memo(LineDuplicationBlock)
+    blocksLoaded={true}
     duplicated={false}
     index={2}
     key="2"
@@ -570,8 +538,6 @@ exports[`should render correctly with duplication information 1`] = `
         "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
       }
     }
-    onPopupToggle={[MockFunction]}
-    popupOpen={false}
     renderDuplicationPopup={[MockFunction]}
   />
   <LineCode
@@ -683,7 +649,7 @@ exports[`should render correctly with issues info 1`] = `
   className="source-line source-line-filtered"
   data-line-number={16}
 >
-  <LineNumber
+  <Memo(LineNumber)
     line={
       Object {
         "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
@@ -697,10 +663,8 @@ exports[`should render correctly with issues info 1`] = `
         "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
       }
     }
-    onPopupToggle={[MockFunction]}
-    popupOpen={false}
   />
-  <LineSCM
+  <Memo(LineSCM)
     line={
       Object {
         "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
@@ -714,10 +678,8 @@ exports[`should render correctly with issues info 1`] = `
         "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
       }
     }
-    onPopupToggle={[MockFunction]}
-    popupOpen={false}
   />
-  <LineIssuesIndicator
+  <Memo(LineIssuesIndicator)
     issues={
       Array [
         Object {
@@ -784,6 +746,7 @@ exports[`should render correctly with issues info 1`] = `
         },
       ]
     }
+    issuesOpen={false}
     line={
       Object {
         "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
@@ -908,7 +871,7 @@ exports[`should render correctly: no SCM 1`] = `
   className="source-line source-line-filtered"
   data-line-number={16}
 >
-  <LineNumber
+  <Memo(LineNumber)
     line={
       Object {
         "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
@@ -922,8 +885,6 @@ exports[`should render correctly: no SCM 1`] = `
         "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
       }
     }
-    onPopupToggle={[MockFunction]}
-    popupOpen={false}
   />
   <td
     className="source-meta source-line-issues"
index 16436ccd4dac60f3eaf3773b6a5106b31060f09c..94fdaa21400b139c43b704d58883422e0de8e2c1 100644 (file)
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`render covered line 1`] = `
+exports[`should render correctly: covered 1`] = `
 <td
   className="source-meta source-line-coverage source-line-covered"
   data-line-number={3}
@@ -10,13 +10,14 @@ exports[`render covered line 1`] = `
     placement="right"
   >
     <div
+      aria-label="source_viewer.tooltip.covered"
       className="source-line-bar"
     />
   </Tooltip>
 </td>
 `;
 
-exports[`render line with unknown coverage 1`] = `
+exports[`should render correctly: no data 1`] = `
 <td
   className="source-meta source-line-coverage"
   data-line-number={3}
@@ -31,7 +32,41 @@ exports[`render line with unknown coverage 1`] = `
 </td>
 `;
 
-exports[`render uncovered line 1`] = `
+exports[`should render correctly: partially covered, 0 conditions 1`] = `
+<td
+  className="source-meta source-line-coverage source-line-partially-covered"
+  data-line-number={3}
+>
+  <Tooltip
+    overlay="source_viewer.tooltip.partially-covered"
+    placement="right"
+  >
+    <div
+      aria-label="source_viewer.tooltip.partially-covered"
+      className="source-line-bar"
+    />
+  </Tooltip>
+</td>
+`;
+
+exports[`should render correctly: partially covered, 10 conditions 1`] = `
+<td
+  className="source-meta source-line-coverage source-line-partially-covered"
+  data-line-number={3}
+>
+  <Tooltip
+    overlay="source_viewer.tooltip.partially-covered"
+    placement="right"
+  >
+    <div
+      aria-label="source_viewer.tooltip.partially-covered"
+      className="source-line-bar"
+    />
+  </Tooltip>
+</td>
+`;
+
+exports[`should render correctly: uncovered 1`] = `
 <td
   className="source-meta source-line-coverage source-line-uncovered"
   data-line-number={3}
@@ -41,6 +76,7 @@ exports[`render uncovered line 1`] = `
     placement="right"
   >
     <div
+      aria-label="source_viewer.tooltip.uncovered"
       className="source-line-bar"
     />
   </Tooltip>
index 86a35b7a0837e83d5ee78071a89925ae783b33d6..2be051a9d1d99677750bfdf227b4d11f9227d5f4 100644 (file)
@@ -1,33 +1,41 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`render duplicated line 1`] = `
+exports[`should render correctly: default 1`] = `
 <td
-  className="source-meta source-line-duplications-extra source-line-duplicated"
+  className="source-meta source-line-duplications source-line-duplicated"
   data-index={1}
   data-line-number={3}
 >
-  <Toggler
-    onRequestClose={[Function]}
-    open={false}
+  <Tooltip
+    overlay="source_viewer.tooltip.duplicated_block"
+    placement="right"
   >
-    <Tooltip
-      overlay="source_viewer.tooltip.duplicated_block"
-      placement="right"
-    >
-      <div
-        className="source-line-bar"
-        onClick={[Function]}
-        role="button"
-        tabIndex={0}
-      />
-    </Tooltip>
-  </Toggler>
+    <div>
+      <Toggler
+        onRequestClose={[Function]}
+        open={false}
+        overlay={
+          <DropdownOverlay
+            placement="right-top"
+          />
+        }
+      >
+        <div
+          aria-label="source_viewer.tooltip.duplicated_block"
+          className="source-line-bar"
+          onClick={[Function]}
+          role="button"
+          tabIndex={0}
+        />
+      </Toggler>
+    </div>
+  </Tooltip>
 </td>
 `;
 
-exports[`render not duplicated line 1`] = `
+exports[`should render correctly: not duplicated 1`] = `
 <td
-  className="source-meta source-line-duplications-extra"
+  className="source-meta source-line-duplications"
   data-index={1}
   data-line-number={3}
 >
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.tsx.snap
deleted file mode 100644 (file)
index d40e9e6..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`render duplicated line 1`] = `
-<Tooltip
-  overlay="source_viewer.tooltip.duplicated_line"
-  placement="right"
->
-  <td
-    className="source-meta source-line-duplications source-line-duplicated"
-    onClick={[Function]}
-    role="button"
-    tabIndex={0}
-  >
-    <div
-      className="source-line-bar"
-    />
-  </td>
-</Tooltip>
-`;
-
-exports[`render not duplicated line 1`] = `
-<td
-  className="source-meta source-line-duplications"
->
-  <div
-    className="source-line-bar"
-  />
-</td>
-`;
index fbb8d6eb661016eeefee548464378aa3764ff993..160ac74b311585c568490c88dabfa7bc8f09b21d 100644 (file)
@@ -1,44 +1,52 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`should render correctly 1`] = `
+exports[`should render correctly: default 1`] = `
 <td
   className="source-meta source-line-issues source-line-with-issues"
   data-line-number={3}
-  onClick={[Function]}
-  role="button"
-  tabIndex={0}
 >
-  <IssueIcon
-    type="BUG"
-  />
   <span
-    className="source-line-issues-counter"
+    aria-label="source_viewer.issues_on_line.show"
+    onClick={[Function]}
+    role="button"
+    tabIndex={0}
   >
-    2
+    <IssueIcon
+      type="BUG"
+    />
+    <span
+      className="source-line-issues-counter"
+    >
+      2
+    </span>
   </span>
 </td>
 `;
 
-exports[`should render correctly 2`] = `
+exports[`should render correctly: diff issue types 1`] = `
 <td
   className="source-meta source-line-issues source-line-with-issues"
   data-line-number={3}
-  onClick={[Function]}
-  role="button"
-  tabIndex={0}
 >
-  <IssueIcon
-    type="VULNERABILITY"
-  />
   <span
-    className="source-line-issues-counter"
+    aria-label="source_viewer.issues_on_line.show"
+    onClick={[Function]}
+    role="button"
+    tabIndex={0}
   >
-    2
+    <IssueIcon
+      type="VULNERABILITY"
+    />
+    <span
+      className="source-line-issues-counter"
+    >
+      2
+    </span>
   </span>
 </td>
 `;
 
-exports[`should render correctly for no issues 1`] = `
+exports[`should render correctly: no issues 1`] = `
 <td
   className="source-meta source-line-issues"
   data-line-number={3}
index a6bb1627d08b8f68ce9b114fa37138152b6a2fd6..0d2a6cc0f783cd8f2ce2abf40bf79bab54780fcf 100644 (file)
@@ -1,24 +1,13 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`render line 0 1`] = `
-<td
-  className="source-meta source-line-number"
-/>
-`;
-
-exports[`render line 3 1`] = `
+exports[`should render correctly: default 1`] = `
 <td
   className="source-meta source-line-number"
   data-line-number={3}
-  onClick={[Function]}
-  role="button"
-  tabIndex={0}
 >
-  <Toggler
-    onRequestClose={[Function]}
-    open={false}
+  <Dropdown
     overlay={
-      <LineOptionsPopup
+      <Memo(LineOptionsPopup)
         line={
           Object {
             "line": 3,
@@ -26,6 +15,20 @@ exports[`render line 3 1`] = `
         }
       />
     }
-  />
+    overlayPlacement="right-top"
+  >
+    <span
+      aria-label="source_viewer.line_X.3"
+      role="button"
+    >
+      3
+    </span>
+  </Dropdown>
 </td>
 `;
+
+exports[`should render correctly: no line number 1`] = `
+<td
+  className="source-meta source-line-number"
+/>
+`;
index 936909da9dcb6e99f6032270dd90f73d07dd34e6..d0c730eba8936ebd73ccb4801eb128ed5de5fa5c 100644 (file)
@@ -1,33 +1,28 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`should render 1`] = `
-<DropdownOverlay
-  placement="right-top"
+exports[`should render correctly 1`] = `
+<div
+  className="source-viewer-bubble-popup nowrap"
 >
-  <div
-    className="source-viewer-bubble-popup nowrap"
-  >
-    <Link
-      className="js-get-permalink"
-      onClick={[Function]}
-      onlyActiveOnIndex={false}
-      rel="noopener noreferrer"
-      style={Object {}}
-      target="_blank"
-      to={
-        Object {
-          "pathname": "/code",
-          "query": Object {
-            "branch": "feature",
-            "id": "prj",
-            "line": 3,
-            "selected": "foo",
-          },
-        }
+  <Link
+    className="js-get-permalink"
+    onlyActiveOnIndex={false}
+    rel="noopener noreferrer"
+    style={Object {}}
+    target="_blank"
+    to={
+      Object {
+        "pathname": "/code",
+        "query": Object {
+          "branch": "feature",
+          "id": "prj",
+          "line": 3,
+          "selected": "foo",
+        },
       }
-    >
-      component_viewer.get_permalink
-    </Link>
-  </div>
-</DropdownOverlay>
+    }
+  >
+    component_viewer.get_permalink
+  </Link>
+</div>
 `;
index b72bebfac307958e5657f998a4201aab6f898c93..87b8700d5e0fe8dad7d53cacf329d14baa98a8f0 100644 (file)
@@ -1,45 +1,13 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`does not render scm details 1`] = `
+exports[`should render correctly: default 1`] = `
 <td
   className="source-meta source-line-scm"
   data-line-number={3}
-  onClick={[Function]}
-  role="button"
-  tabIndex={0}
 >
-  <Toggler
-    onRequestClose={[Function]}
-    open={false}
+  <Dropdown
     overlay={
-      <SCMPopup
-        line={
-          Object {
-            "line": 3,
-            "scmAuthor": "foo",
-            "scmDate": "2017-01-01",
-            "scmRevision": "foo",
-          }
-        }
-      />
-    }
-  />
-</td>
-`;
-
-exports[`render scm details 1`] = `
-<td
-  className="source-meta source-line-scm"
-  data-line-number={3}
-  onClick={[Function]}
-  role="button"
-  tabIndex={0}
->
-  <Toggler
-    onRequestClose={[Function]}
-    open={false}
-    overlay={
-      <SCMPopup
+      <Memo(SCMPopup)
         line={
           Object {
             "line": 3,
@@ -49,74 +17,84 @@ exports[`render scm details 1`] = `
         }
       />
     }
+    overlayPlacement="right-top"
   >
     <div
-      className="source-line-scm-inner"
-      data-author="foo"
-    />
-  </Toggler>
+      aria-label="source_viewer.author_X.foo, source_viewer.click_for_scm_info"
+      role="button"
+    >
+      <div
+        className="source-line-scm-inner"
+      >
+        foo
+      </div>
+    </div>
+  </Dropdown>
 </td>
 `;
 
-exports[`render scm details for the first line 1`] = `
+exports[`should render correctly: no author 1`] = `
 <td
   className="source-meta source-line-scm"
   data-line-number={3}
-  onClick={[Function]}
-  role="button"
-  tabIndex={0}
 >
-  <Toggler
-    onRequestClose={[Function]}
-    open={false}
+  <Dropdown
     overlay={
-      <SCMPopup
+      <Memo(SCMPopup)
         line={
           Object {
             "line": 3,
-            "scmAuthor": "foo",
             "scmDate": "2017-01-01",
-            "scmRevision": "foo",
           }
         }
       />
     }
+    overlayPlacement="right-top"
   >
     <div
-      className="source-line-scm-inner"
-      data-author="foo"
-    />
-  </Toggler>
+      aria-label="source_viewer.click_for_scm_info"
+      role="button"
+    >
+      <div
+        className="source-line-scm-inner"
+      >
+        …
+      </div>
+    </div>
+  </Dropdown>
 </td>
 `;
 
-exports[`renders ellipsis when no author info 1`] = `
+exports[`should render correctly: same commit 1`] = `
 <td
   className="source-meta source-line-scm"
   data-line-number={3}
-  onClick={[Function]}
-  role="button"
-  tabIndex={0}
 >
-  <Toggler
-    onRequestClose={[Function]}
-    open={false}
+  <Dropdown
     overlay={
-      <SCMPopup
+      <Memo(SCMPopup)
         line={
           Object {
             "line": 3,
+            "scmAuthor": "foo",
             "scmDate": "2017-01-01",
             "scmRevision": "foo",
           }
         }
       />
     }
+    overlayPlacement="right-top"
   >
     <div
-      className="source-line-scm-inner"
-      data-author="…"
-    />
-  </Toggler>
+      aria-label="source_viewer.author_X.foo, source_viewer.click_for_scm_info"
+      role="button"
+    >
+      <div
+        className="source-line-scm-inner"
+      >
+         
+      </div>
+    </div>
+  </Dropdown>
 </td>
 `;
index 99378cb457db117ca5fdfed5b92935491eafba39..a05f410a191060c16f3774451dc6d23ea3983594 100644 (file)
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`should render 1`] = `
-<DropdownOverlay
-  placement="right-top"
+exports[`should render correctly: default 1`] = `
+<div
+  className="source-viewer-bubble-popup abs-width-400"
 >
+  <div>
+    <h4>
+      author
+    </h4>
+    foo
+  </div>
+  <div
+    className="spacer-top"
+  >
+    <h4>
+      source_viewer.tooltip.scm.commited_on
+    </h4>
+    <DateFormatter
+      date="2017-01-01"
+    />
+  </div>
+  <div
+    className="spacer-top"
+  >
+    <h4>
+      source_viewer.tooltip.scm.revision
+    </h4>
+    bar
+  </div>
+</div>
+`;
+
+exports[`should render correctly: no author 1`] = `
+<div
+  className="source-viewer-bubble-popup abs-width-400"
+>
+  <div
+    className=""
+  >
+    <h4>
+      source_viewer.tooltip.scm.commited_on
+    </h4>
+    <DateFormatter
+      date="2017-01-01"
+    />
+  </div>
+  <div
+    className="spacer-top"
+  >
+    <h4>
+      source_viewer.tooltip.scm.revision
+    </h4>
+    bar
+  </div>
+</div>
+`;
+
+exports[`should render correctly: no date 1`] = `
+<div
+  className="source-viewer-bubble-popup abs-width-400"
+>
+  <div>
+    <h4>
+      author
+    </h4>
+    foo
+  </div>
+  <div
+    className="spacer-top"
+  >
+    <h4>
+      source_viewer.tooltip.scm.revision
+    </h4>
+    bar
+  </div>
+</div>
+`;
+
+exports[`should render correctly: no revision 1`] = `
+<div
+  className="source-viewer-bubble-popup abs-width-400"
+>
+  <div>
+    <h4>
+      author
+    </h4>
+    foo
+  </div>
   <div
-    className="source-viewer-bubble-popup abs-width-400"
+    className="spacer-top"
   >
-    <div>
-      foo
-    </div>
-    <div
-      className="spacer-top"
-    >
-      <DateFormatter
-        date="2017-01-01"
-      />
-    </div>
-  </div>
-</DropdownOverlay>
+    <h4>
+      source_viewer.tooltip.scm.commited_on
+    </h4>
+    <DateFormatter
+      date="2017-01-01"
+    />
+  </div>
+</div>
 `;
index 1a059e1664a8b6f533d0202c995f15427d19d0e7..ac445523a142276008e2d51c78c2912f415f704f 100644 (file)
@@ -2488,6 +2488,9 @@ source_viewer.view_all_issues=See all issues in this file
 source_viewer.covered=Covered by the following tests
 source_viewer.not_covered=Not covered by tests
 source_viewer.conditions=conditions
+source_viewer.line_X=Line: {0}
+source_viewer.click_for_scm_info=Click to see SCM information
+source_viewer.author_X=Author: {0}
 
 source_viewer.tooltip.duplicated_line=This line is duplicated. Click to see duplicated blocks.
 source_viewer.tooltip.duplicated_block=Duplicated block. Click for details.
@@ -2498,6 +2501,11 @@ source_viewer.tooltip.partially-covered.conditions=Partially covered by tests ({
 source_viewer.tooltip.uncovered=Not covered by tests.
 source_viewer.tooltip.uncovered.conditions=Not covered by tests ({0} conditions).
 source_viewer.tooltip.no_information_about_tests=There is no extra information about test files.
+source_viewer.tooltip.scm.commited_on=Committed on
+source_viewer.tooltip.scm.revision=Revision
+
+source_viewer.issues_on_line.show=Click to show all issues on this line
+source_viewer.issues_on_line.hide=Click to hide all issues on this line
 
 source_viewer.load_more_code=Load More Code
 source_viewer.loading_more_code=Loading More Code...