Migrate gr-diff-host_test from js to ts

Release-Notes: skip
Change-Id: Ie32ada2dc337aca359535e3046477ae7347b6991
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index eb6d071..df2ba28 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -116,7 +116,8 @@
   return !!(diff.binary && (isA || isB));
 }
 
-interface LineInfo {
+// visible for testing
+export interface LineInfo {
   beforeNumber?: LineNumber;
   afterNumber?: LineNumber;
 }
@@ -277,15 +278,18 @@
 
   private readonly getChecksModel = resolve(this, checksModelToken);
 
-  private readonly reporting = getAppContext().reportingService;
+  // visible for testing
+  readonly reporting = getAppContext().reportingService;
 
   private readonly flags = getAppContext().flagsService;
 
   private readonly restApiService = getAppContext().restApiService;
 
-  private readonly jsAPI = getAppContext().jsApiService;
+  // visible for testing
+  readonly jsAPI = getAppContext().jsApiService;
 
-  private readonly syntaxLayer: GrSyntaxLayerWorker;
+  // visible for testing
+  readonly syntaxLayer: GrSyntaxLayerWorker;
 
   private checksSubscription?: Subscription;
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
deleted file mode 100644
index f2b99c5..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
+++ /dev/null
@@ -1,1587 +0,0 @@
-/**
- * @license
- * Copyright (C) 2018 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 './gr-diff-host.js';
-
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {createDefaultDiffPrefs, Side} from '../../../constants/constants.js';
-import {createChange, createComment, createCommentThread} from '../../../test/test-data-generators.js';
-import {addListenerForTest, mockPromise, stubRestApi, waitUntil} from '../../../test/test-utils.js';
-import {EditPatchSetNum, ParentPatchSetNum} from '../../../types/common.js';
-import {CoverageType} from '../../../types/types.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {GrDiffBuilderImage} from '../../../embed/diff/gr-diff-builder/gr-diff-builder-image.js';
-import {waitForEventOnce} from '../../../utils/event-util.js';
-
-const basicFixture = fixtureFromElement('gr-diff-host');
-
-suite('gr-diff-host tests', () => {
-  let element;
-
-  let loggedIn;
-
-  setup(async () => {
-    loggedIn = false;
-    stubRestApi('getLoggedIn').callsFake(() => Promise.resolve(loggedIn));
-    element = basicFixture.instantiate();
-    element.changeNum = 123;
-    element.path = 'some/path';
-    sinon.stub(element.reporting, 'time');
-    sinon.stub(element.reporting, 'timeEnd');
-    await flush();
-  });
-
-  suite('plugin layers', () => {
-    const pluginLayers = [{annotate: () => {}}, {annotate: () => {}}];
-    setup(() => {
-      element = basicFixture.instantiate();
-      sinon.stub(element.jsAPI, 'getDiffLayers').returns(pluginLayers);
-      element.changeNum = 123;
-      element.path = 'some/path';
-    });
-    test('plugin layers requested', async () => {
-      element.patchRange = {};
-      element.change = createChange();
-      stubRestApi('getDiff').returns(Promise.resolve({content: []}));
-      await element.reload();
-      assert(element.jsAPI.getDiffLayers.called);
-    });
-  });
-
-  suite('render reporting', () => {
-    test('ends total and syntax timer after syntax layer', async () => {
-      sinon.stub(element.reporting, 'diffViewContentDisplayed');
-      let notifySyntaxProcessed;
-      sinon.stub(element.syntaxLayer, 'process').returns(
-          new Promise(resolve => {
-            notifySyntaxProcessed = resolve;
-          })
-      );
-      stubRestApi('getDiff').returns(Promise.resolve({content: []}));
-      element.patchRange = {};
-      element.change = createChange();
-      element.prefs = createDefaultDiffPrefs();
-      element.reload(true);
-      // Multiple cascading microtasks are scheduled.
-      await flush();
-      notifySyntaxProcessed();
-      await waitUntil(() => element.reporting.timeEnd.callCount === 4);
-      const calls = element.reporting.timeEnd.getCalls();
-      assert.equal(calls.length, 4);
-      assert.equal(calls[0].args[0], 'Diff Load Render');
-      assert.equal(calls[1].args[0], 'Diff Content Render');
-      assert.equal(calls[2].args[0], 'Diff Syntax Render');
-      assert.equal(calls[3].args[0], 'Diff Total Render');
-      assert.isTrue(element.reporting.diffViewContentDisplayed.called);
-    });
-
-    test('completes reload promise after syntax layer processing', async () => {
-      let notifySyntaxProcessed;
-      sinon.stub(element.syntaxLayer, 'process').returns(new Promise(
-          resolve => {
-            notifySyntaxProcessed = resolve;
-          }));
-      stubRestApi('getDiff').returns(
-          Promise.resolve({content: []}));
-      element.patchRange = {};
-      element.change = createChange();
-      let reloadComplete = false;
-      element.prefs = createDefaultDiffPrefs();
-      element.reload().then(() => {
-        reloadComplete = true;
-      });
-      // Multiple cascading microtasks are scheduled.
-      await flush();
-      assert.isFalse(reloadComplete);
-      notifySyntaxProcessed();
-      await waitUntil(() => reloadComplete);
-      assert.isTrue(reloadComplete);
-    });
-  });
-
-  test('reload() cancels before network resolves', () => {
-    const cancelStub = sinon.stub(element.$.diff, 'cancel');
-
-    // Stub the network calls into requests that never resolve.
-    sinon.stub(element, '_getDiff').callsFake(() => new Promise(() => {}));
-    element.patchRange = {};
-    element.change = createChange();
-
-    // Needs to be set to something first for it to cancel.
-    element.diff = {
-      content: [{
-        a: ['foo'],
-      }],
-    };
-
-    element.reload();
-    assert.isTrue(cancelStub.called);
-  });
-
-  test('reload() loads files weblinks', async () => {
-    element.change = createChange();
-    const weblinksStub = sinon.stub(GerritNav, '_generateWeblinks')
-        .returns({name: 'stubb', url: '#s'});
-    stubRestApi('getDiff').returns(Promise.resolve({
-      content: [],
-    }));
-    element.projectName = 'test-project';
-    element.path = 'test-path';
-    element.commitRange = {baseCommit: 'test-base', commit: 'test-commit'};
-    element.patchRange = {};
-
-    await element.reload();
-
-    assert.equal(weblinksStub.callCount, 3);
-    assert.deepEqual(weblinksStub.firstCall.args[0], {
-      commit: 'test-base',
-      file: 'test-path',
-      options: {
-        weblinks: undefined,
-      },
-      repo: 'test-project',
-      type: GerritNav.WeblinkType.EDIT});
-    assert.deepEqual(element.editWeblinks, [{
-      name: 'stubb', url: '#s',
-    }]);
-    assert.deepEqual(weblinksStub.secondCall.args[0], {
-      commit: 'test-base',
-      file: 'test-path',
-      options: {
-        weblinks: undefined,
-      },
-      repo: 'test-project',
-      type: GerritNav.WeblinkType.FILE});
-    assert.deepEqual(weblinksStub.thirdCall.args[0], {
-      commit: 'test-commit',
-      file: 'test-path',
-      options: {
-        weblinks: undefined,
-      },
-      repo: 'test-project',
-      type: GerritNav.WeblinkType.FILE});
-    assert.deepEqual(element.filesWeblinks, {
-      meta_a: [{name: 'stubb', url: '#s'}],
-      meta_b: [{name: 'stubb', url: '#s'}],
-    });
-  });
-
-  test('prefetch getDiff', async () => {
-    const diffRestApiStub = stubRestApi('getDiff')
-        .returns(Promise.resolve({content: []}));
-    element.changeNum = 123;
-    element.patchRange = {basePatchNum: 1, patchNum: 2};
-    element.path = 'file.txt';
-    element.prefetchDiff();
-    await element._getDiff();
-    assert.isTrue(diffRestApiStub.calledOnce);
-  });
-
-  test('_getDiff handles null diff responses', async () => {
-    stubRestApi('getDiff').returns(Promise.resolve(null));
-    element.changeNum = 123;
-    element.patchRange = {basePatchNum: 1, patchNum: 2};
-    element.path = 'file.txt';
-    await element._getDiff();
-  });
-
-  test('reload resolves on error', () => {
-    const onErrStub = sinon.stub(element, '_handleGetDiffError');
-    const error = new Response(null, {ok: false, status: 500});
-    stubRestApi('getDiff').callsFake(
-        (changeNum, basePatchNum, patchNum, path, whitespace, onErr) => {
-          onErr(error);
-        });
-    element.patchRange = {};
-    return element.reload().then(() => {
-      assert.isTrue(onErrStub.calledOnce);
-    });
-  });
-
-  suite('_handleGetDiffError', () => {
-    let serverErrorStub;
-    let pageErrorStub;
-
-    setup(() => {
-      serverErrorStub = sinon.stub();
-      addListenerForTest(document, 'server-error', serverErrorStub);
-      pageErrorStub = sinon.stub();
-      addListenerForTest(document, 'page-error', pageErrorStub);
-    });
-
-    test('page error on HTTP-409', () => {
-      element._handleGetDiffError({status: 409});
-      assert.isTrue(serverErrorStub.calledOnce);
-      assert.isFalse(pageErrorStub.called);
-      assert.isNotOk(element._errorMessage);
-    });
-
-    test('server error on non-HTTP-409', () => {
-      element._handleGetDiffError({
-        status: 500,
-        text: () => Promise.resolve(''),
-      });
-      assert.isFalse(serverErrorStub.called);
-      assert.isTrue(pageErrorStub.calledOnce);
-      assert.isNotOk(element._errorMessage);
-    });
-
-    test('error message if showLoadFailure', () => {
-      element.showLoadFailure = true;
-      element._handleGetDiffError({status: 500, statusText: 'Failure!'});
-      assert.isFalse(serverErrorStub.called);
-      assert.isFalse(pageErrorStub.called);
-      assert.equal(element._errorMessage,
-          'Encountered error when loading the diff: 500 Failure!');
-    });
-  });
-
-  suite('image diffs', () => {
-    let mockFile1;
-    let mockFile2;
-    setup(() => {
-      mockFile1 = {
-        body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
-        'wsAAAAAAAAAAAAAAAAA/w==',
-        type: 'image/bmp',
-      };
-      mockFile2 = {
-        body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
-        'wsAAAAAAAAAAAAA/////w==',
-        type: 'image/bmp',
-      };
-
-      element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
-      element.change = createChange();
-      element.comments = {
-        left: [],
-        right: [],
-        meta: {patchRange: element.patchRange},
-      };
-    });
-
-    test('renders image diffs with same file name', async () => {
-      const mockDiff = {
-        meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
-        meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
-          lines: 560},
-        intraline_status: 'OK',
-        change_type: 'MODIFIED',
-        diff_header: [
-          'diff --git a/carrot.jpg b/carrot.jpg',
-          'index 2adc47d..f9c2f2c 100644',
-          '--- a/carrot.jpg',
-          '+++ b/carrot.jpg',
-          'Binary files differ',
-        ],
-        content: [{skip: 66}],
-        binary: true,
-      };
-      stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
-      stubRestApi('getImagesForDiff').returns(Promise.resolve({
-        baseImage: {
-          ...mockFile1,
-          _expectedType: 'image/jpeg',
-          _name: 'carrot.jpg',
-        },
-        revisionImage: {
-          ...mockFile2,
-          _expectedType: 'image/jpeg',
-          _name: 'carrot.jpg',
-        },
-      }));
-
-      element.prefs = createDefaultDiffPrefs();
-      element.reload();
-      await waitForEventOnce(element, 'render');
-
-      // Recognizes that it should be an image diff.
-      assert.isTrue(element.isImageDiff);
-      assert.instanceOf(
-          element.$.diff.diffBuilder.builder, GrDiffBuilderImage);
-
-      // Left image rendered with the parent commit's version of the file.
-      const leftImage =
-          element.$.diff.$.diffTable.querySelector('td.left img');
-      const leftLabel =
-          element.$.diff.$.diffTable.querySelector('td.left label');
-      const leftLabelContent = leftLabel.querySelector('.label');
-      const leftLabelName = leftLabel.querySelector('.name');
-
-      const rightImage =
-          element.$.diff.$.diffTable.querySelector('td.right img');
-      const rightLabel = element.$.diff.$.diffTable.querySelector(
-          'td.right label');
-      const rightLabelContent = rightLabel.querySelector('.label');
-      const rightLabelName = rightLabel.querySelector('.name');
-
-      assert.isOk(leftImage);
-      assert.equal(leftImage.getAttribute('src'),
-          'data:image/bmp;base64,' + mockFile1.body);
-      assert.isTrue(leftLabelContent.textContent.includes('image/bmp'));
-      assert.isNotOk(leftLabelName);
-
-      assert.isOk(rightImage);
-      assert.equal(rightImage.getAttribute('src'),
-          'data:image/bmp;base64,' + mockFile2.body);
-      assert.isTrue(rightLabelContent.textContent.includes('image/bmp'));
-      assert.isNotOk(rightLabelName);
-    });
-
-    test('renders image diffs with a different file name', async () => {
-      const mockDiff = {
-        meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
-        meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg',
-          lines: 560},
-        intraline_status: 'OK',
-        change_type: 'MODIFIED',
-        diff_header: [
-          'diff --git a/carrot.jpg b/carrot2.jpg',
-          'index 2adc47d..f9c2f2c 100644',
-          '--- a/carrot.jpg',
-          '+++ b/carrot2.jpg',
-          'Binary files differ',
-        ],
-        content: [{skip: 66}],
-        binary: true,
-      };
-      stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
-      stubRestApi('getImagesForDiff').returns(Promise.resolve({
-        baseImage: {
-          ...mockFile1,
-          _expectedType: 'image/jpeg',
-          _name: 'carrot.jpg',
-        },
-        revisionImage: {
-          ...mockFile2,
-          _expectedType: 'image/jpeg',
-          _name: 'carrot2.jpg',
-        },
-      }));
-
-      element.prefs = createDefaultDiffPrefs();
-      element.reload();
-      await waitForEventOnce(element, 'render');
-
-      // Recognizes that it should be an image diff.
-      assert.isTrue(element.isImageDiff);
-      assert.instanceOf(
-          element.$.diff.diffBuilder.builder, GrDiffBuilderImage);
-
-      // Left image rendered with the parent commit's version of the file.
-      const leftImage =
-          element.$.diff.$.diffTable.querySelector('td.left img');
-      const leftLabel =
-          element.$.diff.$.diffTable.querySelector('td.left label');
-      const leftLabelContent = leftLabel.querySelector('.label');
-      const leftLabelName = leftLabel.querySelector('.name');
-
-      const rightImage =
-          element.$.diff.$.diffTable.querySelector('td.right img');
-      const rightLabel = element.$.diff.$.diffTable.querySelector(
-          'td.right label');
-      const rightLabelContent = rightLabel.querySelector('.label');
-      const rightLabelName = rightLabel.querySelector('.name');
-
-      assert.isOk(rightLabelName);
-      assert.isOk(leftLabelName);
-      assert.equal(leftLabelName.textContent, mockDiff.meta_a.name);
-      assert.equal(rightLabelName.textContent, mockDiff.meta_b.name);
-
-      assert.isOk(leftImage);
-      assert.equal(leftImage.getAttribute('src'),
-          'data:image/bmp;base64,' + mockFile1.body);
-      assert.isTrue(leftLabelContent.textContent.includes('image/bmp'));
-
-      assert.isOk(rightImage);
-      assert.equal(rightImage.getAttribute('src'),
-          'data:image/bmp;base64,' + mockFile2.body);
-      assert.isTrue(rightLabelContent.textContent.includes('image/bmp'));
-    });
-
-    test('renders added image', async () => {
-      const mockDiff = {
-        meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
-          lines: 560},
-        intraline_status: 'OK',
-        change_type: 'ADDED',
-        diff_header: [
-          'diff --git a/carrot.jpg b/carrot.jpg',
-          'index 0000000..f9c2f2c 100644',
-          '--- /dev/null',
-          '+++ b/carrot.jpg',
-          'Binary files differ',
-        ],
-        content: [{skip: 66}],
-        binary: true,
-      };
-      stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
-      stubRestApi('getImagesForDiff').returns(Promise.resolve({
-        baseImage: null,
-        revisionImage: {
-          ...mockFile2,
-          _expectedType: 'image/jpeg',
-          _name: 'carrot2.jpg',
-        },
-      }));
-
-      const promise = mockPromise();
-      element.addEventListener('render', () => {
-        // Recognizes that it should be an image diff.
-        assert.isTrue(element.isImageDiff);
-        assert.instanceOf(
-            element.$.diff.diffBuilder.builder, GrDiffBuilderImage);
-
-        const leftImage =
-            element.$.diff.$.diffTable.querySelector('td.left img');
-        const rightImage =
-            element.$.diff.$.diffTable.querySelector('td.right img');
-
-        assert.isNotOk(leftImage);
-        assert.isOk(rightImage);
-        promise.resolve();
-      });
-
-      element.prefs = createDefaultDiffPrefs();
-      element.reload();
-      await promise;
-    });
-
-    test('renders removed image', async () => {
-      const mockDiff = {
-        meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg',
-          lines: 560},
-        intraline_status: 'OK',
-        change_type: 'DELETED',
-        diff_header: [
-          'diff --git a/carrot.jpg b/carrot.jpg',
-          'index f9c2f2c..0000000 100644',
-          '--- a/carrot.jpg',
-          '+++ /dev/null',
-          'Binary files differ',
-        ],
-        content: [{skip: 66}],
-        binary: true,
-      };
-      stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
-      stubRestApi('getImagesForDiff').returns(Promise.resolve({
-        baseImage: {
-          ...mockFile1,
-          _expectedType: 'image/jpeg',
-          _name: 'carrot.jpg',
-        },
-        revisionImage: null,
-      }));
-
-      const promise = mockPromise();
-      element.addEventListener('render', () => {
-        // Recognizes that it should be an image diff.
-        assert.isTrue(element.isImageDiff);
-        assert.instanceOf(
-            element.$.diff.diffBuilder.builder, GrDiffBuilderImage);
-
-        const leftImage =
-            element.$.diff.$.diffTable.querySelector('td.left img');
-        const rightImage =
-            element.$.diff.$.diffTable.querySelector('td.right img');
-
-        assert.isOk(leftImage);
-        assert.isNotOk(rightImage);
-        promise.resolve();
-      });
-
-      element.prefs = createDefaultDiffPrefs();
-      element.reload();
-      await promise;
-    });
-
-    test('does not render disallowed image type', async () => {
-      const mockDiff = {
-        meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil',
-          lines: 560},
-        intraline_status: 'OK',
-        change_type: 'DELETED',
-        diff_header: [
-          'diff --git a/carrot.jpg b/carrot.jpg',
-          'index f9c2f2c..0000000 100644',
-          '--- a/carrot.jpg',
-          '+++ /dev/null',
-          'Binary files differ',
-        ],
-        content: [{skip: 66}],
-        binary: true,
-      };
-      mockFile1.type = 'image/jpeg-evil';
-
-      stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
-      stubRestApi('getImagesForDiff').returns(Promise.resolve({
-        baseImage: {
-          ...mockFile1,
-          _expectedType: 'image/jpeg',
-          _name: 'carrot.jpg',
-        },
-        revisionImage: null,
-      }));
-
-      const promise = mockPromise();
-      element.addEventListener('render', () => {
-        // Recognizes that it should be an image diff.
-        assert.isTrue(element.isImageDiff);
-        assert.instanceOf(
-            element.$.diff.diffBuilder.builder, GrDiffBuilderImage);
-        const leftImage =
-            element.$.diff.$.diffTable.querySelector('td.left img');
-        assert.isNotOk(leftImage);
-        promise.resolve();
-      });
-
-      element.prefs = createDefaultDiffPrefs();
-      element.reload();
-      await promise;
-    });
-  });
-
-  test('cannot create comments when not logged in', () => {
-    element.patchRange = {
-      basePatchNum: 'PARENT',
-      patchNum: 2,
-    };
-    const showAuthRequireSpy = sinon.spy();
-    element.addEventListener('show-auth-required', showAuthRequireSpy);
-
-    element.dispatchEvent(new CustomEvent('create-comment', {
-      detail: {
-        lineNum: 3,
-        side: Side.LEFT,
-        path: '/p',
-      },
-    }));
-
-    const threads = dom(element.$.diff)
-        .queryDistributedElements('gr-comment-thread');
-
-    assert.equal(threads.length, 0);
-
-    assert.isTrue(showAuthRequireSpy.called);
-  });
-
-  test('delegates cancel()', () => {
-    const stub = sinon.stub(element.$.diff, 'cancel');
-    element.patchRange = {};
-    element.cancel();
-    assert.isTrue(stub.calledOnce);
-    assert.equal(stub.lastCall.args.length, 0);
-  });
-
-  test('delegates getCursorStops()', () => {
-    const returnValue = [document.createElement('b')];
-    const stub = sinon.stub(element.$.diff, 'getCursorStops')
-        .returns(returnValue);
-    assert.equal(element.getCursorStops(), returnValue);
-    assert.isTrue(stub.calledOnce);
-    assert.equal(stub.lastCall.args.length, 0);
-  });
-
-  test('delegates isRangeSelected()', () => {
-    const returnValue = true;
-    const stub = sinon.stub(element.$.diff, 'isRangeSelected')
-        .returns(returnValue);
-    assert.equal(element.isRangeSelected(), returnValue);
-    assert.isTrue(stub.calledOnce);
-    assert.equal(stub.lastCall.args.length, 0);
-  });
-
-  test('delegates toggleLeftDiff()', () => {
-    const stub = sinon.stub(element.$.diff, 'toggleLeftDiff');
-    element.toggleLeftDiff();
-    assert.isTrue(stub.calledOnce);
-    assert.equal(stub.lastCall.args.length, 0);
-  });
-
-  suite('blame', () => {
-    setup(async () => {
-      element = basicFixture.instantiate();
-      element.changeNum = 123;
-      element.path = 'some/path';
-      await flush();
-    });
-
-    test('clearBlame', () => {
-      element._blame = [];
-      const setBlameSpy = sinon.spy(element.$.diff.diffBuilder, 'setBlame');
-      element.clearBlame();
-      assert.isNull(element._blame);
-      assert.isTrue(setBlameSpy.calledWithExactly(null));
-      assert.equal(element.isBlameLoaded, false);
-    });
-
-    test('loadBlame', () => {
-      const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
-      const showAlertStub = sinon.stub();
-      element.addEventListener('show-alert', showAlertStub);
-      const getBlameStub = stubRestApi('getBlame')
-          .returns(Promise.resolve(mockBlame));
-      element.changeNum = 42;
-      element.patchRange = {patchNum: 5, basePatchNum: 4};
-      element.path = 'foo/bar.baz';
-      return element.loadBlame().then(() => {
-        assert.isTrue(getBlameStub.calledWithExactly(
-            42, 5, 'foo/bar.baz', true));
-        assert.isFalse(showAlertStub.called);
-        assert.equal(element._blame, mockBlame);
-        assert.equal(element.isBlameLoaded, true);
-      });
-    });
-
-    test('loadBlame empty', () => {
-      const mockBlame = [];
-      const showAlertStub = sinon.stub();
-      element.addEventListener('show-alert', showAlertStub);
-      stubRestApi('getBlame')
-          .returns(Promise.resolve(mockBlame));
-      element.changeNum = 42;
-      element.patchRange = {patchNum: 5, basePatchNum: 4};
-      element.path = 'foo/bar.baz';
-      return element.loadBlame()
-          .then(() => {
-            assert.isTrue(false, 'Promise should not resolve');
-          })
-          .catch(() => {
-            assert.isTrue(showAlertStub.calledOnce);
-            assert.isNull(element._blame);
-            assert.equal(element.isBlameLoaded, false);
-          });
-    });
-  });
-
-  test('getThreadEls() returns .comment-threads', () => {
-    const threadEl = document.createElement('gr-comment-thread');
-    threadEl.className = 'comment-thread';
-    element.$.diff.appendChild(threadEl);
-    assert.deepEqual(element.getThreadEls(), [threadEl]);
-  });
-
-  test('delegates addDraftAtLine(el)', () => {
-    const param0 = document.createElement('b');
-    const stub = sinon.stub(element.$.diff, 'addDraftAtLine');
-    element.addDraftAtLine(param0);
-    assert.isTrue(stub.calledOnce);
-    assert.equal(stub.lastCall.args.length, 1);
-    assert.equal(stub.lastCall.args[0], param0);
-  });
-
-  test('delegates clearDiffContent()', () => {
-    const stub = sinon.stub(element.$.diff, 'clearDiffContent');
-    element.clearDiffContent();
-    assert.isTrue(stub.calledOnce);
-    assert.equal(stub.lastCall.args.length, 0);
-  });
-
-  test('delegates toggleAllContext()', () => {
-    const stub = sinon.stub(element.$.diff, 'toggleAllContext');
-    element.toggleAllContext();
-    assert.isTrue(stub.calledOnce);
-    assert.equal(stub.lastCall.args.length, 0);
-  });
-
-  test('passes in noAutoRender', () => {
-    const value = true;
-    element.noAutoRender = value;
-    assert.equal(element.$.diff.noAutoRender, value);
-  });
-
-  test('passes in path', () => {
-    const value = 'some/file/path';
-    element.path = value;
-    assert.equal(element.$.diff.path, value);
-  });
-
-  test('passes in prefs', () => {
-    const value = {};
-    element.prefs = value;
-    assert.equal(element.$.diff.prefs, value);
-  });
-
-  test('passes in displayLine', () => {
-    const value = true;
-    element.displayLine = value;
-    assert.equal(element.$.diff.displayLine, value);
-  });
-
-  test('passes in hidden', () => {
-    const value = true;
-    element.hidden = value;
-    assert.equal(element.$.diff.hidden, value);
-    assert.isNotNull(element.getAttribute('hidden'));
-  });
-
-  test('passes in noRenderOnPrefsChange', () => {
-    const value = true;
-    element.noRenderOnPrefsChange = value;
-    assert.equal(element.$.diff.noRenderOnPrefsChange, value);
-  });
-
-  test('passes in lineWrapping', () => {
-    const value = true;
-    element.lineWrapping = value;
-    assert.equal(element.$.diff.lineWrapping, value);
-  });
-
-  test('passes in viewMode', () => {
-    const value = 'SIDE_BY_SIDE';
-    element.viewMode = value;
-    assert.equal(element.$.diff.viewMode, value);
-  });
-
-  test('passes in lineOfInterest', () => {
-    const value = {lineNum: 123, side: Side.LEFT};
-    element.lineOfInterest = value;
-    assert.equal(element.$.diff.lineOfInterest, value);
-  });
-
-  suite('_reportDiff', () => {
-    let reportStub;
-
-    setup(async () => {
-      element = basicFixture.instantiate();
-      element.changeNum = 123;
-      element.path = 'file.txt';
-      element.patchRange = {basePatchNum: 1};
-      reportStub = sinon.stub(element.reporting, 'reportInteraction');
-      await flush();
-    });
-
-    test('null and content-less', () => {
-      element._reportDiff(null);
-      assert.isFalse(reportStub.called);
-
-      element._reportDiff({});
-      assert.isFalse(reportStub.called);
-    });
-
-    test('diff w/ no delta', () => {
-      const diff = {
-        content: [
-          {ab: ['foo', 'bar']},
-          {ab: ['baz', 'foo']},
-        ],
-      };
-      element._reportDiff(diff);
-      assert.isTrue(reportStub.calledOnce);
-      assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero');
-      assert.isUndefined(reportStub.lastCall.args[1]);
-    });
-
-    test('diff w/ no rebase delta', () => {
-      const diff = {
-        content: [
-          {ab: ['foo', 'bar']},
-          {a: ['baz', 'foo']},
-          {ab: ['foo', 'bar']},
-          {a: ['baz', 'foo'], b: ['bar', 'baz']},
-          {ab: ['foo', 'bar']},
-          {b: ['baz', 'foo']},
-          {ab: ['foo', 'bar']},
-        ],
-      };
-      element._reportDiff(diff);
-      assert.isTrue(reportStub.calledOnce);
-      assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero');
-      assert.isUndefined(reportStub.lastCall.args[1]);
-    });
-
-    test('diff w/ some rebase delta', () => {
-      const diff = {
-        content: [
-          {ab: ['foo', 'bar']},
-          {a: ['baz', 'foo'], due_to_rebase: true},
-          {ab: ['foo', 'bar']},
-          {a: ['baz', 'foo'], b: ['bar', 'baz']},
-          {ab: ['foo', 'bar']},
-          {b: ['baz', 'foo'], due_to_rebase: true},
-          {ab: ['foo', 'bar']},
-          {a: ['baz', 'foo']},
-        ],
-      };
-      element._reportDiff(diff);
-      assert.isTrue(reportStub.calledOnce);
-      assert.isTrue(reportStub.calledWith(
-          'rebase-percent-nonzero',
-          {percentRebaseDelta: 50}
-      ));
-    });
-
-    test('diff w/ all rebase delta', () => {
-      const diff = {content: [{
-        a: ['foo', 'bar'],
-        b: ['baz', 'foo'],
-        due_to_rebase: true,
-      }]};
-      element._reportDiff(diff);
-      assert.isTrue(reportStub.calledOnce);
-      assert.isTrue(reportStub.calledWith(
-          'rebase-percent-nonzero',
-          {percentRebaseDelta: 100}
-      ));
-    });
-
-    test('diff against parent event', () => {
-      element.patchRange.basePatchNum = 'PARENT';
-      const diff = {content: [{
-        a: ['foo', 'bar'],
-        b: ['baz', 'foo'],
-      }]};
-      element._reportDiff(diff);
-      assert.isTrue(reportStub.calledOnce);
-      assert.equal(reportStub.lastCall.args[0], 'diff-against-parent');
-      assert.isUndefined(reportStub.lastCall.args[1]);
-    });
-  });
-
-  suite('createCheckEl method', () => {
-    test('start_line:12', () => {
-      const result = {
-        codePointers: [{range: {start_line: 12}}],
-      };
-      const el = element.createCheckEl(result);
-      assert.equal(el.getAttribute('slot'), 'right-12');
-      assert.equal(el.getAttribute('diff-side'), 'right');
-      assert.equal(el.getAttribute('line-num'), '12');
-      assert.equal(el.getAttribute('range'), null);
-      assert.equal(el.result, result);
-    });
-
-    test('start_line:13 end_line:14 without char positions', () => {
-      const result = {
-        codePointers: [{range: {start_line: 13, end_line: 14}}],
-      };
-      const el = element.createCheckEl(result);
-      assert.equal(el.getAttribute('slot'), 'right-14');
-      assert.equal(el.getAttribute('diff-side'), 'right');
-      assert.equal(el.getAttribute('line-num'), '14');
-      assert.equal(el.getAttribute('range'), null);
-      assert.equal(el.result, result);
-    });
-
-    test('start_line:13 end_line:14 with char positions', () => {
-      const result = {
-        codePointers: [
-          {
-            range: {
-              start_line: 13,
-              end_line: 14,
-              start_character: 5,
-              end_character: 7,
-            },
-          },
-        ],
-      };
-      const el = element.createCheckEl(result);
-      assert.equal(el.getAttribute('slot'), 'right-14');
-      assert.equal(el.getAttribute('diff-side'), 'right');
-      assert.equal(el.getAttribute('line-num'), '14');
-      assert.equal(el.getAttribute('range'),
-          '{"start_line":13,' +
-          '"end_line":14,' +
-          '"start_character":5,' +
-          '"end_character":7}');
-      assert.equal(el.result, result);
-    });
-
-    test('empty range', () => {
-      const result = {
-        codePointers: [{range: {}}],
-      };
-      const el = element.createCheckEl(result);
-      assert.equal(el.getAttribute('slot'), 'right-FILE');
-      assert.equal(el.getAttribute('diff-side'), 'right');
-      assert.equal(el.getAttribute('line-num'), 'FILE');
-      assert.equal(el.getAttribute('range'), null);
-      assert.equal(el.result, result);
-    });
-  });
-
-  suite('create-comment', () => {
-    setup(async () => {
-      loggedIn = true;
-      element.connectedCallback();
-      await flush();
-    });
-
-    test('creates comments if they do not exist yet', () => {
-      element.patchRange = {
-        basePatchNum: 'PARENT',
-        patchNum: 2,
-      };
-
-      element.dispatchEvent(new CustomEvent('create-comment', {
-        detail: {
-          lineNum: 3,
-          side: Side.LEFT,
-          path: '/p',
-        },
-      }));
-
-      let threads = dom(element.$.diff)
-          .queryDistributedElements('gr-comment-thread');
-
-      assert.equal(threads.length, 1);
-      assert.equal(threads[0].thread.commentSide, 'PARENT');
-      assert.equal(threads[0].getAttribute('diff-side'), Side.LEFT);
-      assert.equal(threads[0].thread.range, undefined);
-      assert.equal(threads[0].thread.patchNum, 2);
-
-      // Try to fetch a thread with a different range.
-      const range = {
-        start_line: 1,
-        start_character: 1,
-        end_line: 1,
-        end_character: 3,
-      };
-      element.patchRange = {
-        basePatchNum: 'PARENT',
-        patchNum: 3,
-      };
-
-      element.dispatchEvent(new CustomEvent('create-comment', {
-        detail: {
-          lineNum: 1,
-          side: Side.LEFT,
-          path: '/p',
-          range,
-        },
-      }));
-
-      threads = dom(element.$.diff)
-          .queryDistributedElements('gr-comment-thread');
-
-      assert.equal(threads.length, 2);
-      assert.equal(threads[0].thread.commentSide, 'PARENT');
-      assert.equal(threads[0].getAttribute('diff-side'), Side.LEFT);
-      assert.equal(threads[1].thread.range, range);
-      assert.equal(threads[1].thread.patchNum, 3);
-    });
-
-    test('should not be on parent if on the right', () => {
-      element.patchRange = {
-        basePatchNum: 2,
-        patchNum: 3,
-      };
-
-      element.dispatchEvent(new CustomEvent('create-comment', {
-        detail: {
-          side: Side.RIGHT,
-        },
-      }));
-
-      const threadEl = dom(element.$.diff)
-          .queryDistributedElements('gr-comment-thread')[0];
-
-      assert.equal(threadEl.thread.commentSide, 'REVISION');
-      assert.equal(threadEl.getAttribute('diff-side'), Side.RIGHT);
-    });
-
-    test('should be on parent if right and base is PARENT', () => {
-      element.patchRange = {
-        basePatchNum: 'PARENT',
-        patchNum: 3,
-      };
-
-      element.dispatchEvent(new CustomEvent('create-comment', {
-        detail: {
-          side: Side.LEFT,
-        },
-      }));
-
-      const threadEl = dom(element.$.diff)
-          .queryDistributedElements('gr-comment-thread')[0];
-
-      assert.equal(threadEl.thread.commentSide, 'PARENT');
-      assert.equal(threadEl.getAttribute('diff-side'), Side.LEFT);
-    });
-
-    test('should be on parent if right and base negative', () => {
-      element.patchRange = {
-        basePatchNum: -2, // merge parents have negative numbers
-        patchNum: 3,
-      };
-
-      element.dispatchEvent(new CustomEvent('create-comment', {
-        detail: {
-          side: Side.LEFT,
-        },
-      }));
-
-      const threadEl = dom(element.$.diff)
-          .queryDistributedElements('gr-comment-thread')[0];
-
-      assert.equal(threadEl.thread.commentSide, 'PARENT');
-      assert.equal(threadEl.getAttribute('diff-side'), Side.LEFT);
-    });
-
-    test('should not be on parent otherwise', () => {
-      element.patchRange = {
-        basePatchNum: 2, // merge parents have negative numbers
-        patchNum: 3,
-      };
-
-      element.dispatchEvent(new CustomEvent('create-comment', {
-        detail: {
-          side: Side.LEFT,
-        },
-      }));
-
-      const threadEl = dom(element.$.diff)
-          .queryDistributedElements('gr-comment-thread')[0];
-
-      assert.equal(threadEl.thread.commentSide, 'REVISION');
-      assert.equal(threadEl.getAttribute('diff-side'), Side.LEFT);
-    });
-
-    test('thread should use old file path if first created ' +
-    'on patch set (left) before renaming', async () => {
-      element.patchRange = {
-        basePatchNum: 2,
-        patchNum: 3,
-      };
-      element.file = {basePath: 'file_renamed.txt', path: element.path};
-      await flush();
-
-      element.dispatchEvent(new CustomEvent('create-comment', {
-        detail: {
-          side: Side.LEFT,
-          path: '/p',
-        },
-      }));
-
-      const threads = dom(element.$.diff)
-          .queryDistributedElements('gr-comment-thread');
-
-      assert.equal(threads.length, 1);
-      assert.equal(threads[0].getAttribute('diff-side'), Side.LEFT);
-      assert.equal(threads[0].thread.path, element.file.basePath);
-    });
-
-    test('thread should use new file path if first created ' +
-    'on patch set (right) after renaming', async () => {
-      element.patchRange = {
-        basePatchNum: 2,
-        patchNum: 3,
-      };
-      element.file = {basePath: 'file_renamed.txt', path: element.path};
-      await flush();
-
-      element.dispatchEvent(new CustomEvent('create-comment', {
-        detail: {
-          side: Side.RIGHT,
-          path: '/p',
-        },
-      }));
-
-      const threads = dom(element.$.diff)
-          .queryDistributedElements('gr-comment-thread');
-
-      assert.equal(threads.length, 1);
-      assert.equal(threads[0].getAttribute('diff-side'), Side.RIGHT);
-      assert.equal(threads[0].thread.path, element.file.path);
-    });
-
-    test('multiple threads created on the same range', async () => {
-      element.patchRange = {
-        basePatchNum: 2,
-        patchNum: 3,
-      };
-      element.file = {basePath: 'file_renamed.txt', path: element.path};
-      await flush();
-
-      const comment = {
-        ...createComment(),
-        range: {
-          start_line: 1,
-          start_character: 1,
-          end_line: 2,
-          end_character: 2,
-        },
-        patch_set: 3,
-      };
-      const thread = createCommentThread([comment]);
-      element.threads = [thread];
-
-      let threads = dom(element.$.diff)
-          .queryDistributedElements('gr-comment-thread');
-
-      assert.equal(threads.length, 1);
-      element.threads= [...element.threads, thread];
-
-      threads = dom(element.$.diff)
-          .queryDistributedElements('gr-comment-thread');
-      // Threads have same rootId so element is reused
-      assert.equal(threads.length, 1);
-
-      const newThread = {...thread};
-      newThread.rootId = 'differentRootId';
-      element.threads= [...element.threads, newThread];
-      threads = dom(element.$.diff)
-          .queryDistributedElements('gr-comment-thread');
-      // New thread has a different rootId
-      assert.equal(threads.length, 2);
-    });
-
-    test('unsaved thread changes to draft', async () => {
-      element.patchRange = {
-        basePatchNum: 2,
-        patchNum: 3,
-      };
-      element.file = {basePath: 'file_renamed.txt', path: element.path};
-      element.threads = [];
-      await flush();
-
-      element.dispatchEvent(new CustomEvent('create-comment', {
-        detail: {
-          side: Side.RIGHT,
-          path: element.path,
-          lineNum: 13,
-        },
-      }));
-      await flush();
-      assert.equal(element.getThreadEls().length, 1);
-      const threadEl = element.getThreadEls()[0];
-      assert.equal(threadEl.thread.line, 13);
-      assert.isDefined(threadEl.unsavedComment);
-      assert.equal(threadEl.thread.comments.length, 0);
-
-      const draftThread = createCommentThread([{
-        path: element.path,
-        patch_set: 3,
-        line: 13,
-        __draft: true,
-      }]);
-      element.threads = [draftThread];
-      await flush();
-
-      // We expect that no additional thread element was created.
-      assert.equal(element.getThreadEls().length, 1);
-      // In fact the thread element must still be the same.
-      assert.equal(element.getThreadEls()[0], threadEl);
-      // But it must have been updated from unsaved to draft:
-      assert.isUndefined(threadEl.unsavedComment);
-      assert.equal(threadEl.thread.comments.length, 1);
-    });
-
-    test('thread should use new file path if first created ' +
-    'on patch set (left) but is base', async () => {
-      element.patchRange = {
-        basePatchNum: 'PARENT',
-        patchNum: 3,
-      };
-      element.file = {basePath: 'file_renamed.txt', path: element.path};
-      await flush();
-
-      element.dispatchEvent(new CustomEvent('create-comment', {
-        detail: {
-          side: Side.LEFT,
-          path: '/p',
-        },
-      }));
-
-      const threads =
-          dom(element.$.diff).queryDistributedElements('gr-comment-thread');
-
-      assert.equal(threads.length, 1);
-      assert.equal(threads[0].getAttribute('diff-side'), Side.LEFT);
-      assert.equal(threads[0].thread.path, element.file.path);
-    });
-
-    test('cannot create thread on an edit', () => {
-      const alertSpy = sinon.spy();
-      element.addEventListener('show-alert', alertSpy);
-
-      const diffSide = Side.LEFT;
-      element.patchRange = {
-        basePatchNum: EditPatchSetNum,
-        patchNum: 3,
-      };
-      element.dispatchEvent(new CustomEvent('create-comment', {
-        detail: {
-          side: diffSide,
-          path: '/p',
-        },
-      }));
-
-      const threads =
-          dom(element.$.diff).queryDistributedElements('gr-comment-thread');
-      assert.equal(threads.length, 0);
-      assert.isTrue(alertSpy.called);
-    });
-
-    test('cannot create thread on an edit base', () => {
-      const alertSpy = sinon.spy();
-      element.addEventListener('show-alert', alertSpy);
-
-      const diffSide = Side.LEFT;
-      element.patchRange = {
-        basePatchNum: ParentPatchSetNum,
-        patchNum: EditPatchSetNum,
-      };
-      element.dispatchEvent(new CustomEvent('create-comment', {
-        detail: {
-          side: diffSide,
-          path: '/p',
-        },
-      }));
-
-      const threads = dom(element.$.diff)
-          .queryDistributedElements('gr-comment-thread');
-      assert.equal(threads.length, 0);
-      assert.isTrue(alertSpy.called);
-    });
-  });
-
-  test('_filterThreadElsForLocation with no threads', () => {
-    const line = {beforeNumber: 3, afterNumber: 5};
-
-    const threads = [];
-    assert.deepEqual(element._filterThreadElsForLocation(threads, line), []);
-    assert.deepEqual(element._filterThreadElsForLocation(threads, line,
-        Side.LEFT), []);
-    assert.deepEqual(element._filterThreadElsForLocation(threads, line,
-        Side.RIGHT), []);
-  });
-
-  test('_filterThreadElsForLocation for line comments', () => {
-    const line = {beforeNumber: 3, afterNumber: 5};
-
-    const l3 = document.createElement('div');
-    l3.setAttribute('line-num', 3);
-    l3.setAttribute('diff-side', Side.LEFT);
-
-    const l5 = document.createElement('div');
-    l5.setAttribute('line-num', 5);
-    l5.setAttribute('diff-side', Side.LEFT);
-
-    const r3 = document.createElement('div');
-    r3.setAttribute('line-num', 3);
-    r3.setAttribute('diff-side', Side.RIGHT);
-
-    const r5 = document.createElement('div');
-    r5.setAttribute('line-num', 5);
-    r5.setAttribute('diff-side', Side.RIGHT);
-
-    const threadEls = [l3, l5, r3, r5];
-    assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
-        Side.LEFT), [l3]);
-    assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
-        Side.RIGHT), [r5]);
-  });
-
-  test('_filterThreadElsForLocation for file comments', () => {
-    const line = {beforeNumber: 'FILE', afterNumber: 'FILE'};
-
-    const l = document.createElement('div');
-    l.setAttribute('diff-side', Side.LEFT);
-    l.setAttribute('line-num', 'FILE');
-
-    const r = document.createElement('div');
-    r.setAttribute('diff-side', Side.RIGHT);
-    r.setAttribute('line-num', 'FILE');
-
-    const threadEls = [l, r];
-    assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
-        Side.LEFT), [l]);
-    assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
-        Side.RIGHT), [r]);
-  });
-
-  suite('syntax layer with syntax_highlighting on', () => {
-    setup(() => {
-      const prefs = {
-        line_length: 10,
-        show_tabs: true,
-        tab_size: 4,
-        context: -1,
-        syntax_highlighting: true,
-      };
-      element.patchRange = {};
-      element.prefs = prefs;
-      element.changeNum = 123;
-      element.change = createChange();
-      element.path = 'some/path';
-    });
-
-    test('gr-diff-host provides syntax highlighting layer', async () => {
-      stubRestApi('getDiff').returns(Promise.resolve({content: []}));
-      await element.reload();
-      assert.equal(element.$.diff.layers[1], element.syntaxLayer);
-    });
-
-    test('rendering normal-sized diff does not disable syntax', () => {
-      element.diff = {
-        content: [{
-          a: ['foo'],
-        }],
-      };
-      assert.isTrue(element.syntaxLayer.enabled);
-    });
-
-    test('rendering large diff disables syntax', () => {
-      // Before it renders, set the first diff line to 500 '*' characters.
-      element.diff = {
-        content: [{
-          a: [new Array(501).join('*')],
-        }],
-      };
-      assert.isFalse(element.syntaxLayer.enabled);
-    });
-
-    test('starts syntax layer processing on render event', async () => {
-      sinon.stub(element.syntaxLayer, 'process')
-          .returns(Promise.resolve());
-      stubRestApi('getDiff').returns(Promise.resolve({content: []}));
-      await element.reload();
-      element.dispatchEvent(
-          new CustomEvent('render', {bubbles: true, composed: true}));
-      assert.isTrue(element.syntaxLayer.process.called);
-    });
-  });
-
-  suite('syntax layer with syntax_highlighting off', () => {
-    setup(() => {
-      const prefs = {
-        line_length: 10,
-        show_tabs: true,
-        tab_size: 4,
-        context: -1,
-      };
-      element.diff = {
-        content: [{
-          a: ['foo'],
-        }],
-      };
-      element.patchRange = {};
-      element.change = createChange();
-      element.prefs = prefs;
-    });
-
-    test('gr-diff-host provides syntax highlighting layer', async () => {
-      stubRestApi('getDiff').returns(Promise.resolve({content: []}));
-      await element.reload();
-      assert.equal(element.$.diff.layers[1], element.syntaxLayer);
-    });
-
-    test('syntax layer should be disabled', () => {
-      assert.isFalse(element.syntaxLayer.enabled);
-    });
-
-    test('still disabled for large diff', () => {
-      // Before it renders, set the first diff line to 500 '*' characters.
-      element.diff = {
-        content: [{
-          a: [new Array(501).join('*')],
-        }],
-      };
-      assert.isFalse(element.syntaxLayer.enabled);
-    });
-  });
-
-  suite('coverage layer', () => {
-    let notifyStub;
-    let coverageProviderStub;
-    const exampleRanges = [
-      {
-        type: CoverageType.COVERED,
-        side: Side.RIGHT,
-        code_range: {
-          start_line: 1,
-          end_line: 2,
-        },
-      },
-      {
-        type: CoverageType.NOT_COVERED,
-        side: Side.RIGHT,
-        code_range: {
-          start_line: 3,
-          end_line: 4,
-        },
-      },
-    ];
-
-    setup(async () => {
-      notifyStub = sinon.stub();
-      coverageProviderStub = sinon.stub().returns(
-          Promise.resolve(exampleRanges));
-
-      element = basicFixture.instantiate();
-      sinon.stub(element.jsAPI, 'getCoverageAnnotationApis').returns(
-          Promise.resolve([{
-            notify: notifyStub,
-            getCoverageProvider() {
-              return coverageProviderStub;
-            },
-          }]));
-      element.changeNum = 123;
-      element.change = createChange();
-      element.path = 'some/path';
-      const prefs = {
-        line_length: 10,
-        show_tabs: true,
-        tab_size: 4,
-        context: -1,
-      };
-      element.diff = {
-        content: [{
-          a: ['foo'],
-        }],
-      };
-      element.patchRange = {};
-      element.prefs = prefs;
-      stubRestApi('getDiff').returns(Promise.resolve(element.diff));
-      await flush();
-    });
-
-    test('getCoverageAnnotationApis should be called', async () => {
-      await element.reload();
-      assert.isTrue(element.jsAPI.getCoverageAnnotationApis.calledOnce);
-    });
-
-    test('coverageRangeChanged should be called', async () => {
-      await element.reload();
-      assert.equal(notifyStub.callCount, 2);
-      assert.isTrue(notifyStub.calledWithExactly(
-          'some/path', 1, 2, Side.RIGHT));
-      assert.isTrue(notifyStub.calledWithExactly(
-          'some/path', 3, 4, Side.RIGHT));
-    });
-
-    test('provider is called with appropriate params', async () => {
-      element.patchRange.basePatchNum = 1;
-      element.patchRange.patchNum = 3;
-
-      await element.reload();
-      assert.isTrue(coverageProviderStub.calledWithExactly(
-          123, 'some/path', 1, 3, element.change));
-    });
-
-    test('provider is called with appropriate params - special patchset values',
-        async () => {
-          element.patchRange.basePatchNum = 'PARENT';
-          element.patchRange.patchNum = 'invalid';
-
-          await element.reload();
-          assert.isTrue(coverageProviderStub.calledWithExactly(
-              123, 'some/path', undefined, undefined, element.change));
-        });
-  });
-
-  suite('trailing newlines', () => {
-    setup(() => {
-    });
-
-    suite('_lastChunkForSide', () => {
-      test('deltas', () => {
-        const diff = {content: [
-          {a: ['foo', 'bar'], b: ['baz']},
-          {ab: ['foo', 'bar', 'baz']},
-          {b: ['foo']},
-        ]};
-        assert.equal(element._lastChunkForSide(diff, false), diff.content[2]);
-        assert.equal(element._lastChunkForSide(diff, true), diff.content[1]);
-
-        diff.content.push({a: ['foo'], b: ['bar']});
-        assert.equal(element._lastChunkForSide(diff, false), diff.content[3]);
-        assert.equal(element._lastChunkForSide(diff, true), diff.content[3]);
-      });
-
-      test('addition with a undefined', () => {
-        const diff = {content: [
-          {b: ['foo', 'bar', 'baz']},
-        ]};
-        assert.equal(element._lastChunkForSide(diff, false), diff.content[0]);
-        assert.isNull(element._lastChunkForSide(diff, true));
-      });
-
-      test('addition with a empty', () => {
-        const diff = {content: [
-          {a: [], b: ['foo', 'bar', 'baz']},
-        ]};
-        assert.equal(element._lastChunkForSide(diff, false), diff.content[0]);
-        assert.isNull(element._lastChunkForSide(diff, true));
-      });
-
-      test('deletion with b undefined', () => {
-        const diff = {content: [
-          {a: ['foo', 'bar', 'baz']},
-        ]};
-        assert.isNull(element._lastChunkForSide(diff, false));
-        assert.equal(element._lastChunkForSide(diff, true), diff.content[0]);
-      });
-
-      test('deletion with b empty', () => {
-        const diff = {content: [
-          {a: ['foo', 'bar', 'baz'], b: []},
-        ]};
-        assert.isNull(element._lastChunkForSide(diff, false));
-        assert.equal(element._lastChunkForSide(diff, true), diff.content[0]);
-      });
-
-      test('empty', () => {
-        const diff = {content: []};
-        assert.isNull(element._lastChunkForSide(diff, false));
-        assert.isNull(element._lastChunkForSide(diff, true));
-      });
-    });
-
-    suite('_hasTrailingNewlines', () => {
-      test('shared no trailing', () => {
-        const diff = undefined;
-        sinon.stub(element, '_lastChunkForSide')
-            .returns({ab: ['foo', 'bar']});
-        assert.isFalse(element._hasTrailingNewlines(diff, false));
-        assert.isFalse(element._hasTrailingNewlines(diff, true));
-      });
-
-      test('delta trailing in right', () => {
-        const diff = undefined;
-        sinon.stub(element, '_lastChunkForSide')
-            .returns({a: ['foo', 'bar'], b: ['baz', '']});
-        assert.isTrue(element._hasTrailingNewlines(diff, false));
-        assert.isFalse(element._hasTrailingNewlines(diff, true));
-      });
-
-      test('addition', () => {
-        const diff = undefined;
-        sinon.stub(element, '_lastChunkForSide').callsFake((diff, leftSide) => {
-          if (leftSide) { return null; }
-          return {b: ['foo', '']};
-        });
-        assert.isTrue(element._hasTrailingNewlines(diff, false));
-        assert.isNull(element._hasTrailingNewlines(diff, true));
-      });
-
-      test('deletion', () => {
-        const diff = undefined;
-        sinon.stub(element, '_lastChunkForSide').callsFake((diff, leftSide) => {
-          if (!leftSide) { return null; }
-          return {a: ['foo']};
-        });
-        assert.isNull(element._hasTrailingNewlines(diff, false));
-        assert.isFalse(element._hasTrailingNewlines(diff, true));
-      });
-    });
-  });
-});
-
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
new file mode 100644
index 0000000..446d569
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
@@ -0,0 +1,1725 @@
+/**
+ * @license
+ * Copyright 2018 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import './gr-diff-host';
+import {
+  CommentSide,
+  createDefaultDiffPrefs,
+  Side,
+} from '../../../constants/constants';
+import {
+  createBlame,
+  createChange,
+  createComment,
+  createCommentThread,
+  createDiff,
+  createPatchRange,
+  createRunResult,
+} from '../../../test/test-data-generators';
+import {
+  addListenerForTest,
+  mockPromise,
+  query,
+  queryAll,
+  queryAndAssert,
+  stubReporting,
+  stubRestApi,
+  waitUntil,
+} from '../../../test/test-utils';
+import {
+  BasePatchSetNum,
+  BlameInfo,
+  CommentRange,
+  CommitId,
+  EditPatchSetNum,
+  ImageInfo,
+  NumericChangeId,
+  ParentPatchSetNum,
+  PatchSetNum,
+  RepoName,
+  RevisionPatchSetNum,
+  UrlEncodedCommentId,
+} from '../../../types/common';
+import {CoverageType} from '../../../types/types';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {GrDiffBuilderImage} from '../../../embed/diff/gr-diff-builder/gr-diff-builder-image';
+import {waitForEventOnce} from '../../../utils/event-util';
+import {GrDiffHost, LineInfo} from './gr-diff-host';
+import {DiffInfo, DiffViewMode, IgnoreWhitespaceType} from '../../../api/diff';
+import {ErrorCallback} from '../../../api/rest';
+import {SinonStub} from 'sinon';
+import {RunResult} from '../../../models/checks/checks-model';
+import {GrCommentThread} from '../../shared/gr-comment-thread/gr-comment-thread';
+import {assertIsDefined} from '../../../utils/common-util';
+import {GrAnnotationActionsInterface} from '../../shared/gr-js-api-interface/gr-annotation-actions-js-api';
+
+const basicFixture = fixtureFromElement('gr-diff-host');
+
+suite('gr-diff-host tests', () => {
+  let element: GrDiffHost;
+  let loggedIn: boolean;
+
+  setup(async () => {
+    loggedIn = false;
+    stubRestApi('getLoggedIn').callsFake(() => Promise.resolve(loggedIn));
+    element = basicFixture.instantiate();
+    element.changeNum = 123 as NumericChangeId;
+    element.path = 'some/path';
+    await flush();
+  });
+
+  suite('plugin layers', () => {
+    let getDiffLayersStub: sinon.SinonStub;
+    const pluginLayers = [{annotate: () => {}}, {annotate: () => {}}];
+    setup(() => {
+      element = basicFixture.instantiate();
+      getDiffLayersStub = sinon
+        .stub(element.jsAPI, 'getDiffLayers')
+        .returns(pluginLayers);
+      element.changeNum = 123 as NumericChangeId;
+      element.path = 'some/path';
+    });
+    test('plugin layers requested', async () => {
+      element.change = createChange();
+      stubRestApi('getDiff').returns(Promise.resolve(createDiff()));
+      await element.reload();
+      assert(getDiffLayersStub.called);
+    });
+  });
+
+  suite('render reporting', () => {
+    test('ends total and syntax timer after syntax layer', async () => {
+      const displayedStub = stubReporting('diffViewContentDisplayed');
+      const timeEndStub = sinon.stub(element.reporting, 'timeEnd');
+      let notifySyntaxProcessed: () => void = () => {};
+      sinon.stub(element.syntaxLayer, 'process').returns(
+        new Promise(resolve => {
+          notifySyntaxProcessed = resolve;
+        })
+      );
+      stubRestApi('getDiff').returns(Promise.resolve(createDiff()));
+      element.patchRange = createPatchRange();
+      element.change = createChange();
+      element.prefs = createDefaultDiffPrefs();
+      element.reload(true);
+      // Multiple cascading microtasks are scheduled.
+      await flush();
+      notifySyntaxProcessed();
+      await waitUntil(() => timeEndStub.callCount === 4);
+      const calls = timeEndStub.getCalls();
+      assert.equal(calls.length, 4);
+      assert.equal(calls[0].args[0], 'Diff Load Render');
+      assert.equal(calls[1].args[0], 'Diff Content Render');
+      assert.equal(calls[2].args[0], 'Diff Syntax Render');
+      assert.equal(calls[3].args[0], 'Diff Total Render');
+      assert.isTrue(displayedStub.called);
+    });
+
+    test('completes reload promise after syntax layer processing', async () => {
+      let notifySyntaxProcessed: () => void = () => {};
+      sinon.stub(element.syntaxLayer, 'process').returns(
+        new Promise(resolve => {
+          notifySyntaxProcessed = resolve;
+        })
+      );
+      stubRestApi('getDiff').returns(Promise.resolve(createDiff()));
+      element.patchRange = createPatchRange();
+      element.change = createChange();
+      let reloadComplete = false;
+      element.prefs = createDefaultDiffPrefs();
+      element.reload().then(() => {
+        reloadComplete = true;
+      });
+      // Multiple cascading microtasks are scheduled.
+      await flush();
+      assert.isFalse(reloadComplete);
+      notifySyntaxProcessed();
+      await waitUntil(() => reloadComplete);
+      assert.isTrue(reloadComplete);
+    });
+  });
+
+  test('reload() cancels before network resolves', () => {
+    const cancelStub = sinon.stub(element.$.diff, 'cancel');
+
+    // Stub the network calls into requests that never resolve.
+    sinon.stub(element, '_getDiff').callsFake(() => new Promise(() => {}));
+    element.patchRange = createPatchRange();
+    element.change = createChange();
+
+    // Needs to be set to something first for it to cancel.
+    element.diff = createDiff();
+
+    element.reload();
+    assert.isTrue(cancelStub.called);
+  });
+
+  test('reload() loads files weblinks', async () => {
+    element.change = createChange();
+    const weblinksStub = sinon
+      .stub(GerritNav, '_generateWeblinks')
+      .returns({name: 'stubb', url: '#s'});
+    stubRestApi('getDiff').returns(Promise.resolve(createDiff()));
+    element.projectName = 'test-project' as RepoName;
+    element.path = 'test-path';
+    element.commitRange = {
+      baseCommit: 'test-base' as CommitId,
+      commit: 'test-commit' as CommitId,
+    };
+    element.patchRange = createPatchRange();
+
+    await element.reload();
+
+    assert.equal(weblinksStub.callCount, 3);
+    assert.deepEqual(weblinksStub.firstCall.args[0], {
+      commit: 'test-base' as CommitId,
+      file: 'test-path',
+      options: {
+        weblinks: undefined,
+      },
+      repo: 'test-project' as RepoName,
+      type: GerritNav.WeblinkType.EDIT,
+    });
+    assert.deepEqual(element.editWeblinks, [
+      {
+        name: 'stubb',
+        url: '#s',
+      },
+    ]);
+    assert.deepEqual(weblinksStub.secondCall.args[0], {
+      commit: 'test-base' as CommitId,
+      file: 'test-path',
+      options: {
+        weblinks: undefined,
+      },
+      repo: 'test-project' as RepoName,
+      type: GerritNav.WeblinkType.FILE,
+    });
+    assert.deepEqual(weblinksStub.thirdCall.args[0], {
+      commit: 'test-commit' as CommitId,
+      file: 'test-path',
+      options: {
+        weblinks: undefined,
+      },
+      repo: 'test-project' as RepoName,
+      type: GerritNav.WeblinkType.FILE,
+    });
+    assert.deepEqual(element.filesWeblinks, {
+      meta_a: [{name: 'stubb', url: '#s'}],
+      meta_b: [{name: 'stubb', url: '#s'}],
+    });
+  });
+
+  test('prefetch getDiff', async () => {
+    const diffRestApiStub = stubRestApi('getDiff').returns(
+      Promise.resolve(createDiff())
+    );
+    element.changeNum = 123 as NumericChangeId;
+    element.patchRange = createPatchRange();
+    element.path = 'file.txt';
+    element.prefetchDiff();
+    await element._getDiff();
+    assert.isTrue(diffRestApiStub.calledOnce);
+  });
+
+  test('_getDiff handles undefined diff responses', async () => {
+    stubRestApi('getDiff').returns(Promise.resolve(undefined));
+    element.changeNum = 123 as NumericChangeId;
+    element.patchRange = createPatchRange();
+    element.path = 'file.txt';
+    await element._getDiff();
+  });
+
+  test('reload resolves on error', () => {
+    const onErrStub = sinon.stub(element, '_handleGetDiffError');
+    const error = new Response(null, {status: 500});
+    stubRestApi('getDiff').callsFake(
+      (
+        _1: NumericChangeId,
+        _2: PatchSetNum,
+        _3: PatchSetNum,
+        _4: string,
+        _5?: IgnoreWhitespaceType,
+        onErr?: ErrorCallback
+      ) => {
+        if (onErr) onErr(error);
+        return Promise.resolve(undefined);
+      }
+    );
+    element.patchRange = createPatchRange();
+    return element.reload().then(() => {
+      assert.isTrue(onErrStub.calledOnce);
+    });
+  });
+
+  suite('_handleGetDiffError', () => {
+    let serverErrorStub: sinon.SinonStub;
+    let pageErrorStub: sinon.SinonStub;
+
+    setup(() => {
+      serverErrorStub = sinon.stub();
+      addListenerForTest(document, 'server-error', serverErrorStub);
+      pageErrorStub = sinon.stub();
+      addListenerForTest(document, 'page-error', pageErrorStub);
+    });
+
+    test('page error on HTTP-409', () => {
+      element._handleGetDiffError({status: 409} as Response);
+      assert.isTrue(serverErrorStub.calledOnce);
+      assert.isFalse(pageErrorStub.called);
+      assert.isNotOk(element._errorMessage);
+    });
+
+    test('server error on non-HTTP-409', () => {
+      element._handleGetDiffError({
+        status: 500,
+        text: () => Promise.resolve(''),
+      } as Response);
+      assert.isFalse(serverErrorStub.called);
+      assert.isTrue(pageErrorStub.calledOnce);
+      assert.isNotOk(element._errorMessage);
+    });
+
+    test('error message if showLoadFailure', () => {
+      element.showLoadFailure = true;
+      element._handleGetDiffError({
+        status: 500,
+        statusText: 'Failure!',
+      } as Response);
+      assert.isFalse(serverErrorStub.called);
+      assert.isFalse(pageErrorStub.called);
+      assert.equal(
+        element._errorMessage,
+        'Encountered error when loading the diff: 500 Failure!'
+      );
+    });
+  });
+
+  suite('image diffs', () => {
+    let mockFile1: ImageInfo;
+    let mockFile2: ImageInfo;
+    setup(() => {
+      mockFile1 = {
+        body:
+          'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
+          'wsAAAAAAAAAAAAAAAAA/w==',
+        type: 'image/bmp',
+      };
+      mockFile2 = {
+        body:
+          'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
+          'wsAAAAAAAAAAAAA/////w==',
+        type: 'image/bmp',
+      };
+
+      element.patchRange = createPatchRange();
+      element.change = createChange();
+    });
+
+    test('renders image diffs with same file name', async () => {
+      const mockDiff: DiffInfo = {
+        meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+        meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
+        intraline_status: 'OK',
+        change_type: 'MODIFIED',
+        diff_header: [
+          'diff --git a/carrot.jpg b/carrot.jpg',
+          'index 2adc47d..f9c2f2c 100644',
+          '--- a/carrot.jpg',
+          '+++ b/carrot.jpg',
+          'Binary files differ',
+        ],
+        content: [{skip: 66}],
+        binary: true,
+      };
+      stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
+      stubRestApi('getImagesForDiff').returns(
+        Promise.resolve({
+          baseImage: {
+            ...mockFile1,
+            _expectedType: 'image/jpeg',
+            _name: 'carrot.jpg',
+          },
+          revisionImage: {
+            ...mockFile2,
+            _expectedType: 'image/jpeg',
+            _name: 'carrot.jpg',
+          },
+        })
+      );
+
+      element.prefs = createDefaultDiffPrefs();
+      element.reload();
+      await waitForEventOnce(element, 'render');
+
+      // Recognizes that it should be an image diff.
+      assert.isTrue(element.isImageDiff);
+      assert.instanceOf(element.$.diff.diffBuilder.builder, GrDiffBuilderImage);
+
+      // Left image rendered with the parent commit's version of the file.
+      const diffTable = element.$.diff.$.diffTable;
+      const leftImage = queryAndAssert(diffTable, 'td.left img');
+      const leftLabel = queryAndAssert(diffTable, 'td.left label');
+      const leftLabelContent = leftLabel.querySelector('.label');
+      const leftLabelName = leftLabel.querySelector('.name');
+
+      const rightImage = queryAndAssert(diffTable, 'td.right img');
+      const rightLabel = queryAndAssert(diffTable, 'td.right label');
+      const rightLabelContent = rightLabel.querySelector('.label');
+      const rightLabelName = rightLabel.querySelector('.name');
+
+      assert.isOk(leftImage);
+      assert.equal(
+        leftImage.getAttribute('src'),
+        'data:image/bmp;base64,' + mockFile1.body
+      );
+      assert.isTrue(leftLabelContent?.textContent?.includes('image/bmp'));
+      assert.isNotOk(leftLabelName);
+
+      assert.isOk(rightImage);
+      assert.equal(
+        rightImage.getAttribute('src'),
+        'data:image/bmp;base64,' + mockFile2.body
+      );
+      assert.isTrue(rightLabelContent?.textContent?.includes('image/bmp'));
+      assert.isNotOk(rightLabelName);
+    });
+
+    test('renders image diffs with a different file name', async () => {
+      const mockDiff: DiffInfo = {
+        meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+        meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg', lines: 560},
+        intraline_status: 'OK',
+        change_type: 'MODIFIED',
+        diff_header: [
+          'diff --git a/carrot.jpg b/carrot2.jpg',
+          'index 2adc47d..f9c2f2c 100644',
+          '--- a/carrot.jpg',
+          '+++ b/carrot2.jpg',
+          'Binary files differ',
+        ],
+        content: [{skip: 66}],
+        binary: true,
+      };
+      stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
+      stubRestApi('getImagesForDiff').returns(
+        Promise.resolve({
+          baseImage: {
+            ...mockFile1,
+            _expectedType: 'image/jpeg',
+            _name: 'carrot.jpg',
+          },
+          revisionImage: {
+            ...mockFile2,
+            _expectedType: 'image/jpeg',
+            _name: 'carrot2.jpg',
+          },
+        })
+      );
+
+      element.prefs = createDefaultDiffPrefs();
+      element.reload();
+      await waitForEventOnce(element, 'render');
+
+      // Recognizes that it should be an image diff.
+      assert.isTrue(element.isImageDiff);
+      assert.instanceOf(element.$.diff.diffBuilder.builder, GrDiffBuilderImage);
+
+      // Left image rendered with the parent commit's version of the file.
+      const diffTable = element.$.diff.$.diffTable;
+      const leftImage = queryAndAssert(diffTable, 'td.left img');
+      const leftLabel = queryAndAssert(diffTable, 'td.left label');
+      const leftLabelContent = leftLabel.querySelector('.label');
+      const leftLabelName = leftLabel.querySelector('.name');
+
+      const rightImage = queryAndAssert(diffTable, 'td.right img');
+      const rightLabel = queryAndAssert(diffTable, 'td.right label');
+      const rightLabelContent = rightLabel.querySelector('.label');
+      const rightLabelName = rightLabel.querySelector('.name');
+
+      assert.isOk(rightLabelName);
+      assert.isOk(leftLabelName);
+      assert.equal(leftLabelName?.textContent, mockDiff.meta_a?.name);
+      assert.equal(rightLabelName?.textContent, mockDiff.meta_b?.name);
+
+      assert.isOk(leftImage);
+      assert.equal(
+        leftImage.getAttribute('src'),
+        'data:image/bmp;base64,' + mockFile1.body
+      );
+      assert.isTrue(leftLabelContent?.textContent?.includes('image/bmp'));
+
+      assert.isOk(rightImage);
+      assert.equal(
+        rightImage.getAttribute('src'),
+        'data:image/bmp;base64,' + mockFile2.body
+      );
+      assert.isTrue(rightLabelContent?.textContent?.includes('image/bmp'));
+    });
+
+    test('renders added image', async () => {
+      const mockDiff: DiffInfo = {
+        meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
+        intraline_status: 'OK',
+        change_type: 'ADDED',
+        diff_header: [
+          'diff --git a/carrot.jpg b/carrot.jpg',
+          'index 0000000..f9c2f2c 100644',
+          '--- /dev/null',
+          '+++ b/carrot.jpg',
+          'Binary files differ',
+        ],
+        content: [{skip: 66}],
+        binary: true,
+      };
+      stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
+      stubRestApi('getImagesForDiff').returns(
+        Promise.resolve({
+          baseImage: null,
+          revisionImage: {
+            ...mockFile2,
+            _expectedType: 'image/jpeg',
+            _name: 'carrot2.jpg',
+          },
+        })
+      );
+
+      const promise = mockPromise();
+      element.addEventListener('render', () => {
+        // Recognizes that it should be an image diff.
+        assert.isTrue(element.isImageDiff);
+        assert.instanceOf(
+          element.$.diff.diffBuilder.builder,
+          GrDiffBuilderImage
+        );
+
+        const diffTable = element.$.diff.$.diffTable;
+
+        const leftImage = query(diffTable, 'td.left img');
+        const rightImage = queryAndAssert(diffTable, 'td.right img');
+
+        assert.isNotOk(leftImage);
+        assert.isOk(rightImage);
+        promise.resolve();
+      });
+
+      element.prefs = createDefaultDiffPrefs();
+      element.reload();
+      await promise;
+    });
+
+    test('renders removed image', async () => {
+      const mockDiff: DiffInfo = {
+        meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
+        intraline_status: 'OK',
+        change_type: 'DELETED',
+        diff_header: [
+          'diff --git a/carrot.jpg b/carrot.jpg',
+          'index f9c2f2c..0000000 100644',
+          '--- a/carrot.jpg',
+          '+++ /dev/null',
+          'Binary files differ',
+        ],
+        content: [{skip: 66}],
+        binary: true,
+      };
+      stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
+      stubRestApi('getImagesForDiff').returns(
+        Promise.resolve({
+          baseImage: {
+            ...mockFile1,
+            _expectedType: 'image/jpeg',
+            _name: 'carrot.jpg',
+          },
+          revisionImage: null,
+        })
+      );
+
+      const promise = mockPromise();
+      element.addEventListener('render', () => {
+        // Recognizes that it should be an image diff.
+        assert.isTrue(element.isImageDiff);
+        assert.instanceOf(
+          element.$.diff.diffBuilder.builder,
+          GrDiffBuilderImage
+        );
+
+        const diffTable = element.$.diff.$.diffTable;
+
+        const leftImage = queryAndAssert(diffTable, 'td.left img');
+        const rightImage = query(diffTable, 'td.right img');
+
+        assert.isOk(leftImage);
+        assert.isNotOk(rightImage);
+        promise.resolve();
+      });
+
+      element.prefs = createDefaultDiffPrefs();
+      element.reload();
+      await promise;
+    });
+
+    test('does not render disallowed image type', async () => {
+      const mockDiff: DiffInfo = {
+        meta_a: {
+          name: 'carrot.jpg',
+          content_type: 'image/jpeg-evil',
+          lines: 560,
+        },
+        intraline_status: 'OK',
+        change_type: 'DELETED',
+        diff_header: [
+          'diff --git a/carrot.jpg b/carrot.jpg',
+          'index f9c2f2c..0000000 100644',
+          '--- a/carrot.jpg',
+          '+++ /dev/null',
+          'Binary files differ',
+        ],
+        content: [{skip: 66}],
+        binary: true,
+      };
+      mockFile1.type = 'image/jpeg-evil';
+
+      stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
+      stubRestApi('getImagesForDiff').returns(
+        Promise.resolve({
+          baseImage: {
+            ...mockFile1,
+            _expectedType: 'image/jpeg',
+            _name: 'carrot.jpg',
+          },
+          revisionImage: null,
+        })
+      );
+
+      const promise = mockPromise();
+      element.addEventListener('render', () => {
+        // Recognizes that it should be an image diff.
+        assert.isTrue(element.isImageDiff);
+        assert.instanceOf(
+          element.$.diff.diffBuilder.builder,
+          GrDiffBuilderImage
+        );
+        const diffTable = element.$.diff.$.diffTable;
+
+        const leftImage = query(diffTable, 'td.left img');
+        assert.isNotOk(leftImage);
+        promise.resolve();
+      });
+
+      element.prefs = createDefaultDiffPrefs();
+      element.reload();
+      await promise;
+    });
+  });
+
+  test('cannot create comments when not logged in', () => {
+    element.patchRange = createPatchRange();
+    const showAuthRequireSpy = sinon.spy();
+    element.addEventListener('show-auth-required', showAuthRequireSpy);
+
+    element.dispatchEvent(
+      new CustomEvent('create-comment', {
+        detail: {
+          lineNum: 3,
+          side: Side.LEFT,
+          path: '/p',
+        },
+      })
+    );
+
+    const threads = queryAll(element.$.diff, 'gr-comment-thread');
+    assert.equal(threads.length, 0);
+    assert.isTrue(showAuthRequireSpy.called);
+  });
+
+  test('delegates cancel()', () => {
+    const stub = sinon.stub(element.$.diff, 'cancel');
+    element.patchRange = createPatchRange();
+    element.cancel();
+    assert.isTrue(stub.calledOnce);
+    assert.equal(stub.lastCall.args.length, 0);
+  });
+
+  test('delegates getCursorStops()', () => {
+    const returnValue = [document.createElement('b')];
+    const stub = sinon
+      .stub(element.$.diff, 'getCursorStops')
+      .returns(returnValue);
+    assert.equal(element.getCursorStops(), returnValue);
+    assert.isTrue(stub.calledOnce);
+    assert.equal(stub.lastCall.args.length, 0);
+  });
+
+  test('delegates isRangeSelected()', () => {
+    const returnValue = true;
+    const stub = sinon
+      .stub(element.$.diff, 'isRangeSelected')
+      .returns(returnValue);
+    assert.equal(element.isRangeSelected(), returnValue);
+    assert.isTrue(stub.calledOnce);
+    assert.equal(stub.lastCall.args.length, 0);
+  });
+
+  test('delegates toggleLeftDiff()', () => {
+    const stub = sinon.stub(element.$.diff, 'toggleLeftDiff');
+    element.toggleLeftDiff();
+    assert.isTrue(stub.calledOnce);
+    assert.equal(stub.lastCall.args.length, 0);
+  });
+
+  suite('blame', () => {
+    setup(async () => {
+      element = basicFixture.instantiate();
+      element.changeNum = 123 as NumericChangeId;
+      element.path = 'some/path';
+      await flush();
+    });
+
+    test('clearBlame', () => {
+      element._blame = [];
+      const setBlameSpy = sinon.spy(element.$.diff.diffBuilder, 'setBlame');
+      element.clearBlame();
+      assert.isNull(element._blame);
+      assert.isTrue(setBlameSpy.calledWithExactly(null));
+      assert.equal(element.isBlameLoaded, false);
+    });
+
+    test('loadBlame', () => {
+      const mockBlame: BlameInfo[] = [createBlame()];
+      const showAlertStub = sinon.stub();
+      element.addEventListener('show-alert', showAlertStub);
+      const getBlameStub = stubRestApi('getBlame').returns(
+        Promise.resolve(mockBlame)
+      );
+      const changeNum = 42 as NumericChangeId;
+      element.changeNum = changeNum;
+      element.patchRange = createPatchRange();
+      element.path = 'foo/bar.baz';
+      return element.loadBlame().then(() => {
+        assert.isTrue(
+          getBlameStub.calledWithExactly(
+            changeNum,
+            1 as RevisionPatchSetNum,
+            'foo/bar.baz',
+            true
+          )
+        );
+        assert.isFalse(showAlertStub.called);
+        assert.equal(element._blame, mockBlame);
+        assert.equal(element.isBlameLoaded, true);
+      });
+    });
+
+    test('loadBlame empty', () => {
+      const mockBlame: BlameInfo[] = [];
+      const showAlertStub = sinon.stub();
+      element.addEventListener('show-alert', showAlertStub);
+      stubRestApi('getBlame').returns(Promise.resolve(mockBlame));
+      const changeNum = 42 as NumericChangeId;
+      element.changeNum = changeNum;
+      element.patchRange = createPatchRange();
+      element.path = 'foo/bar.baz';
+      return element
+        .loadBlame()
+        .then(() => {
+          assert.isTrue(false, 'Promise should not resolve');
+        })
+        .catch(() => {
+          assert.isTrue(showAlertStub.calledOnce);
+          assert.isNull(element._blame);
+          assert.equal(element.isBlameLoaded, false);
+        });
+    });
+  });
+
+  test('getThreadEls() returns .comment-threads', () => {
+    const threadEl = document.createElement('gr-comment-thread');
+    threadEl.className = 'comment-thread';
+    element.$.diff.appendChild(threadEl);
+    assert.deepEqual(element.getThreadEls(), [threadEl]);
+  });
+
+  test('delegates addDraftAtLine(el)', () => {
+    const param0 = document.createElement('b');
+    const stub = sinon.stub(element.$.diff, 'addDraftAtLine');
+    element.addDraftAtLine(param0);
+    assert.isTrue(stub.calledOnce);
+    assert.equal(stub.lastCall.args.length, 1);
+    assert.equal(stub.lastCall.args[0], param0);
+  });
+
+  test('delegates clearDiffContent()', () => {
+    const stub = sinon.stub(element.$.diff, 'clearDiffContent');
+    element.clearDiffContent();
+    assert.isTrue(stub.calledOnce);
+    assert.equal(stub.lastCall.args.length, 0);
+  });
+
+  test('delegates toggleAllContext()', () => {
+    const stub = sinon.stub(element.$.diff, 'toggleAllContext');
+    element.toggleAllContext();
+    assert.isTrue(stub.calledOnce);
+    assert.equal(stub.lastCall.args.length, 0);
+  });
+
+  test('passes in noAutoRender', () => {
+    const value = true;
+    element.noAutoRender = value;
+    assert.equal(element.$.diff.noAutoRender, value);
+  });
+
+  test('passes in path', () => {
+    const value = 'some/file/path';
+    element.path = value;
+    assert.equal(element.$.diff.path, value);
+  });
+
+  test('passes in prefs', () => {
+    const value = createDefaultDiffPrefs();
+    element.prefs = value;
+    assert.equal(element.$.diff.prefs, value);
+  });
+
+  test('passes in displayLine', () => {
+    const value = true;
+    element.displayLine = value;
+    assert.equal(element.$.diff.displayLine, value);
+  });
+
+  test('passes in hidden', () => {
+    const value = true;
+    element.hidden = value;
+    assert.equal(element.$.diff.hidden, value);
+    assert.isNotNull(element.getAttribute('hidden'));
+  });
+
+  test('passes in noRenderOnPrefsChange', () => {
+    const value = true;
+    element.noRenderOnPrefsChange = value;
+    assert.equal(element.$.diff.noRenderOnPrefsChange, value);
+  });
+
+  test('passes in lineWrapping', () => {
+    const value = true;
+    element.lineWrapping = value;
+    assert.equal(element.$.diff.lineWrapping, value);
+  });
+
+  test('passes in viewMode', () => {
+    const value = DiffViewMode.SIDE_BY_SIDE;
+    element.viewMode = value;
+    assert.equal(element.$.diff.viewMode, value);
+  });
+
+  test('passes in lineOfInterest', () => {
+    const value = {lineNum: 123, side: Side.LEFT};
+    element.lineOfInterest = value;
+    assert.equal(element.$.diff.lineOfInterest, value);
+  });
+
+  suite('_reportDiff', () => {
+    let reportStub: SinonStub;
+
+    setup(async () => {
+      element = basicFixture.instantiate();
+      element.changeNum = 123 as NumericChangeId;
+      element.path = 'file.txt';
+      element.patchRange = createPatchRange(1, 2);
+      reportStub = sinon.stub(element.reporting, 'reportInteraction');
+      await flush();
+    });
+
+    test('undefined', () => {
+      element._reportDiff(undefined);
+      assert.isFalse(reportStub.called);
+    });
+
+    test('diff w/ no delta', () => {
+      const diff: DiffInfo = {
+        ...createDiff(),
+        content: [{ab: ['foo', 'bar']}, {ab: ['baz', 'foo']}],
+      };
+      element._reportDiff(diff);
+      assert.isTrue(reportStub.calledOnce);
+      assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero');
+      assert.isUndefined(reportStub.lastCall.args[1]);
+    });
+
+    test('diff w/ no rebase delta', () => {
+      const diff: DiffInfo = {
+        ...createDiff(),
+        content: [
+          {ab: ['foo', 'bar']},
+          {a: ['baz', 'foo']},
+          {ab: ['foo', 'bar']},
+          {a: ['baz', 'foo'], b: ['bar', 'baz']},
+          {ab: ['foo', 'bar']},
+          {b: ['baz', 'foo']},
+          {ab: ['foo', 'bar']},
+        ],
+      };
+      element._reportDiff(diff);
+      assert.isTrue(reportStub.calledOnce);
+      assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero');
+      assert.isUndefined(reportStub.lastCall.args[1]);
+    });
+
+    test('diff w/ some rebase delta', () => {
+      const diff: DiffInfo = {
+        ...createDiff(),
+        content: [
+          {ab: ['foo', 'bar']},
+          {a: ['baz', 'foo'], due_to_rebase: true},
+          {ab: ['foo', 'bar']},
+          {a: ['baz', 'foo'], b: ['bar', 'baz']},
+          {ab: ['foo', 'bar']},
+          {b: ['baz', 'foo'], due_to_rebase: true},
+          {ab: ['foo', 'bar']},
+          {a: ['baz', 'foo']},
+        ],
+      };
+      element._reportDiff(diff);
+      assert.isTrue(reportStub.calledOnce);
+      assert.isTrue(
+        reportStub.calledWith('rebase-percent-nonzero', {
+          percentRebaseDelta: 50,
+        })
+      );
+    });
+
+    test('diff w/ all rebase delta', () => {
+      const diff: DiffInfo = {
+        ...createDiff(),
+        content: [
+          {
+            a: ['foo', 'bar'],
+            b: ['baz', 'foo'],
+            due_to_rebase: true,
+          },
+        ],
+      };
+      element._reportDiff(diff);
+      assert.isTrue(reportStub.calledOnce);
+      assert.isTrue(
+        reportStub.calledWith('rebase-percent-nonzero', {
+          percentRebaseDelta: 100,
+        })
+      );
+    });
+
+    test('diff against parent event', () => {
+      element.patchRange = createPatchRange();
+      const diff: DiffInfo = {
+        ...createDiff(),
+        content: [
+          {
+            a: ['foo', 'bar'],
+            b: ['baz', 'foo'],
+          },
+        ],
+      };
+      element._reportDiff(diff);
+      assert.isTrue(reportStub.calledOnce);
+      assert.equal(reportStub.lastCall.args[0], 'diff-against-parent');
+      assert.isUndefined(reportStub.lastCall.args[1]);
+    });
+  });
+
+  suite('createCheckEl method', () => {
+    test('start_line:12', () => {
+      const result: RunResult = {
+        ...createRunResult(),
+        codePointers: [{path: 'a', range: {start_line: 12} as CommentRange}],
+      };
+      const el = element.createCheckEl(result);
+      assert.equal(el.getAttribute('slot'), 'right-12');
+      assert.equal(el.getAttribute('diff-side'), 'right');
+      assert.equal(el.getAttribute('line-num'), '12');
+      assert.equal(el.getAttribute('range'), null);
+      assert.equal(el.result, result);
+    });
+
+    test('start_line:13 end_line:14 without char positions', () => {
+      const result: RunResult = {
+        ...createRunResult(),
+        codePointers: [
+          {path: 'a', range: {start_line: 13, end_line: 14} as CommentRange},
+        ],
+      };
+      const el = element.createCheckEl(result);
+      assert.equal(el.getAttribute('slot'), 'right-14');
+      assert.equal(el.getAttribute('diff-side'), 'right');
+      assert.equal(el.getAttribute('line-num'), '14');
+      assert.equal(el.getAttribute('range'), null);
+      assert.equal(el.result, result);
+    });
+
+    test('start_line:13 end_line:14 with char positions', () => {
+      const result: RunResult = {
+        ...createRunResult(),
+        codePointers: [
+          {
+            path: 'a',
+            range: {
+              start_line: 13,
+              end_line: 14,
+              start_character: 5,
+              end_character: 7,
+            },
+          },
+        ],
+      };
+      const el = element.createCheckEl(result);
+      assert.equal(el.getAttribute('slot'), 'right-14');
+      assert.equal(el.getAttribute('diff-side'), 'right');
+      assert.equal(el.getAttribute('line-num'), '14');
+      assert.equal(
+        el.getAttribute('range'),
+        '{"start_line":13,' +
+          '"end_line":14,' +
+          '"start_character":5,' +
+          '"end_character":7}'
+      );
+      assert.equal(el.result, result);
+    });
+
+    test('empty range', () => {
+      const result: RunResult = {
+        ...createRunResult(),
+        codePointers: [{path: 'a', range: {} as CommentRange}],
+      };
+      const el = element.createCheckEl(result);
+      assert.equal(el.getAttribute('slot'), 'right-FILE');
+      assert.equal(el.getAttribute('diff-side'), 'right');
+      assert.equal(el.getAttribute('line-num'), 'FILE');
+      assert.equal(el.getAttribute('range'), null);
+      assert.equal(el.result, result);
+    });
+  });
+
+  suite('create-comment', () => {
+    setup(async () => {
+      loggedIn = true;
+      element.connectedCallback();
+      await flush();
+    });
+
+    test('creates comments if they do not exist yet', async () => {
+      element.patchRange = createPatchRange();
+      element.dispatchEvent(
+        new CustomEvent('create-comment', {
+          detail: {
+            lineNum: 3,
+            side: Side.LEFT,
+            path: '/p',
+          },
+        })
+      );
+
+      let threads =
+        element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+
+      assert.equal(threads.length, 1);
+      assert.equal(threads[0].thread?.commentSide, CommentSide.PARENT);
+      assert.equal(threads[0].getAttribute('diff-side'), Side.LEFT);
+      assert.equal(threads[0].thread?.range, undefined);
+      assert.equal(threads[0].thread?.patchNum, 1 as PatchSetNum);
+
+      // Try to fetch a thread with a different range.
+      const range = {
+        start_line: 1,
+        start_character: 1,
+        end_line: 1,
+        end_character: 3,
+      };
+      element.patchRange = createPatchRange();
+
+      element.dispatchEvent(
+        new CustomEvent('create-comment', {
+          detail: {
+            lineNum: 1,
+            side: Side.LEFT,
+            path: '/p',
+            range,
+          },
+        })
+      );
+
+      threads =
+        element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+
+      assert.equal(threads.length, 2);
+      assert.equal(threads[0].thread?.commentSide, CommentSide.PARENT);
+      assert.equal(threads[0].getAttribute('diff-side'), Side.LEFT);
+      assert.equal(threads[1].thread?.range, range);
+      assert.equal(threads[1].thread?.patchNum, 1 as PatchSetNum);
+    });
+
+    test('should not be on parent if on the right', async () => {
+      element.patchRange = createPatchRange(2, 3);
+      element.dispatchEvent(
+        new CustomEvent('create-comment', {
+          detail: {
+            side: Side.RIGHT,
+          },
+        })
+      );
+      await flush();
+
+      const threads =
+        element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+      const threadEl = threads[0];
+
+      assert.equal(threadEl.thread?.commentSide, CommentSide.REVISION);
+      assert.equal(threadEl.getAttribute('diff-side'), Side.RIGHT);
+    });
+
+    test('should be on parent if right and base is PARENT', () => {
+      element.patchRange = createPatchRange();
+
+      element.dispatchEvent(
+        new CustomEvent('create-comment', {
+          detail: {
+            side: Side.LEFT,
+          },
+        })
+      );
+
+      const threads =
+        element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+      const threadEl = threads[0];
+
+      assert.equal(threadEl.thread?.commentSide, CommentSide.PARENT);
+      assert.equal(threadEl.getAttribute('diff-side'), Side.LEFT);
+    });
+
+    test('should be on parent if right and base negative', () => {
+      element.patchRange = createPatchRange(-2, 3);
+
+      element.dispatchEvent(
+        new CustomEvent('create-comment', {
+          detail: {
+            side: Side.LEFT,
+          },
+        })
+      );
+
+      const threads =
+        element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+      const threadEl = threads[0];
+
+      assert.equal(threadEl.thread?.commentSide, CommentSide.PARENT);
+      assert.equal(threadEl.getAttribute('diff-side'), Side.LEFT);
+    });
+
+    test('should not be on parent otherwise', () => {
+      element.patchRange = createPatchRange(2, 3);
+      element.dispatchEvent(
+        new CustomEvent('create-comment', {
+          detail: {
+            side: Side.LEFT,
+          },
+        })
+      );
+
+      const threads =
+        element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+      const threadEl = threads[0];
+
+      assert.equal(threadEl.thread?.commentSide, CommentSide.REVISION);
+      assert.equal(threadEl.getAttribute('diff-side'), Side.LEFT);
+    });
+
+    test(
+      'thread should use old file path if first created ' +
+        'on patch set (left) before renaming',
+      async () => {
+        element.patchRange = createPatchRange(2, 3);
+        element.file = {basePath: 'file_renamed.txt', path: element.path ?? ''};
+        await flush();
+
+        element.dispatchEvent(
+          new CustomEvent('create-comment', {
+            detail: {
+              side: Side.LEFT,
+              path: '/p',
+            },
+          })
+        );
+
+        const threads =
+          element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+
+        assert.equal(threads.length, 1);
+        assert.equal(threads[0].getAttribute('diff-side'), Side.LEFT);
+        assert.equal(threads[0].thread?.path, element.file.basePath);
+      }
+    );
+
+    test(
+      'thread should use new file path if first created ' +
+        'on patch set (right) after renaming',
+      async () => {
+        element.patchRange = createPatchRange(2, 3);
+        element.file = {basePath: 'file_renamed.txt', path: element.path ?? ''};
+        await flush();
+
+        element.dispatchEvent(
+          new CustomEvent('create-comment', {
+            detail: {
+              side: Side.RIGHT,
+              path: '/p',
+            },
+          })
+        );
+
+        const threads =
+          element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+
+        assert.equal(threads.length, 1);
+        assert.equal(threads[0].getAttribute('diff-side'), Side.RIGHT);
+        assert.equal(threads[0].thread?.path, element.file.path);
+      }
+    );
+
+    test('multiple threads created on the same range', async () => {
+      element.patchRange = createPatchRange(2, 3);
+
+      element.file = {basePath: 'file_renamed.txt', path: element.path ?? ''};
+      await flush();
+
+      const comment = {
+        ...createComment(),
+        range: {
+          start_line: 1,
+          start_character: 1,
+          end_line: 2,
+          end_character: 2,
+        },
+        patch_set: 3 as PatchSetNum,
+      };
+      const thread = createCommentThread([comment]);
+      element.threads = [thread];
+
+      let threads =
+        element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+
+      assert.equal(threads.length, 1);
+      element.threads = [...element.threads, thread];
+
+      threads =
+        element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+      // Threads have same rootId so element is reused
+      assert.equal(threads.length, 1);
+
+      const newThread = {...thread};
+      newThread.rootId = 'differentRootId' as UrlEncodedCommentId;
+      element.threads = [...element.threads, newThread];
+      threads =
+        element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+      // New thread has a different rootId
+      assert.equal(threads.length, 2);
+    });
+
+    test('unsaved thread changes to draft', async () => {
+      element.patchRange = createPatchRange(2, 3);
+      element.file = {basePath: 'file_renamed.txt', path: element.path ?? ''};
+      element.threads = [];
+      await flush();
+
+      element.dispatchEvent(
+        new CustomEvent('create-comment', {
+          detail: {
+            side: Side.RIGHT,
+            path: element.path,
+            lineNum: 13,
+          },
+        })
+      );
+      await flush();
+      assert.equal(element.getThreadEls().length, 1);
+      const threadEl = element.getThreadEls()[0];
+      assert.equal(threadEl.thread?.line, 13);
+      assert.isDefined(threadEl.unsavedComment);
+      assert.equal(threadEl.thread?.comments.length, 0);
+
+      const draftThread = createCommentThread([
+        {
+          path: element.path,
+          patch_set: 3 as PatchSetNum,
+          line: 13,
+          __draft: true,
+        },
+      ]);
+      element.threads = [draftThread];
+      await flush();
+
+      // We expect that no additional thread element was created.
+      assert.equal(element.getThreadEls().length, 1);
+      // In fact the thread element must still be the same.
+      assert.equal(element.getThreadEls()[0], threadEl);
+      // But it must have been updated from unsaved to draft:
+      assert.isUndefined(threadEl.unsavedComment);
+      assert.equal(threadEl.thread?.comments.length, 1);
+    });
+
+    test(
+      'thread should use new file path if first created ' +
+        'on patch set (left) but is base',
+      async () => {
+        element.patchRange = createPatchRange();
+        element.file = {basePath: 'file_renamed.txt', path: element.path ?? ''};
+        await flush();
+
+        element.dispatchEvent(
+          new CustomEvent('create-comment', {
+            detail: {
+              side: Side.LEFT,
+              path: '/p',
+            },
+          })
+        );
+
+        const threads =
+          element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+
+        assert.equal(threads.length, 1);
+        assert.equal(threads[0].getAttribute('diff-side'), Side.LEFT);
+        assert.equal(threads[0].thread?.path, element.file.path);
+      }
+    );
+
+    test('cannot create thread on an edit', () => {
+      const alertSpy = sinon.spy();
+      element.addEventListener('show-alert', alertSpy);
+
+      const diffSide = Side.RIGHT;
+      element.patchRange = {
+        basePatchNum: 3 as BasePatchSetNum,
+        patchNum: EditPatchSetNum,
+      };
+      element.dispatchEvent(
+        new CustomEvent('create-comment', {
+          detail: {
+            side: diffSide,
+            path: '/p',
+          },
+        })
+      );
+
+      const threads =
+        element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+      assert.equal(threads.length, 0);
+      assert.isTrue(alertSpy.called);
+    });
+
+    test('cannot create thread on an edit base', () => {
+      const alertSpy = sinon.spy();
+      element.addEventListener('show-alert', alertSpy);
+
+      const diffSide = Side.LEFT;
+      element.patchRange = {
+        basePatchNum: ParentPatchSetNum,
+        patchNum: EditPatchSetNum,
+      };
+      element.dispatchEvent(
+        new CustomEvent('create-comment', {
+          detail: {
+            side: diffSide,
+            path: '/p',
+          },
+        })
+      );
+
+      const threads =
+        element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+      assert.equal(threads.length, 0);
+      assert.isTrue(alertSpy.called);
+    });
+  });
+
+  test('_filterThreadElsForLocation with no threads', () => {
+    const line = {beforeNumber: 3, afterNumber: 5};
+    const threads: GrCommentThread[] = [];
+    assert.deepEqual(
+      element._filterThreadElsForLocation(threads, line, Side.LEFT),
+      []
+    );
+    assert.deepEqual(
+      element._filterThreadElsForLocation(threads, line, Side.RIGHT),
+      []
+    );
+  });
+
+  test('_filterThreadElsForLocation for line comments', () => {
+    const line = {beforeNumber: 3, afterNumber: 5};
+
+    const l3 = document.createElement('gr-comment-thread');
+    l3.setAttribute('line-num', '3');
+    l3.setAttribute('diff-side', Side.LEFT);
+
+    const l5 = document.createElement('gr-comment-thread');
+    l5.setAttribute('line-num', '5');
+    l5.setAttribute('diff-side', Side.LEFT);
+
+    const r3 = document.createElement('gr-comment-thread');
+    r3.setAttribute('line-num', '3');
+    r3.setAttribute('diff-side', Side.RIGHT);
+
+    const r5 = document.createElement('gr-comment-thread');
+    r5.setAttribute('line-num', '5');
+    r5.setAttribute('diff-side', Side.RIGHT);
+
+    const threadEls: GrCommentThread[] = [l3, l5, r3, r5];
+    assert.deepEqual(
+      element._filterThreadElsForLocation(threadEls, line, Side.LEFT),
+      [l3]
+    );
+    assert.deepEqual(
+      element._filterThreadElsForLocation(threadEls, line, Side.RIGHT),
+      [r5]
+    );
+  });
+
+  test('_filterThreadElsForLocation for file comments', () => {
+    const line: LineInfo = {beforeNumber: 'FILE', afterNumber: 'FILE'};
+
+    const l = document.createElement('gr-comment-thread');
+    l.setAttribute('diff-side', Side.LEFT);
+    l.setAttribute('line-num', 'FILE');
+
+    const r = document.createElement('gr-comment-thread');
+    r.setAttribute('diff-side', Side.RIGHT);
+    r.setAttribute('line-num', 'FILE');
+
+    const threadEls: GrCommentThread[] = [l, r];
+    assert.deepEqual(
+      element._filterThreadElsForLocation(threadEls, line, Side.LEFT),
+      [l]
+    );
+    assert.deepEqual(
+      element._filterThreadElsForLocation(threadEls, line, Side.RIGHT),
+      [r]
+    );
+  });
+
+  suite('syntax layer with syntax_highlighting on', () => {
+    setup(() => {
+      const prefs = {
+        ...createDefaultDiffPrefs(),
+        line_length: 10,
+        show_tabs: true,
+        tab_size: 4,
+        context: -1,
+        syntax_highlighting: true,
+      };
+      element.patchRange = createPatchRange();
+      element.prefs = prefs;
+      element.changeNum = 123 as NumericChangeId;
+      element.change = createChange();
+      element.path = 'some/path';
+    });
+
+    test('gr-diff-host provides syntax highlighting layer', async () => {
+      stubRestApi('getDiff').returns(Promise.resolve(createDiff()));
+      await element.reload();
+      assertIsDefined(element.$.diff.layers);
+      assert.equal(element.$.diff.layers[1], element.syntaxLayer);
+    });
+
+    test('rendering normal-sized diff does not disable syntax', () => {
+      element.diff = createDiff();
+      assert.isTrue(element.syntaxLayer.enabled);
+    });
+
+    test('rendering large diff disables syntax', () => {
+      // Before it renders, set the first diff line to 500 '*' characters.
+      element.diff = {
+        ...createDiff(),
+        content: [
+          {
+            a: [new Array(501).join('*')],
+          },
+        ],
+      };
+      assert.isFalse(element.syntaxLayer.enabled);
+    });
+
+    test('starts syntax layer processing on render event', async () => {
+      const stub = sinon
+        .stub(element.syntaxLayer, 'process')
+        .returns(Promise.resolve());
+      stubRestApi('getDiff').returns(Promise.resolve(createDiff()));
+      await element.reload();
+      element.dispatchEvent(
+        new CustomEvent('render', {bubbles: true, composed: true})
+      );
+      assert.isTrue(stub.called);
+    });
+  });
+
+  suite('syntax layer with syntax_highlighting off', () => {
+    setup(() => {
+      const prefs = {
+        ...createDefaultDiffPrefs(),
+        line_length: 10,
+        show_tabs: true,
+        tab_size: 4,
+        context: -1,
+        syntax_highlighting: false,
+      };
+      element.diff = createDiff();
+      element.patchRange = createPatchRange();
+      element.change = createChange();
+      element.prefs = prefs;
+    });
+
+    test('gr-diff-host provides syntax highlighting layer', async () => {
+      stubRestApi('getDiff').returns(Promise.resolve(createDiff()));
+      await element.reload();
+      assertIsDefined(element.$.diff.layers);
+      assert.equal(element.$.diff.layers[1], element.syntaxLayer);
+    });
+
+    test('syntax layer should be disabled', () => {
+      assert.isFalse(element.syntaxLayer.enabled);
+    });
+
+    test('still disabled for large diff', () => {
+      // Before it renders, set the first diff line to 500 '*' characters.
+      element.diff = {
+        ...createDiff(),
+        content: [
+          {
+            a: [new Array(501).join('*')],
+          },
+        ],
+      };
+      assert.isFalse(element.syntaxLayer.enabled);
+    });
+  });
+
+  suite('coverage layer', () => {
+    let notifyStub: SinonStub;
+    let coverageProviderStub: SinonStub;
+    let getCoverageAnnotationApisStub: SinonStub;
+    const exampleRanges = [
+      {
+        type: CoverageType.COVERED,
+        side: Side.RIGHT,
+        code_range: {
+          start_line: 1,
+          end_line: 2,
+        },
+      },
+      {
+        type: CoverageType.NOT_COVERED,
+        side: Side.RIGHT,
+        code_range: {
+          start_line: 3,
+          end_line: 4,
+        },
+      },
+    ];
+
+    setup(async () => {
+      notifyStub = sinon.stub();
+      coverageProviderStub = sinon
+        .stub()
+        .returns(Promise.resolve(exampleRanges));
+
+      element = basicFixture.instantiate();
+      getCoverageAnnotationApisStub = sinon
+        .stub(element.jsAPI, 'getCoverageAnnotationApis')
+        .returns(
+          Promise.resolve([
+            {
+              notify: notifyStub,
+              getCoverageProvider() {
+                return coverageProviderStub;
+              },
+            } as unknown as GrAnnotationActionsInterface,
+          ])
+        );
+      element.changeNum = 123 as NumericChangeId;
+      element.change = createChange();
+      element.path = 'some/path';
+      const prefs = {
+        ...createDefaultDiffPrefs(),
+        line_length: 10,
+        show_tabs: true,
+        tab_size: 4,
+        context: -1,
+      };
+      element.diff = {
+        ...createDiff(),
+        content: [{a: ['foo']}],
+      };
+      element.patchRange = createPatchRange();
+      element.prefs = prefs;
+      stubRestApi('getDiff').returns(Promise.resolve(element.diff));
+      await flush();
+    });
+
+    test('getCoverageAnnotationApis should be called', async () => {
+      await element.reload();
+      assert.isTrue(getCoverageAnnotationApisStub.calledOnce);
+    });
+
+    test('coverageRangeChanged should be called', async () => {
+      await element.reload();
+      assert.equal(notifyStub.callCount, 2);
+      assert.isTrue(
+        notifyStub.calledWithExactly('some/path', 1, 2, Side.RIGHT)
+      );
+      assert.isTrue(
+        notifyStub.calledWithExactly('some/path', 3, 4, Side.RIGHT)
+      );
+    });
+
+    test('provider is called with appropriate params', async () => {
+      element.patchRange = createPatchRange(1, 3);
+
+      await element.reload();
+      assert.isTrue(
+        coverageProviderStub.calledWithExactly(
+          123,
+          'some/path',
+          1,
+          3,
+          element.change
+        )
+      );
+    });
+
+    test('provider is called with appropriate params - special patchset values', async () => {
+      element.patchRange = createPatchRange();
+
+      await element.reload();
+      assert.isTrue(
+        coverageProviderStub.calledWithExactly(
+          123,
+          'some/path',
+          undefined,
+          1,
+          element.change
+        )
+      );
+    });
+  });
+
+  suite('trailing newlines', () => {
+    setup(() => {});
+
+    suite('_lastChunkForSide', () => {
+      test('deltas', () => {
+        const diff: DiffInfo = {
+          ...createDiff(),
+          content: [
+            {a: ['foo', 'bar'], b: ['baz']},
+            {ab: ['foo', 'bar', 'baz']},
+            {b: ['foo']},
+          ],
+        };
+        assert.equal(element._lastChunkForSide(diff, false), diff.content[2]);
+        assert.equal(element._lastChunkForSide(diff, true), diff.content[1]);
+
+        diff.content.push({a: ['foo'], b: ['bar']});
+        assert.equal(element._lastChunkForSide(diff, false), diff.content[3]);
+        assert.equal(element._lastChunkForSide(diff, true), diff.content[3]);
+      });
+
+      test('addition with a undefined', () => {
+        const diff: DiffInfo = {
+          ...createDiff(),
+          content: [{b: ['foo', 'bar', 'baz']}],
+        };
+        assert.equal(element._lastChunkForSide(diff, false), diff.content[0]);
+        assert.isNull(element._lastChunkForSide(diff, true));
+      });
+
+      test('addition with a empty', () => {
+        const diff: DiffInfo = {
+          ...createDiff(),
+          content: [{a: [], b: ['foo', 'bar', 'baz']}],
+        };
+        assert.equal(element._lastChunkForSide(diff, false), diff.content[0]);
+        assert.isNull(element._lastChunkForSide(diff, true));
+      });
+
+      test('deletion with b undefined', () => {
+        const diff: DiffInfo = {
+          ...createDiff(),
+          content: [{a: ['foo', 'bar', 'baz']}],
+        };
+        assert.isNull(element._lastChunkForSide(diff, false));
+        assert.equal(element._lastChunkForSide(diff, true), diff.content[0]);
+      });
+
+      test('deletion with b empty', () => {
+        const diff: DiffInfo = {
+          ...createDiff(),
+          content: [{a: ['foo', 'bar', 'baz'], b: []}],
+        };
+        assert.isNull(element._lastChunkForSide(diff, false));
+        assert.equal(element._lastChunkForSide(diff, true), diff.content[0]);
+      });
+
+      test('empty', () => {
+        const diff: DiffInfo = {...createDiff(), content: []};
+        assert.isNull(element._lastChunkForSide(diff, false));
+        assert.isNull(element._lastChunkForSide(diff, true));
+      });
+    });
+
+    suite('_hasTrailingNewlines', () => {
+      test('shared no trailing', () => {
+        const diff = undefined;
+        sinon.stub(element, '_lastChunkForSide').returns({ab: ['foo', 'bar']});
+        assert.isFalse(element._hasTrailingNewlines(diff, false));
+        assert.isFalse(element._hasTrailingNewlines(diff, true));
+      });
+
+      test('delta trailing in right', () => {
+        const diff = undefined;
+        sinon
+          .stub(element, '_lastChunkForSide')
+          .returns({a: ['foo', 'bar'], b: ['baz', '']});
+        assert.isTrue(element._hasTrailingNewlines(diff, false));
+        assert.isFalse(element._hasTrailingNewlines(diff, true));
+      });
+
+      test('addition', () => {
+        const diff: DiffInfo | undefined = undefined;
+        sinon
+          .stub(element, '_lastChunkForSide')
+          .callsFake((_: DiffInfo | undefined, leftSide: boolean) => {
+            if (leftSide) {
+              return null;
+            }
+            return {b: ['foo', '']};
+          });
+        assert.isTrue(element._hasTrailingNewlines(diff, false));
+        assert.isNull(element._hasTrailingNewlines(diff, true));
+      });
+
+      test('deletion', () => {
+        const diff: DiffInfo | undefined = undefined;
+        sinon
+          .stub(element, '_lastChunkForSide')
+          .callsFake((_: DiffInfo | undefined, leftSide: boolean) => {
+            if (!leftSide) {
+              return null;
+            }
+            return {a: ['foo']};
+          });
+        assert.isNull(element._hasTrailingNewlines(diff, false));
+        assert.isFalse(element._hasTrailingNewlines(diff, true));
+      });
+    });
+  });
+});
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 8e8fe42..e90ad2c 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -22,6 +22,7 @@
   ApprovalInfo,
   AuthInfo,
   BasePatchSetNum,
+  BlameInfo,
   BranchName,
   ChangeConfigInfo,
   ChangeId,
@@ -54,6 +55,7 @@
   MaxObjectSizeLimitInfo,
   MergeableInfo,
   NumericChangeId,
+  PatchRange,
   PatchSetNum,
   PluginConfigInfo,
   PreferencesInfo,
@@ -64,6 +66,7 @@
   RequirementType,
   Reviewers,
   RevisionInfo,
+  RevisionPatchSetNum,
   RobotCommentInfo,
   RobotId,
   RobotRunId,
@@ -258,6 +261,16 @@
   };
 }
 
+export function createPatchRange(
+  basePatchNum?: number,
+  patchNum?: number
+): PatchRange {
+  return {
+    basePatchNum: (basePatchNum ?? 'PARENT') as BasePatchSetNum,
+    patchNum: (patchNum ?? 1) as RevisionPatchSetNum,
+  };
+}
+
 export function createRevision(
   patchSetNum = 1,
   description = ''
@@ -603,6 +616,16 @@
   };
 }
 
+export function createBlame(): BlameInfo {
+  return {
+    author: 'test-author',
+    id: 'test-id',
+    time: 123,
+    commit_msg: 'test-commit-message',
+    ranges: [],
+  };
+}
+
 export function createMergeable(): MergeableInfo {
   return {
     submit_type: SubmitType.MERGE_IF_NECESSARY,
@@ -843,7 +866,9 @@
   };
 }
 
-export function createCommentThread(comments: Array<Partial<CommentInfo>>) {
+export function createCommentThread(
+  comments: Array<Partial<CommentInfo | DraftInfo>>
+) {
   if (!comments.length) {
     throw new Error('comment is required to create a thread');
   }