Migrate gr-diff-builder-element_test from js to ts

Release-Notes: skip
Change-Id: Ieae0bfb7ca9c13a51f3aa07fa89f123450396e57
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
index 269b56d..efd6f10 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
@@ -64,7 +64,21 @@
 
 declare global {
   interface HTMLElementEventMap {
+    /**
+     * Fired when the diff begins rendering - both for full renders and for
+     * partial rerenders.
+     */
+    'render-start': CustomEvent<{}>;
+    /**
+     * Fired whenever a new chunk of lines has been rendered synchronously - this
+     * only happens for full renders.
+     */
     'render-progress': CustomEvent<RenderProgressEventDetail>;
+    /**
+     * Fired when the diff finishes rendering text content - both for full
+     * renders and for partial rerenders.
+     */
+    'render-content': CustomEvent<{}>;
   }
 }
 
@@ -106,27 +120,6 @@
     return htmlTemplate;
   }
 
-  /**
-   * Fired when the diff begins rendering - both for full renders and for
-   * partial rerenders.
-   *
-   * @event render-start
-   */
-
-  /**
-   * Fired whenever a new chunk of lines has been rendered synchronously - this
-   * only happens for full renders.
-   *
-   * @event render-progress
-   */
-
-  /**
-   * Fired when the diff finishes rendering text content - both for full
-   * renders and for partial rerenders.
-   *
-   * @event render-content
-   */
-
   @property({type: Object})
   diff?: DiffInfo;
 
@@ -210,7 +203,8 @@
 
   private rangeLayer = new GrRangedCommentLayer();
 
-  private processor = new GrDiffProcessor();
+  // visible for testing
+  processor = new GrDiffProcessor();
 
   constructor() {
     super();
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.js b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.js
deleted file mode 100644
index 8c15ddd..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.js
+++ /dev/null
@@ -1,1084 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import '../../../test/common-test-setup-karma.js';
-import {createDiff} from '../../../test/test-data-generators.js';
-import './gr-diff-builder-element.js';
-import {stubBaseUrl} from '../../../test/test-utils.js';
-import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
-import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line.js';
-import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-import {DiffViewMode, Side} from '../../../api/diff.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
-import {GrDiffBuilderLegacy} from './gr-diff-builder-legacy.js';
-import {waitForEventOnce} from '../../../utils/event-util.js';
-
-const basicFixture = fixtureFromTemplate(html`
-    <gr-diff-builder>
-      <table id="diffTable"></table>
-    </gr-diff-builder>
-`);
-
-const divWithTextFixture = fixtureFromTemplate(html`
-<div>Lorem ipsum dolor sit amet, suspendisse inceptos vehicula</div>
-`);
-
-const mockDiffFixture = fixtureFromTemplate(html`
-<gr-diff-builder view-mode="SIDE_BY_SIDE">
-      <table id="diffTable"></table>
-    </gr-diff-builder>
-`);
-
-// GrDiffBuilderElement forces these prefs to be set - tests that do not care
-// about these values can just set these defaults.
-const DEFAULT_PREFS = {
-  line_length: 10,
-  show_tabs: true,
-  tab_size: 4,
-};
-
-suite('gr-diff-builder tests', () => {
-  let prefs;
-  let element;
-  let builder;
-
-  const LINE_BREAK_HTML = '<span class="style-scope gr-diff br"></span>';
-  const WBR_HTML = '<wbr class="style-scope gr-diff">';
-
-  setup(() => {
-    element = basicFixture.instantiate();
-    stubRestApi('getLoggedIn').returns(Promise.resolve(false));
-    stubRestApi('getProjectConfig').returns(Promise.resolve({}));
-    stubBaseUrl('/r');
-    prefs = {...DEFAULT_PREFS};
-    builder = new GrDiffBuilderLegacy({content: []}, prefs);
-  });
-
-  test('line_length applied with <wbr> if line_wrapping is true', () => {
-    builder._prefs = {line_wrapping: true, tab_size: 4, line_length: 50};
-    const text = 'a'.repeat(51);
-
-    const line = {text, highlights: []};
-    const expected = 'a'.repeat(50) + WBR_HTML + 'a';
-    const result = builder.createTextEl(undefined, line).firstChild.innerHTML;
-    assert.equal(result, expected);
-  });
-
-  test('line_length applied with line break if line_wrapping is false', () => {
-    builder._prefs = {line_wrapping: false, tab_size: 4, line_length: 50};
-    const text = 'a'.repeat(51);
-
-    const line = {text, highlights: []};
-    const expected = 'a'.repeat(50) + LINE_BREAK_HTML + 'a';
-    const result = builder.createTextEl(undefined, line).firstChild.innerHTML;
-    assert.equal(result, expected);
-  });
-
-  [DiffViewMode.UNIFIED, DiffViewMode.SIDE_BY_SIDE]
-      .forEach(mode => {
-        test(`line_length used for regular files under ${mode}`, () => {
-          element.path = '/a.txt';
-          element.viewMode = mode;
-          element.diff = {};
-          element.prefs = {tab_size: 4, line_length: 50};
-          builder = element._getDiffBuilder();
-          assert.equal(builder._prefs.line_length, 50);
-        });
-
-        test(`line_length ignored for commit msg under ${mode}`, () => {
-          element.path = '/COMMIT_MSG';
-          element.viewMode = mode;
-          element.diff = {};
-          element.prefs = {tab_size: 4, line_length: 50};
-          builder = element._getDiffBuilder();
-          assert.equal(builder._prefs.line_length, 72);
-        });
-      });
-
-  test('createTextEl linewrap with tabs', () => {
-    const text = '\t'.repeat(7) + '!';
-    const line = {text, highlights: []};
-    const el = builder.createTextEl(undefined, line);
-    assert.equal(el.innerText, text);
-    // With line length 10 and tab size 2, there should be a line break
-    // after every two tabs.
-    const newlineEl = el.querySelector('.contentText > .br');
-    assert.isOk(newlineEl);
-    assert.equal(
-        el.querySelector('.contentText .tab:nth-child(2)').nextSibling,
-        newlineEl);
-  });
-
-  test('_handlePreferenceError throws with invalid preference', () => {
-    element.prefs = {tab_size: 0};
-    assert.throws(() => element._getDiffBuilder());
-  });
-
-  test('_handlePreferenceError triggers alert and javascript error', () => {
-    const errorStub = sinon.stub();
-    element.addEventListener('show-alert', errorStub);
-    assert.throws(() => element._handlePreferenceError('tab size'));
-    assert.equal(errorStub.lastCall.args[0].detail.message,
-        `The value of the 'tab size' user preference is invalid. ` +
-      `Fix in diff preferences`);
-  });
-
-  suite('intraline differences', () => {
-    let el;
-    let str;
-    let annotateElementSpy;
-    let layer;
-    const lineNumberEl = document.createElement('td');
-
-    function slice(str, start, end) {
-      return Array.from(str).slice(start, end)
-          .join('');
-    }
-
-    setup(() => {
-      el = divWithTextFixture.instantiate();
-      str = el.textContent;
-      annotateElementSpy = sinon.spy(GrAnnotation, 'annotateElement');
-      layer = document.createElement('gr-diff-builder')
-          ._createIntralineLayer();
-    });
-
-    test('annotate no highlights', () => {
-      const line = {
-        text: str,
-        highlights: [],
-      };
-
-      layer.annotate(el, lineNumberEl, line);
-
-      // The content is unchanged.
-      assert.isFalse(annotateElementSpy.called);
-      assert.equal(el.childNodes.length, 1);
-      assert.instanceOf(el.childNodes[0], Text);
-      assert.equal(str, el.childNodes[0].textContent);
-    });
-
-    test('annotate with highlights', () => {
-      const line = {
-        text: str,
-        highlights: [
-          {startIndex: 6, endIndex: 12},
-          {startIndex: 18, endIndex: 22},
-        ],
-      };
-      const str0 = slice(str, 0, 6);
-      const str1 = slice(str, 6, 12);
-      const str2 = slice(str, 12, 18);
-      const str3 = slice(str, 18, 22);
-      const str4 = slice(str, 22);
-
-      layer.annotate(el, lineNumberEl, line);
-
-      assert.isTrue(annotateElementSpy.called);
-      assert.equal(el.childNodes.length, 5);
-
-      assert.instanceOf(el.childNodes[0], Text);
-      assert.equal(el.childNodes[0].textContent, str0);
-
-      assert.notInstanceOf(el.childNodes[1], Text);
-      assert.equal(el.childNodes[1].textContent, str1);
-
-      assert.instanceOf(el.childNodes[2], Text);
-      assert.equal(el.childNodes[2].textContent, str2);
-
-      assert.notInstanceOf(el.childNodes[3], Text);
-      assert.equal(el.childNodes[3].textContent, str3);
-
-      assert.instanceOf(el.childNodes[4], Text);
-      assert.equal(el.childNodes[4].textContent, str4);
-    });
-
-    test('annotate without endIndex', () => {
-      const line = {
-        text: str,
-        highlights: [
-          {startIndex: 28},
-        ],
-      };
-
-      const str0 = slice(str, 0, 28);
-      const str1 = slice(str, 28);
-
-      layer.annotate(el, lineNumberEl, line);
-
-      assert.isTrue(annotateElementSpy.called);
-      assert.equal(el.childNodes.length, 2);
-
-      assert.instanceOf(el.childNodes[0], Text);
-      assert.equal(el.childNodes[0].textContent, str0);
-
-      assert.notInstanceOf(el.childNodes[1], Text);
-      assert.equal(el.childNodes[1].textContent, str1);
-    });
-
-    test('annotate ignores empty highlights', () => {
-      const line = {
-        text: str,
-        highlights: [
-          {startIndex: 28, endIndex: 28},
-        ],
-      };
-
-      layer.annotate(el, lineNumberEl, line);
-
-      assert.isFalse(annotateElementSpy.called);
-      assert.equal(el.childNodes.length, 1);
-    });
-
-    test('annotate handles unicode', () => {
-      // Put some unicode into the string:
-      str = str.replace(/\s/g, '💢');
-      el.textContent = str;
-      const line = {
-        text: str,
-        highlights: [
-          {startIndex: 6, endIndex: 12},
-        ],
-      };
-
-      const str0 = slice(str, 0, 6);
-      const str1 = slice(str, 6, 12);
-      const str2 = slice(str, 12);
-
-      layer.annotate(el, lineNumberEl, line);
-
-      assert.isTrue(annotateElementSpy.called);
-      assert.equal(el.childNodes.length, 3);
-
-      assert.instanceOf(el.childNodes[0], Text);
-      assert.equal(el.childNodes[0].textContent, str0);
-
-      assert.notInstanceOf(el.childNodes[1], Text);
-      assert.equal(el.childNodes[1].textContent, str1);
-
-      assert.instanceOf(el.childNodes[2], Text);
-      assert.equal(el.childNodes[2].textContent, str2);
-    });
-
-    test('annotate handles unicode w/o endIndex', () => {
-      // Put some unicode into the string:
-      str = str.replace(/\s/g, '💢');
-      el.textContent = str;
-
-      const line = {
-        text: str,
-        highlights: [
-          {startIndex: 6},
-        ],
-      };
-
-      const str0 = slice(str, 0, 6);
-      const str1 = slice(str, 6);
-
-      layer.annotate(el, lineNumberEl, line);
-
-      assert.isTrue(annotateElementSpy.called);
-      assert.equal(el.childNodes.length, 2);
-
-      assert.instanceOf(el.childNodes[0], Text);
-      assert.equal(el.childNodes[0].textContent, str0);
-
-      assert.notInstanceOf(el.childNodes[1], Text);
-      assert.equal(el.childNodes[1].textContent, str1);
-    });
-  });
-
-  suite('tab indicators', () => {
-    let element;
-    let layer;
-    const lineNumberEl = document.createElement('td');
-
-    setup(() => {
-      element = basicFixture.instantiate();
-      element._showTabs = true;
-      layer = element._createTabIndicatorLayer();
-    });
-
-    test('does nothing with empty line', () => {
-      const line = {text: ''};
-      const el = document.createElement('div');
-      const annotateElementStub =
-          sinon.stub(GrAnnotation, 'annotateElement');
-
-      layer.annotate(el, lineNumberEl, line);
-
-      assert.isFalse(annotateElementStub.called);
-    });
-
-    test('does nothing with no tabs', () => {
-      const str = 'lorem ipsum no tabs';
-      const line = {text: str};
-      const el = document.createElement('div');
-      el.textContent = str;
-      const annotateElementStub =
-          sinon.stub(GrAnnotation, 'annotateElement');
-
-      layer.annotate(el, lineNumberEl, line);
-
-      assert.isFalse(annotateElementStub.called);
-    });
-
-    test('annotates tab at beginning', () => {
-      const str = '\tlorem upsum';
-      const line = {text: str};
-      const el = document.createElement('div');
-      el.textContent = str;
-      const annotateElementStub =
-          sinon.stub(GrAnnotation, 'annotateElement');
-
-      layer.annotate(el, lineNumberEl, line);
-
-      assert.equal(annotateElementStub.callCount, 1);
-      const args = annotateElementStub.getCalls()[0].args;
-      assert.equal(args[0], el);
-      assert.equal(args[1], 0, 'offset of tab indicator');
-      assert.equal(args[2], 1, 'length of tab indicator');
-      assert.include(args[3], 'tab-indicator');
-    });
-
-    test('does not annotate when disabled', () => {
-      element._showTabs = false;
-
-      const str = '\tlorem upsum';
-      const line = {text: str};
-      const el = document.createElement('div');
-      el.textContent = str;
-      const annotateElementStub =
-          sinon.stub(GrAnnotation, 'annotateElement');
-
-      layer.annotate(el, lineNumberEl, line);
-
-      assert.isFalse(annotateElementStub.called);
-    });
-
-    test('annotates multiple in beginning', () => {
-      const str = '\t\tlorem upsum';
-      const line = {text: str};
-      const el = document.createElement('div');
-      el.textContent = str;
-      const annotateElementStub =
-          sinon.stub(GrAnnotation, 'annotateElement');
-
-      layer.annotate(el, lineNumberEl, line);
-
-      assert.equal(annotateElementStub.callCount, 2);
-
-      let args = annotateElementStub.getCalls()[0].args;
-      assert.equal(args[0], el);
-      assert.equal(args[1], 0, 'offset of tab indicator');
-      assert.equal(args[2], 1, 'length of tab indicator');
-      assert.include(args[3], 'tab-indicator');
-
-      args = annotateElementStub.getCalls()[1].args;
-      assert.equal(args[0], el);
-      assert.equal(args[1], 1, 'offset of tab indicator');
-      assert.equal(args[2], 1, 'length of tab indicator');
-      assert.include(args[3], 'tab-indicator');
-    });
-
-    test('annotates intermediate tabs', () => {
-      const str = 'lorem\tupsum';
-      const line = {text: str};
-      const el = document.createElement('div');
-      el.textContent = str;
-      const annotateElementStub =
-          sinon.stub(GrAnnotation, 'annotateElement');
-
-      layer.annotate(el, lineNumberEl, line);
-
-      assert.equal(annotateElementStub.callCount, 1);
-      const args = annotateElementStub.getCalls()[0].args;
-      assert.equal(args[0], el);
-      assert.equal(args[1], 5, 'offset of tab indicator');
-      assert.equal(args[2], 1, 'length of tab indicator');
-      assert.include(args[3], 'tab-indicator');
-    });
-  });
-
-  suite('layers', () => {
-    let element;
-    let initialLayersCount;
-    let withLayerCount;
-    setup(() => {
-      const layers = [];
-      element = basicFixture.instantiate();
-      element.layers = layers;
-      element._showTrailingWhitespace = true;
-      element._setupAnnotationLayers();
-      initialLayersCount = element._layers.length;
-    });
-
-    test('no layers', () => {
-      element._setupAnnotationLayers();
-      assert.equal(element._layers.length, initialLayersCount);
-    });
-
-    suite('with layers', () => {
-      const layers = [{}, {}];
-      setup(() => {
-        element = basicFixture.instantiate();
-        element.layers = layers;
-        element._showTrailingWhitespace = true;
-        element._setupAnnotationLayers();
-        withLayerCount = element._layers.length;
-      });
-      test('with layers', () => {
-        element._setupAnnotationLayers();
-        assert.equal(element._layers.length, withLayerCount);
-        assert.equal(initialLayersCount + layers.length,
-            withLayerCount);
-      });
-    });
-  });
-
-  suite('trailing whitespace', () => {
-    let element;
-    let layer;
-    const lineNumberEl = document.createElement('td');
-
-    setup(() => {
-      element = basicFixture.instantiate();
-      element._showTrailingWhitespace = true;
-      layer = element._createTrailingWhitespaceLayer();
-    });
-
-    test('does nothing with empty line', () => {
-      const line = {text: ''};
-      const el = document.createElement('div');
-      const annotateElementStub =
-          sinon.stub(GrAnnotation, 'annotateElement');
-      layer.annotate(el, lineNumberEl, line);
-      assert.isFalse(annotateElementStub.called);
-    });
-
-    test('does nothing with no trailing whitespace', () => {
-      const str = 'lorem ipsum blah blah';
-      const line = {text: str};
-      const el = document.createElement('div');
-      el.textContent = str;
-      const annotateElementStub =
-          sinon.stub(GrAnnotation, 'annotateElement');
-      layer.annotate(el, lineNumberEl, line);
-      assert.isFalse(annotateElementStub.called);
-    });
-
-    test('annotates trailing spaces', () => {
-      const str = 'lorem ipsum   ';
-      const line = {text: str};
-      const el = document.createElement('div');
-      el.textContent = str;
-      const annotateElementStub =
-          sinon.stub(GrAnnotation, 'annotateElement');
-      layer.annotate(el, lineNumberEl, line);
-      assert.isTrue(annotateElementStub.called);
-      assert.equal(annotateElementStub.lastCall.args[1], 11);
-      assert.equal(annotateElementStub.lastCall.args[2], 3);
-    });
-
-    test('annotates trailing tabs', () => {
-      const str = 'lorem ipsum\t\t\t';
-      const line = {text: str};
-      const el = document.createElement('div');
-      el.textContent = str;
-      const annotateElementStub =
-          sinon.stub(GrAnnotation, 'annotateElement');
-      layer.annotate(el, lineNumberEl, line);
-      assert.isTrue(annotateElementStub.called);
-      assert.equal(annotateElementStub.lastCall.args[1], 11);
-      assert.equal(annotateElementStub.lastCall.args[2], 3);
-    });
-
-    test('annotates mixed trailing whitespace', () => {
-      const str = 'lorem ipsum\t \t';
-      const line = {text: str};
-      const el = document.createElement('div');
-      el.textContent = str;
-      const annotateElementStub =
-          sinon.stub(GrAnnotation, 'annotateElement');
-      layer.annotate(el, lineNumberEl, line);
-      assert.isTrue(annotateElementStub.called);
-      assert.equal(annotateElementStub.lastCall.args[1], 11);
-      assert.equal(annotateElementStub.lastCall.args[2], 3);
-    });
-
-    test('unicode preceding trailing whitespace', () => {
-      const str = '💢\t';
-      const line = {text: str};
-      const el = document.createElement('div');
-      el.textContent = str;
-      const annotateElementStub =
-          sinon.stub(GrAnnotation, 'annotateElement');
-      layer.annotate(el, lineNumberEl, line);
-      assert.isTrue(annotateElementStub.called);
-      assert.equal(annotateElementStub.lastCall.args[1], 1);
-      assert.equal(annotateElementStub.lastCall.args[2], 1);
-    });
-
-    test('does not annotate when disabled', () => {
-      element._showTrailingWhitespace = false;
-      const str = 'lorem upsum\t \t ';
-      const line = {text: str};
-      const el = document.createElement('div');
-      el.textContent = str;
-      const annotateElementStub =
-          sinon.stub(GrAnnotation, 'annotateElement');
-      layer.annotate(el, lineNumberEl, line);
-      assert.isFalse(annotateElementStub.called);
-    });
-  });
-
-  suite('rendering text, images and binary files', () => {
-    let processStub;
-    let keyLocations;
-    let content;
-
-    setup(() => {
-      element = basicFixture.instantiate();
-      element.viewMode = 'SIDE_BY_SIDE';
-      processStub = sinon.stub(element.processor, 'process')
-          .returns(Promise.resolve());
-      keyLocations = {left: {}, right: {}};
-      element.prefs = {
-        ...DEFAULT_PREFS,
-        context: -1,
-        syntax_highlighting: true,
-      };
-      content = [{
-        a: ['all work and no play make andybons a dull boy'],
-        b: ['elgoog elgoog elgoog'],
-      }, {
-        ab: [
-          'Non eram nescius, Brute, cum, quae summis ingeniis ',
-          'exquisitaque doctrina philosophi Graeco sermone tractavissent',
-        ],
-      }];
-    });
-
-    test('text', async () => {
-      element.diff = {content};
-      element.render(keyLocations);
-      await waitForEventOnce(element, 'render-content');
-      assert.isTrue(processStub.calledOnce);
-      assert.isFalse(processStub.lastCall.args[1]);
-    });
-
-    test('image', async () => {
-      element.diff = {content, binary: true};
-      element.isImageDiff = true;
-      element.render(keyLocations);
-      await waitForEventOnce(element, 'render-content');
-      assert.isTrue(processStub.calledOnce);
-      assert.isTrue(processStub.lastCall.args[1]);
-    });
-
-    test('binary', async () => {
-      element.diff = {content, binary: true};
-      element.render(keyLocations);
-      await waitForEventOnce(element, 'render-content');
-      assert.isTrue(processStub.calledOnce);
-      assert.isTrue(processStub.lastCall.args[1]);
-    });
-  });
-
-  suite('rendering', () => {
-    let content;
-    let outputEl;
-    let keyLocations;
-
-    setup(async () => {
-      const prefs = {...DEFAULT_PREFS};
-      content = [
-        {
-          a: ['all work and no play make andybons a dull boy'],
-          b: ['elgoog elgoog elgoog'],
-        },
-        {
-          ab: [
-            'Non eram nescius, Brute, cum, quae summis ingeniis ',
-            'exquisitaque doctrina philosophi Graeco sermone tractavissent',
-          ],
-        },
-      ];
-      element = basicFixture.instantiate();
-      sinon.stub(element, 'dispatchEvent');
-      outputEl = element.querySelector('#diffTable');
-      keyLocations = {left: {}, right: {}};
-      sinon.stub(element, '_getDiffBuilder').callsFake(() => {
-        const builder = new GrDiffBuilderSideBySide({content}, prefs, outputEl);
-        sinon.stub(builder, 'addColumns');
-        builder.buildSectionElement = function(group) {
-          const section = document.createElement('stub');
-          section.textContent = group.lines
-              .reduce((acc, line) => acc + line.text, '');
-          return section;
-        };
-        return builder;
-      });
-      element.diff = {content};
-      element.prefs = prefs;
-      await element.render(keyLocations);
-    });
-
-    test('addColumns is called', () => {
-      assert.isTrue(element._builder.addColumns.called);
-    });
-
-    test('getGroupsByLineRange one line', () => {
-      const section = outputEl.querySelector('stub:nth-of-type(3)');
-      const groups = element._builder.getGroupsByLineRange(1, 1, 'left');
-      assert.equal(groups.length, 1);
-      assert.strictEqual(groups[0].element, section);
-    });
-
-    test('getGroupsByLineRange over diff', () => {
-      const section = [
-        outputEl.querySelector('stub:nth-of-type(3)'),
-        outputEl.querySelector('stub:nth-of-type(4)'),
-      ];
-      const groups = element._builder.getGroupsByLineRange(1, 2, 'left');
-      assert.equal(groups.length, 2);
-      assert.strictEqual(groups[0].element, section[0]);
-      assert.strictEqual(groups[1].element, section[1]);
-    });
-
-    test('render-start and render-content are fired', async () => {
-      await new Promise(resolve => afterNextRender(element, resolve));
-      const firedEventTypes = element.dispatchEvent.getCalls()
-          .map(c => c.args[0].type);
-      assert.include(firedEventTypes, 'render-start');
-      assert.include(firedEventTypes, 'render-content');
-    });
-
-    test('cancel cancels the processor', () => {
-      const processorCancelStub = sinon.stub(element.processor, 'cancel');
-      element.cancel();
-      assert.isTrue(processorCancelStub.called);
-    });
-  });
-
-  suite('context hiding and expanding', () => {
-    setup(async () => {
-      element = basicFixture.instantiate();
-      sinon.stub(element, 'dispatchEvent');
-      const afterNextRenderPromise = new Promise((resolve, reject) => {
-        afterNextRender(element, resolve);
-      });
-      element.diff = {
-        content: [
-          {ab: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(i => `unchanged ${i}`)},
-          {a: ['before'], b: ['after']},
-          {ab: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(i => `unchanged ${10 + i}`)},
-        ],
-      };
-      element.viewMode = DiffViewMode.SIDE_BY_SIDE;
-
-      const keyLocations = {left: {}, right: {}};
-      element.prefs = {
-        ...DEFAULT_PREFS,
-        context: 1,
-      };
-      await element.render(keyLocations);
-      // Make sure all listeners are installed.
-      await afterNextRenderPromise;
-    });
-
-    test('hides lines behind two context controls', () => {
-      const contextControls = element.querySelectorAll('gr-context-controls');
-      assert.equal(contextControls.length, 2);
-
-      const diffRows = element.querySelectorAll('.diff-row');
-      // The first two are LOST and FILE line
-      assert.equal(diffRows.length, 2 + 1 + 1 + 1);
-      assert.include(diffRows[2].textContent, 'unchanged 10');
-      assert.include(diffRows[3].textContent, 'before');
-      assert.include(diffRows[3].textContent, 'after');
-      assert.include(diffRows[4].textContent, 'unchanged 11');
-    });
-
-    test('clicking +x common lines expands those lines', () => {
-      const contextControls = element.querySelectorAll('gr-context-controls');
-      const topExpandCommonButton = contextControls[0].shadowRoot
-          .querySelectorAll('.showContext')[0];
-      assert.include(topExpandCommonButton.textContent, '+9 common lines');
-      topExpandCommonButton.click();
-      const diffRows = element.querySelectorAll('.diff-row');
-      // The first two are LOST and FILE line
-      assert.equal(diffRows.length, 2 + 10 + 1 + 1);
-      assert.include(diffRows[2].textContent, 'unchanged 1');
-      assert.include(diffRows[3].textContent, 'unchanged 2');
-      assert.include(diffRows[4].textContent, 'unchanged 3');
-      assert.include(diffRows[5].textContent, 'unchanged 4');
-      assert.include(diffRows[6].textContent, 'unchanged 5');
-      assert.include(diffRows[7].textContent, 'unchanged 6');
-      assert.include(diffRows[8].textContent, 'unchanged 7');
-      assert.include(diffRows[9].textContent, 'unchanged 8');
-      assert.include(diffRows[10].textContent, 'unchanged 9');
-      assert.include(diffRows[11].textContent, 'unchanged 10');
-      assert.include(diffRows[12].textContent, 'before');
-      assert.include(diffRows[12].textContent, 'after');
-      assert.include(diffRows[13].textContent, 'unchanged 11');
-    });
-
-    test('unhideLine shows the line with context', async () => {
-      element.dispatchEvent.reset();
-      element.unhideLine(4, Side.LEFT);
-
-      const diffRows = element.querySelectorAll('.diff-row');
-      // The first two are LOST and FILE line
-      // Lines 3-5 (Line 4 plus 1 context in each direction) will be expanded
-      // Because context expanders do not hide <3 lines, lines 1-2 will also
-      // be shown.
-      // Lines 6-9 continue to be hidden
-      assert.equal(diffRows.length, 2 + 5 + 1 + 1 + 1);
-      assert.include(diffRows[2].textContent, 'unchanged 1');
-      assert.include(diffRows[3].textContent, 'unchanged 2');
-      assert.include(diffRows[4].textContent, 'unchanged 3');
-      assert.include(diffRows[5].textContent, 'unchanged 4');
-      assert.include(diffRows[6].textContent, 'unchanged 5');
-      assert.include(diffRows[7].textContent, 'unchanged 10');
-      assert.include(diffRows[8].textContent, 'before');
-      assert.include(diffRows[8].textContent, 'after');
-      assert.include(diffRows[9].textContent, 'unchanged 11');
-
-      await new Promise(resolve => afterNextRender(element, resolve));
-      const firedEventTypes = element.dispatchEvent.getCalls()
-          .map(c => c.args[0].type);
-      assert.include(firedEventTypes, 'render-content');
-    });
-  });
-
-  suite('mock-diff', () => {
-    let element;
-    let builder;
-    let diff;
-    let keyLocations;
-
-    setup(async () => {
-      element = mockDiffFixture.instantiate();
-      diff = createDiff();
-      element.diff = diff;
-
-      keyLocations = {left: {}, right: {}};
-
-      element.prefs = {
-        line_length: 80,
-        show_tabs: true,
-        tab_size: 4,
-      };
-      await element.render(keyLocations);
-      builder = element._builder;
-    });
-
-    test('aria-labels on added line numbers', () => {
-      const deltaLineNumberButton = element.diffElement.querySelectorAll(
-          '.lineNumButton.right')[5];
-
-      assert.isOk(deltaLineNumberButton);
-      assert.equal(deltaLineNumberButton.getAttribute('aria-label'), '5 added');
-    });
-
-    test('aria-labels on removed line numbers', () => {
-      const deltaLineNumberButton = element.diffElement.querySelectorAll(
-          '.lineNumButton.left')[10];
-
-      assert.isOk(deltaLineNumberButton);
-      assert.equal(
-          deltaLineNumberButton.getAttribute('aria-label'), '10 removed');
-    });
-
-    test('getContentByLine', () => {
-      let actual;
-
-      actual = builder.getContentByLine(2, 'left');
-      assert.equal(actual.textContent, diff.content[0].ab[1]);
-
-      actual = builder.getContentByLine(2, 'right');
-      assert.equal(actual.textContent, diff.content[0].ab[1]);
-
-      actual = builder.getContentByLine(5, 'left');
-      assert.equal(actual.textContent, diff.content[2].ab[0]);
-
-      actual = builder.getContentByLine(5, 'right');
-      assert.equal(actual.textContent, diff.content[1].b[0]);
-    });
-
-    test('getContentTdByLineEl works both with button and td', () => {
-      const diffRow = element.diffElement.querySelectorAll('tr.diff-row')[2];
-
-      const lineNumTdLeft = diffRow.querySelector('td.lineNum.left');
-      const lineNumButtonLeft = lineNumTdLeft.querySelector('button');
-      const contentTdLeft = diffRow.querySelectorAll('.content')[0];
-
-      const lineNumTdRight = diffRow.querySelector('td.lineNum.right');
-      const lineNumButtonRight = lineNumTdRight.querySelector('button');
-      const contentTdRight = diffRow.querySelectorAll('.content')[1];
-
-      assert.equal(element.getContentTdByLineEl(lineNumTdLeft), contentTdLeft);
-      assert.equal(
-          element.getContentTdByLineEl(lineNumButtonLeft), contentTdLeft);
-      assert.equal(
-          element.getContentTdByLineEl(lineNumTdRight), contentTdRight);
-      assert.equal(
-          element.getContentTdByLineEl(lineNumButtonRight), contentTdRight);
-    });
-
-    test('findLinesByRange', () => {
-      const lines = [];
-      const elems = [];
-      const start = 6;
-      const end = 10;
-      const count = end - start + 1;
-
-      builder.findLinesByRange(start, end, 'right', lines, elems);
-
-      assert.equal(lines.length, count);
-      assert.equal(elems.length, count);
-
-      for (let i = 0; i < 5; i++) {
-        assert.instanceOf(lines[i], GrDiffLine);
-        assert.equal(lines[i].afterNumber, start + i);
-        assert.instanceOf(elems[i], HTMLElement);
-        assert.equal(lines[i].text, elems[i].textContent);
-      }
-    });
-
-    test('renderContentByRange', () => {
-      const spy = sinon.spy(builder, 'createTextEl');
-      const start = 9;
-      const end = 14;
-      const count = end - start + 1;
-
-      builder.renderContentByRange(start, end, 'left');
-
-      assert.equal(spy.callCount, count);
-      spy.getCalls().forEach((call, i) => {
-        assert.equal(call.args[1].beforeNumber, start + i);
-      });
-    });
-
-    test('renderContentByRange non-existent elements', () => {
-      const spy = sinon.spy(builder, 'createTextEl');
-
-      sinon.stub(builder, 'getLineNumberEl').returns(
-          document.createElement('div')
-      );
-      sinon.stub(builder, 'findLinesByRange').callsFake(
-          (s, e, d, lines, elements) => {
-            // Add a line and a corresponding element.
-            lines.push(new GrDiffLine(GrDiffLineType.BOTH));
-            const tr = document.createElement('tr');
-            const td = document.createElement('td');
-            const el = document.createElement('div');
-            tr.appendChild(td);
-            td.appendChild(el);
-            elements.push(el);
-
-            // Add 2 lines without corresponding elements.
-            lines.push(new GrDiffLine(GrDiffLineType.BOTH));
-            lines.push(new GrDiffLine(GrDiffLineType.BOTH));
-          });
-
-      builder.renderContentByRange(1, 10, 'left');
-      // Should be called only once because only one line had a corresponding
-      // element.
-      assert.equal(spy.callCount, 1);
-    });
-
-    test('getLineNumberEl side-by-side left', () => {
-      const contentEl = builder.getContentByLine(5, 'left',
-          element.$.diffTable);
-      const lineNumberEl = builder.getLineNumberEl(contentEl, 'left');
-      assert.isTrue(lineNumberEl.classList.contains('lineNum'));
-      assert.isTrue(lineNumberEl.classList.contains('left'));
-    });
-
-    test('getLineNumberEl side-by-side right', () => {
-      const contentEl = builder.getContentByLine(5, 'right',
-          element.$.diffTable);
-      const lineNumberEl = builder.getLineNumberEl(contentEl, 'right');
-      assert.isTrue(lineNumberEl.classList.contains('lineNum'));
-      assert.isTrue(lineNumberEl.classList.contains('right'));
-    });
-
-    test('getLineNumberEl unified left', async () => {
-      // Re-render as unified:
-      element.viewMode = 'UNIFIED_DIFF';
-      await element.render(keyLocations);
-      builder = element._builder;
-
-      const contentEl = builder.getContentByLine(5, 'left',
-          element.$.diffTable);
-      const lineNumberEl = builder.getLineNumberEl(contentEl, 'left');
-      assert.isTrue(lineNumberEl.classList.contains('lineNum'));
-      assert.isTrue(lineNumberEl.classList.contains('left'));
-    });
-
-    test('getLineNumberEl unified right', async () => {
-      // Re-render as unified:
-      element.viewMode = 'UNIFIED_DIFF';
-      await element.render(keyLocations);
-      builder = element._builder;
-
-      const contentEl = builder.getContentByLine(5, 'right',
-          element.$.diffTable);
-      const lineNumberEl = builder.getLineNumberEl(contentEl, 'right');
-      assert.isTrue(lineNumberEl.classList.contains('lineNum'));
-      assert.isTrue(lineNumberEl.classList.contains('right'));
-    });
-
-    test('getNextContentOnSide side-by-side left', () => {
-      const startElem = builder.getContentByLine(5, 'left',
-          element.$.diffTable);
-      const expectedStartString = diff.content[2].ab[0];
-      const expectedNextString = diff.content[2].ab[1];
-      assert.equal(startElem.textContent, expectedStartString);
-
-      const nextElem = builder.getNextContentOnSide(startElem,
-          'left');
-      assert.equal(nextElem.textContent, expectedNextString);
-    });
-
-    test('getNextContentOnSide side-by-side right', () => {
-      const startElem = builder.getContentByLine(5, 'right',
-          element.$.diffTable);
-      const expectedStartString = diff.content[1].b[0];
-      const expectedNextString = diff.content[1].b[1];
-      assert.equal(startElem.textContent, expectedStartString);
-
-      const nextElem = builder.getNextContentOnSide(startElem,
-          'right');
-      assert.equal(nextElem.textContent, expectedNextString);
-    });
-
-    test('getNextContentOnSide unified left', async () => {
-      // Re-render as unified:
-      element.viewMode = 'UNIFIED_DIFF';
-      await element.render(keyLocations);
-      builder = element._builder;
-
-      const startElem = builder.getContentByLine(5, 'left',
-          element.$.diffTable);
-      const expectedStartString = diff.content[2].ab[0];
-      const expectedNextString = diff.content[2].ab[1];
-      assert.equal(startElem.textContent, expectedStartString);
-
-      const nextElem = builder.getNextContentOnSide(startElem,
-          'left');
-      assert.equal(nextElem.textContent, expectedNextString);
-    });
-
-    test('getNextContentOnSide unified right', async () => {
-      // Re-render as unified:
-      element.viewMode = 'UNIFIED_DIFF';
-      await element.render(keyLocations);
-      builder = element._builder;
-
-      const startElem = builder.getContentByLine(5, 'right',
-          element.$.diffTable);
-      const expectedStartString = diff.content[1].b[0];
-      const expectedNextString = diff.content[1].b[1];
-      assert.equal(startElem.textContent, expectedStartString);
-
-      const nextElem = builder.getNextContentOnSide(startElem,
-          'right');
-      assert.equal(nextElem.textContent, expectedNextString);
-    });
-  });
-
-  suite('blame', () => {
-    let mockBlame;
-
-    setup(() => {
-      mockBlame = [
-        {id: 'commit 1', ranges: [{start: 1, end: 2}, {start: 10, end: 16}]},
-        {id: 'commit 2', ranges: [{start: 4, end: 10}, {start: 17, end: 32}]},
-      ];
-    });
-
-    test('setBlame attempts to render each blamed line', () => {
-      const getBlameStub = sinon.stub(builder, 'getBlameTdByLine')
-          .returns(null);
-      builder.setBlame(mockBlame);
-      assert.equal(getBlameStub.callCount, 32);
-    });
-
-    test('getBlameCommitForBaseLine', () => {
-      sinon.stub(builder, 'getBlameTdByLine').returns(undefined);
-      builder.setBlame(mockBlame);
-      assert.isOk(builder.getBlameCommitForBaseLine(1));
-      assert.equal(builder.getBlameCommitForBaseLine(1).id, 'commit 1');
-
-      assert.isOk(builder.getBlameCommitForBaseLine(11));
-      assert.equal(builder.getBlameCommitForBaseLine(11).id, 'commit 1');
-
-      assert.isOk(builder.getBlameCommitForBaseLine(32));
-      assert.equal(builder.getBlameCommitForBaseLine(32).id, 'commit 2');
-
-      assert.isUndefined(builder.getBlameCommitForBaseLine(33));
-    });
-
-    test('getBlameCommitForBaseLine w/o blame returns null', () => {
-      assert.isUndefined(builder.getBlameCommitForBaseLine(1));
-      assert.isUndefined(builder.getBlameCommitForBaseLine(11));
-      assert.isUndefined(builder.getBlameCommitForBaseLine(31));
-    });
-
-    test('createBlameCell', () => {
-      const mockBlameInfo = {
-        time: 1576155200,
-        id: 1234567890,
-        author: 'Clark Kent',
-        commit_msg: 'Testing Commit',
-        ranges: [1],
-      };
-      const getBlameStub = sinon.stub(builder, 'getBlameCommitForBaseLine')
-          .returns(mockBlameInfo);
-      const line = new GrDiffLine(GrDiffLineType.BOTH);
-      line.beforeNumber = 3;
-      line.afterNumber = 5;
-
-      const result = builder.createBlameCell(line.beforeNumber);
-
-      assert.isTrue(getBlameStub.calledWithExactly(3));
-      assert.equal(result.getAttribute('data-line-number'), '3');
-      expect(result).dom.to.equal(/* HTML */`
-        <span class="gr-diff style-scope">
-          <a class="blameDate gr-diff style-scope" href="/r/q/1234567890">
-            12/12/2019
-          </a>
-          <span class="blameAuthor gr-diff style-scope">Clark</span>
-          <gr-hovercard class="gr-diff style-scope">
-            <span class="blameHoverCard gr-diff style-scope">
-              Commit 1234567890<br>
-              Author: Clark Kent<br>
-              Date: 12/12/2019<br>
-              <br>
-              Testing Commit
-            </span>
-          </gr-hovercard>
-        </span>
-      `);
-    });
-  });
-});
-
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts
new file mode 100644
index 0000000..b441ad3
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts
@@ -0,0 +1,1149 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import {
+  createConfig,
+  createDiff,
+  createEmptyDiff,
+} from '../../../test/test-data-generators';
+import './gr-diff-builder-element';
+import {queryAndAssert, stubBaseUrl} from '../../../test/test-utils';
+import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
+import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line';
+import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side';
+import {html} from '@polymer/polymer/lib/utils/html-tag';
+import {
+  DiffContent,
+  DiffInfo,
+  DiffLayer,
+  DiffPreferencesInfo,
+  DiffViewMode,
+  Side,
+} from '../../../api/diff';
+import {stubRestApi} from '../../../test/test-utils';
+import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
+import {GrDiffBuilderLegacy} from './gr-diff-builder-legacy';
+import {waitForEventOnce} from '../../../utils/event-util';
+import {GrDiffBuilderElement} from './gr-diff-builder-element';
+import {createDefaultDiffPrefs} from '../../../constants/constants';
+import {KeyLocations} from '../gr-diff-processor/gr-diff-processor';
+import {BlameInfo} from '../../../types/common';
+
+const basicFixture = fixtureFromTemplate(html`
+  <gr-diff-builder>
+    <table id="diffTable"></table>
+  </gr-diff-builder>
+`);
+
+const divWithTextFixture = fixtureFromTemplate(html`
+  <div>Lorem ipsum dolor sit amet, suspendisse inceptos vehicula</div>
+`);
+
+const mockDiffFixture = fixtureFromTemplate(html`
+  <gr-diff-builder view-mode="SIDE_BY_SIDE">
+    <table id="diffTable"></table>
+  </gr-diff-builder>
+`);
+
+const DEFAULT_PREFS = createDefaultDiffPrefs();
+
+suite('gr-diff-builder tests', () => {
+  let element: GrDiffBuilderElement;
+  let builder: GrDiffBuilderLegacy;
+
+  const LINE_BREAK_HTML = '<span class="style-scope gr-diff br"></span>';
+  const WBR_HTML = '<wbr class="style-scope gr-diff">';
+
+  const setBuilderPrefs = (prefs: Partial<DiffPreferencesInfo>) => {
+    builder = new GrDiffBuilderSideBySide(
+      createEmptyDiff(),
+      {...createDefaultDiffPrefs(), ...prefs},
+      element.diffElement
+    );
+  };
+
+  const line = (text: string) => {
+    const line = new GrDiffLine(GrDiffLineType.BOTH);
+    line.text = text;
+    return line;
+  };
+
+  setup(() => {
+    element = basicFixture.instantiate() as GrDiffBuilderElement;
+    stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+    stubRestApi('getProjectConfig').returns(Promise.resolve(createConfig()));
+    stubBaseUrl('/r');
+    setBuilderPrefs({});
+  });
+
+  test('line_length applied with <wbr> if line_wrapping is true', () => {
+    setBuilderPrefs({line_wrapping: true, tab_size: 4, line_length: 50});
+    const text = 'a'.repeat(51);
+    const expected = 'a'.repeat(50) + WBR_HTML + 'a';
+    const result = builder.createTextEl(null, line(text)).firstElementChild
+      ?.innerHTML;
+    assert.equal(result, expected);
+  });
+
+  test('line_length applied with line break if line_wrapping is false', () => {
+    setBuilderPrefs({line_wrapping: false, tab_size: 4, line_length: 50});
+    const text = 'a'.repeat(51);
+    const expected = 'a'.repeat(50) + LINE_BREAK_HTML + 'a';
+    const result = builder.createTextEl(null, line(text)).firstElementChild
+      ?.innerHTML;
+    assert.equal(result, expected);
+  });
+
+  [DiffViewMode.UNIFIED, DiffViewMode.SIDE_BY_SIDE].forEach(mode => {
+    test(`line_length used for regular files under ${mode}`, () => {
+      element.path = '/a.txt';
+      element.viewMode = mode;
+      element.diff = createEmptyDiff();
+      element.prefs = {
+        ...createDefaultDiffPrefs(),
+        tab_size: 4,
+        line_length: 50,
+      };
+      builder = element._getDiffBuilder() as GrDiffBuilderLegacy;
+      assert.equal(builder._prefs.line_length, 50);
+    });
+
+    test(`line_length ignored for commit msg under ${mode}`, () => {
+      element.path = '/COMMIT_MSG';
+      element.viewMode = mode;
+      element.diff = createEmptyDiff();
+      element.prefs = {
+        ...createDefaultDiffPrefs(),
+        tab_size: 4,
+        line_length: 50,
+      };
+      builder = element._getDiffBuilder() as GrDiffBuilderLegacy;
+      assert.equal(builder._prefs.line_length, 72);
+    });
+  });
+
+  test('createTextEl linewrap with tabs', () => {
+    setBuilderPrefs({tab_size: 4, line_length: 10});
+    const text = '\t'.repeat(7) + '!';
+    const el = builder.createTextEl(null, line(text));
+    assert.equal(el.innerText, text);
+    // With line length 10 and tab size 4, there should be a line break
+    // after every two tabs.
+    const newlineEl = el.querySelector('.contentText > .br');
+    assert.isOk(newlineEl);
+    assert.equal(
+      el.querySelector('.contentText .tab:nth-child(2)')?.nextSibling,
+      newlineEl
+    );
+  });
+
+  test('_handlePreferenceError throws with invalid preference', () => {
+    element.prefs = {...createDefaultDiffPrefs(), tab_size: 0};
+    assert.throws(() => element._getDiffBuilder());
+  });
+
+  test('_handlePreferenceError triggers alert and javascript error', () => {
+    const errorStub = sinon.stub();
+    element.addEventListener('show-alert', errorStub);
+    assert.throws(() => element._handlePreferenceError('tab size'));
+    assert.equal(
+      errorStub.lastCall.args[0].detail.message,
+      "The value of the 'tab size' user preference is invalid. " +
+        'Fix in diff preferences'
+    );
+  });
+
+  suite('intraline differences', () => {
+    let el: HTMLElement;
+    let str: string;
+    let annotateElementSpy: sinon.SinonSpy;
+    let layer: DiffLayer;
+    const lineNumberEl = document.createElement('td');
+
+    function slice(str: string, start: number, end?: number) {
+      return Array.from(str).slice(start, end).join('');
+    }
+
+    setup(() => {
+      el = divWithTextFixture.instantiate() as HTMLElement;
+      str = el.textContent ?? '';
+      annotateElementSpy = sinon.spy(GrAnnotation, 'annotateElement');
+      layer = document.createElement('gr-diff-builder')._createIntralineLayer();
+    });
+
+    test('annotate no highlights', () => {
+      layer.annotate(el, lineNumberEl, line(str), Side.LEFT);
+
+      // The content is unchanged.
+      assert.isFalse(annotateElementSpy.called);
+      assert.equal(el.childNodes.length, 1);
+      assert.instanceOf(el.childNodes[0], Text);
+      assert.equal(str, el.childNodes[0].textContent);
+    });
+
+    test('annotate with highlights', () => {
+      const l = line(str);
+      l.highlights = [
+        {contentIndex: 0, startIndex: 6, endIndex: 12},
+        {contentIndex: 0, startIndex: 18, endIndex: 22},
+      ];
+      const str0 = slice(str, 0, 6);
+      const str1 = slice(str, 6, 12);
+      const str2 = slice(str, 12, 18);
+      const str3 = slice(str, 18, 22);
+      const str4 = slice(str, 22);
+
+      layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+      assert.isTrue(annotateElementSpy.called);
+      assert.equal(el.childNodes.length, 5);
+
+      assert.instanceOf(el.childNodes[0], Text);
+      assert.equal(el.childNodes[0].textContent, str0);
+
+      assert.notInstanceOf(el.childNodes[1], Text);
+      assert.equal(el.childNodes[1].textContent, str1);
+
+      assert.instanceOf(el.childNodes[2], Text);
+      assert.equal(el.childNodes[2].textContent, str2);
+
+      assert.notInstanceOf(el.childNodes[3], Text);
+      assert.equal(el.childNodes[3].textContent, str3);
+
+      assert.instanceOf(el.childNodes[4], Text);
+      assert.equal(el.childNodes[4].textContent, str4);
+    });
+
+    test('annotate without endIndex', () => {
+      const l = line(str);
+      l.highlights = [{contentIndex: 0, startIndex: 28}];
+
+      const str0 = slice(str, 0, 28);
+      const str1 = slice(str, 28);
+
+      layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+      assert.isTrue(annotateElementSpy.called);
+      assert.equal(el.childNodes.length, 2);
+
+      assert.instanceOf(el.childNodes[0], Text);
+      assert.equal(el.childNodes[0].textContent, str0);
+
+      assert.notInstanceOf(el.childNodes[1], Text);
+      assert.equal(el.childNodes[1].textContent, str1);
+    });
+
+    test('annotate ignores empty highlights', () => {
+      const l = line(str);
+      l.highlights = [{contentIndex: 0, startIndex: 28, endIndex: 28}];
+
+      layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+      assert.isFalse(annotateElementSpy.called);
+      assert.equal(el.childNodes.length, 1);
+    });
+
+    test('annotate handles unicode', () => {
+      // Put some unicode into the string:
+      str = str.replace(/\s/g, '💢');
+      el.textContent = str;
+      const l = line(str);
+      l.highlights = [{contentIndex: 0, startIndex: 6, endIndex: 12}];
+
+      const str0 = slice(str, 0, 6);
+      const str1 = slice(str, 6, 12);
+      const str2 = slice(str, 12);
+
+      layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+      assert.isTrue(annotateElementSpy.called);
+      assert.equal(el.childNodes.length, 3);
+
+      assert.instanceOf(el.childNodes[0], Text);
+      assert.equal(el.childNodes[0].textContent, str0);
+
+      assert.notInstanceOf(el.childNodes[1], Text);
+      assert.equal(el.childNodes[1].textContent, str1);
+
+      assert.instanceOf(el.childNodes[2], Text);
+      assert.equal(el.childNodes[2].textContent, str2);
+    });
+
+    test('annotate handles unicode w/o endIndex', () => {
+      // Put some unicode into the string:
+      str = str.replace(/\s/g, '💢');
+      el.textContent = str;
+
+      const l = line(str);
+      l.highlights = [{contentIndex: 0, startIndex: 6}];
+
+      const str0 = slice(str, 0, 6);
+      const str1 = slice(str, 6);
+
+      layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+      assert.isTrue(annotateElementSpy.called);
+      assert.equal(el.childNodes.length, 2);
+
+      assert.instanceOf(el.childNodes[0], Text);
+      assert.equal(el.childNodes[0].textContent, str0);
+
+      assert.notInstanceOf(el.childNodes[1], Text);
+      assert.equal(el.childNodes[1].textContent, str1);
+    });
+  });
+
+  suite('tab indicators', () => {
+    let element: GrDiffBuilderElement;
+    let layer: DiffLayer;
+    const lineNumberEl = document.createElement('td');
+
+    setup(() => {
+      element = basicFixture.instantiate() as GrDiffBuilderElement;
+      element._showTabs = true;
+      layer = element._createTabIndicatorLayer();
+    });
+
+    test('does nothing with empty line', () => {
+      const l = line('');
+      const el = document.createElement('div');
+      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+      layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+      assert.isFalse(annotateElementStub.called);
+    });
+
+    test('does nothing with no tabs', () => {
+      const str = 'lorem ipsum no tabs';
+      const l = line(str);
+      const el = document.createElement('div');
+      el.textContent = str;
+      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+      layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+      assert.isFalse(annotateElementStub.called);
+    });
+
+    test('annotates tab at beginning', () => {
+      const str = '\tlorem upsum';
+      const l = line(str);
+      const el = document.createElement('div');
+      el.textContent = str;
+      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+      layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+      assert.equal(annotateElementStub.callCount, 1);
+      const args = annotateElementStub.getCalls()[0].args;
+      assert.equal(args[0], el);
+      assert.equal(args[1], 0, 'offset of tab indicator');
+      assert.equal(args[2], 1, 'length of tab indicator');
+      assert.include(args[3], 'tab-indicator');
+    });
+
+    test('does not annotate when disabled', () => {
+      element._showTabs = false;
+
+      const str = '\tlorem upsum';
+      const l = line(str);
+      const el = document.createElement('div');
+      el.textContent = str;
+      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+      layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+      assert.isFalse(annotateElementStub.called);
+    });
+
+    test('annotates multiple in beginning', () => {
+      const str = '\t\tlorem upsum';
+      const l = line(str);
+      const el = document.createElement('div');
+      el.textContent = str;
+      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+      layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+      assert.equal(annotateElementStub.callCount, 2);
+
+      let args = annotateElementStub.getCalls()[0].args;
+      assert.equal(args[0], el);
+      assert.equal(args[1], 0, 'offset of tab indicator');
+      assert.equal(args[2], 1, 'length of tab indicator');
+      assert.include(args[3], 'tab-indicator');
+
+      args = annotateElementStub.getCalls()[1].args;
+      assert.equal(args[0], el);
+      assert.equal(args[1], 1, 'offset of tab indicator');
+      assert.equal(args[2], 1, 'length of tab indicator');
+      assert.include(args[3], 'tab-indicator');
+    });
+
+    test('annotates intermediate tabs', () => {
+      const str = 'lorem\tupsum';
+      const l = line(str);
+      const el = document.createElement('div');
+      el.textContent = str;
+      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+      layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+      assert.equal(annotateElementStub.callCount, 1);
+      const args = annotateElementStub.getCalls()[0].args;
+      assert.equal(args[0], el);
+      assert.equal(args[1], 5, 'offset of tab indicator');
+      assert.equal(args[2], 1, 'length of tab indicator');
+      assert.include(args[3], 'tab-indicator');
+    });
+  });
+
+  suite('layers', () => {
+    let element: GrDiffBuilderElement;
+    let initialLayersCount = 0;
+    let withLayerCount = 0;
+    setup(() => {
+      const layers: DiffLayer[] = [];
+      element = basicFixture.instantiate() as GrDiffBuilderElement;
+      element.layers = layers;
+      element._showTrailingWhitespace = true;
+      element._setupAnnotationLayers();
+      initialLayersCount = element._layers.length;
+    });
+
+    test('no layers', () => {
+      element._setupAnnotationLayers();
+      assert.equal(element._layers.length, initialLayersCount);
+    });
+
+    suite('with layers', () => {
+      const layers: DiffLayer[] = [{annotate: () => {}}, {annotate: () => {}}];
+      setup(() => {
+        element = basicFixture.instantiate() as GrDiffBuilderElement;
+        element.layers = layers;
+        element._showTrailingWhitespace = true;
+        element._setupAnnotationLayers();
+        withLayerCount = element._layers.length;
+      });
+      test('with layers', () => {
+        element._setupAnnotationLayers();
+        assert.equal(element._layers.length, withLayerCount);
+        assert.equal(initialLayersCount + layers.length, withLayerCount);
+      });
+    });
+  });
+
+  suite('trailing whitespace', () => {
+    let element: GrDiffBuilderElement;
+    let layer: DiffLayer;
+    const lineNumberEl = document.createElement('td');
+
+    setup(() => {
+      element = basicFixture.instantiate() as GrDiffBuilderElement;
+      element._showTrailingWhitespace = true;
+      layer = element._createTrailingWhitespaceLayer();
+    });
+
+    test('does nothing with empty line', () => {
+      const l = line('');
+      const el = document.createElement('div');
+      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      layer.annotate(el, lineNumberEl, l, Side.LEFT);
+      assert.isFalse(annotateElementStub.called);
+    });
+
+    test('does nothing with no trailing whitespace', () => {
+      const str = 'lorem ipsum blah blah';
+      const l = line(str);
+      const el = document.createElement('div');
+      el.textContent = str;
+      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      layer.annotate(el, lineNumberEl, l, Side.LEFT);
+      assert.isFalse(annotateElementStub.called);
+    });
+
+    test('annotates trailing spaces', () => {
+      const str = 'lorem ipsum   ';
+      const l = line(str);
+      const el = document.createElement('div');
+      el.textContent = str;
+      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      layer.annotate(el, lineNumberEl, l, Side.LEFT);
+      assert.isTrue(annotateElementStub.called);
+      assert.equal(annotateElementStub.lastCall.args[1], 11);
+      assert.equal(annotateElementStub.lastCall.args[2], 3);
+    });
+
+    test('annotates trailing tabs', () => {
+      const str = 'lorem ipsum\t\t\t';
+      const l = line(str);
+      const el = document.createElement('div');
+      el.textContent = str;
+      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      layer.annotate(el, lineNumberEl, l, Side.LEFT);
+      assert.isTrue(annotateElementStub.called);
+      assert.equal(annotateElementStub.lastCall.args[1], 11);
+      assert.equal(annotateElementStub.lastCall.args[2], 3);
+    });
+
+    test('annotates mixed trailing whitespace', () => {
+      const str = 'lorem ipsum\t \t';
+      const l = line(str);
+      const el = document.createElement('div');
+      el.textContent = str;
+      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      layer.annotate(el, lineNumberEl, l, Side.LEFT);
+      assert.isTrue(annotateElementStub.called);
+      assert.equal(annotateElementStub.lastCall.args[1], 11);
+      assert.equal(annotateElementStub.lastCall.args[2], 3);
+    });
+
+    test('unicode preceding trailing whitespace', () => {
+      const str = '💢\t';
+      const l = line(str);
+      const el = document.createElement('div');
+      el.textContent = str;
+      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      layer.annotate(el, lineNumberEl, l, Side.LEFT);
+      assert.isTrue(annotateElementStub.called);
+      assert.equal(annotateElementStub.lastCall.args[1], 1);
+      assert.equal(annotateElementStub.lastCall.args[2], 1);
+    });
+
+    test('does not annotate when disabled', () => {
+      element._showTrailingWhitespace = false;
+      const str = 'lorem upsum\t \t ';
+      const l = line(str);
+      const el = document.createElement('div');
+      el.textContent = str;
+      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      layer.annotate(el, lineNumberEl, l, Side.LEFT);
+      assert.isFalse(annotateElementStub.called);
+    });
+  });
+
+  suite('rendering text, images and binary files', () => {
+    let processStub: sinon.SinonStub;
+    let keyLocations: KeyLocations;
+    let content: DiffContent[] = [];
+
+    setup(() => {
+      element = basicFixture.instantiate() as GrDiffBuilderElement;
+      element.viewMode = 'SIDE_BY_SIDE';
+      processStub = sinon
+        .stub(element.processor, 'process')
+        .returns(Promise.resolve());
+      keyLocations = {left: {}, right: {}};
+      element.prefs = {
+        ...DEFAULT_PREFS,
+        context: -1,
+        syntax_highlighting: true,
+      };
+      content = [
+        {
+          a: ['all work and no play make andybons a dull boy'],
+          b: ['elgoog elgoog elgoog'],
+        },
+        {
+          ab: [
+            'Non eram nescius, Brute, cum, quae summis ingeniis ',
+            'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+          ],
+        },
+      ];
+    });
+
+    test('text', async () => {
+      element.diff = {...createEmptyDiff(), content};
+      element.render(keyLocations);
+      await waitForEventOnce(element, 'render-content');
+      assert.isTrue(processStub.calledOnce);
+      assert.isFalse(processStub.lastCall.args[1]);
+    });
+
+    test('image', async () => {
+      element.diff = {...createEmptyDiff(), content, binary: true};
+      element.isImageDiff = true;
+      element.render(keyLocations);
+      await waitForEventOnce(element, 'render-content');
+      assert.isTrue(processStub.calledOnce);
+      assert.isTrue(processStub.lastCall.args[1]);
+    });
+
+    test('binary', async () => {
+      element.diff = {...createEmptyDiff(), content, binary: true};
+      element.render(keyLocations);
+      await waitForEventOnce(element, 'render-content');
+      assert.isTrue(processStub.calledOnce);
+      assert.isTrue(processStub.lastCall.args[1]);
+    });
+  });
+
+  suite('rendering', () => {
+    let content: DiffContent[];
+    let outputEl: HTMLTableElement;
+    let keyLocations: KeyLocations;
+    let addColumnsStub: sinon.SinonStub;
+    let dispatchStub: sinon.SinonStub;
+    let builder: GrDiffBuilderSideBySide;
+
+    setup(() => {
+      const prefs = {...DEFAULT_PREFS};
+      content = [
+        {
+          a: ['all work and no play make andybons a dull boy'],
+          b: ['elgoog elgoog elgoog'],
+        },
+        {
+          ab: [
+            'Non eram nescius, Brute, cum, quae summis ingeniis ',
+            'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+          ],
+        },
+      ];
+      element = basicFixture.instantiate() as GrDiffBuilderElement;
+      dispatchStub = sinon.stub(element, 'dispatchEvent');
+      outputEl = queryAndAssert(element, '#diffTable');
+      keyLocations = {left: {}, right: {}};
+      sinon.stub(element, '_getDiffBuilder').callsFake(() => {
+        builder = new GrDiffBuilderSideBySide(
+          {...createEmptyDiff(), content},
+          prefs,
+          outputEl
+        );
+        addColumnsStub = sinon.stub(builder, 'addColumns');
+        builder.buildSectionElement = function (group) {
+          const section = document.createElement('stub');
+          section.textContent = group.lines.reduce(
+            (acc, line) => acc + line.text,
+            ''
+          );
+          return section;
+        };
+        return builder;
+      });
+      element.diff = {...createEmptyDiff(), content};
+      element.prefs = prefs;
+      element.render(keyLocations);
+    });
+
+    test('addColumns is called', () => {
+      assert.isTrue(addColumnsStub.called);
+    });
+
+    test('getGroupsByLineRange one line', () => {
+      const section = outputEl.querySelector<HTMLElement>(
+        'stub:nth-of-type(3)'
+      );
+      const groups = builder.getGroupsByLineRange(1, 1, Side.LEFT);
+      assert.equal(groups.length, 1);
+      assert.strictEqual(groups[0].element, section);
+    });
+
+    test('getGroupsByLineRange over diff', () => {
+      const section = [
+        outputEl.querySelector<HTMLElement>('stub:nth-of-type(3)'),
+        outputEl.querySelector<HTMLElement>('stub:nth-of-type(4)'),
+      ];
+      const groups = builder.getGroupsByLineRange(1, 2, Side.LEFT);
+      assert.equal(groups.length, 2);
+      assert.strictEqual(groups[0].element, section[0]);
+      assert.strictEqual(groups[1].element, section[1]);
+    });
+
+    test('render-start and render-content are fired', async () => {
+      await new Promise(resolve => afterNextRender(element, resolve));
+      const firedEventTypes = dispatchStub.getCalls().map(c => c.args[0].type);
+      assert.include(firedEventTypes, 'render-start');
+      assert.include(firedEventTypes, 'render-content');
+    });
+
+    test('cancel cancels the processor', () => {
+      const processorCancelStub = sinon.stub(element.processor, 'cancel');
+      element.cancel();
+      assert.isTrue(processorCancelStub.called);
+    });
+  });
+
+  suite('context hiding and expanding', () => {
+    let dispatchStub: sinon.SinonStub;
+
+    setup(async () => {
+      element = basicFixture.instantiate() as GrDiffBuilderElement;
+      dispatchStub = sinon.stub(element, 'dispatchEvent');
+      const afterNextRenderPromise = new Promise(resolve => {
+        afterNextRender(element, resolve);
+      });
+      element.diff = {
+        ...createEmptyDiff(),
+        content: [
+          {ab: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(i => `unchanged ${i}`)},
+          {a: ['before'], b: ['after']},
+          {ab: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(i => `unchanged ${10 + i}`)},
+        ],
+      };
+      element.viewMode = DiffViewMode.SIDE_BY_SIDE;
+
+      const keyLocations: KeyLocations = {left: {}, right: {}};
+      element.prefs = {
+        ...DEFAULT_PREFS,
+        context: 1,
+      };
+      element.render(keyLocations);
+      // Make sure all listeners are installed.
+      await afterNextRenderPromise;
+    });
+
+    test('hides lines behind two context controls', () => {
+      const contextControls = element.querySelectorAll('gr-context-controls');
+      assert.equal(contextControls.length, 2);
+
+      const diffRows = element.querySelectorAll('.diff-row');
+      // The first two are LOST and FILE line
+      assert.equal(diffRows.length, 2 + 1 + 1 + 1);
+      assert.include(diffRows[2].textContent, 'unchanged 10');
+      assert.include(diffRows[3].textContent, 'before');
+      assert.include(diffRows[3].textContent, 'after');
+      assert.include(diffRows[4].textContent, 'unchanged 11');
+    });
+
+    test('clicking +x common lines expands those lines', () => {
+      const contextControls = element.querySelectorAll('gr-context-controls');
+      const topExpandCommonButton =
+        contextControls[0].shadowRoot?.querySelectorAll<HTMLElement>(
+          '.showContext'
+        )[0];
+      assert.isOk(topExpandCommonButton);
+      assert.include(topExpandCommonButton!.textContent, '+9 common lines');
+      topExpandCommonButton!.click();
+      const diffRows = element.querySelectorAll('.diff-row');
+      // The first two are LOST and FILE line
+      assert.equal(diffRows.length, 2 + 10 + 1 + 1);
+      assert.include(diffRows[2].textContent, 'unchanged 1');
+      assert.include(diffRows[3].textContent, 'unchanged 2');
+      assert.include(diffRows[4].textContent, 'unchanged 3');
+      assert.include(diffRows[5].textContent, 'unchanged 4');
+      assert.include(diffRows[6].textContent, 'unchanged 5');
+      assert.include(diffRows[7].textContent, 'unchanged 6');
+      assert.include(diffRows[8].textContent, 'unchanged 7');
+      assert.include(diffRows[9].textContent, 'unchanged 8');
+      assert.include(diffRows[10].textContent, 'unchanged 9');
+      assert.include(diffRows[11].textContent, 'unchanged 10');
+      assert.include(diffRows[12].textContent, 'before');
+      assert.include(diffRows[12].textContent, 'after');
+      assert.include(diffRows[13].textContent, 'unchanged 11');
+    });
+
+    test('unhideLine shows the line with context', async () => {
+      dispatchStub.reset();
+      element.unhideLine(4, Side.LEFT);
+
+      const diffRows = element.querySelectorAll('.diff-row');
+      // The first two are LOST and FILE line
+      // Lines 3-5 (Line 4 plus 1 context in each direction) will be expanded
+      // Because context expanders do not hide <3 lines, lines 1-2 will also
+      // be shown.
+      // Lines 6-9 continue to be hidden
+      assert.equal(diffRows.length, 2 + 5 + 1 + 1 + 1);
+      assert.include(diffRows[2].textContent, 'unchanged 1');
+      assert.include(diffRows[3].textContent, 'unchanged 2');
+      assert.include(diffRows[4].textContent, 'unchanged 3');
+      assert.include(diffRows[5].textContent, 'unchanged 4');
+      assert.include(diffRows[6].textContent, 'unchanged 5');
+      assert.include(diffRows[7].textContent, 'unchanged 10');
+      assert.include(diffRows[8].textContent, 'before');
+      assert.include(diffRows[8].textContent, 'after');
+      assert.include(diffRows[9].textContent, 'unchanged 11');
+
+      await new Promise(resolve => afterNextRender(element, resolve));
+      const firedEventTypes = dispatchStub.getCalls().map(c => c.args[0].type);
+      assert.include(firedEventTypes, 'render-content');
+    });
+  });
+
+  suite('mock-diff', () => {
+    let element: GrDiffBuilderElement;
+    let builder: GrDiffBuilderSideBySide;
+    let diff: DiffInfo;
+    let keyLocations: KeyLocations;
+
+    setup(() => {
+      element = mockDiffFixture.instantiate() as GrDiffBuilderElement;
+      diff = createDiff();
+      element.diff = diff;
+
+      keyLocations = {left: {}, right: {}};
+
+      element.prefs = {
+        ...createDefaultDiffPrefs(),
+        line_length: 80,
+        show_tabs: true,
+        tab_size: 4,
+      };
+      element.render(keyLocations);
+      builder = element._builder as GrDiffBuilderSideBySide;
+    });
+
+    test('aria-labels on added line numbers', () => {
+      const deltaLineNumberButton = element.diffElement.querySelectorAll(
+        '.lineNumButton.right'
+      )[5];
+
+      assert.isOk(deltaLineNumberButton);
+      assert.equal(deltaLineNumberButton.getAttribute('aria-label'), '5 added');
+    });
+
+    test('aria-labels on removed line numbers', () => {
+      const deltaLineNumberButton = element.diffElement.querySelectorAll(
+        '.lineNumButton.left'
+      )[10];
+
+      assert.isOk(deltaLineNumberButton);
+      assert.equal(
+        deltaLineNumberButton.getAttribute('aria-label'),
+        '10 removed'
+      );
+    });
+
+    test('getContentByLine', () => {
+      let actual: HTMLElement | null;
+
+      actual = builder.getContentByLine(2, Side.LEFT);
+      assert.equal(actual?.textContent, diff.content[0].ab?.[1]);
+
+      actual = builder.getContentByLine(2, Side.RIGHT);
+      assert.equal(actual?.textContent, diff.content[0].ab?.[1]);
+
+      actual = builder.getContentByLine(5, Side.LEFT);
+      assert.equal(actual?.textContent, diff.content[2].ab?.[0]);
+
+      actual = builder.getContentByLine(5, Side.RIGHT);
+      assert.equal(actual?.textContent, diff.content[1].b?.[0]);
+    });
+
+    test('getContentTdByLineEl works both with button and td', () => {
+      const diffRow = element.diffElement.querySelectorAll('tr.diff-row')[2];
+
+      const lineNumTdLeft = queryAndAssert(diffRow, 'td.lineNum.left');
+      const lineNumButtonLeft = queryAndAssert(lineNumTdLeft, 'button');
+      const contentTdLeft = diffRow.querySelectorAll('.content')[0];
+
+      const lineNumTdRight = queryAndAssert(diffRow, 'td.lineNum.right');
+      const lineNumButtonRight = queryAndAssert(lineNumTdRight, 'button');
+      const contentTdRight = diffRow.querySelectorAll('.content')[1];
+
+      assert.equal(element.getContentTdByLineEl(lineNumTdLeft), contentTdLeft);
+      assert.equal(
+        element.getContentTdByLineEl(lineNumButtonLeft),
+        contentTdLeft
+      );
+      assert.equal(
+        element.getContentTdByLineEl(lineNumTdRight),
+        contentTdRight
+      );
+      assert.equal(
+        element.getContentTdByLineEl(lineNumButtonRight),
+        contentTdRight
+      );
+    });
+
+    test('findLinesByRange', () => {
+      const lines: GrDiffLine[] = [];
+      const elems: HTMLElement[] = [];
+      const start = 6;
+      const end = 10;
+      const count = end - start + 1;
+
+      builder.findLinesByRange(start, end, Side.RIGHT, lines, elems);
+
+      assert.equal(lines.length, count);
+      assert.equal(elems.length, count);
+
+      for (let i = 0; i < 5; i++) {
+        assert.instanceOf(lines[i], GrDiffLine);
+        assert.equal(lines[i].afterNumber, start + i);
+        assert.instanceOf(elems[i], HTMLElement);
+        assert.equal(lines[i].text, elems[i].textContent);
+      }
+    });
+
+    test('renderContentByRange', () => {
+      const spy = sinon.spy(builder, 'createTextEl');
+      const start = 9;
+      const end = 14;
+      const count = end - start + 1;
+
+      builder.renderContentByRange(start, end, Side.LEFT);
+
+      assert.equal(spy.callCount, count);
+      spy.getCalls().forEach((call, i: number) => {
+        assert.equal(call.args[1].beforeNumber, start + i);
+      });
+    });
+
+    test('renderContentByRange non-existent elements', () => {
+      const spy = sinon.spy(builder, 'createTextEl');
+
+      sinon
+        .stub(builder, 'getLineNumberEl')
+        .returns(document.createElement('div'));
+      sinon
+        .stub(builder, 'findLinesByRange')
+        .callsFake((_1, _2, _3, lines, elements) => {
+          // Add a line and a corresponding element.
+          lines?.push(new GrDiffLine(GrDiffLineType.BOTH));
+          const tr = document.createElement('tr');
+          const td = document.createElement('td');
+          const el = document.createElement('div');
+          tr.appendChild(td);
+          td.appendChild(el);
+          elements?.push(el);
+
+          // Add 2 lines without corresponding elements.
+          lines?.push(new GrDiffLine(GrDiffLineType.BOTH));
+          lines?.push(new GrDiffLine(GrDiffLineType.BOTH));
+        });
+
+      builder.renderContentByRange(1, 10, Side.LEFT);
+      // Should be called only once because only one line had a corresponding
+      // element.
+      assert.equal(spy.callCount, 1);
+    });
+
+    test('getLineNumberEl side-by-side left', () => {
+      const contentEl = builder.getContentByLine(
+        5,
+        Side.LEFT,
+        element.$.diffTable as HTMLTableElement
+      );
+      assert.isOk(contentEl);
+      const lineNumberEl = builder.getLineNumberEl(contentEl!, Side.LEFT);
+      assert.isOk(lineNumberEl);
+      assert.isTrue(lineNumberEl!.classList.contains('lineNum'));
+      assert.isTrue(lineNumberEl!.classList.contains(Side.LEFT));
+    });
+
+    test('getLineNumberEl side-by-side right', () => {
+      const contentEl = builder.getContentByLine(
+        5,
+        Side.RIGHT,
+        element.$.diffTable as HTMLTableElement
+      );
+      assert.isOk(contentEl);
+      const lineNumberEl = builder.getLineNumberEl(contentEl!, Side.RIGHT);
+      assert.isOk(lineNumberEl);
+      assert.isTrue(lineNumberEl!.classList.contains('lineNum'));
+      assert.isTrue(lineNumberEl!.classList.contains(Side.RIGHT));
+    });
+
+    test('getLineNumberEl unified left', async () => {
+      // Re-render as unified:
+      element.viewMode = 'UNIFIED_DIFF';
+      element.render(keyLocations);
+      builder = element._builder as GrDiffBuilderSideBySide;
+
+      const contentEl = builder.getContentByLine(
+        5,
+        Side.LEFT,
+        element.$.diffTable as HTMLTableElement
+      );
+      assert.isOk(contentEl);
+      const lineNumberEl = builder.getLineNumberEl(contentEl!, Side.LEFT);
+      assert.isOk(lineNumberEl);
+      assert.isTrue(lineNumberEl!.classList.contains('lineNum'));
+      assert.isTrue(lineNumberEl!.classList.contains(Side.LEFT));
+    });
+
+    test('getLineNumberEl unified right', async () => {
+      // Re-render as unified:
+      element.viewMode = 'UNIFIED_DIFF';
+      element.render(keyLocations);
+      builder = element._builder as GrDiffBuilderSideBySide;
+
+      const contentEl = builder.getContentByLine(
+        5,
+        Side.RIGHT,
+        element.$.diffTable as HTMLTableElement
+      );
+      assert.isOk(contentEl);
+      const lineNumberEl = builder.getLineNumberEl(contentEl!, Side.RIGHT);
+      assert.isOk(lineNumberEl);
+      assert.isTrue(lineNumberEl!.classList.contains('lineNum'));
+      assert.isTrue(lineNumberEl!.classList.contains(Side.RIGHT));
+    });
+
+    test('getNextContentOnSide side-by-side left', () => {
+      const startElem = builder.getContentByLine(
+        5,
+        Side.LEFT,
+        element.$.diffTable as HTMLTableElement
+      );
+      assert.isOk(startElem);
+      const expectedStartString = diff.content[2].ab?.[0];
+      const expectedNextString = diff.content[2].ab?.[1];
+      assert.equal(startElem!.textContent, expectedStartString);
+
+      const nextElem = builder.getNextContentOnSide(startElem!, Side.LEFT);
+      assert.isOk(nextElem);
+      assert.equal(nextElem!.textContent, expectedNextString);
+    });
+
+    test('getNextContentOnSide side-by-side right', () => {
+      const startElem = builder.getContentByLine(
+        5,
+        Side.RIGHT,
+        element.$.diffTable as HTMLTableElement
+      );
+      const expectedStartString = diff.content[1].b?.[0];
+      const expectedNextString = diff.content[1].b?.[1];
+      assert.isOk(startElem);
+      assert.equal(startElem!.textContent, expectedStartString);
+
+      const nextElem = builder.getNextContentOnSide(startElem!, Side.RIGHT);
+      assert.isOk(nextElem);
+      assert.equal(nextElem!.textContent, expectedNextString);
+    });
+
+    test('getNextContentOnSide unified left', async () => {
+      // Re-render as unified:
+      element.viewMode = 'UNIFIED_DIFF';
+      element.render(keyLocations);
+      builder = element._builder as GrDiffBuilderSideBySide;
+
+      const startElem = builder.getContentByLine(
+        5,
+        Side.LEFT,
+        element.$.diffTable as HTMLTableElement
+      );
+      const expectedStartString = diff.content[2].ab?.[0];
+      const expectedNextString = diff.content[2].ab?.[1];
+      assert.isOk(startElem);
+      assert.equal(startElem!.textContent, expectedStartString);
+
+      const nextElem = builder.getNextContentOnSide(startElem!, Side.LEFT);
+      assert.isOk(nextElem);
+      assert.equal(nextElem!.textContent, expectedNextString);
+    });
+
+    test('getNextContentOnSide unified right', async () => {
+      // Re-render as unified:
+      element.viewMode = 'UNIFIED_DIFF';
+      element.render(keyLocations);
+      builder = element._builder as GrDiffBuilderSideBySide;
+
+      const startElem = builder.getContentByLine(
+        5,
+        Side.RIGHT,
+        element.$.diffTable as HTMLTableElement
+      );
+      const expectedStartString = diff.content[1].b?.[0];
+      const expectedNextString = diff.content[1].b?.[1];
+      assert.isOk(startElem);
+      assert.equal(startElem!.textContent, expectedStartString);
+
+      const nextElem = builder.getNextContentOnSide(startElem!, Side.RIGHT);
+      assert.isOk(nextElem);
+      assert.equal(nextElem!.textContent, expectedNextString);
+    });
+  });
+
+  suite('blame', () => {
+    let mockBlame: BlameInfo[];
+
+    setup(() => {
+      mockBlame = [
+        {
+          author: 'test-author',
+          time: 314,
+          commit_msg: 'test-commit-message',
+          id: 'commit 1',
+          ranges: [
+            {start: 1, end: 2},
+            {start: 10, end: 16},
+          ],
+        },
+        {
+          author: 'test-author',
+          time: 314,
+          commit_msg: 'test-commit-message',
+          id: 'commit 2',
+          ranges: [
+            {start: 4, end: 10},
+            {start: 17, end: 32},
+          ],
+        },
+      ];
+    });
+
+    test('setBlame attempts to render each blamed line', () => {
+      const getBlameStub = sinon
+        .stub(builder, 'getBlameTdByLine')
+        .returns(undefined);
+      builder.setBlame(mockBlame);
+      assert.equal(getBlameStub.callCount, 32);
+    });
+
+    test('getBlameCommitForBaseLine', () => {
+      sinon.stub(builder, 'getBlameTdByLine').returns(undefined);
+      builder.setBlame(mockBlame);
+      assert.isOk(builder.getBlameCommitForBaseLine(1));
+      assert.equal(builder.getBlameCommitForBaseLine(1)?.id, 'commit 1');
+
+      assert.isOk(builder.getBlameCommitForBaseLine(11));
+      assert.equal(builder.getBlameCommitForBaseLine(11)?.id, 'commit 1');
+
+      assert.isOk(builder.getBlameCommitForBaseLine(32));
+      assert.equal(builder.getBlameCommitForBaseLine(32)?.id, 'commit 2');
+
+      assert.isUndefined(builder.getBlameCommitForBaseLine(33));
+    });
+
+    test('getBlameCommitForBaseLine w/o blame returns null', () => {
+      assert.isUndefined(builder.getBlameCommitForBaseLine(1));
+      assert.isUndefined(builder.getBlameCommitForBaseLine(11));
+      assert.isUndefined(builder.getBlameCommitForBaseLine(31));
+    });
+
+    test('createBlameCell', () => {
+      const mockBlameInfo = {
+        time: 1576155200,
+        id: '1234567890',
+        author: 'Clark Kent',
+        commit_msg: 'Testing Commit',
+        ranges: [{start: 4, end: 10}],
+      };
+      const getBlameStub = sinon
+        .stub(builder, 'getBlameCommitForBaseLine')
+        .returns(mockBlameInfo);
+      const line = new GrDiffLine(GrDiffLineType.BOTH);
+      line.beforeNumber = 3;
+      line.afterNumber = 5;
+
+      const result = builder.createBlameCell(line.beforeNumber);
+
+      assert.isTrue(getBlameStub.calledWithExactly(3));
+      assert.equal(result.getAttribute('data-line-number'), '3');
+      expect(result).dom.to.equal(/* HTML */ `
+        <span class="gr-diff style-scope">
+          <a class="blameDate gr-diff style-scope" href="/r/q/1234567890">
+            12/12/2019
+          </a>
+          <span class="blameAuthor gr-diff style-scope">Clark</span>
+          <gr-hovercard class="gr-diff style-scope">
+            <span class="blameHoverCard gr-diff style-scope">
+              Commit 1234567890<br />
+              Author: Clark Kent<br />
+              Date: 12/12/2019<br />
+              <br />
+              Testing Commit
+            </span>
+          </gr-hovercard>
+        </span>
+      `);
+    });
+  });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
index ceadc94..c04d156 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
@@ -162,10 +162,8 @@
    *
    * TODO(brohlfs): Consolidate this with getLineEl... methods in html file.
    */
-  private getLineNumberEl(
-    content: HTMLElement,
-    side: Side
-  ): HTMLElement | null {
+  // visible for testing
+  getLineNumberEl(content: HTMLElement, side: Side): HTMLElement | null {
     let row: HTMLElement | null = content;
     while (row && !row.classList.contains('diff-row')) row = row.parentElement;
     return row ? (row.querySelector('.lineNum.' + side) as HTMLElement) : null;
@@ -349,7 +347,8 @@
     });
   }
 
-  protected createTextEl(
+  // visible for testing
+  createTextEl(
     lineNumberEl: HTMLElement | null,
     line: GrDiffLine,
     side?: Side
@@ -491,7 +490,8 @@
    * Create a blame cell for the given base line. Blame information will be
    * included in the cell if available.
    */
-  protected createBlameCell(lineNumber: LineNumber): HTMLTableCellElement {
+  // visible for testing
+  createBlameCell(lineNumber: LineNumber): HTMLTableCellElement {
     const blameTd = createElementDiff('td', 'blame') as HTMLTableCellElement;
     blameTd.setAttribute('data-line-number', lineNumber.toString());
     if (!lineNumber) return blameTd;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
index a711215..f2690bc 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
@@ -44,7 +44,8 @@
     };
   }
 
-  protected override buildSectionElement(group: GrDiffGroup) {
+  // visible for testing
+  override buildSectionElement(group: GrDiffGroup) {
     const sectionEl = createElementDiff('tbody', 'section');
     sectionEl.classList.add(group.type);
     if (group.isTotal()) {
@@ -147,7 +148,8 @@
     return td;
   }
 
-  protected override getNextContentOnSide(
+  // visible for testing
+  override getNextContentOnSide(
     content: HTMLElement,
     side: Side
   ): HTMLElement | null {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
index add7ffa..4b664e2 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
@@ -89,7 +89,8 @@
 
   protected readonly numLinesLeft: number;
 
-  protected readonly _prefs: DiffPreferencesInfo;
+  // visible for testing
+  readonly _prefs: DiffPreferencesInfo;
 
   protected readonly renderPrefs?: RenderPreferences;
 
@@ -194,7 +195,8 @@
     group.element = element;
   }
 
-  private getGroupsByLineRange(
+  // visible for testing
+  getGroupsByLineRange(
     startLine: LineNumber,
     endLine: LineNumber,
     side: Side
@@ -257,7 +259,8 @@
    *        TODO: Change `null` to `undefined` in paramete type. Also: Do we
    *        really need to support null/undefined? Also change to camelCase.
    */
-  protected findLinesByRange(
+  // visible for testing
+  findLinesByRange(
     start: LineNumber,
     end: LineNumber,
     side: Side,
@@ -352,9 +355,8 @@
    *
    * @return The commit information.
    */
-  protected getBlameCommitForBaseLine(
-    lineNum: LineNumber
-  ): BlameInfo | undefined {
+  // visible for testing
+  getBlameCommitForBaseLine(lineNum: LineNumber): BlameInfo | undefined {
     for (const blameCommit of this.blameInfo) {
       for (const range of blameCommit.ranges) {
         if (range.start <= lineNum && range.end >= lineNum) {
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 829a36b..8e8fe42 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -469,6 +469,24 @@
   };
 }
 
+export function createEmptyDiff(): DiffInfo {
+  return {
+    meta_a: {
+      name: 'empty-left.txt',
+      content_type: 'text/plain',
+      lines: 1,
+    },
+    meta_b: {
+      name: 'empty-right.txt',
+      content_type: 'text/plain',
+      lines: 1,
+    },
+    intraline_status: 'OK',
+    change_type: 'MODIFIED',
+    content: [],
+  };
+}
+
 export function createDiff(): DiffInfo {
   return {
     meta_a: {