Merge "Await promise in async method"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index ea50b10..4179ad1 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -77,6 +77,7 @@
 import {ValueChangedEvent} from '../../../types/events';
 import {subscribe} from '../../lit/subscription-controller';
 import {when} from 'lit/directives/when';
+import {classMap} from 'lit/directives/class-map';
 import {incrementalRepeat} from '../../lit/incremental-repeat';
 import {ifDefined} from 'lit/directives/if-defined';
 import {KnownExperimentId} from '../../../services/flags/flags';
@@ -218,6 +219,9 @@
   files: NormalizedFileInfo[] = [];
 
   // Private but used in tests.
+  @state() filesLeftBase: NormalizedFileInfo[] = [];
+
+  // Private but used in tests.
   @state()
   loggedIn = false;
 
@@ -397,6 +401,15 @@
         }
         .status {
           margin-right: var(--spacing-s);
+          display: flex;
+          width: 20px;
+          justify-content: flex-end;
+        }
+        .status.extended {
+          width: 60px;
+        }
+        .status > * {
+          display: block;
         }
         .path {
           cursor: pointer;
@@ -694,6 +707,13 @@
     );
     subscribe(
       this,
+      () => this.getFilesModel().filesLeftBase$,
+      files => {
+        this.filesLeftBase = [...files];
+      }
+    );
+    subscribe(
+      this,
       () => this.getBrowserModel().diffViewMode$,
       diffView => {
         this.diffViewMode = diffView;
@@ -1012,16 +1032,55 @@
 
   private renderFileStatus(file?: NormalizedFileInfo) {
     if (!this.flagsService.isEnabled(KnownExperimentId.MORE_FILES_INFO)) return;
-    let status: FileInfoStatus | undefined = undefined;
-    if (file && !isMagicPath(file?.__path)) {
-      status = file.status ?? FileInfoStatus.MODIFIED;
-    }
-    // TODO: Add support for showing more file info when comparing two patchsets.
-    return html`<div class="status" role="gridcell">
-      <gr-file-status .status=${status}></gr-file-status>
+    const hasExtendedStatus = this.filesLeftBase.length > 0;
+    const leftStatus = this.renderFileStatusLeft(file?.__path);
+    const rightStatus = this.renderFileStatusRight(file);
+    return html`<div
+      class=${classMap({status: true, extended: hasExtendedStatus})}
+      role="gridcell"
+    >
+      ${leftStatus}${rightStatus}
     </div>`;
   }
 
+  private renderFileStatusRight(file?: NormalizedFileInfo) {
+    if (!file) return nothing;
+    if (isMagicPath(file.__path)) return nothing;
+
+    const hasExtendedStatus = this.filesLeftBase.length > 0;
+    const fileWasAlreadyChanged = this.filesLeftBase.some(
+      info => info.__path === file?.__path
+    );
+    const newlyChanged = hasExtendedStatus && !fileWasAlreadyChanged;
+
+    const status = file?.status ?? FileInfoStatus.MODIFIED;
+    const postfix = ` in patchset ${this.patchRange?.patchNum}`;
+
+    return html`<gr-file-status
+      .status=${status}
+      .labelPostfix=${postfix}
+      ?newlyChanged=${newlyChanged}
+    ></gr-file-status>`;
+  }
+
+  private renderFileStatusLeft(path?: string) {
+    if (this.filesLeftBase.length === 0) return nothing;
+    if (isMagicPath(path)) return nothing;
+    const file = this.filesLeftBase.find(info => info.__path === path);
+    if (!file) return nothing;
+
+    const status = file.status ?? FileInfoStatus.MODIFIED;
+    const postfix = ` in patchset ${this.patchRange?.basePatchNum}`;
+
+    return html`
+      <gr-file-status
+        .status=${status}
+        .labelPostfix=${postfix}
+      ></gr-file-status>
+      <iron-icon icon="gr-icons:arrow-right"></iron-icon>
+    `;
+  }
+
   private renderFilePath(file: NormalizedFileInfo) {
     return html` <span class="path" role="gridcell">
       <a class="pathLink" href=${ifDefined(this.computeDiffURL(file.__path))}>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
index fdb2f25..d96ad74 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
@@ -15,6 +15,7 @@
   stubRestApi,
   waitUntil,
   pressKey,
+  stubFlags,
 } from '../../../test/test-utils';
 import {
   BasePatchSetNum,
@@ -254,6 +255,22 @@
       </div>`);
     });
 
+    test('renders file status column', async () => {
+      stubFlags('isEnabled').returns(true);
+      element.files = createFiles(1, {lines_inserted: 9});
+      element.filesLeftBase = createFiles(1, {lines_inserted: 9});
+      await element.updateComplete;
+      const fileRows = queryAll<HTMLDivElement>(element, '.file-row');
+      const statusCol = queryAndAssert(fileRows?.[0], '.status');
+      expect(statusCol).dom.equal(/* HTML */ `
+        <div class="extended status" role="gridcell">
+          <gr-file-status></gr-file-status>
+          <iron-icon icon="gr-icons:arrow-right"></iron-icon>
+          <gr-file-status></gr-file-status>
+        </div>
+      `);
+    });
+
     test('correct number of files are shown', async () => {
       element.fileListIncrement = 300;
       element.files = createFiles(500);
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index 782c942..af08d68 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -228,7 +228,7 @@
     if (this.disableEndpoints)
       return html`<div class="votes-cell">${slot}</div>`;
 
-    const endpointName = this.calculateEndpointName(requirement.name);
+    const endpointName = this.computeEndpointName(requirement.name);
     return html`<gr-endpoint-decorator class="votes-cell" name=${endpointName}>
       <gr-endpoint-param
         name="change"
@@ -421,10 +421,7 @@
   }
 
   private computeTriggerVotesTitle() {
-    const submit_requirements = orderSubmitRequirements(
-      getRequirements(this.change)
-    );
-    if (submit_requirements.length === 0) {
+    if (getRequirements(this.change).length === 0) {
       // This is special case for old changes without submit requirements.
       return 'Label Votes';
     } else {
@@ -433,7 +430,7 @@
   }
 
   // not private for tests
-  calculateEndpointName(requirementName: string) {
+  computeEndpointName(requirementName: string) {
     // remove class name annnotation after ~
     const name = requirementName.split('~')[0];
     const normalizedName = charsOnly(name).toLowerCase();
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
index 7a8beeb..d4ec144 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
@@ -262,7 +262,7 @@
 
   test('calculateEndpointName()', () => {
     assert.equal(
-      element.calculateEndpointName('code-owners~CodeOwnerSub'),
+      element.computeEndpointName('code-owners~CodeOwnerSub'),
       'submit-requirement-codeowners'
     );
   });
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index cd2eee0..c3d45be 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -177,7 +177,7 @@
    * @event show-alert
    */
 
-  @property({type: Object, observer: '_paramsChanged'})
+  @property({type: Object, observer: 'paramsChanged'})
   params?: AppElementParams;
 
   @property({type: Object})
@@ -286,63 +286,57 @@
     return [
       listen(Shortcut.LEFT_PANE, _ => this.cursor?.moveLeft()),
       listen(Shortcut.RIGHT_PANE, _ => this.cursor?.moveRight()),
-      listen(Shortcut.NEXT_LINE, _ => this._handleNextLine()),
-      listen(Shortcut.PREV_LINE, _ => this._handlePrevLine()),
+      listen(Shortcut.NEXT_LINE, _ => this.handleNextLine()),
+      listen(Shortcut.PREV_LINE, _ => this.handlePrevLine()),
       listen(Shortcut.VISIBLE_LINE, _ => this.cursor?.moveToVisibleArea()),
       listen(Shortcut.NEXT_FILE_WITH_COMMENTS, _ =>
-        this._moveToNextFileWithComment()
+        this.moveToNextFileWithComment()
       ),
       listen(Shortcut.PREV_FILE_WITH_COMMENTS, _ =>
-        this._moveToPreviousFileWithComment()
+        this.moveToPreviousFileWithComment()
       ),
-      listen(Shortcut.NEW_COMMENT, _ => this._handleNewComment()),
+      listen(Shortcut.NEW_COMMENT, _ => this.handleNewComment()),
       listen(Shortcut.SAVE_COMMENT, _ => {}),
-      listen(Shortcut.NEXT_FILE, _ => this._handleNextFile()),
-      listen(Shortcut.PREV_FILE, _ => this._handlePrevFile()),
-      listen(Shortcut.NEXT_CHUNK, _ => this._handleNextChunk()),
-      listen(Shortcut.PREV_CHUNK, _ => this._handlePrevChunk()),
-      listen(Shortcut.NEXT_COMMENT_THREAD, _ =>
-        this._handleNextCommentThread()
-      ),
-      listen(Shortcut.PREV_COMMENT_THREAD, _ =>
-        this._handlePrevCommentThread()
-      ),
-      listen(Shortcut.OPEN_REPLY_DIALOG, _ => this._handleOpenReplyDialog()),
-      listen(Shortcut.TOGGLE_LEFT_PANE, _ => this._handleToggleLeftPane()),
+      listen(Shortcut.NEXT_FILE, _ => this.handleNextFile()),
+      listen(Shortcut.PREV_FILE, _ => this.handlePrevFile()),
+      listen(Shortcut.NEXT_CHUNK, _ => this.handleNextChunk()),
+      listen(Shortcut.PREV_CHUNK, _ => this.handlePrevChunk()),
+      listen(Shortcut.NEXT_COMMENT_THREAD, _ => this.handleNextCommentThread()),
+      listen(Shortcut.PREV_COMMENT_THREAD, _ => this.handlePrevCommentThread()),
+      listen(Shortcut.OPEN_REPLY_DIALOG, _ => this.handleOpenReplyDialog()),
+      listen(Shortcut.TOGGLE_LEFT_PANE, _ => this.handleToggleLeftPane()),
       listen(Shortcut.OPEN_DOWNLOAD_DIALOG, _ =>
-        this._handleOpenDownloadDialog()
+        this.handleOpenDownloadDialog()
       ),
-      listen(Shortcut.UP_TO_CHANGE, _ => this._handleUpToChange()),
-      listen(Shortcut.OPEN_DIFF_PREFS, _ => this._handleCommaKey()),
-      listen(Shortcut.TOGGLE_DIFF_MODE, _ => this._handleToggleDiffMode()),
+      listen(Shortcut.UP_TO_CHANGE, _ => this.handleUpToChange()),
+      listen(Shortcut.OPEN_DIFF_PREFS, _ => this.handleCommaKey()),
+      listen(Shortcut.TOGGLE_DIFF_MODE, _ => this.handleToggleDiffMode()),
       listen(Shortcut.TOGGLE_FILE_REVIEWED, e => {
         if (this._throttledToggleFileReviewed) {
           this._throttledToggleFileReviewed(e);
         }
       }),
       listen(Shortcut.TOGGLE_ALL_DIFF_CONTEXT, _ =>
-        this._handleToggleAllDiffContext()
+        this.handleToggleAllDiffContext()
       ),
       listen(Shortcut.NEXT_UNREVIEWED_FILE, _ =>
-        this._handleNextUnreviewedFile()
+        this.handleNextUnreviewedFile()
       ),
-      listen(Shortcut.TOGGLE_BLAME, _ => this._handleToggleBlame()),
+      listen(Shortcut.TOGGLE_BLAME, _ => this.handleToggleBlame()),
       listen(Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, _ =>
         this._handleToggleHideAllCommentThreads()
       ),
-      listen(Shortcut.OPEN_FILE_LIST, _ => this._handleOpenFileList()),
-      listen(Shortcut.DIFF_AGAINST_BASE, _ => this._handleDiffAgainstBase()),
-      listen(Shortcut.DIFF_AGAINST_LATEST, _ =>
-        this._handleDiffAgainstLatest()
-      ),
+      listen(Shortcut.OPEN_FILE_LIST, _ => this.handleOpenFileList()),
+      listen(Shortcut.DIFF_AGAINST_BASE, _ => this.handleDiffAgainstBase()),
+      listen(Shortcut.DIFF_AGAINST_LATEST, _ => this.handleDiffAgainstLatest()),
       listen(Shortcut.DIFF_BASE_AGAINST_LEFT, _ =>
-        this._handleDiffBaseAgainstLeft()
+        this.handleDiffBaseAgainstLeft()
       ),
       listen(Shortcut.DIFF_RIGHT_AGAINST_LATEST, _ =>
-        this._handleDiffRightAgainstLatest()
+        this.handleDiffRightAgainstLatest()
       ),
       listen(Shortcut.DIFF_BASE_AGAINST_LATEST, _ =>
-        this._handleDiffBaseAgainstLatest()
+        this.handleDiffBaseAgainstLatest()
       ),
       listen(Shortcut.EXPAND_ALL_COMMENT_THREADS, _ => {}), // docOnly
       listen(Shortcut.COLLAPSE_ALL_COMMENT_THREADS, _ => {}), // docOnly
@@ -385,7 +379,7 @@
     super.connectedCallback();
     this.connected$.next(true);
     this._throttledToggleFileReviewed = throttleWrap(_ =>
-      this._handleToggleFileReviewed()
+      this.handleToggleFileReviewed()
     );
     this._getLoggedIn().then(loggedIn => {
       this._loggedIn = loggedIn;
@@ -470,7 +464,7 @@
     this.subscriptions.push(
       this.getChangeModel().diffPath$.subscribe(path => (this._path = path))
     );
-    this.addEventListener('open-fix-preview', e => this._onOpenFixPreview(e));
+    this.addEventListener('open-fix-preview', e => this.onOpenFixPreview(e));
     this.cursor = new GrDiffCursor();
     this._onRenderHandler = (_: Event) => {
       // We have to wait until render because at the time of connectedCallback,
@@ -515,7 +509,7 @@
     const loggedIn = await this._getLoggedIn();
     if (!loggedIn) return;
     if (!diffPrefs.manual_review) {
-      this._setReviewed(true, patchNum);
+      this.setReviewed(true, patchNum);
     }
   }
 
@@ -600,17 +594,14 @@
       });
   }
 
-  _getPreferences() {
-    return this.restApiService.getPreferences();
-  }
-
   _handleReviewedChange(e: Event) {
-    this._setReviewed(
+    this.setReviewed(
       ((dom(e) as EventApi).rootTarget as HTMLInputElement).checked
     );
   }
 
-  _setReviewed(
+  // Private but used in tests.
+  setReviewed(
     reviewed: boolean,
     patchNum: RevisionPatchSetNum | undefined = this._patchRange?.patchNum
   ) {
@@ -627,16 +618,17 @@
     );
   }
 
-  _handleToggleFileReviewed() {
-    this._setReviewed(!this.$.reviewed.checked);
+  // Private but used in tests.
+  handleToggleFileReviewed() {
+    this.setReviewed(!this.$.reviewed.checked);
   }
 
-  _handlePrevLine() {
+  private handlePrevLine() {
     this.$.diffHost.displayLine = true;
     this.cursor?.moveUp();
   }
 
-  _onOpenFixPreview(e: OpenFixPreviewEvent) {
+  private onOpenFixPreview(e: OpenFixPreviewEvent) {
     this.$.applyFixDialog.open(e);
   }
 
@@ -660,12 +652,13 @@
     this._isImageDiff = e.detail.value;
   }
 
-  _handleNextLine() {
+  private handleNextLine() {
     this.$.diffHost.displayLine = true;
     this.cursor?.moveDown();
   }
 
-  _moveToPreviousFileWithComment() {
+  // Private but used in tests.
+  moveToPreviousFileWithComment() {
     if (!this._commentSkips) return;
     if (!this._change) return;
     if (!this._patchRange?.patchNum) return;
@@ -673,7 +666,7 @@
     // If there is no previous diff with comments, then return to the change
     // view.
     if (!this._commentSkips.previous) {
-      this._navToChangeView();
+      this.navToChangeView();
       return;
     }
 
@@ -685,14 +678,15 @@
     );
   }
 
-  _moveToNextFileWithComment() {
+  // Private but used in tests.
+  moveToNextFileWithComment() {
     if (!this._commentSkips) return;
     if (!this._change) return;
     if (!this._patchRange?.patchNum) return;
 
     // If there is no next diff with comments, then return to the change view.
     if (!this._commentSkips.next) {
-      this._navToChangeView();
+      this.navToChangeView();
       return;
     }
 
@@ -704,34 +698,34 @@
     );
   }
 
-  _handleNewComment() {
+  private handleNewComment() {
     this.classList.remove('hideComments');
     this.cursor?.createCommentInPlace();
   }
 
-  _handlePrevFile() {
+  private handlePrevFile() {
     if (!this._path) return;
     if (!this._fileList) return;
-    this._navToFile(this._path, this._fileList, -1);
+    this.navToFile(this._path, this._fileList, -1);
   }
 
-  _handleNextFile() {
+  private handleNextFile() {
     if (!this._path) return;
     if (!this._fileList) return;
-    this._navToFile(this._path, this._fileList, 1);
+    this.navToFile(this._path, this._fileList, 1);
   }
 
-  _handleNextChunk() {
+  private handleNextChunk() {
     const result = this.cursor?.moveToNextChunk();
     if (result === CursorMoveResult.CLIPPED && this.cursor?.isAtEnd()) {
       this.showToastAndNavigateFile('next', 'n');
     }
   }
 
-  _handleNextCommentThread() {
+  private handleNextCommentThread() {
     const result = this.cursor?.moveToNextCommentThread();
     if (result === CursorMoveResult.CLIPPED) {
-      this._navigateToNextFileWithCommentThread();
+      this.navigateToNextFileWithCommentThread();
     }
   }
 
@@ -770,36 +764,36 @@
       file => file === this._path || !this.reviewedFiles.has(file)
     );
 
-    this._navToFile(this._path, unreviewedFiles, direction === 'next' ? 1 : -1);
+    this.navToFile(this._path, unreviewedFiles, direction === 'next' ? 1 : -1);
   }
 
-  _handlePrevChunk() {
+  private handlePrevChunk() {
     this.cursor?.moveToPreviousChunk();
     if (this.cursor?.isAtStart()) {
       this.showToastAndNavigateFile('previous', 'p');
     }
   }
 
-  _handlePrevCommentThread() {
+  private handlePrevCommentThread() {
     this.cursor?.moveToPreviousCommentThread();
   }
 
-  // Similar to gr-change-view._handleOpenReplyDialog
-  _handleOpenReplyDialog() {
+  // Similar to gr-change-view.handleOpenReplyDialog
+  private handleOpenReplyDialog() {
     this._getLoggedIn().then(isLoggedIn => {
       if (!isLoggedIn) {
         fireEvent(this, 'show-auth-required');
         return;
       }
-      this._navToChangeView(true);
+      this.navToChangeView(true);
     });
   }
 
-  _handleToggleLeftPane() {
+  private handleToggleLeftPane() {
     this.$.diffHost.toggleLeftDiff();
   }
 
-  _handleOpenDownloadDialog() {
+  private handleOpenDownloadDialog() {
     this.$.downloadOverlay.open().then(() => {
       this.$.downloadOverlay.setFocusStops(
         this.$.downloadDialog.getFocusStops()
@@ -812,16 +806,17 @@
     this.$.downloadOverlay.close();
   }
 
-  _handleUpToChange() {
-    this._navToChangeView();
+  private handleUpToChange() {
+    this.navToChangeView();
   }
 
-  _handleCommaKey() {
+  private handleCommaKey() {
     if (!this._loggedIn) return;
     this.$.diffPreferencesDialog.open();
   }
 
-  _handleToggleDiffMode() {
+  // Private but used in tests.
+  handleToggleDiffMode() {
     if (!this._userPrefs) return;
     if (this._userPrefs.diff_view === DiffViewMode.SIDE_BY_SIDE) {
       this.userModel.updatePreferences({diff_view: DiffViewMode.UNIFIED});
@@ -832,7 +827,8 @@
     }
   }
 
-  _navToChangeView(openReplyDialog = false) {
+  // Private but used in tests.
+  navToChangeView(openReplyDialog = false) {
     if (!this._changeNum || !this._patchRange?.patchNum) {
       return;
     }
@@ -844,13 +840,14 @@
     );
   }
 
-  _navToFile(
+  // Private but used in tests.
+  navToFile(
     path: string,
     fileList: string[],
     direction: -1 | 1,
     navigateToFirstComment?: boolean
   ) {
-    const newPath = this._getNavLinkPath(path, fileList, direction);
+    const newPath = this.getNavLinkPath(path, fileList, direction);
     if (!newPath) return;
     if (!this._change) return;
     if (!this._patchRange) return;
@@ -899,7 +896,7 @@
     if (!fileList) return null;
     if (!direction) return null;
 
-    const newPath = this._getNavLinkPath(path, fileList, direction);
+    const newPath = this.getNavLinkPath(path, fileList, direction);
     if (!newPath) {
       return null;
     }
@@ -945,7 +942,7 @@
    * patch range.
    * @param direction Either 1 (next file) or -1 (prev file).
    */
-  _getNavLinkPath(path: string, fileList: string[], direction: -1 | 1) {
+  private getNavLinkPath(path: string, fileList: string[], direction: -1 | 1) {
     if (!path || !fileList || fileList.length === 0) {
       return null;
     }
@@ -966,12 +963,14 @@
     return {path: fileList[idx]};
   }
 
-  _initLineOfInterestAndCursor(leftSide: boolean) {
+  // Private but used in tests.
+  initLineOfInterestAndCursor(leftSide: boolean) {
     this.$.diffHost.lineOfInterest = this._getLineOfInterest(leftSide);
-    this._initCursor(leftSide);
+    this.initCursor(leftSide);
   }
 
-  _displayDiffBaseAgainstLeftToast() {
+  // Private but used in tests.
+  displayDiffBaseAgainstLeftToast() {
     if (!this._patchRange) return;
     fireAlert(
       this,
@@ -981,7 +980,7 @@
     );
   }
 
-  _displayDiffAgainstLatestToast(latestPatchNum?: PatchSetNum) {
+  private displayDiffAgainstLatestToast(latestPatchNum?: PatchSetNum) {
     if (!this._patchRange) return;
     const leftPatchset =
       this._patchRange.basePatchNum === PARENT
@@ -995,20 +994,20 @@
     );
   }
 
-  _displayToasts() {
+  private displayToasts() {
     if (!this._patchRange) return;
     if (this._patchRange.basePatchNum !== PARENT) {
-      this._displayDiffBaseAgainstLeftToast();
+      this.displayDiffBaseAgainstLeftToast();
       return;
     }
     const latestPatchNum = computeLatestPatchNum(this._allPatchSets);
     if (this._patchRange.patchNum !== latestPatchNum) {
-      this._displayDiffAgainstLatestToast(latestPatchNum);
+      this.displayDiffAgainstLatestToast(latestPatchNum);
       return;
     }
   }
 
-  _initCommitRange() {
+  private initCommitRange() {
     let commit: CommitId | undefined;
     let baseCommit: CommitId | undefined;
     if (!this._change) return;
@@ -1030,7 +1029,7 @@
     this._commitRange = commit && baseCommit ? {commit, baseCommit} : undefined;
   }
 
-  _updateUrlToDiffUrl(lineNum?: number, leftSide?: boolean) {
+  private updateUrlToDiffUrl(lineNum?: number, leftSide?: boolean) {
     if (!this._change) return;
     if (!this._patchRange) return;
     if (!this._changeNum) return;
@@ -1047,7 +1046,8 @@
     history.replaceState(null, '', url);
   }
 
-  _initPatchRange() {
+  // Private but used in tests.
+  initPatchRange() {
     let leftSide = false;
     if (!this._change) return;
     if (this.params?.view !== GerritView.DIFF) return;
@@ -1084,17 +1084,18 @@
       }
     }
     assertIsDefined(this._patchRange, '_patchRange');
-    this._initLineOfInterestAndCursor(leftSide);
+    this.initLineOfInterestAndCursor(leftSide);
 
     if (this.params?.commentId) {
       // url is of type /comment/{commentId} which isn't meaningful
-      this._updateUrlToDiffUrl(this._focusLineNum, leftSide);
+      this.updateUrlToDiffUrl(this._focusLineNum, leftSide);
     }
 
     this._commentMap = this._getPaths(this._patchRange);
   }
 
-  _isFileUnchanged(diff?: DiffInfo) {
+  // Private but used in tests.
+  isFileUnchanged(diff?: DiffInfo) {
     if (!diff || !diff.content) return false;
     return !diff.content.some(
       content =>
@@ -1124,7 +1125,8 @@
     );
   }
 
-  _paramsChanged(value: AppElementParams) {
+  // Private but used in tests.
+  paramsChanged(value: AppElementParams) {
     if (value.view !== GerritView.DIFF) {
       return;
     }
@@ -1191,15 +1193,15 @@
     return Promise.all(promises)
       .then(() => {
         this._loading = false;
-        this._initPatchRange();
-        this._initCommitRange();
+        this.initPatchRange();
+        this.initCommitRange();
         return this.$.diffHost.reload(true);
       })
       .then(() => {
         this.reporting.diffViewDisplayed();
       })
       .then(() => {
-        const fileUnchanged = this._isFileUnchanged(this._diff);
+        const fileUnchanged = this.isFileUnchanged(this._diff);
         if (fileUnchanged && value.commentLink) {
           assertIsDefined(this._change, '_change');
           assertIsDefined(this._path, '_path');
@@ -1228,11 +1230,11 @@
           return;
         }
         if (value.commentLink) {
-          this._displayToasts();
+          this.displayToasts();
         }
         // If the blame was loaded for a previous file and user navigates to
         // another file, then we load the blame for this file too
-        if (this._isBlameLoaded) this._loadBlame();
+        if (this._isBlameLoaded) this.loadBlame();
       });
   }
 
@@ -1243,8 +1245,9 @@
 
   /**
    * If the params specify a diff address then configure the diff cursor.
+   * Private but used in tests.
    */
-  _initCursor(leftSide: boolean) {
+  initCursor(leftSide: boolean) {
     if (this._focusLineNum === undefined) {
       return;
     }
@@ -1436,7 +1439,7 @@
   ) {
     // for on-comment-anchor-tap side can be PARENT/REVISIONS
     // for on-line-selected side can be left/right
-    this._updateUrlToDiffUrl(
+    this.updateUrlToDiffUrl(
       detail.number,
       detail.side === Side.LEFT || detail.side === CommentSide.PARENT
     );
@@ -1620,7 +1623,8 @@
     return loaded && !loading ? 'Hide blame' : 'Show blame';
   }
 
-  _loadBlame() {
+  // Private but used in tests.
+  loadBlame() {
     this._isBlameLoading = true;
     fireAlert(this, LOADING_BLAME);
     this.$.diffHost
@@ -1643,10 +1647,10 @@
       this.$.diffHost.clearBlame();
       return;
     }
-    this._loadBlame();
+    this.loadBlame();
   }
 
-  _handleToggleBlame() {
+  private handleToggleBlame() {
     this._toggleBlame();
   }
 
@@ -1654,11 +1658,12 @@
     toggleClass(this, 'hideComments');
   }
 
-  _handleOpenFileList() {
+  private handleOpenFileList() {
     this.$.dropdown.open();
   }
 
-  _handleDiffAgainstBase() {
+  // Private but used in tests.
+  handleDiffAgainstBase() {
     if (!this._change) return;
     if (!this._path) return;
     if (!this._patchRange) return;
@@ -1674,7 +1679,8 @@
     );
   }
 
-  _handleDiffBaseAgainstLeft() {
+  // Private but used in tests.
+  handleDiffBaseAgainstLeft() {
     if (!this._change) return;
     if (!this._path) return;
     if (!this._patchRange) return;
@@ -1694,7 +1700,8 @@
     );
   }
 
-  _handleDiffAgainstLatest() {
+  // Private but used in tests.
+  handleDiffAgainstLatest() {
     if (!this._change) return;
     if (!this._path) return;
     if (!this._patchRange) return;
@@ -1713,7 +1720,8 @@
     );
   }
 
-  _handleDiffRightAgainstLatest() {
+  // Private but used in tests.
+  handleDiffRightAgainstLatest() {
     if (!this._change) return;
     if (!this._path) return;
     if (!this._patchRange) return;
@@ -1731,7 +1739,8 @@
     );
   }
 
-  _handleDiffBaseAgainstLatest() {
+  // Private but used in tests.
+  handleDiffBaseAgainstLatest() {
     if (!this._change) return;
     if (!this._path) return;
     if (!this._patchRange) return;
@@ -1768,16 +1777,16 @@
     return '';
   }
 
-  _handleToggleAllDiffContext() {
+  private handleToggleAllDiffContext() {
     this.$.diffHost.toggleAllContext();
   }
 
-  _handleNextUnreviewedFile() {
-    this._setReviewed(true);
+  private handleNextUnreviewedFile() {
+    this.setReviewed(true);
     this.navigateToUnreviewedFile('next');
   }
 
-  _navigateToNextFileWithCommentThread() {
+  private navigateToNextFileWithCommentThread() {
     if (!this._path) return;
     if (!this._fileList) return;
     if (!this._patchRange) return;
@@ -1788,7 +1797,7 @@
     const filesWithComments = this._fileList.filter(
       file => file === this._path || hasComment(file)
     );
-    this._navToFile(this._path, filesWithComments, 1, true);
+    this.navToFile(this._path, filesWithComments, 1, true);
   }
 
   _handleReloadingDiffPreference() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
index 32a8d1e..db17f54 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
@@ -154,9 +154,9 @@
     test('params change triggers diffViewDisplayed()', () => {
       const diffViewDisplayedStub = stubReporting('diffViewDisplayed');
       sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
-      sinon.stub(element, '_initPatchRange');
+      sinon.stub(element, 'initPatchRange');
       sinon.stub(element, '_getFiles');
-      const paramsChangedSpy = sinon.spy(element, '_paramsChanged');
+      const paramsChangedSpy = sinon.spy(element, 'paramsChanged');
       element.params = {
         view: GerritNav.View.DIFF,
         changeNum: 42 as NumericChangeId,
@@ -179,14 +179,14 @@
       setup(() => {
         initLineOfInterestAndCursorStub = sinon.stub(
           element,
-          '_initLineOfInterestAndCursor'
+          'initLineOfInterestAndCursor'
         );
         getUrlStub = sinon.stub(GerritNav, 'getUrlForDiffById');
         replaceStateStub = sinon.stub(history, 'replaceState');
         sinon.stub(element, '_getFiles');
         stubReporting('diffViewDisplayed');
         sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
-        paramsChangedSpy = sinon.spy(element, '_paramsChanged');
+        paramsChangedSpy = sinon.spy(element, 'paramsChanged');
         element.getChangeModel().setState({
           change: {
             ...createParsedChange(),
@@ -252,10 +252,10 @@
       // Blame loads for subsequent files if it was loaded for one file
       element._isBlameLoaded = true;
       stubReporting('diffViewDisplayed');
-      const loadBlameStub = sinon.stub(element, '_loadBlame');
+      const loadBlameStub = sinon.stub(element, 'loadBlame');
       sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
-      const paramsChangedSpy = sinon.spy(element, '_paramsChanged');
-      sinon.stub(element, '_initPatchRange');
+      const paramsChangedSpy = sinon.spy(element, 'paramsChanged');
+      sinon.stub(element, 'initPatchRange');
       sinon.stub(element, '_getFiles');
       element.params = {
         view: GerritNav.View.DIFF,
@@ -288,10 +288,10 @@
         discardedDrafts: [],
       });
       stubReporting('diffViewDisplayed');
-      sinon.stub(element, '_loadBlame');
+      sinon.stub(element, 'loadBlame');
       sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
-      sinon.stub(element, '_isFileUnchanged').returns(true);
-      const paramsChangedSpy = sinon.spy(element, '_paramsChanged');
+      sinon.stub(element, 'isFileUnchanged').returns(true);
+      const paramsChangedSpy = sinon.spy(element, 'paramsChanged');
       element.getChangeModel().setState({
         change: {
           ...createParsedChange(),
@@ -339,10 +339,10 @@
         discardedDrafts: [],
       });
       stubReporting('diffViewDisplayed');
-      sinon.stub(element, '_loadBlame');
+      sinon.stub(element, 'loadBlame');
       sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
-      sinon.stub(element, '_isFileUnchanged').returns(true);
-      const paramsChangedSpy = sinon.spy(element, '_paramsChanged');
+      sinon.stub(element, 'isFileUnchanged').returns(true);
+      const paramsChangedSpy = sinon.spy(element, 'paramsChanged');
       element.getChangeModel().setState({
         change: {
           ...createParsedChange(),
@@ -366,7 +366,7 @@
       });
     });
 
-    test('_isFileUnchanged', () => {
+    test('isFileUnchanged', () => {
       let diff: DiffInfo = {
         ...createDiff(),
         content: [
@@ -374,12 +374,12 @@
           {b: ['ancd'], a: ['xx']},
         ],
       };
-      assert.equal(element._isFileUnchanged(diff), false);
+      assert.equal(element.isFileUnchanged(diff), false);
       diff = {
         ...createDiff(),
         content: [{ab: ['abcd']}, {ab: ['ancd']}],
       };
-      assert.equal(element._isFileUnchanged(diff), true);
+      assert.equal(element.isFileUnchanged(diff), true);
       diff = {
         ...createDiff(),
         content: [
@@ -387,7 +387,7 @@
           {b: ['ancd'], ab: ['xx']},
         ],
       };
-      assert.equal(element._isFileUnchanged(diff), false);
+      assert.equal(element.isFileUnchanged(diff), false);
       diff = {
         ...createDiff(),
         content: [
@@ -395,7 +395,7 @@
           {b: ['ancd'], ab: ['xx'], common: true},
         ],
       };
-      assert.equal(element._isFileUnchanged(diff), true);
+      assert.equal(element.isFileUnchanged(diff), true);
     });
 
     test('diff toast to go to latest is shown and not base', async () => {
@@ -414,9 +414,9 @@
       });
 
       stubReporting('diffViewDisplayed');
-      sinon.stub(element, '_loadBlame');
+      sinon.stub(element, 'loadBlame');
       sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
-      const paramsChangedSpy = sinon.spy(element, '_paramsChanged');
+      const paramsChangedSpy = sinon.spy(element, 'paramsChanged');
       element._change = undefined;
       element.getChangeModel().setState({
         change: {
@@ -429,8 +429,8 @@
         patchNum: 2 as RevisionPatchSetNum,
         basePatchNum: 1 as BasePatchSetNum,
       };
-      sinon.stub(element, '_isFileUnchanged').returns(false);
-      const toastStub = sinon.stub(element, '_displayDiffBaseAgainstLeftToast');
+      sinon.stub(element, 'isFileUnchanged').returns(false);
+      const toastStub = sinon.stub(element, 'displayDiffBaseAgainstLeftToast');
       element.params = {
         view: GerritNav.View.DIFF,
         changeNum: 42 as NumericChangeId,
@@ -594,10 +594,10 @@
         )
       );
 
-      // Note that stubbing _setReviewed means that the value of the
+      // Note that stubbing setReviewed means that the value of the
       // `element.$.reviewed` checkbox is not flipped.
-      const setReviewedStub = sinon.stub(element, '_setReviewed');
-      const handleToggleSpy = sinon.spy(element, '_handleToggleFileReviewed');
+      const setReviewedStub = sinon.stub(element, 'setReviewed');
+      const handleToggleSpy = sinon.spy(element, 'handleToggleFileReviewed');
       element.$.reviewed.checked = false;
       assert.isFalse(handleToggleSpy.called);
       assert.isFalse(setReviewedStub.called);
@@ -682,7 +682,7 @@
         patchNum: 10 as RevisionPatchSetNum,
       };
       const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
-      element._handleDiffAgainstBase();
+      element.handleDiffAgainstBase();
       const args = diffNavStub.getCall(0).args;
       assert.equal(args[2], 10 as RevisionPatchSetNum);
       assert.isNotOk(args[3]);
@@ -698,13 +698,13 @@
         patchNum: 10 as RevisionPatchSetNum,
       };
       const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
-      element._handleDiffAgainstLatest();
+      element.handleDiffAgainstLatest();
       const args = diffNavStub.getCall(0).args;
       assert.equal(args[2], 12 as RevisionPatchSetNum);
       assert.equal(args[3], 5 as BasePatchSetNum);
     });
 
-    test('_handleDiffBaseAgainstLeft', () => {
+    test('handleDiffBaseAgainstLeft', () => {
       element._change = {
         ...createParsedChange(),
         revisions: createRevisions(10),
@@ -718,7 +718,7 @@
         dashboard: 'id' as DashboardId,
       };
       const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
-      element._handleDiffBaseAgainstLeft();
+      element.handleDiffBaseAgainstLeft();
       assert(diffNavStub.called);
       const args = diffNavStub.getCall(0).args;
       assert.equal(args[2], 1 as RevisionPatchSetNum);
@@ -726,7 +726,7 @@
       assert.isNotOk(args[4]);
     });
 
-    test('_handleDiffBaseAgainstLeft when initially navigating to a comment', () => {
+    test('handleDiffBaseAgainstLeft when initially navigating to a comment', () => {
       element._change = {
         ...createParsedChange(),
         revisions: createRevisions(10),
@@ -735,7 +735,7 @@
         patchNum: 3 as RevisionPatchSetNum,
         basePatchNum: 1 as BasePatchSetNum,
       };
-      sinon.stub(element, '_paramsChanged');
+      sinon.stub(element, 'paramsChanged');
       element.params = {
         commentLink: true,
         view: GerritView.DIFF,
@@ -743,7 +743,7 @@
       };
       element._focusLineNum = 10;
       const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
-      element._handleDiffBaseAgainstLeft();
+      element.handleDiffBaseAgainstLeft();
       assert(diffNavStub.called);
       const args = diffNavStub.getCall(0).args;
       assert.equal(args[2], 1 as RevisionPatchSetNum);
@@ -751,7 +751,7 @@
       assert.equal(args[4], 10);
     });
 
-    test('_handleDiffRightAgainstLatest', () => {
+    test('handleDiffRightAgainstLatest', () => {
       element._change = {
         ...createParsedChange(),
         revisions: createRevisions(10),
@@ -761,14 +761,14 @@
         patchNum: 3 as RevisionPatchSetNum,
       };
       const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
-      element._handleDiffRightAgainstLatest();
+      element.handleDiffRightAgainstLatest();
       assert(diffNavStub.called);
       const args = diffNavStub.getCall(0).args;
       assert.equal(args[2], 10 as RevisionPatchSetNum);
       assert.equal(args[3], 3 as BasePatchSetNum);
     });
 
-    test('_handleDiffBaseAgainstLatest', () => {
+    test('handleDiffBaseAgainstLatest', () => {
       element._change = {
         ...createParsedChange(),
         revisions: createRevisions(10),
@@ -778,7 +778,7 @@
         patchNum: 3 as RevisionPatchSetNum,
       };
       const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
-      element._handleDiffBaseAgainstLatest();
+      element.handleDiffBaseAgainstLatest();
       assert(diffNavStub.called);
       const args = diffNavStub.getCall(0).args;
       assert.equal(args[2], 10 as RevisionPatchSetNum);
@@ -1609,13 +1609,13 @@
       flush();
 
       assert.isTrue(element._editMode);
-      element._setReviewed(true);
+      element.setReviewed(true);
       assert.isFalse(saveReviewedStub.called);
     });
 
     test('hash is determined from params', async () => {
       sinon.stub(element.$.diffHost, 'reload');
-      const initLineStub = sinon.stub(element, '_initLineOfInterestAndCursor');
+      const initLineStub = sinon.stub(element, 'initLineOfInterestAndCursor');
 
       element._loggedIn = true;
       element.params = {
@@ -1650,7 +1650,7 @@
       assert.equal(select.mode, diffDisplay.viewMode);
 
       // We will simulate a user change of the selected mode.
-      element._handleToggleDiffMode();
+      element.handleToggleDiffMode();
       assert.isTrue(
         userStub.calledWithExactly({
           diff_view: DiffViewMode.UNIFIED,
@@ -1696,7 +1696,7 @@
       };
       setup(async () => {
         sinon.stub(element.$.diffHost, 'reload');
-        sinon.stub(element, '_initCursor');
+        sinon.stub(element, 'initCursor');
         element._change = change;
         await flush();
         await element.$.diffHost.updateComplete;
@@ -1736,34 +1736,34 @@
       });
     });
 
-    test('_initCursor', () => {
+    test('initCursor', () => {
       assertIsDefined(element.cursor);
       assert.isNotOk(element.cursor.initialLineNumber);
 
       // Does nothing when params specify no cursor address:
-      element._initCursor(false);
+      element.initCursor(false);
       assert.isNotOk(element.cursor.initialLineNumber);
 
       // Does nothing when params specify side but no number:
-      element._initCursor(true);
+      element.initCursor(true);
       assert.isNotOk(element.cursor.initialLineNumber);
 
       // Revision hash: specifies lineNum but not side.
 
       element._focusLineNum = 234;
-      element._initCursor(false);
+      element.initCursor(false);
       assert.equal(element.cursor.initialLineNumber, 234);
       assert.equal(element.cursor.side, Side.RIGHT);
 
       // Base hash: specifies lineNum and side.
       element._focusLineNum = 345;
-      element._initCursor(true);
+      element.initCursor(true);
       assert.equal(element.cursor.initialLineNumber, 345);
       assert.equal(element.cursor.side, Side.LEFT);
 
       // Specifies right side:
       element._focusLineNum = 123;
-      element._initCursor(false);
+      element.initCursor(false);
       assert.equal(element.cursor.initialLineNumber, 123);
       assert.equal(element.cursor.side, Side.RIGHT);
     });
@@ -1839,14 +1839,14 @@
       assert.isTrue(getUrlStub.lastCall.args[6]);
     });
 
-    test('_handleToggleDiffMode', () => {
+    test('handleToggleDiffMode', () => {
       const userStub = stubUsers('updatePreferences');
       element._userPrefs = {
         ...createDefaultPreferences(),
         diff_view: DiffViewMode.SIDE_BY_SIDE,
       };
 
-      element._handleToggleDiffMode();
+      element.handleToggleDiffMode();
       assert.deepEqual(userStub.lastCall.args[0], {
         diff_view: DiffViewMode.UNIFIED,
       });
@@ -1856,13 +1856,13 @@
         diff_view: DiffViewMode.UNIFIED,
       };
 
-      element._handleToggleDiffMode();
+      element.handleToggleDiffMode();
       assert.deepEqual(userStub.lastCall.args[0], {
         diff_view: DiffViewMode.SIDE_BY_SIDE,
       });
     });
 
-    suite('_initPatchRange', () => {
+    suite('initPatchRange', () => {
       setup(async () => {
         getDiffRestApiStub.returns(Promise.resolve(createDiff()));
         element.params = {
@@ -1875,7 +1875,7 @@
       });
       test('empty', () => {
         sinon.stub(element, '_getPaths').returns({});
-        element._initPatchRange();
+        element.initPatchRange();
         assert.equal(Object.keys(element._commentMap ?? {}).length, 0);
       });
 
@@ -1890,7 +1890,7 @@
           basePatchNum: 3 as BasePatchSetNum,
           patchNum: 5 as RevisionPatchSetNum,
         };
-        element._initPatchRange();
+        element.initPatchRange();
         assert.deepEqual(Object.keys(element._commentMap ?? {}), [
           'path/to/file/one.cpp',
           'path-to/file/two.py',
@@ -1951,7 +1951,7 @@
         let navToDiffStub: SinonStub;
 
         setup(() => {
-          navToChangeStub = sinon.stub(element, '_navToChangeView');
+          navToChangeStub = sinon.stub(element, 'navToChangeView');
           navToDiffStub = sinon.stub(GerritNav, 'navigateToDiff');
           element._files = getFilesFromFileList([
             'path/one.jpg',
@@ -1964,9 +1964,9 @@
           };
         });
 
-        suite('_moveToPreviousFileWithComment', () => {
+        suite('moveToPreviousFileWithComment', () => {
           test('no skips', () => {
-            element._moveToPreviousFileWithComment();
+            element.moveToPreviousFileWithComment();
             assert.isFalse(navToChangeStub.called);
             assert.isFalse(navToDiffStub.called);
           });
@@ -1979,7 +1979,7 @@
             element._commentMap = commentMap;
             element._path = element._fileList![1]!;
 
-            element._moveToPreviousFileWithComment();
+            element.moveToPreviousFileWithComment();
             assert.isTrue(navToChangeStub.calledOnce);
             assert.isFalse(navToDiffStub.called);
           });
@@ -1992,15 +1992,15 @@
             element._commentMap = commentMap;
             element._path = element._fileList![1]!;
 
-            element._moveToPreviousFileWithComment();
+            element.moveToPreviousFileWithComment();
             assert.isFalse(navToChangeStub.called);
             assert.isTrue(navToDiffStub.calledOnce);
           });
         });
 
-        suite('_moveToNextFileWithComment', () => {
+        suite('moveToNextFileWithComment', () => {
           test('no skips', () => {
-            element._moveToNextFileWithComment();
+            element.moveToNextFileWithComment();
             assert.isFalse(navToChangeStub.called);
             assert.isFalse(navToDiffStub.called);
           });
@@ -2013,7 +2013,7 @@
             element._commentMap = commentMap;
             element._path = element._fileList![1];
 
-            element._moveToNextFileWithComment();
+            element.moveToNextFileWithComment();
             assert.isTrue(navToChangeStub.calledOnce);
             assert.isFalse(navToDiffStub.called);
           });
@@ -2026,7 +2026,7 @@
             element._commentMap = commentMap;
             element._path = element._fileList![1];
 
-            element._moveToNextFileWithComment();
+            element.moveToNextFileWithComment();
             assert.isFalse(navToChangeStub.called);
             assert.isTrue(navToDiffStub.calledOnce);
           });
@@ -2131,7 +2131,7 @@
 
       setup(() => {
         dispatchEventStub = sinon.stub(element, 'dispatchEvent').callThrough();
-        navToFileStub = sinon.stub(element, '_navToFile');
+        navToFileStub = sinon.stub(element, 'navToFile');
         assertIsDefined(element.cursor);
         moveToPreviousChunkStub = sinon.stub(
           element.cursor,
@@ -2252,8 +2252,8 @@
       element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
       element.reviewedFiles = new Set(['file1', 'file2']);
       element._path = 'file1';
-      const reviewedStub = sinon.stub(element, '_setReviewed');
-      const navStub = sinon.stub(element, '_navToFile');
+      const reviewedStub = sinon.stub(element, 'setReviewed');
+      const navStub = sinon.stub(element, 'navToFile');
       MockInteractions.pressAndReleaseKeyOn(element, 77, null, 'M');
       flush();
 
@@ -2263,7 +2263,7 @@
 
     test('File change should trigger navigateToDiff once', async () => {
       element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
-      sinon.stub(element, '_initLineOfInterestAndCursor');
+      sinon.stub(element, 'initLineOfInterestAndCursor');
       const navigateToDiffStub = sinon.stub(GerritNav, 'navigateToDiff');
 
       // Load file1
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
index 9cb80db..8dfc16e 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
@@ -78,6 +78,12 @@
   @property({type: Object})
   query: AutocompleteQuery = () => Promise.resolve([]);
 
+  @query('#input')
+  input?: PaperInputElementExt;
+
+  @query('#autocomplete')
+  grAutocomplete?: GrAutocomplete;
+
   static override get styles() {
     return [
       sharedStyles,
@@ -152,8 +158,9 @@
         .verticalAlign=${'auto'}
         .horizontalAlign=${'auto'}
         .verticalOffset=${this.verticalOffset}
-        allowOutsideScroll
-        @iron-overlay-canceled=${this.cancel}
+        .allowOutsideScroll=${true}
+        .noCancelOnEscKey=${true}
+        .noCancelOnOutsideClick=${true}
       >
         <div class="dropdown-content" slot="dropdown-content">
           <div class="inputContainer" part="input-container">
@@ -200,6 +207,7 @@
         .text=${this.inputText}
         .query=${this.query}
         @commit=${this.handleCommit}
+        @cancel=${this.cancel}
         @text-changed=${(e: CustomEvent) => {
           this.inputText = e.detail.value;
         }}
@@ -256,9 +264,8 @@
     if (this.readOnly || this.editing) return;
     return this.openDropdown().then(() => {
       this.nativeInput.focus();
-      const input = this.getInput();
-      if (!input?.value) return;
-      this.nativeInput.setSelectionRange(0, input.value.length);
+      if (!this.input?.value) return;
+      this.nativeInput.setSelectionRange(0, this.input.value.length);
     });
   }
 
@@ -301,9 +308,8 @@
       return;
     }
     this.dropdown?.close();
-    const input = this.getInput();
-    if (input) {
-      this.value = input.value ?? undefined;
+    if (this.input) {
+      this.value = this.input.value ?? undefined;
     } else {
       this.value = this.inputText || '';
     }
@@ -327,14 +333,13 @@
   }
 
   private get nativeInput(): HTMLInputElement {
-    return (this.getInput()?.$.nativeInput ||
-      this.getInput()?.inputElement ||
-      this.getGrAutocomplete()) as HTMLInputElement;
+    return (this.input?.$.nativeInput ||
+      this.input?.inputElement ||
+      this.grAutocomplete) as HTMLInputElement;
   }
 
   private handleEnter(event: KeyboardEvent) {
-    const grAutocomplete = this.getGrAutocomplete();
-    if (event.composedPath().some(el => el === grAutocomplete)) {
+    if (event.composedPath().some(el => el === this.grAutocomplete)) {
       return;
     }
     const inputContainer = queryAndAssert(this, '.inputContainer');
@@ -347,17 +352,20 @@
   }
 
   private handleEsc(event: KeyboardEvent) {
-    const inputContainer = queryAndAssert(this, '.inputContainer');
-    const isEventFromInput = event
-      .composedPath()
-      .some(element => element === inputContainer);
-    if (isEventFromInput) {
-      this.cancel();
+    // If autocomplete is used, it's handling the ESC instead.
+    if (!this.autocomplete) {
+      const inputContainer = queryAndAssert(this, '.inputContainer');
+      const isEventFromInput = event
+        .composedPath()
+        .some(element => element === inputContainer);
+      if (isEventFromInput) {
+        this.cancel();
+      }
     }
   }
 
   private handleCommit() {
-    this.getInput()?.focus();
+    this.input?.focus();
   }
 
   private computeLabelClass() {
@@ -371,12 +379,4 @@
     }
     return classes.join(' ');
   }
-
-  getInput(): PaperInputElementExt | null {
-    return this.shadowRoot!.querySelector<PaperInputElementExt>('#input');
-  }
-
-  getGrAutocomplete(): GrAutocomplete | null {
-    return this.shadowRoot!.querySelector<GrAutocomplete>('#autocomplete');
-  }
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts
index f09e2d2..85c670d 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts
@@ -47,7 +47,6 @@
       value text
     </label>
     <iron-dropdown
-      allowoutsidescroll=""
       aria-disabled="false"
       aria-hidden="true"
       horizontal-align="auto"
diff --git a/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status.ts b/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status.ts
index ab30138..33bbaa0 100644
--- a/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status.ts
+++ b/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status.ts
@@ -10,12 +10,34 @@
 import '@polymer/iron-icon/iron-icon';
 import {iconStyles} from '../../../styles/gr-icon-styles';
 
+function statusString(status: FileInfoStatus) {
+  if (!status) return '';
+  switch (status) {
+    case FileInfoStatus.ADDED:
+      return 'Added';
+    case FileInfoStatus.COPIED:
+      return 'Copied';
+    case FileInfoStatus.DELETED:
+      return 'Deleted';
+    case FileInfoStatus.MODIFIED:
+      return 'Modified';
+    case FileInfoStatus.RENAMED:
+      return 'Renamed';
+    case FileInfoStatus.REWRITTEN:
+      return 'Rewritten';
+    case FileInfoStatus.UNMODIFIED:
+      return 'Unchanged';
+    default:
+      assertNever(status, `Unsupported status: ${status}`);
+  }
+}
+
 /**
  * This is a square colored box with a single letter, which can be used as a
  * prefix column before file names.
  *
  * It can also show an additional "new" icon for indicating that a file was
- * newly added in a patchset.
+ * newly changed in a patchset.
  *
  * `gr-file-status-chip` was a chip for being shown after a file name. It will
  * become obsolete.
@@ -26,18 +48,25 @@
   status?: FileInfoStatus;
 
   /**
-   * Show an additional "new" icon for indicating that a file was newly added
+   * Show an additional "new" icon for indicating that a file was newly changed
    * in a patchset.
    */
   @property({type: Boolean})
-  new = false;
+  newlyChanged = false;
+
+  /**
+   * What postfix should the tooltip have? For example you can set
+   * ' in ps 5', such that the 'Added' tooltip becomes 'Added in ps 5'.
+   */
+  @property({type: String})
+  labelPostfix = '';
 
   static override get styles() {
     return [
       iconStyles,
       css`
         :host {
-          display: block;
+          display: inline-block;
         }
         iron-icon.new {
           margin-right: var(--spacing-xs);
@@ -70,51 +99,34 @@
   }
 
   override render() {
-    return html`${this.renderNew()}${this.renderStatus()}`;
+    return html`${this.renderNewlyChanged()}${this.renderStatus()}`;
   }
 
   private renderStatus() {
     const classes = ['status', this.status];
-    const label = this.computeLabel();
-    const text = this.status ?? ' ';
     return html`<div
       class=${classes.join(' ')}
       tabindex="0"
-      title=${label}
-      aria-label=${label}
+      title=${this.computeLabel()}
+      aria-label=${this.computeLabel()}
     >
-      ${text}
+      ${this.status ?? ''}
     </div>`;
   }
 
-  private renderNew() {
-    if (!this.new) return;
+  private renderNewlyChanged() {
+    if (!this.newlyChanged) return;
     return html`<iron-icon
       class="size-16 new"
       icon="gr-icons:new"
+      title=${this.computeLabel()}
     ></iron-icon>`;
   }
 
   private computeLabel() {
     if (!this.status) return '';
-    switch (this.status) {
-      case FileInfoStatus.ADDED:
-        return 'Added';
-      case FileInfoStatus.COPIED:
-        return 'Copied';
-      case FileInfoStatus.DELETED:
-        return 'Deleted';
-      case FileInfoStatus.MODIFIED:
-        return 'Modified';
-      case FileInfoStatus.RENAMED:
-        return 'Renamed';
-      case FileInfoStatus.REWRITTEN:
-        return 'Rewritten';
-      case FileInfoStatus.UNMODIFIED:
-        return 'Unchanged';
-      default:
-        assertNever(this.status, `Unsupported status: ${this.status}`);
-    }
+    const prefix = this.newlyChanged ? 'Newly ' : '';
+    return `${prefix}${statusString(this.status)}${this.labelPostfix}`;
   }
 }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status_test.ts b/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status_test.ts
index 614b97c8..682f4e9 100644
--- a/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status_test.ts
@@ -19,7 +19,7 @@
 
   const setStatus = async (status?: FileInfoStatus, newly = false) => {
     element.status = status;
-    element.new = newly;
+    element.newlyChanged = newly;
     await element.updateComplete;
   };
 
@@ -42,8 +42,17 @@
     test('newly added', async () => {
       await setStatus(FileInfoStatus.ADDED, true);
       expect(element).shadowDom.to.equal(/* HTML */ `
-        <iron-icon class="new size-16" icon="gr-icons:new"></iron-icon>
-        <div class="A status" aria-label="Added" tabindex="0" title="Added">
+        <iron-icon
+          class="new size-16"
+          icon="gr-icons:new"
+          title="Newly Added"
+        ></iron-icon>
+        <div
+          class="A status"
+          aria-label="Newly Added"
+          tabindex="0"
+          title="Newly Added"
+        >
           A
         </div>
       `);
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
index fc38332..3176f61 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
@@ -166,7 +166,7 @@
       <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons:new_releases -->
       <g id="new"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M23 12l-2.44-2.78.34-3.68-3.61-.82-1.89-3.18L12 3 8.6 1.54 6.71 4.72l-3.61.81.34 3.68L1 12l2.44 2.78-.34 3.69 3.61.82 1.89 3.18L12 21l3.4 1.46 1.89-3.18 3.61-.82-.34-3.68L23 12zm-4.51 2.11l.26 2.79-2.74.62-1.43 2.41L12 18.82l-2.58 1.11-1.43-2.41-2.74-.62.26-2.8L3.66 12l1.85-2.12-.26-2.78 2.74-.61 1.43-2.41L12 5.18l2.58-1.11 1.43 2.41 2.74.62-.26 2.79L20.34 12l-1.85 2.11zM11 15h2v2h-2zm0-8h2v6h-2z"/></g>
       <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons:arrow_right_alt -->
-      <g id="arrow_right"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M14 6l-1.41 1.41L16.17 11H4v2h12.17l-3.58 3.59L14 18l6-6z"/></g>
+      <g id="arrow-right"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M14 6l-1.41 1.41L16.17 11H4v2h12.17l-3.58 3.59L14 18l6-6z"/></g>
     </defs>
   </svg>
 </iron-iconset-svg>`;
diff --git a/polygerrit-ui/app/models/change/files-model.ts b/polygerrit-ui/app/models/change/files-model.ts
index 139a5a9..e26167c 100644
--- a/polygerrit-ui/app/models/change/files-model.ts
+++ b/polygerrit-ui/app/models/change/files-model.ts
@@ -121,6 +121,16 @@
     ([files, commentedPaths]) => addUnmodified(files, commentedPaths)
   );
 
+  public readonly filesLeftBase$ = select(
+    this.state$,
+    state => state.filesLeftBase
+  );
+
+  public readonly filesRightBase$ = select(
+    this.state$,
+    state => state.filesRightBase
+  );
+
   private subscriptions: Subscription[] = [];
 
   constructor(