]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12022 Animate snippets
authorJeremy Davis <jeremy.davis@sonarsource.com>
Mon, 1 Jul 2019 16:53:17 +0000 (18:53 +0200)
committerSonarTech <sonartech@sonarsource.com>
Thu, 18 Jul 2019 18:21:12 +0000 (20:21 +0200)
server/sonar-web/src/main/js/app/types.d.ts
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetViewer.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetViewer-test.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/SnippetViewer-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts
server/sonar-web/src/main/js/apps/issues/styles.css

index 415a5f2e2762fd24505d8e611ba49bbf67071621..09accb78d73b079d77cd9563924d67b5652f40a0 100644 (file)
@@ -792,6 +792,13 @@ declare namespace T {
     type: 'SHORT';
   }
 
+  export interface Snippet {
+    start: number;
+    end: number;
+    index: number;
+    toDelete?: boolean;
+  }
+
   export interface SnippetGroup extends SnippetsByComponent {
     locations: T.FlowLocation[];
   }
index d9e14742a19b1837898c1368bf9b1fca97aeaa55..90332683273c4c2e7f7437d904d1c288c909d69e 100644 (file)
  */
 import * as React from 'react';
 import * as classNames from 'classnames';
-import { createSnippets, expandSnippet, EXPAND_BY_LINES, MERGE_DISTANCE } from './utils';
+import {
+  createSnippets,
+  expandSnippet,
+  EXPAND_BY_LINES,
+  MERGE_DISTANCE,
+  linesForSnippets
+} from './utils';
 import SnippetViewer from './SnippetViewer';
 import SourceViewerHeaderSlim from '../../../components/SourceViewer/SourceViewerHeaderSlim';
 import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus';
@@ -57,11 +63,12 @@ interface State {
   highlightedSymbols: string[];
   loading: boolean;
   openIssuesByLine: T.Dict<boolean>;
-  snippets: T.SourceLine[][];
+  snippets: T.Snippet[];
 }
 
 export default class ComponentSourceSnippetViewer extends React.PureComponent<Props, State> {
   mounted = false;
+  rootNodeRef = React.createRef<HTMLDivElement>();
   state: State = {
     additionalLines: {},
     highlightedSymbols: [],
@@ -80,35 +87,78 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr
   }
 
   createSnippetsFromProps() {
-    const snippets = createSnippets(
-      this.props.snippetGroup.locations,
-      this.props.snippetGroup.sources,
-      this.props.last
-    );
+    const snippets = createSnippets(this.props.snippetGroup.locations, this.props.last);
     this.setState({ snippets });
   }
 
+  getNodes(index: number): { wrapper: HTMLElement; table: HTMLElement } | undefined {
+    const root = this.rootNodeRef.current;
+    if (!root) {
+      return undefined;
+    }
+    const element = root.querySelector(`#snippet-wrapper-${index}`);
+    if (!element) {
+      return undefined;
+    }
+    const wrapper = element.querySelector<HTMLElement>('.snippet');
+    if (!wrapper) {
+      return undefined;
+    }
+    const table = wrapper.firstChild as HTMLElement;
+    if (!table) {
+      return undefined;
+    }
+
+    return { wrapper, table };
+  }
+
+  setMaxHeight(index: number, value?: number, up = false) {
+    const nodes = this.getNodes(index);
+
+    if (!nodes) {
+      return;
+    }
+
+    const { wrapper, table } = nodes;
+
+    const maxHeight = value !== undefined ? value : table.getBoundingClientRect().height;
+
+    if (up) {
+      const startHeight = wrapper.getBoundingClientRect().height;
+      table.style.transition = 'none';
+      table.style.marginTop = `${startHeight - maxHeight}px`;
+
+      // animate!
+      setTimeout(() => {
+        table.style.transition = '';
+        table.style.marginTop = '0px';
+        wrapper.style.maxHeight = `${maxHeight + 20}px`;
+      }, 0);
+    } else {
+      wrapper.style.maxHeight = `${maxHeight + 20}px`;
+    }
+  }
+
   expandBlock = (snippetIndex: number, direction: T.ExpandDirection) => {
     const { branchLike, snippetGroup } = this.props;
     const { key } = snippetGroup.component;
     const { snippets } = this.state;
-
-    const snippet = snippets[snippetIndex];
-
+    const snippet = snippets.find(s => s.index === snippetIndex);
+    if (!snippet) {
+      return;
+    }
     // Extend by EXPAND_BY_LINES and add buffer for merging snippets
     const extension = EXPAND_BY_LINES + MERGE_DISTANCE - 1;
-
     const range =
       direction === 'up'
         ? {
-            from: Math.max(1, snippet[0].line - extension),
-            to: snippet[0].line - 1
+            from: Math.max(1, snippet.start - extension),
+            to: snippet.start - 1
           }
         : {
-            from: snippet[snippet.length - 1].line + 1,
-            to: snippet[snippet.length - 1].line + extension
+            from: snippet.end + 1,
+            to: snippet.end + extension
           };
-
     getSources({
       key,
       ...range,
@@ -122,27 +172,55 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr
         }, {})
       )
       .then(
-        newLinesMapped => {
-          if (this.mounted) {
-            this.setState(({ additionalLines, snippets }) => {
-              const combinedLines = { ...additionalLines, ...newLinesMapped };
-
-              return {
-                additionalLines: combinedLines,
-                snippets: expandSnippet({
-                  direction,
-                  lines: { ...combinedLines, ...this.props.snippetGroup.sources },
-                  snippetIndex,
-                  snippets
-                })
-              };
-            });
-          }
-        },
+        newLinesMapped => this.animateBlockExpansion(snippetIndex, direction, newLinesMapped),
         () => {}
       );
   };
 
+  animateBlockExpansion(
+    snippetIndex: number,
+    direction: T.ExpandDirection,
+    newLinesMapped: T.Dict<T.SourceLine>
+  ) {
+    if (this.mounted) {
+      const { snippets } = this.state;
+
+      const newSnippets = expandSnippet({
+        direction,
+        snippetIndex,
+        snippets
+      });
+
+      const deletedSnippets = newSnippets.filter(s => s.toDelete);
+
+      // set max-height to current height for CSS transitions
+      deletedSnippets.forEach(s => this.setMaxHeight(s.index));
+      this.setMaxHeight(snippetIndex);
+
+      this.setState(
+        ({ additionalLines, snippets }) => {
+          const combinedLines = { ...additionalLines, ...newLinesMapped };
+          return {
+            additionalLines: combinedLines,
+            snippets
+          };
+        },
+        () => {
+          // Set max-height 0 to trigger CSS transitions
+          deletedSnippets.forEach(s => {
+            this.setMaxHeight(s.index, 0);
+          });
+          this.setMaxHeight(snippetIndex, undefined, direction === 'up');
+
+          // Wait for transition to finish before updating dom
+          setTimeout(() => {
+            this.setState({ snippets: newSnippets.filter(s => !s.toDelete) });
+          }, 200);
+        }
+      );
+    }
+  }
+
   expandComponent = () => {
     const { branchLike, snippetGroup } = this.props;
     const { key } = snippetGroup.component;
@@ -152,7 +230,14 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr
     getSources({ key, ...getBranchLikeQuery(branchLike) }).then(
       lines => {
         if (this.mounted) {
-          this.setState({ loading: false, snippets: [lines] });
+          this.setState(({ additionalLines }) => {
+            const combinedLines = { ...additionalLines, ...lines };
+            return {
+              additionalLines: combinedLines,
+              loading: false,
+              snippets: [{ start: 0, end: lines[lines.length - 1].line, index: -1 }]
+            };
+          });
         }
       },
       () => {
@@ -222,7 +307,6 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr
         issue={this.props.issue}
         issuePopup={this.props.issuePopup}
         issuesByLine={issuesByLine}
-        key={index}
         last={last}
         loadDuplications={this.loadDuplications}
         locations={this.props.locations}
@@ -240,19 +324,26 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr
 
   render() {
     const { branchLike, duplications, issue, issuesByLine, last, snippetGroup } = this.props;
-    const { loading, snippets } = this.state;
+    const { additionalLines, loading, snippets } = this.state;
     const locations = locationsByLine([issue]);
 
     const fullyShown =
       snippets.length === 1 &&
       snippetGroup.component.measures &&
-      snippets[0].length === parseInt(snippetGroup.component.measures.lines || '', 10);
+      snippets[0].end - snippets[0].start ===
+        parseInt(snippetGroup.component.measures.lines || '', 10);
+
+    const snippetLines = linesForSnippets(snippets, {
+      ...snippetGroup.sources,
+      ...additionalLines
+    });
 
     return (
       <div
         className={classNames('component-source-container', {
           'source-duplications-expanded': duplications && duplications.length > 0
-        })}>
+        })}
+        ref={this.rootNodeRef}>
         <SourceViewerHeaderSlim
           branchLike={branchLike}
           expandable={!fullyShown}
@@ -260,15 +351,17 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr
           onExpand={this.expandComponent}
           sourceViewerFile={snippetGroup.component}
         />
-        {snippets.map((snippet, index) =>
-          this.renderSnippet({
-            snippet,
-            index,
-            issuesByLine: last ? issuesByLine : {},
-            locationsByLine: last && index === snippets.length - 1 ? locations : {},
-            last: last && index === snippets.length - 1
-          })
-        )}
+        {snippetLines.map((snippet, index) => (
+          <div id={`snippet-wrapper-${snippets[index].index}`} key={snippets[index].index}>
+            {this.renderSnippet({
+              snippet,
+              index: snippets[index].index,
+              issuesByLine: last ? issuesByLine : {},
+              locationsByLine: last && index === snippets.length - 1 ? locations : {},
+              last: last && index === snippets.length - 1
+            })}
+          </div>
+        ))}
       </div>
     );
   }
index daa6af037f4763d87199ebdf65aed0508d4e04c1..91ded7f72fd57bbdd3263115c334161f528eb6ab 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import classNames from 'classnames';
 import ExpandSnippetIcon from 'sonar-ui-common/components/icons/ExpandSnippetIcon';
 import { scrollHorizontally } from 'sonar-ui-common/helpers/scrolling';
 import { translate } from 'sonar-ui-common/helpers/l10n';
@@ -191,46 +192,48 @@ export default class SnippetViewer extends React.PureComponent<Props> {
 
     return (
       <div className="source-viewer-code snippet" ref={this.node}>
-        <table className="source-table">
-          <tbody>
-            {snippet[0].line > 1 && (
-              <tr className="expand-block expand-block-above">
-                <td colSpan={5}>
-                  <button
-                    aria-label={translate('source_viewer.expand_above')}
-                    onClick={this.expandBlock('up')}
-                    type="button">
-                    <ExpandSnippetIcon />
-                  </button>
-                </td>
-              </tr>
-            )}
-            {snippet.map((line, index) =>
-              this.renderLine({
-                displayDuplications,
-                index,
-                issuesForLine: issuesByLine[line.line] || [],
-                issueLocations: locationsByLine[line.line] || [],
-                line,
-                snippet,
-                symbols: symbols[line.line],
-                verticalBuffer: index === snippet.length - 1 ? verticalBuffer : 0
-              })
-            )}
-            {(!lastLine || snippet[snippet.length - 1].line < lastLine) && (
-              <tr className="expand-block expand-block-below">
-                <td colSpan={5}>
-                  <button
-                    aria-label={translate('source_viewer.expand_below')}
-                    onClick={this.expandBlock('down')}
-                    type="button">
-                    <ExpandSnippetIcon />
-                  </button>
-                </td>
-              </tr>
-            )}
-          </tbody>
-        </table>
+        <div>
+          {snippet[0].line > 1 && (
+            <div className="expand-block expand-block-above">
+              <button
+                aria-label={translate('source_viewer.expand_above')}
+                onClick={this.expandBlock('up')}
+                type="button">
+                <ExpandSnippetIcon />
+              </button>
+            </div>
+          )}
+          <table
+            className={classNames('source-table', {
+              'expand-up': snippet[0].line > 1,
+              'expand-down': !lastLine || snippet[snippet.length - 1].line < lastLine
+            })}>
+            <tbody>
+              {snippet.map((line, index) =>
+                this.renderLine({
+                  displayDuplications,
+                  index,
+                  issuesForLine: issuesByLine[line.line] || [],
+                  issueLocations: locationsByLine[line.line] || [],
+                  line,
+                  snippet,
+                  symbols: symbols[line.line],
+                  verticalBuffer: index === snippet.length - 1 ? verticalBuffer : 0
+                })
+              )}
+            </tbody>
+          </table>
+          {(!lastLine || snippet[snippet.length - 1].line < lastLine) && (
+            <div className="expand-block expand-block-below">
+              <button
+                aria-label={translate('source_viewer.expand_below')}
+                onClick={this.expandBlock('down')}
+                type="button">
+                <ExpandSnippetIcon />
+              </button>
+            </div>
+          )}
+        </div>
       </div>
     );
   }
index 2bfa852f381f5b9be7fd00db114d564b7d81cd0a..0b78f105f23471fcbd5e9c418f436a3217b52299 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { shallow } from 'enzyme';
+import { shallow, mount, ReactWrapper } from 'enzyme';
 import { times } from 'lodash';
 import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
 import ComponentSourceSnippetViewer from '../ComponentSourceSnippetViewer';
@@ -70,7 +70,7 @@ it('should expand block', async () => {
 
   expect(getSources).toHaveBeenCalledWith({ from: 19, key: 'a', to: 31 });
   expect(wrapper.state('snippets')).toHaveLength(2);
-  expect(wrapper.state('snippets')[0]).toHaveLength(15);
+  expect(wrapper.state('snippets')[0]).toEqual({ index: 0, start: 22, end: 36 });
   expect(Object.keys(wrapper.state('additionalLines'))).toHaveLength(10);
 });
 
@@ -99,7 +99,7 @@ it('should expand full component', async () => {
 
   expect(getSources).toHaveBeenCalledWith({ key: 'a' });
   expect(wrapper.state('snippets')).toHaveLength(1);
-  expect(wrapper.state('snippets')[0]).toHaveLength(14);
+  expect(wrapper.state('snippets')[0]).toEqual({ index: -1, start: 0, end: 13 });
 });
 
 it('should get the right branch when expanding', async () => {
@@ -190,6 +190,107 @@ it('should correctly handle lines actions', () => {
   );
 });
 
+describe('getNodes', () => {
+  const snippetGroup: T.SnippetGroup = {
+    component: mockSourceViewerFile(),
+    locations: [],
+    sources: []
+  };
+  const wrapper = mount<ComponentSourceSnippetViewer>(
+    <ComponentSourceSnippetViewer
+      branchLike={mockMainBranch()}
+      duplications={undefined}
+      duplicationsByLine={undefined}
+      highlightedLocationMessage={{ index: 0, text: '' }}
+      issue={mockIssue()}
+      issuesByLine={{}}
+      last={false}
+      linePopup={undefined}
+      loadDuplications={jest.fn()}
+      locations={[]}
+      onIssueChange={jest.fn()}
+      onIssuePopupToggle={jest.fn()}
+      onLinePopupToggle={jest.fn()}
+      onLocationSelect={jest.fn()}
+      renderDuplicationPopup={jest.fn()}
+      scroll={jest.fn()}
+      snippetGroup={snippetGroup}
+    />
+  );
+
+  it('should return undefined if any node is missing', async () => {
+    await waitAndUpdate(wrapper);
+    const rootNode = wrapper.instance().rootNodeRef;
+    mockDom(rootNode.current!);
+    expect(wrapper.instance().getNodes(0)).toBeUndefined();
+    expect(wrapper.instance().getNodes(1)).toBeUndefined();
+    expect(wrapper.instance().getNodes(2)).toBeUndefined();
+  });
+
+  it('should return elements if dom is correct', async () => {
+    await waitAndUpdate(wrapper);
+    const rootNode = wrapper.instance().rootNodeRef;
+    mockDom(rootNode.current!);
+    expect(wrapper.instance().getNodes(3)).not.toBeUndefined();
+  });
+});
+
+describe('getHeight', () => {
+  jest.useFakeTimers();
+
+  const snippetGroup: T.SnippetGroup = {
+    component: mockSourceViewerFile(),
+    locations: [],
+    sources: []
+  };
+  const wrapper = mount<ComponentSourceSnippetViewer>(
+    <ComponentSourceSnippetViewer
+      branchLike={mockMainBranch()}
+      duplications={undefined}
+      duplicationsByLine={undefined}
+      highlightedLocationMessage={{ index: 0, text: '' }}
+      issue={mockIssue()}
+      issuesByLine={{}}
+      last={false}
+      linePopup={undefined}
+      loadDuplications={jest.fn()}
+      locations={[]}
+      onIssueChange={jest.fn()}
+      onIssuePopupToggle={jest.fn()}
+      onLinePopupToggle={jest.fn()}
+      onLocationSelect={jest.fn()}
+      renderDuplicationPopup={jest.fn()}
+      scroll={jest.fn()}
+      snippetGroup={snippetGroup}
+    />
+  );
+
+  it('should set maxHeight to current height', async () => {
+    await waitAndUpdate(wrapper);
+
+    const nodes = mockDomForSizes(wrapper, { wrapperHeight: 42, tableHeight: 68 });
+    wrapper.instance().setMaxHeight(0);
+
+    expect(nodes.wrapper.getAttribute('style')).toBe('max-height: 88px;');
+    expect(nodes.table.getAttribute('style')).toBeNull();
+  });
+
+  it('should set margin and then maxHeight for a nice upwards animation', async () => {
+    await waitAndUpdate(wrapper);
+
+    const nodes = mockDomForSizes(wrapper, { wrapperHeight: 42, tableHeight: 68 });
+    wrapper.instance().setMaxHeight(0, undefined, true);
+
+    expect(nodes.wrapper.getAttribute('style')).toBeNull();
+    expect(nodes.table.getAttribute('style')).toBe('transition: none; margin-top: -26px;');
+
+    jest.runAllTimers();
+
+    expect(nodes.wrapper.getAttribute('style')).toBe('max-height: 88px;');
+    expect(nodes.table.getAttribute('style')).toBe('margin-top: 0px;');
+  });
+});
+
 function shallowRender(props: Partial<ComponentSourceSnippetViewer['props']> = {}) {
   const snippetGroup: T.SnippetGroup = {
     component: mockSourceViewerFile(),
@@ -219,3 +320,47 @@ function shallowRender(props: Partial<ComponentSourceSnippetViewer['props']> = {
     />
   );
 }
+
+function mockDom(refNode: HTMLDivElement) {
+  refNode.querySelector = jest.fn(query => {
+    const index = query.split('-').pop();
+
+    switch (index) {
+      case '0':
+        return null;
+      case '1':
+        return mount(<div />).getDOMNode();
+      case '2':
+        return mount(
+          <div>
+            <div className="snippet" />
+          </div>
+        ).getDOMNode();
+      case '3':
+        return mount(
+          <div>
+            <div className="snippet">
+              <div />
+            </div>
+          </div>
+        ).getDOMNode();
+      default:
+        return null;
+    }
+  });
+}
+
+function mockDomForSizes(
+  componentWrapper: ReactWrapper<{}, {}, ComponentSourceSnippetViewer>,
+  { wrapperHeight = 0, tableHeight = 0 }
+) {
+  const wrapper = mount(<div className="snippet" />).getDOMNode();
+  wrapper.getBoundingClientRect = jest.fn().mockReturnValue({ height: wrapperHeight });
+  const table = mount(<div />).getDOMNode();
+  table.getBoundingClientRect = jest.fn().mockReturnValue({ height: tableHeight });
+  componentWrapper.instance().getNodes = jest.fn().mockReturnValue({
+    wrapper,
+    table
+  });
+  return { wrapper, table };
+}
index e2081bb97cb07477f751aa78456bfa419d81ec88..1dccbb712e691934f7209c4d25ffdd5401fdc5d7 100644 (file)
@@ -4,212 +4,206 @@ exports[`should render correctly 1`] = `
 <div
   className="source-viewer-code snippet"
 >
-  <table
-    className="source-table"
-  >
-    <tbody>
-      <tr
-        className="expand-block expand-block-above"
+  <div>
+    <div
+      className="expand-block expand-block-above"
+    >
+      <button
+        aria-label="source_viewer.expand_above"
+        onClick={[Function]}
+        type="button"
       >
-        <td
-          colSpan={5}
-        >
-          <button
-            aria-label="source_viewer.expand_above"
-            onClick={[Function]}
-            type="button"
-          >
-            <ExpandSnippetIcon />
-          </button>
-        </td>
-      </tr>
-      <Line
-        branchLike={
-          Object {
-            "analysisDate": "2018-01-01",
-            "isMain": true,
-            "name": "master",
-          }
-        }
-        displayAllIssues={false}
-        displayCoverage={true}
-        displayDuplications={false}
-        displayIssues={true}
-        displayLocationMarkers={true}
-        duplications={Array []}
-        duplicationsCount={0}
-        highlighted={false}
-        issueLocations={Array []}
-        issues={Array []}
-        key="5"
-        last={false}
-        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": 5,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        loadDuplications={[MockFunction]}
-        onIssueChange={[MockFunction]}
-        onIssuePopupToggle={[MockFunction]}
-        onIssueSelect={[Function]}
-        onIssueUnselect={[Function]}
-        onIssuesClose={[MockFunction]}
-        onIssuesOpen={[MockFunction]}
-        onLinePopupToggle={[MockFunction]}
-        onLocationSelect={[MockFunction]}
-        onSymbolClick={[MockFunction]}
-        renderDuplicationPopup={[MockFunction]}
-        scroll={[Function]}
-        secondaryIssueLocations={Array []}
-        verticalBuffer={0}
-      />
-      <Line
-        branchLike={
-          Object {
-            "analysisDate": "2018-01-01",
-            "isMain": true,
-            "name": "master",
-          }
-        }
-        displayAllIssues={false}
-        displayCoverage={true}
-        displayDuplications={false}
-        displayIssues={true}
-        displayLocationMarkers={true}
-        duplications={Array []}
-        duplicationsCount={0}
-        highlighted={false}
-        issueLocations={Array []}
-        issues={Array []}
-        key="6"
-        last={false}
-        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": 6,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        loadDuplications={[MockFunction]}
-        onIssueChange={[MockFunction]}
-        onIssuePopupToggle={[MockFunction]}
-        onIssueSelect={[Function]}
-        onIssueUnselect={[Function]}
-        onIssuesClose={[MockFunction]}
-        onIssuesOpen={[MockFunction]}
-        onLinePopupToggle={[MockFunction]}
-        onLocationSelect={[MockFunction]}
-        onSymbolClick={[MockFunction]}
-        previousLine={
-          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": 5,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        renderDuplicationPopup={[MockFunction]}
-        scroll={[Function]}
-        secondaryIssueLocations={Array []}
-        verticalBuffer={0}
-      />
-      <Line
-        branchLike={
-          Object {
-            "analysisDate": "2018-01-01",
-            "isMain": true,
-            "name": "master",
-          }
-        }
-        displayAllIssues={false}
-        displayCoverage={true}
-        displayDuplications={false}
-        displayIssues={true}
-        displayLocationMarkers={true}
-        duplications={Array []}
-        duplicationsCount={0}
-        highlighted={false}
-        issueLocations={Array []}
-        issues={Array []}
-        key="7"
-        last={false}
-        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": 7,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        loadDuplications={[MockFunction]}
-        onIssueChange={[MockFunction]}
-        onIssuePopupToggle={[MockFunction]}
-        onIssueSelect={[Function]}
-        onIssueUnselect={[Function]}
-        onIssuesClose={[MockFunction]}
-        onIssuesOpen={[MockFunction]}
-        onLinePopupToggle={[MockFunction]}
-        onLocationSelect={[MockFunction]}
-        onSymbolClick={[MockFunction]}
-        previousLine={
-          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": 6,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        renderDuplicationPopup={[MockFunction]}
-        scroll={[Function]}
-        secondaryIssueLocations={Array []}
-        verticalBuffer={0}
-      />
-      <tr
-        className="expand-block expand-block-below"
+        <ExpandSnippetIcon />
+      </button>
+    </div>
+    <table
+      className="source-table expand-up expand-down"
+    >
+      <tbody>
+        <Line
+          branchLike={
+            Object {
+              "analysisDate": "2018-01-01",
+              "isMain": true,
+              "name": "master",
+            }
+          }
+          displayAllIssues={false}
+          displayCoverage={true}
+          displayDuplications={false}
+          displayIssues={true}
+          displayLocationMarkers={true}
+          duplications={Array []}
+          duplicationsCount={0}
+          highlighted={false}
+          issueLocations={Array []}
+          issues={Array []}
+          key="5"
+          last={false}
+          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": 5,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          loadDuplications={[MockFunction]}
+          onIssueChange={[MockFunction]}
+          onIssuePopupToggle={[MockFunction]}
+          onIssueSelect={[Function]}
+          onIssueUnselect={[Function]}
+          onIssuesClose={[MockFunction]}
+          onIssuesOpen={[MockFunction]}
+          onLinePopupToggle={[MockFunction]}
+          onLocationSelect={[MockFunction]}
+          onSymbolClick={[MockFunction]}
+          renderDuplicationPopup={[MockFunction]}
+          scroll={[Function]}
+          secondaryIssueLocations={Array []}
+          verticalBuffer={0}
+        />
+        <Line
+          branchLike={
+            Object {
+              "analysisDate": "2018-01-01",
+              "isMain": true,
+              "name": "master",
+            }
+          }
+          displayAllIssues={false}
+          displayCoverage={true}
+          displayDuplications={false}
+          displayIssues={true}
+          displayLocationMarkers={true}
+          duplications={Array []}
+          duplicationsCount={0}
+          highlighted={false}
+          issueLocations={Array []}
+          issues={Array []}
+          key="6"
+          last={false}
+          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": 6,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          loadDuplications={[MockFunction]}
+          onIssueChange={[MockFunction]}
+          onIssuePopupToggle={[MockFunction]}
+          onIssueSelect={[Function]}
+          onIssueUnselect={[Function]}
+          onIssuesClose={[MockFunction]}
+          onIssuesOpen={[MockFunction]}
+          onLinePopupToggle={[MockFunction]}
+          onLocationSelect={[MockFunction]}
+          onSymbolClick={[MockFunction]}
+          previousLine={
+            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": 5,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          renderDuplicationPopup={[MockFunction]}
+          scroll={[Function]}
+          secondaryIssueLocations={Array []}
+          verticalBuffer={0}
+        />
+        <Line
+          branchLike={
+            Object {
+              "analysisDate": "2018-01-01",
+              "isMain": true,
+              "name": "master",
+            }
+          }
+          displayAllIssues={false}
+          displayCoverage={true}
+          displayDuplications={false}
+          displayIssues={true}
+          displayLocationMarkers={true}
+          duplications={Array []}
+          duplicationsCount={0}
+          highlighted={false}
+          issueLocations={Array []}
+          issues={Array []}
+          key="7"
+          last={false}
+          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": 7,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          loadDuplications={[MockFunction]}
+          onIssueChange={[MockFunction]}
+          onIssuePopupToggle={[MockFunction]}
+          onIssueSelect={[Function]}
+          onIssueUnselect={[Function]}
+          onIssuesClose={[MockFunction]}
+          onIssuesOpen={[MockFunction]}
+          onLinePopupToggle={[MockFunction]}
+          onLocationSelect={[MockFunction]}
+          onSymbolClick={[MockFunction]}
+          previousLine={
+            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": 6,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          renderDuplicationPopup={[MockFunction]}
+          scroll={[Function]}
+          secondaryIssueLocations={Array []}
+          verticalBuffer={0}
+        />
+      </tbody>
+    </table>
+    <div
+      className="expand-block expand-block-below"
+    >
+      <button
+        aria-label="source_viewer.expand_below"
+        onClick={[Function]}
+        type="button"
       >
-        <td
-          colSpan={5}
-        >
-          <button
-            aria-label="source_viewer.expand_below"
-            onClick={[Function]}
-            type="button"
-          >
-            <ExpandSnippetIcon />
-          </button>
-        </td>
-      </tr>
-    </tbody>
-  </table>
+        <ExpandSnippetIcon />
+      </button>
+    </div>
+  </div>
 </div>
 `;
 
@@ -217,273 +211,267 @@ exports[`should render correctly when at the bottom of the file 1`] = `
 <div
   className="source-viewer-code snippet"
 >
-  <table
-    className="source-table"
-  >
-    <tbody>
-      <tr
-        className="expand-block expand-block-above"
+  <div>
+    <div
+      className="expand-block expand-block-above"
+    >
+      <button
+        aria-label="source_viewer.expand_above"
+        onClick={[Function]}
+        type="button"
       >
-        <td
-          colSpan={5}
-        >
-          <button
-            aria-label="source_viewer.expand_above"
-            onClick={[Function]}
-            type="button"
-          >
-            <ExpandSnippetIcon />
-          </button>
-        </td>
-      </tr>
-      <Line
-        branchLike={
-          Object {
-            "analysisDate": "2018-01-01",
-            "isMain": true,
-            "name": "master",
-          }
-        }
-        displayAllIssues={false}
-        displayCoverage={true}
-        displayDuplications={false}
-        displayIssues={true}
-        displayLocationMarkers={true}
-        duplications={Array []}
-        duplicationsCount={0}
-        highlighted={false}
-        issueLocations={Array []}
-        issues={Array []}
-        key="10"
-        last={false}
-        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": 10,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        loadDuplications={[MockFunction]}
-        onIssueChange={[MockFunction]}
-        onIssuePopupToggle={[MockFunction]}
-        onIssueSelect={[Function]}
-        onIssueUnselect={[Function]}
-        onIssuesClose={[MockFunction]}
-        onIssuesOpen={[MockFunction]}
-        onLinePopupToggle={[MockFunction]}
-        onLocationSelect={[MockFunction]}
-        onSymbolClick={[MockFunction]}
-        renderDuplicationPopup={[MockFunction]}
-        scroll={[Function]}
-        secondaryIssueLocations={Array []}
-        verticalBuffer={0}
-      />
-      <Line
-        branchLike={
-          Object {
-            "analysisDate": "2018-01-01",
-            "isMain": true,
-            "name": "master",
-          }
-        }
-        displayAllIssues={false}
-        displayCoverage={true}
-        displayDuplications={false}
-        displayIssues={true}
-        displayLocationMarkers={true}
-        duplications={Array []}
-        duplicationsCount={0}
-        highlighted={false}
-        issueLocations={Array []}
-        issues={Array []}
-        key="11"
-        last={false}
-        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": 11,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        loadDuplications={[MockFunction]}
-        onIssueChange={[MockFunction]}
-        onIssuePopupToggle={[MockFunction]}
-        onIssueSelect={[Function]}
-        onIssueUnselect={[Function]}
-        onIssuesClose={[MockFunction]}
-        onIssuesOpen={[MockFunction]}
-        onLinePopupToggle={[MockFunction]}
-        onLocationSelect={[MockFunction]}
-        onSymbolClick={[MockFunction]}
-        previousLine={
-          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": 10,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        renderDuplicationPopup={[MockFunction]}
-        scroll={[Function]}
-        secondaryIssueLocations={Array []}
-        verticalBuffer={0}
-      />
-      <Line
-        branchLike={
-          Object {
-            "analysisDate": "2018-01-01",
-            "isMain": true,
-            "name": "master",
-          }
-        }
-        displayAllIssues={false}
-        displayCoverage={true}
-        displayDuplications={false}
-        displayIssues={true}
-        displayLocationMarkers={true}
-        duplications={Array []}
-        duplicationsCount={0}
-        highlighted={false}
-        issueLocations={Array []}
-        issues={Array []}
-        key="12"
-        last={false}
-        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": 12,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        loadDuplications={[MockFunction]}
-        onIssueChange={[MockFunction]}
-        onIssuePopupToggle={[MockFunction]}
-        onIssueSelect={[Function]}
-        onIssueUnselect={[Function]}
-        onIssuesClose={[MockFunction]}
-        onIssuesOpen={[MockFunction]}
-        onLinePopupToggle={[MockFunction]}
-        onLocationSelect={[MockFunction]}
-        onSymbolClick={[MockFunction]}
-        previousLine={
-          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": 11,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        renderDuplicationPopup={[MockFunction]}
-        scroll={[Function]}
-        secondaryIssueLocations={Array []}
-        verticalBuffer={0}
-      />
-      <Line
-        branchLike={
-          Object {
-            "analysisDate": "2018-01-01",
-            "isMain": true,
-            "name": "master",
-          }
-        }
-        displayAllIssues={false}
-        displayCoverage={true}
-        displayDuplications={false}
-        displayIssues={true}
-        displayLocationMarkers={true}
-        duplications={Array []}
-        duplicationsCount={0}
-        highlighted={false}
-        issueLocations={Array []}
-        issues={Array []}
-        key="13"
-        last={false}
-        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": 13,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        loadDuplications={[MockFunction]}
-        onIssueChange={[MockFunction]}
-        onIssuePopupToggle={[MockFunction]}
-        onIssueSelect={[Function]}
-        onIssueUnselect={[Function]}
-        onIssuesClose={[MockFunction]}
-        onIssuesOpen={[MockFunction]}
-        onLinePopupToggle={[MockFunction]}
-        onLocationSelect={[MockFunction]}
-        onSymbolClick={[MockFunction]}
-        previousLine={
-          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": 12,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        renderDuplicationPopup={[MockFunction]}
-        scroll={[Function]}
-        secondaryIssueLocations={Array []}
-        verticalBuffer={0}
-      />
-      <tr
-        className="expand-block expand-block-below"
+        <ExpandSnippetIcon />
+      </button>
+    </div>
+    <table
+      className="source-table expand-up expand-down"
+    >
+      <tbody>
+        <Line
+          branchLike={
+            Object {
+              "analysisDate": "2018-01-01",
+              "isMain": true,
+              "name": "master",
+            }
+          }
+          displayAllIssues={false}
+          displayCoverage={true}
+          displayDuplications={false}
+          displayIssues={true}
+          displayLocationMarkers={true}
+          duplications={Array []}
+          duplicationsCount={0}
+          highlighted={false}
+          issueLocations={Array []}
+          issues={Array []}
+          key="10"
+          last={false}
+          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": 10,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          loadDuplications={[MockFunction]}
+          onIssueChange={[MockFunction]}
+          onIssuePopupToggle={[MockFunction]}
+          onIssueSelect={[Function]}
+          onIssueUnselect={[Function]}
+          onIssuesClose={[MockFunction]}
+          onIssuesOpen={[MockFunction]}
+          onLinePopupToggle={[MockFunction]}
+          onLocationSelect={[MockFunction]}
+          onSymbolClick={[MockFunction]}
+          renderDuplicationPopup={[MockFunction]}
+          scroll={[Function]}
+          secondaryIssueLocations={Array []}
+          verticalBuffer={0}
+        />
+        <Line
+          branchLike={
+            Object {
+              "analysisDate": "2018-01-01",
+              "isMain": true,
+              "name": "master",
+            }
+          }
+          displayAllIssues={false}
+          displayCoverage={true}
+          displayDuplications={false}
+          displayIssues={true}
+          displayLocationMarkers={true}
+          duplications={Array []}
+          duplicationsCount={0}
+          highlighted={false}
+          issueLocations={Array []}
+          issues={Array []}
+          key="11"
+          last={false}
+          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": 11,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          loadDuplications={[MockFunction]}
+          onIssueChange={[MockFunction]}
+          onIssuePopupToggle={[MockFunction]}
+          onIssueSelect={[Function]}
+          onIssueUnselect={[Function]}
+          onIssuesClose={[MockFunction]}
+          onIssuesOpen={[MockFunction]}
+          onLinePopupToggle={[MockFunction]}
+          onLocationSelect={[MockFunction]}
+          onSymbolClick={[MockFunction]}
+          previousLine={
+            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": 10,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          renderDuplicationPopup={[MockFunction]}
+          scroll={[Function]}
+          secondaryIssueLocations={Array []}
+          verticalBuffer={0}
+        />
+        <Line
+          branchLike={
+            Object {
+              "analysisDate": "2018-01-01",
+              "isMain": true,
+              "name": "master",
+            }
+          }
+          displayAllIssues={false}
+          displayCoverage={true}
+          displayDuplications={false}
+          displayIssues={true}
+          displayLocationMarkers={true}
+          duplications={Array []}
+          duplicationsCount={0}
+          highlighted={false}
+          issueLocations={Array []}
+          issues={Array []}
+          key="12"
+          last={false}
+          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": 12,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          loadDuplications={[MockFunction]}
+          onIssueChange={[MockFunction]}
+          onIssuePopupToggle={[MockFunction]}
+          onIssueSelect={[Function]}
+          onIssueUnselect={[Function]}
+          onIssuesClose={[MockFunction]}
+          onIssuesOpen={[MockFunction]}
+          onLinePopupToggle={[MockFunction]}
+          onLocationSelect={[MockFunction]}
+          onSymbolClick={[MockFunction]}
+          previousLine={
+            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": 11,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          renderDuplicationPopup={[MockFunction]}
+          scroll={[Function]}
+          secondaryIssueLocations={Array []}
+          verticalBuffer={0}
+        />
+        <Line
+          branchLike={
+            Object {
+              "analysisDate": "2018-01-01",
+              "isMain": true,
+              "name": "master",
+            }
+          }
+          displayAllIssues={false}
+          displayCoverage={true}
+          displayDuplications={false}
+          displayIssues={true}
+          displayLocationMarkers={true}
+          duplications={Array []}
+          duplicationsCount={0}
+          highlighted={false}
+          issueLocations={Array []}
+          issues={Array []}
+          key="13"
+          last={false}
+          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": 13,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          loadDuplications={[MockFunction]}
+          onIssueChange={[MockFunction]}
+          onIssuePopupToggle={[MockFunction]}
+          onIssueSelect={[Function]}
+          onIssueUnselect={[Function]}
+          onIssuesClose={[MockFunction]}
+          onIssuesOpen={[MockFunction]}
+          onLinePopupToggle={[MockFunction]}
+          onLocationSelect={[MockFunction]}
+          onSymbolClick={[MockFunction]}
+          previousLine={
+            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": 12,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          renderDuplicationPopup={[MockFunction]}
+          scroll={[Function]}
+          secondaryIssueLocations={Array []}
+          verticalBuffer={0}
+        />
+      </tbody>
+    </table>
+    <div
+      className="expand-block expand-block-below"
+    >
+      <button
+        aria-label="source_viewer.expand_below"
+        onClick={[Function]}
+        type="button"
       >
-        <td
-          colSpan={5}
-        >
-          <button
-            aria-label="source_viewer.expand_below"
-            onClick={[Function]}
-            type="button"
-          >
-            <ExpandSnippetIcon />
-          </button>
-        </td>
-      </tr>
-    </tbody>
-  </table>
+        <ExpandSnippetIcon />
+      </button>
+    </div>
+  </div>
 </div>
 `;
 
@@ -491,440 +479,438 @@ exports[`should render correctly when at the top of the file 1`] = `
 <div
   className="source-viewer-code snippet"
 >
-  <table
-    className="source-table"
-  >
-    <tbody>
-      <Line
-        branchLike={
-          Object {
-            "analysisDate": "2018-01-01",
-            "isMain": true,
-            "name": "master",
-          }
-        }
-        displayAllIssues={false}
-        displayCoverage={true}
-        displayDuplications={false}
-        displayIssues={true}
-        displayLocationMarkers={true}
-        duplications={Array []}
-        duplicationsCount={0}
-        highlighted={false}
-        issueLocations={Array []}
-        issues={Array []}
-        key="1"
-        last={false}
-        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": 1,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        loadDuplications={[MockFunction]}
-        onIssueChange={[MockFunction]}
-        onIssuePopupToggle={[MockFunction]}
-        onIssueSelect={[Function]}
-        onIssueUnselect={[Function]}
-        onIssuesClose={[MockFunction]}
-        onIssuesOpen={[MockFunction]}
-        onLinePopupToggle={[MockFunction]}
-        onLocationSelect={[MockFunction]}
-        onSymbolClick={[MockFunction]}
-        renderDuplicationPopup={[MockFunction]}
-        scroll={[Function]}
-        secondaryIssueLocations={Array []}
-        verticalBuffer={0}
-      />
-      <Line
-        branchLike={
-          Object {
-            "analysisDate": "2018-01-01",
-            "isMain": true,
-            "name": "master",
-          }
-        }
-        displayAllIssues={false}
-        displayCoverage={true}
-        displayDuplications={false}
-        displayIssues={true}
-        displayLocationMarkers={true}
-        duplications={Array []}
-        duplicationsCount={0}
-        highlighted={false}
-        issueLocations={Array []}
-        issues={Array []}
-        key="2"
-        last={false}
-        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": 2,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        loadDuplications={[MockFunction]}
-        onIssueChange={[MockFunction]}
-        onIssuePopupToggle={[MockFunction]}
-        onIssueSelect={[Function]}
-        onIssueUnselect={[Function]}
-        onIssuesClose={[MockFunction]}
-        onIssuesOpen={[MockFunction]}
-        onLinePopupToggle={[MockFunction]}
-        onLocationSelect={[MockFunction]}
-        onSymbolClick={[MockFunction]}
-        previousLine={
-          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": 1,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        renderDuplicationPopup={[MockFunction]}
-        scroll={[Function]}
-        secondaryIssueLocations={Array []}
-        verticalBuffer={0}
-      />
-      <Line
-        branchLike={
-          Object {
-            "analysisDate": "2018-01-01",
-            "isMain": true,
-            "name": "master",
-          }
-        }
-        displayAllIssues={false}
-        displayCoverage={true}
-        displayDuplications={false}
-        displayIssues={true}
-        displayLocationMarkers={true}
-        duplications={Array []}
-        duplicationsCount={0}
-        highlighted={false}
-        issueLocations={Array []}
-        issues={Array []}
-        key="3"
-        last={false}
-        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": 3,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        loadDuplications={[MockFunction]}
-        onIssueChange={[MockFunction]}
-        onIssuePopupToggle={[MockFunction]}
-        onIssueSelect={[Function]}
-        onIssueUnselect={[Function]}
-        onIssuesClose={[MockFunction]}
-        onIssuesOpen={[MockFunction]}
-        onLinePopupToggle={[MockFunction]}
-        onLocationSelect={[MockFunction]}
-        onSymbolClick={[MockFunction]}
-        previousLine={
-          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": 2,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        renderDuplicationPopup={[MockFunction]}
-        scroll={[Function]}
-        secondaryIssueLocations={Array []}
-        verticalBuffer={0}
-      />
-      <Line
-        branchLike={
-          Object {
-            "analysisDate": "2018-01-01",
-            "isMain": true,
-            "name": "master",
-          }
-        }
-        displayAllIssues={false}
-        displayCoverage={true}
-        displayDuplications={false}
-        displayIssues={true}
-        displayLocationMarkers={true}
-        duplications={Array []}
-        duplicationsCount={0}
-        highlighted={false}
-        issueLocations={Array []}
-        issues={Array []}
-        key="4"
-        last={false}
-        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": 4,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        loadDuplications={[MockFunction]}
-        onIssueChange={[MockFunction]}
-        onIssuePopupToggle={[MockFunction]}
-        onIssueSelect={[Function]}
-        onIssueUnselect={[Function]}
-        onIssuesClose={[MockFunction]}
-        onIssuesOpen={[MockFunction]}
-        onLinePopupToggle={[MockFunction]}
-        onLocationSelect={[MockFunction]}
-        onSymbolClick={[MockFunction]}
-        previousLine={
-          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": 3,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        renderDuplicationPopup={[MockFunction]}
-        scroll={[Function]}
-        secondaryIssueLocations={Array []}
-        verticalBuffer={0}
-      />
-      <Line
-        branchLike={
-          Object {
-            "analysisDate": "2018-01-01",
-            "isMain": true,
-            "name": "master",
-          }
-        }
-        displayAllIssues={false}
-        displayCoverage={true}
-        displayDuplications={false}
-        displayIssues={true}
-        displayLocationMarkers={true}
-        duplications={Array []}
-        duplicationsCount={0}
-        highlighted={false}
-        issueLocations={Array []}
-        issues={Array []}
-        key="5"
-        last={false}
-        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": 5,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        loadDuplications={[MockFunction]}
-        onIssueChange={[MockFunction]}
-        onIssuePopupToggle={[MockFunction]}
-        onIssueSelect={[Function]}
-        onIssueUnselect={[Function]}
-        onIssuesClose={[MockFunction]}
-        onIssuesOpen={[MockFunction]}
-        onLinePopupToggle={[MockFunction]}
-        onLocationSelect={[MockFunction]}
-        onSymbolClick={[MockFunction]}
-        previousLine={
-          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": 4,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        renderDuplicationPopup={[MockFunction]}
-        scroll={[Function]}
-        secondaryIssueLocations={Array []}
-        verticalBuffer={0}
-      />
-      <Line
-        branchLike={
-          Object {
-            "analysisDate": "2018-01-01",
-            "isMain": true,
-            "name": "master",
-          }
-        }
-        displayAllIssues={false}
-        displayCoverage={true}
-        displayDuplications={false}
-        displayIssues={true}
-        displayLocationMarkers={true}
-        duplications={Array []}
-        duplicationsCount={0}
-        highlighted={false}
-        issueLocations={Array []}
-        issues={Array []}
-        key="6"
-        last={false}
-        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": 6,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        loadDuplications={[MockFunction]}
-        onIssueChange={[MockFunction]}
-        onIssuePopupToggle={[MockFunction]}
-        onIssueSelect={[Function]}
-        onIssueUnselect={[Function]}
-        onIssuesClose={[MockFunction]}
-        onIssuesOpen={[MockFunction]}
-        onLinePopupToggle={[MockFunction]}
-        onLocationSelect={[MockFunction]}
-        onSymbolClick={[MockFunction]}
-        previousLine={
-          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": 5,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        renderDuplicationPopup={[MockFunction]}
-        scroll={[Function]}
-        secondaryIssueLocations={Array []}
-        verticalBuffer={0}
-      />
-      <Line
-        branchLike={
-          Object {
-            "analysisDate": "2018-01-01",
-            "isMain": true,
-            "name": "master",
-          }
-        }
-        displayAllIssues={false}
-        displayCoverage={true}
-        displayDuplications={false}
-        displayIssues={true}
-        displayLocationMarkers={true}
-        duplications={Array []}
-        duplicationsCount={0}
-        highlighted={false}
-        issueLocations={Array []}
-        issues={Array []}
-        key="7"
-        last={false}
-        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": 7,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        loadDuplications={[MockFunction]}
-        onIssueChange={[MockFunction]}
-        onIssuePopupToggle={[MockFunction]}
-        onIssueSelect={[Function]}
-        onIssueUnselect={[Function]}
-        onIssuesClose={[MockFunction]}
-        onIssuesOpen={[MockFunction]}
-        onLinePopupToggle={[MockFunction]}
-        onLocationSelect={[MockFunction]}
-        onSymbolClick={[MockFunction]}
-        previousLine={
-          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": 6,
-            "scmAuthor": "simon.brandhof@sonarsource.com",
-            "scmDate": "2018-12-11T10:48:39+0100",
-            "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
-          }
-        }
-        renderDuplicationPopup={[MockFunction]}
-        scroll={[Function]}
-        secondaryIssueLocations={Array []}
-        verticalBuffer={0}
-      />
-      <tr
-        className="expand-block expand-block-below"
+  <div>
+    <table
+      className="source-table expand-down"
+    >
+      <tbody>
+        <Line
+          branchLike={
+            Object {
+              "analysisDate": "2018-01-01",
+              "isMain": true,
+              "name": "master",
+            }
+          }
+          displayAllIssues={false}
+          displayCoverage={true}
+          displayDuplications={false}
+          displayIssues={true}
+          displayLocationMarkers={true}
+          duplications={Array []}
+          duplicationsCount={0}
+          highlighted={false}
+          issueLocations={Array []}
+          issues={Array []}
+          key="1"
+          last={false}
+          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": 1,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          loadDuplications={[MockFunction]}
+          onIssueChange={[MockFunction]}
+          onIssuePopupToggle={[MockFunction]}
+          onIssueSelect={[Function]}
+          onIssueUnselect={[Function]}
+          onIssuesClose={[MockFunction]}
+          onIssuesOpen={[MockFunction]}
+          onLinePopupToggle={[MockFunction]}
+          onLocationSelect={[MockFunction]}
+          onSymbolClick={[MockFunction]}
+          renderDuplicationPopup={[MockFunction]}
+          scroll={[Function]}
+          secondaryIssueLocations={Array []}
+          verticalBuffer={0}
+        />
+        <Line
+          branchLike={
+            Object {
+              "analysisDate": "2018-01-01",
+              "isMain": true,
+              "name": "master",
+            }
+          }
+          displayAllIssues={false}
+          displayCoverage={true}
+          displayDuplications={false}
+          displayIssues={true}
+          displayLocationMarkers={true}
+          duplications={Array []}
+          duplicationsCount={0}
+          highlighted={false}
+          issueLocations={Array []}
+          issues={Array []}
+          key="2"
+          last={false}
+          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": 2,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          loadDuplications={[MockFunction]}
+          onIssueChange={[MockFunction]}
+          onIssuePopupToggle={[MockFunction]}
+          onIssueSelect={[Function]}
+          onIssueUnselect={[Function]}
+          onIssuesClose={[MockFunction]}
+          onIssuesOpen={[MockFunction]}
+          onLinePopupToggle={[MockFunction]}
+          onLocationSelect={[MockFunction]}
+          onSymbolClick={[MockFunction]}
+          previousLine={
+            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": 1,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          renderDuplicationPopup={[MockFunction]}
+          scroll={[Function]}
+          secondaryIssueLocations={Array []}
+          verticalBuffer={0}
+        />
+        <Line
+          branchLike={
+            Object {
+              "analysisDate": "2018-01-01",
+              "isMain": true,
+              "name": "master",
+            }
+          }
+          displayAllIssues={false}
+          displayCoverage={true}
+          displayDuplications={false}
+          displayIssues={true}
+          displayLocationMarkers={true}
+          duplications={Array []}
+          duplicationsCount={0}
+          highlighted={false}
+          issueLocations={Array []}
+          issues={Array []}
+          key="3"
+          last={false}
+          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": 3,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          loadDuplications={[MockFunction]}
+          onIssueChange={[MockFunction]}
+          onIssuePopupToggle={[MockFunction]}
+          onIssueSelect={[Function]}
+          onIssueUnselect={[Function]}
+          onIssuesClose={[MockFunction]}
+          onIssuesOpen={[MockFunction]}
+          onLinePopupToggle={[MockFunction]}
+          onLocationSelect={[MockFunction]}
+          onSymbolClick={[MockFunction]}
+          previousLine={
+            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": 2,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          renderDuplicationPopup={[MockFunction]}
+          scroll={[Function]}
+          secondaryIssueLocations={Array []}
+          verticalBuffer={0}
+        />
+        <Line
+          branchLike={
+            Object {
+              "analysisDate": "2018-01-01",
+              "isMain": true,
+              "name": "master",
+            }
+          }
+          displayAllIssues={false}
+          displayCoverage={true}
+          displayDuplications={false}
+          displayIssues={true}
+          displayLocationMarkers={true}
+          duplications={Array []}
+          duplicationsCount={0}
+          highlighted={false}
+          issueLocations={Array []}
+          issues={Array []}
+          key="4"
+          last={false}
+          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": 4,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          loadDuplications={[MockFunction]}
+          onIssueChange={[MockFunction]}
+          onIssuePopupToggle={[MockFunction]}
+          onIssueSelect={[Function]}
+          onIssueUnselect={[Function]}
+          onIssuesClose={[MockFunction]}
+          onIssuesOpen={[MockFunction]}
+          onLinePopupToggle={[MockFunction]}
+          onLocationSelect={[MockFunction]}
+          onSymbolClick={[MockFunction]}
+          previousLine={
+            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": 3,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          renderDuplicationPopup={[MockFunction]}
+          scroll={[Function]}
+          secondaryIssueLocations={Array []}
+          verticalBuffer={0}
+        />
+        <Line
+          branchLike={
+            Object {
+              "analysisDate": "2018-01-01",
+              "isMain": true,
+              "name": "master",
+            }
+          }
+          displayAllIssues={false}
+          displayCoverage={true}
+          displayDuplications={false}
+          displayIssues={true}
+          displayLocationMarkers={true}
+          duplications={Array []}
+          duplicationsCount={0}
+          highlighted={false}
+          issueLocations={Array []}
+          issues={Array []}
+          key="5"
+          last={false}
+          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": 5,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          loadDuplications={[MockFunction]}
+          onIssueChange={[MockFunction]}
+          onIssuePopupToggle={[MockFunction]}
+          onIssueSelect={[Function]}
+          onIssueUnselect={[Function]}
+          onIssuesClose={[MockFunction]}
+          onIssuesOpen={[MockFunction]}
+          onLinePopupToggle={[MockFunction]}
+          onLocationSelect={[MockFunction]}
+          onSymbolClick={[MockFunction]}
+          previousLine={
+            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": 4,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          renderDuplicationPopup={[MockFunction]}
+          scroll={[Function]}
+          secondaryIssueLocations={Array []}
+          verticalBuffer={0}
+        />
+        <Line
+          branchLike={
+            Object {
+              "analysisDate": "2018-01-01",
+              "isMain": true,
+              "name": "master",
+            }
+          }
+          displayAllIssues={false}
+          displayCoverage={true}
+          displayDuplications={false}
+          displayIssues={true}
+          displayLocationMarkers={true}
+          duplications={Array []}
+          duplicationsCount={0}
+          highlighted={false}
+          issueLocations={Array []}
+          issues={Array []}
+          key="6"
+          last={false}
+          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": 6,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          loadDuplications={[MockFunction]}
+          onIssueChange={[MockFunction]}
+          onIssuePopupToggle={[MockFunction]}
+          onIssueSelect={[Function]}
+          onIssueUnselect={[Function]}
+          onIssuesClose={[MockFunction]}
+          onIssuesOpen={[MockFunction]}
+          onLinePopupToggle={[MockFunction]}
+          onLocationSelect={[MockFunction]}
+          onSymbolClick={[MockFunction]}
+          previousLine={
+            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": 5,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          renderDuplicationPopup={[MockFunction]}
+          scroll={[Function]}
+          secondaryIssueLocations={Array []}
+          verticalBuffer={0}
+        />
+        <Line
+          branchLike={
+            Object {
+              "analysisDate": "2018-01-01",
+              "isMain": true,
+              "name": "master",
+            }
+          }
+          displayAllIssues={false}
+          displayCoverage={true}
+          displayDuplications={false}
+          displayIssues={true}
+          displayLocationMarkers={true}
+          duplications={Array []}
+          duplicationsCount={0}
+          highlighted={false}
+          issueLocations={Array []}
+          issues={Array []}
+          key="7"
+          last={false}
+          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": 7,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          loadDuplications={[MockFunction]}
+          onIssueChange={[MockFunction]}
+          onIssuePopupToggle={[MockFunction]}
+          onIssueSelect={[Function]}
+          onIssueUnselect={[Function]}
+          onIssuesClose={[MockFunction]}
+          onIssuesOpen={[MockFunction]}
+          onLinePopupToggle={[MockFunction]}
+          onLocationSelect={[MockFunction]}
+          onSymbolClick={[MockFunction]}
+          previousLine={
+            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": 6,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            }
+          }
+          renderDuplicationPopup={[MockFunction]}
+          scroll={[Function]}
+          secondaryIssueLocations={Array []}
+          verticalBuffer={0}
+        />
+      </tbody>
+    </table>
+    <div
+      className="expand-block expand-block-below"
+    >
+      <button
+        aria-label="source_viewer.expand_below"
+        onClick={[Function]}
+        type="button"
       >
-        <td
-          colSpan={5}
-        >
-          <button
-            aria-label="source_viewer.expand_below"
-            onClick={[Function]}
-            type="button"
-          >
-            <ExpandSnippetIcon />
-          </button>
-        </td>
-      </tr>
-    </tbody>
-  </table>
+        <ExpandSnippetIcon />
+      </button>
+    </div>
+  </div>
 </div>
 `;
index ad114d760a39ff38106dc5a539ace68dc870cf59..7f93c687f3ab2f73822367ac3cbb90153ada4fba 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.
  */
-import { keyBy, range } from 'lodash';
 import { groupLocationsByComponent, createSnippets, expandSnippet } from '../utils';
-import {
-  mockFlowLocation,
-  mockSnippetsByComponent,
-  mockSourceLine
-} from '../../../../helpers/testMocks';
+import { mockFlowLocation, mockSnippetsByComponent } from '../../../../helpers/testMocks';
 
 describe('groupLocationsByComponent', () => {
   it('should handle empty args', () => {
@@ -92,12 +87,11 @@ describe('createSnippets', () => {
           textRange: { startLine: 19, startOffset: 2, endLine: 19, endOffset: 3 }
         })
       ],
-      mockSnippetsByComponent('', [14, 15, 16, 17, 18, 19, 20, 21, 22]).sources,
       false
     );
 
     expect(results).toHaveLength(1);
-    expect(results[0]).toHaveLength(8);
+    expect(results[0]).toEqual({ index: 0, start: 14, end: 21 });
   });
 
   it('should merge snippets correctly, even when not in sequence', () => {
@@ -113,13 +107,12 @@ describe('createSnippets', () => {
           textRange: { startLine: 14, startOffset: 2, endLine: 14, endOffset: 3 }
         })
       ],
-      mockSnippetsByComponent('', [12, 13, 14, 15, 16, 17, 18, 45, 46, 47, 48, 49]).sources,
       false
     );
 
     expect(results).toHaveLength(2);
-    expect(results[0]).toHaveLength(7);
-    expect(results[1]).toHaveLength(5);
+    expect(results[0]).toEqual({ index: 0, start: 12, end: 18 });
+    expect(results[1]).toEqual({ index: 1, start: 45, end: 49 });
   });
 
   it('should merge three snippets together', () => {
@@ -138,56 +131,46 @@ describe('createSnippets', () => {
           textRange: { startLine: 18, startOffset: 2, endLine: 18, endOffset: 3 }
         })
       ],
-      mockSnippetsByComponent('', [14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 45, 46, 47, 48, 49])
-        .sources,
       false
     );
 
     expect(results).toHaveLength(2);
-    expect(results[0]).toHaveLength(11);
-    expect(results[1]).toHaveLength(5);
+    expect(results[0]).toEqual({ index: 0, start: 14, end: 24 });
+    expect(results[1]).toEqual({ index: 1, start: 45, end: 49 });
   });
 });
 
 describe('expandSnippet', () => {
   it('should add lines above', () => {
-    const lines = keyBy(range(4, 19).map(line => mockSourceLine({ line })), 'line');
-    const snippets = [[lines[14], lines[15], lines[16], lines[17], lines[18]]];
+    const snippets = [{ start: 14, end: 18, index: 0 }];
 
-    const result = expandSnippet({ direction: 'up', lines, snippetIndex: 0, snippets });
+    const result = expandSnippet({ direction: 'up', snippetIndex: 0, snippets });
 
     expect(result).toHaveLength(1);
-    expect(result[0]).toHaveLength(15);
-    expect(result[0].map(l => l.line)).toEqual(range(4, 19));
+    expect(result[0]).toEqual({ start: 4, end: 18, index: 0 });
   });
 
   it('should add lines below', () => {
-    const lines = keyBy(range(4, 19).map(line => mockSourceLine({ line })), 'line');
-    const snippets = [[lines[4], lines[5], lines[6], lines[7], lines[8]]];
+    const snippets = [{ start: 4, end: 8, index: 0 }];
 
-    const result = expandSnippet({ direction: 'down', lines, snippetIndex: 0, snippets });
+    const result = expandSnippet({ direction: 'down', snippetIndex: 0, snippets });
 
     expect(result).toHaveLength(1);
-    expect(result[0].map(l => l.line)).toEqual(range(4, 19));
+    expect(result[0]).toEqual({ start: 4, end: 18, index: 0 });
   });
 
   it('should merge snippets if necessary', () => {
-    const lines = keyBy(
-      range(4, 23)
-        .concat(range(38, 43))
-        .map(line => mockSourceLine({ line })),
-      'line'
-    );
     const snippets = [
-      [lines[4], lines[5], lines[6], lines[7], lines[8]],
-      [lines[38], lines[39], lines[40], lines[41], lines[42]],
-      [lines[17], lines[18], lines[19], lines[20], lines[21]]
+      { index: 1, start: 4, end: 8 },
+      { index: 2, start: 38, end: 42 },
+      { index: 3, start: 17, end: 21 }
     ];
 
-    const result = expandSnippet({ direction: 'down', lines, snippetIndex: 0, snippets });
+    const result = expandSnippet({ direction: 'down', snippetIndex: 1, snippets });
 
-    expect(result).toHaveLength(2);
-    expect(result[0].map(l => l.line)).toEqual(range(4, 22));
-    expect(result[1].map(l => l.line)).toEqual(range(38, 43));
+    expect(result).toHaveLength(3);
+    expect(result[0]).toEqual({ index: 1, start: 4, end: 21 });
+    expect(result[1]).toEqual({ index: 2, start: 38, end: 42 });
+    expect(result[2]).toEqual({ index: 3, start: 17, end: 21, toDelete: true });
   });
 });
index 5c34f42dfbbaef90085434fd5f0c9740cb974961..c900f543268814f23ed8ab29c2b7b74030d0ad1e 100644 (file)
@@ -42,62 +42,53 @@ function collision([startA, endA]: number[], [startB, endB]: number[]) {
   return !(startA > endB + MERGE_DISTANCE || endA < startB - MERGE_DISTANCE);
 }
 
-export function createSnippets(
-  locations: T.FlowLocation[],
-  componentLines: T.LineMap = {},
-  last: boolean
-): T.SourceLine[][] {
-  return rangesToSnippets(
-    // For each location's range (2 above and 2 below), and then compare with other ranges
-    // to merge snippets that collide.
-    locations.reduce((snippets: Array<{ start: number; end: number }>, loc, index) => {
-      const startIndex = Math.max(1, loc.textRange.startLine - LINES_ABOVE);
-      const endIndex =
-        loc.textRange.endLine +
-        (last && index === locations.length - 1 ? LINES_BELOW_LAST : LINES_BELOW);
-
-      let firstCollision: { start: number; end: number } | undefined;
-
-      // Remove ranges that collide into the first collision
-      snippets = snippets.filter(snippet => {
-        if (collision([snippet.start, snippet.end], [startIndex, endIndex])) {
-          let keep = false;
-          // Check if we've already collided
-          if (!firstCollision) {
-            firstCollision = snippet;
-            keep = true;
-          }
-          // Merge with first collision:
-          firstCollision.start = Math.min(startIndex, snippet.start, firstCollision.start);
-          firstCollision.end = Math.max(endIndex, snippet.end, firstCollision.end);
-
-          // remove the range if it was not the first collision
-          return keep;
+export function createSnippets(locations: T.FlowLocation[], last: boolean): T.Snippet[] {
+  // For each location's range (2 above and 2 below), and then compare with other ranges
+  // to merge snippets that collide.
+  return locations.reduce((snippets: T.Snippet[], loc, index) => {
+    const startIndex = Math.max(1, loc.textRange.startLine - LINES_ABOVE);
+    const endIndex =
+      loc.textRange.endLine +
+      (last && index === locations.length - 1 ? LINES_BELOW_LAST : LINES_BELOW);
+
+    let firstCollision: { start: number; end: number } | undefined;
+
+    // Remove ranges that collide into the first collision
+    snippets = snippets.filter(snippet => {
+      if (collision([snippet.start, snippet.end], [startIndex, endIndex])) {
+        let keep = false;
+        // Check if we've already collided
+        if (!firstCollision) {
+          firstCollision = snippet;
+          keep = true;
         }
-        return true;
-      });
+        // Merge with first collision:
+        firstCollision.start = Math.min(startIndex, snippet.start, firstCollision.start);
+        firstCollision.end = Math.max(endIndex, snippet.end, firstCollision.end);
 
-      if (firstCollision === undefined) {
-        snippets.push({
-          start: startIndex,
-          end: endIndex
-        });
+        // remove the range if it was not the first collision
+        return keep;
       }
+      return true;
+    });
+
+    if (firstCollision === undefined) {
+      snippets.push({
+        start: startIndex,
+        end: endIndex,
+        index
+      });
+    }
 
-      return snippets;
-    }, []),
-    componentLines
-  );
+    return snippets;
+  }, []);
 }
 
-function rangesToSnippets(
-  ranges: Array<{ start: number; end: number }>,
-  componentLines: T.LineMap
-) {
-  return ranges
-    .map(range => {
+export function linesForSnippets(snippets: T.Snippet[], componentLines: T.LineMap) {
+  return snippets
+    .map(snippet => {
       const lines = [];
-      for (let i = range.start; i <= range.end; i++) {
+      for (let i = snippet.start; i <= snippet.end; i++) {
         if (componentLines[i]) {
           lines.push(componentLines[i]);
         }
@@ -133,51 +124,36 @@ export function groupLocationsByComponent(
 
 export function expandSnippet({
   direction,
-  lines,
   snippetIndex,
   snippets
 }: {
   direction: T.ExpandDirection;
-  lines: T.LineMap;
   snippetIndex: number;
-  snippets: T.SourceLine[][];
+  snippets: T.Snippet[];
 }) {
-  const snippetToExpand = snippets[snippetIndex];
-
-  const snippetToExpandRange = {
-    start: Math.max(0, snippetToExpand[0].line - (direction === 'up' ? EXPAND_BY_LINES : 0)),
-    end:
-      snippetToExpand[snippetToExpand.length - 1].line +
-      (direction === 'down' ? EXPAND_BY_LINES : 0)
-  };
+  const snippetToExpand = snippets.find(s => s.index === snippetIndex);
+  if (!snippetToExpand) {
+    throw new Error(`Snippet ${snippetIndex} not found`);
+  }
+
+  snippetToExpand.start = Math.max(
+    0,
+    snippetToExpand.start - (direction === 'up' ? EXPAND_BY_LINES : 0)
+  );
+  snippetToExpand.end += direction === 'down' ? EXPAND_BY_LINES : 0;
 
-  const ranges: Array<{ start: number; end: number }> = [];
-
-  snippets.forEach((snippet, index: number) => {
-    const snippetRange = {
-      start: snippet[0].line,
-      end: snippet[snippet.length - 1].line
-    };
-
-    if (index === snippetIndex) {
-      // keep expanded snippet
-      ranges.push(snippetToExpandRange);
-    } else if (
-      collision(
-        [snippetRange.start, snippetRange.end],
-        [snippetToExpandRange.start, snippetToExpandRange.end]
-      )
-    ) {
+  return snippets.map(snippet => {
+    if (snippet.index === snippetIndex) {
+      return snippetToExpand;
+    }
+    if (collision([snippet.start, snippet.end], [snippetToExpand.start, snippetToExpand.end])) {
       // Merge with expanded snippet
-      snippetToExpandRange.start = Math.min(snippetRange.start, snippetToExpandRange.start);
-      snippetToExpandRange.end = Math.max(snippetRange.end, snippetToExpandRange.end);
-    } else {
-      // No collision, jsut keep the snippet
-      ranges.push(snippetRange);
+      snippetToExpand.start = Math.min(snippet.start, snippetToExpand.start);
+      snippetToExpand.end = Math.max(snippet.end, snippetToExpand.end);
+      snippet.toDelete = true;
     }
+    return snippet;
   });
-
-  return rangesToSnippets(ranges, lines);
 }
 
 export function inSnippet(line: number, snippet: T.SourceLine[]) {
index e89e0916a7ad697040070e213ca4b952315ecbf5..700eddf3eecd2895ee2a86bd725e316f3ea2507f 100644 (file)
   margin: var(--gridSize);
   border: 1px solid var(--gray80);
   overflow-x: auto;
+  overflow-y: hidden;
+  transition: max-height 0.2s;
 }
 
-.snippet > table {
+.snippet > div {
+  display: table;
+  width: 100%;
+  position: relative;
+  transition: margin-top 0.2s;
+}
+
+.snippet table {
+  width: 100%;
+}
+
+.expand-block {
+  position: absolute;
+  z-index: 2;
   width: 100%;
 }
 
-.expand-block > td > button {
+.expand-block > button {
   background: transparent;
   box-sizing: border-box;
   color: var(--secondFontColor);
   text-align: left;
   cursor: pointer;
 }
-.expand-block > td > button:hover,
-.expand-block > td > button:focus,
-.expand-block > td > button:active {
+.expand-block > button:hover,
+.expand-block > button:focus,
+.expand-block > button:active {
   color: var(--darkBlue);
   outline: none;
 }
 .expand-block-above {
   background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAAXNSR0IArs4c6QAAADdJREFUCB1dzMEKADAIAlBd1v9/bcc2YgRjHh8qq2qTxCQzsX4wM6y30RARF3sy0Es1SIK7Y64OpCES1W69JS4AAAAASUVORK5CYII=');
+  top: 0;
 }
 .expand-block-below {
   background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wQQBjQEQVd5jwAAADhJREFUCNddyTEKADEMA8GVA/7/Z+PGwUp1cGTaYe/tv5lxrLWoKj6SiMzkjZDEG7JtANt0N+ccLrB/KZxXTt7fAAAAAElFTkSuQmCC');
+  bottom: 0;
+}
+
+.source-table.expand-up {
+  margin-top: 20px;
+}
+
+.source-table.expand-down {
+  margin-bottom: 20px;
 }
 
 .issues-my-issues-filter {