Merge changes I83f0153b,I9e4aa92a,I019ce1c6,I7b0bc949,I19d4515d, ...

* changes:
  Migrate gr-diff-builder-unified_test from js to ts
  Migrate gr-diff-cursor_test from js to ts
  Migrate gr-diff-app-context-init_test from js to ts
  Migrate gr-diff-group_test from js to ts
  Migrate gr-diff_test from js to ts
  Migrate gr-diff-builder-element from Polymer to plain class
  Migrate gr-diff-builder-element_test from js to ts
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
index 24719e8..42ff8f6 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
@@ -10,6 +10,7 @@
 import {configModelToken} from '../../../models/config/config-model';
 import {resolve} from '../../../models/dependency';
 import {
+  AccountDetailInfo,
   AccountInfo,
   ChangeInfo,
   NumericChangeId,
@@ -32,6 +33,8 @@
 import {getDisplayName} from '../../../utils/display-name-util';
 import {AccountInputDetail} from '../../shared/gr-account-list/gr-account-list';
 import '@polymer/iron-icon/iron-icon';
+import {getReplyByReason} from '../../../utils/attention-set-util';
+import {intersection} from '../../../utils/common-util';
 
 @customElement('gr-change-list-reviewer-flow')
 export class GrChangeListReviewerFlow extends LitElement {
@@ -72,6 +75,8 @@
 
   private isLoggedIn = false;
 
+  private account?: AccountDetailInfo;
+
   static override get styles() {
     return css`
       gr-dialog {
@@ -125,6 +130,11 @@
       () => getAppContext().userModel.loggedIn$,
       isLoggedIn => (this.isLoggedIn = isLoggedIn)
     );
+    subscribe(
+      this,
+      () => getAppContext().userModel.account$,
+      account => (this.account = account)
+    );
   }
 
   override render() {
@@ -335,7 +345,8 @@
       ])
     );
     const inFlightActions = this.getBulkActionsModel().addReviewers(
-      this.updatedAccountsByReviewerState
+      this.updatedAccountsByReviewerState,
+      getReplyByReason(this.account, this.serverConfig)
     );
 
     await allSettled(
@@ -383,13 +394,7 @@
     const reviewersPerChange = this.selectedChanges.map(
       change => change.reviewers[reviewerState] ?? []
     );
-    if (reviewersPerChange.length === 0) {
-      return [];
-    }
-    // Gets reviewers present in all changes
-    return reviewersPerChange.reduce((a, b) =>
-      a.filter(reviewer => b.includes(reviewer))
-    );
+    return intersection(reviewersPerChange);
   }
 
   private createSuggestionsProvider(
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
index edcad8f..e34a269 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
@@ -245,6 +245,14 @@
             {reviewer: accounts[2]._account_id, state: ReviewerState.REVIEWER},
             {reviewer: accounts[5]._account_id, state: ReviewerState.CC},
           ],
+          ignore_automatic_attention_set_rules: true,
+          // only the reviewer is added to the attention set, not the cc
+          add_to_attention_set: [
+            {
+              reason: '<GERRIT_ACCOUNT_1> replied on the change',
+              user: accounts[2]._account_id,
+            },
+          ],
         },
       ]);
       assert.sameDeepOrderedMembers(saveChangeReviewStub.secondCall.args, [
@@ -255,6 +263,14 @@
             {reviewer: accounts[2]._account_id, state: ReviewerState.REVIEWER},
             {reviewer: accounts[5]._account_id, state: ReviewerState.CC},
           ],
+          ignore_automatic_attention_set_rules: true,
+          // only the reviewer is added to the attention set, not the cc
+          add_to_attention_set: [
+            {
+              reason: '<GERRIT_ACCOUNT_1> replied on the change',
+              user: accounts[2]._account_id,
+            },
+          ],
         },
       ]);
     });
@@ -357,7 +373,7 @@
       );
       await flush();
 
-      // prettier and shadoDom string don't agree on long text in divs
+      // prettier and shadowDom string don't agree on long text in divs
       expect(element).shadowDom.to.equal(
         /* prettier-ignore */
         /* HTML */ `
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 105c0d8..3db1012 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -633,6 +633,12 @@
 
   private connected$ = new BehaviorSubject(false);
 
+  /**
+   * For `connectedCallback()` to distinguish between connecting to the DOM for
+   * the first time or if just re-connecting.
+   */
+  private isFirstConnection = true;
+
   /** Simply reflects the router-model value. */
   // visible for testing
   routerPatchNum?: PatchSetNum;
@@ -649,12 +655,29 @@
       'fullscreen-overlay-opened',
       () => this._handleHideBackgroundContent()
     );
-
     this.addEventListener('fullscreen-overlay-closed', () =>
       this._handleShowBackgroundContent()
     );
-
     this.addEventListener('open-reply-dialog', () => this._openReplyDialog());
+    this.addEventListener('change-message-deleted', () => fireReload(this));
+    this.addEventListener('editable-content-save', e =>
+      this._handleCommitMessageSave(e)
+    );
+    this.addEventListener('editable-content-cancel', () =>
+      this._handleCommitMessageCancel()
+    );
+    this.addEventListener('open-fix-preview', e => this._onOpenFixPreview(e));
+    this.addEventListener('close-fix-preview', e => this._onCloseFixPreview(e));
+
+    this.addEventListener(EventType.SHOW_PRIMARY_TAB, e =>
+      this._setActivePrimaryTab(e)
+    );
+    this.addEventListener('reload', e => {
+      this.loadData(
+        /* isLocationChange= */ false,
+        /* clearPatchset= */ e.detail && e.detail.clearPatchset
+      );
+    });
   }
 
   private setupSubscriptions() {
@@ -699,24 +722,23 @@
 
   override connectedCallback() {
     super.connectedCallback();
+    this.firstConnectedCallback();
     this.connected$.next(true);
-    this.setupSubscriptions();
-    this._throttledToggleChangeStar = throttleWrap<KeyboardEvent>(_ =>
-      this._handleToggleChangeStar()
-    );
-    this._getServerConfig().then(config => {
-      this._serverConfig = config;
-      this._replyDisabled = false;
-    });
 
-    this._getLoggedIn().then(loggedIn => {
-      this._loggedIn = loggedIn;
-      if (loggedIn) {
-        this.restApiService.getAccount().then(acct => {
-          this._account = acct;
-        });
-      }
-    });
+    // Make sure to reverse everything below this line in disconnectedCallback().
+    // Or consider using either firstConnectedCallback() or constructor().
+    this.setupSubscriptions();
+    document.addEventListener('visibilitychange', this.handleVisibilityChange);
+    document.addEventListener('scroll', this.handleScroll);
+  }
+
+  /**
+   * For initialization that should only happen once, not again when
+   * re-connecting to the DOM later.
+   */
+  private firstConnectedCallback() {
+    if (!this.isFirstConnection) return;
+    this.isFirstConnection = false;
 
     getPluginLoader()
       .awaitPluginsLoaded()
@@ -734,26 +756,21 @@
       })
       .then(() => this._initActiveTabs(this.params));
 
-    this.addEventListener('change-message-deleted', () => fireReload(this));
-    this.addEventListener('editable-content-save', e =>
-      this._handleCommitMessageSave(e)
+    this._throttledToggleChangeStar = throttleWrap<KeyboardEvent>(_ =>
+      this._handleToggleChangeStar()
     );
-    this.addEventListener('editable-content-cancel', () =>
-      this._handleCommitMessageCancel()
-    );
-    this.addEventListener('open-fix-preview', e => this._onOpenFixPreview(e));
-    this.addEventListener('close-fix-preview', e => this._onCloseFixPreview(e));
-    document.addEventListener('visibilitychange', this.handleVisibilityChange);
-    document.addEventListener('scroll', this.handleScroll);
+    this._getServerConfig().then(config => {
+      this._serverConfig = config;
+      this._replyDisabled = false;
+    });
 
-    this.addEventListener(EventType.SHOW_PRIMARY_TAB, e =>
-      this._setActivePrimaryTab(e)
-    );
-    this.addEventListener('reload', e => {
-      this.loadData(
-        /* isLocationChange= */ false,
-        /* clearPatchset= */ e.detail && e.detail.clearPatchset
-      );
+    this._getLoggedIn().then(loggedIn => {
+      this._loggedIn = loggedIn;
+      if (loggedIn) {
+        this.restApiService.getAccount().then(acct => {
+          this._account = acct;
+        });
+      }
     });
   }
 
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 8d82e41..66df866 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
@@ -17,11 +17,9 @@
 import '../../../test/common-test-setup-karma';
 import '../../shared/gr-date-formatter/gr-date-formatter';
 import './gr-file-list';
-import {createCommentApiMockWithTemplateElement} from '../../../test/mocks/comment-api';
 import {FilesExpandedState} from '../gr-file-list-constants';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {runA11yAudit} from '../../../test/a11y-test-utils';
-import {html} from '@polymer/polymer/lib/utils/html-tag';
 import {
   listenOnce,
   mockPromise,
@@ -61,12 +59,7 @@
 import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
 import {GrEditFileControls} from '../../edit/gr-edit-file-controls/gr-edit-file-controls';
 
-const commentApiMock = createCommentApiMockWithTemplateElement(
-  'gr-file-list-comment-api-mock',
-  html` <gr-file-list id="fileList"></gr-file-list> `
-);
-
-const basicFixture = fixtureFromElement(commentApiMock.is);
+const basicFixture = fixtureFromElement('gr-file-list');
 
 suite('gr-diff a11y test', () => {
   test('audit', async () => {
@@ -85,7 +78,6 @@
 
 suite('gr-file-list tests', () => {
   let element: GrFileList;
-  let commentApiWrapper: any;
 
   let saveStub: sinon.SinonStub;
 
@@ -103,8 +95,7 @@
 
       // Element must be wrapped in an element with direct access to the
       // comment API.
-      commentApiWrapper = basicFixture.instantiate();
-      element = commentApiWrapper.$.fileList;
+      element = basicFixture.instantiate();
 
       element._loading = false;
       element.diffPrefs = {} as DiffPreferencesInfo;
@@ -1976,8 +1967,7 @@
 
       // Element must be wrapped in an element with direct access to the
       // comment API.
-      commentApiWrapper = basicFixture.instantiate();
-      element = commentApiWrapper.$.fileList;
+      element = basicFixture.instantiate();
       element.diffPrefs = {} as DiffPreferencesInfo;
       element.change = {
         ...createParsedChange(),
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
index 27c445e..8e757da 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
@@ -24,10 +24,8 @@
   LabelNameToValueMap,
 } from '../../../types/common';
 import {GrLabelScoreRow} from '../gr-label-score-row/gr-label-score-row';
-import {getAppContext} from '../../../services/app-context';
 import {
   getTriggerVotes,
-  showNewSubmitRequirements,
   computeLabels,
   Label,
   computeOrderedLabelValues,
@@ -48,8 +46,6 @@
   @property({type: Object})
   account?: AccountInfo;
 
-  private readonly flagsService = getAppContext().flagsService;
-
   static override get styles() {
     return [
       fontStyles,
@@ -57,8 +53,6 @@
         .scoresTable {
           display: table;
           width: 100%;
-        }
-        .scoresTable.newSubmitRequirements {
           table-layout: fixed;
         }
         .mergedMessage,
@@ -91,19 +85,6 @@
   }
 
   override render() {
-    if (showNewSubmitRequirements(this.flagsService, this.change)) {
-      return this.renderNewSubmitRequirements();
-    } else {
-      return this.renderOldSubmitRequirements();
-    }
-  }
-
-  private renderOldSubmitRequirements() {
-    const labels = computeLabels(this.account, this.change);
-    return html`${this.renderLabels(labels)}${this.renderErrorMessages()}`;
-  }
-
-  private renderNewSubmitRequirements() {
     return html`${this.renderSubmitReqsLabels()}${this.renderTriggerVotes()}
     ${this.renderErrorMessages()}`;
   }
@@ -145,13 +126,7 @@
   }
 
   private renderLabels(labels: Label[]) {
-    const newSubReqs = showNewSubmitRequirements(
-      this.flagsService,
-      this.change
-    );
-    return html`<div
-      class="scoresTable ${newSubReqs ? 'newSubmitRequirements' : ''}"
-    >
+    return html`<div class="scoresTable">
       ${labels
         .filter(
           label =>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
index b9cb616..ac487f8 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
@@ -17,10 +17,8 @@
 
 import '../../../test/common-test-setup-karma';
 import './gr-messages-list';
-import {createCommentApiMockWithTemplateElement} from '../../../test/mocks/comment-api';
 import {CombinedMessage, GrMessagesList, TEST_ONLY} from './gr-messages-list';
 import {MessageTag} from '../../../constants/constants';
-import {html} from '@polymer/polymer/lib/utils/html-tag';
 import {
   query,
   queryAll,
@@ -43,16 +41,7 @@
 import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
 import {assertIsDefined} from '../../../utils/common-util';
 
-createCommentApiMockWithTemplateElement(
-  'gr-messages-list-comment-mock-api',
-  html` <gr-messages-list id="messagesList"></gr-messages-list> `
-);
-
-const basicFixture = fixtureFromTemplate(html`
-  <gr-messages-list-comment-mock-api>
-    <gr-messages-list></gr-messages-list>
-  </gr-messages-list-comment-mock-api>
-`);
+const basicFixture = fixtureFromElement('gr-messages-list');
 
 const author = {
   _account_id: 42 as AccountId,
@@ -99,8 +88,6 @@
   let element: GrMessagesList;
   let messages: ChangeMessageInfo[];
 
-  let commentApiWrapper: any;
-
   const getMessages = function () {
     return queryAll<GrMessage>(element, 'gr-message');
   };
@@ -156,13 +143,7 @@
       stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
 
       messages = generateRandomMessages(3);
-      // Element must be wrapped in an element with direct access to the
-      // comment API.
-      commentApiWrapper = basicFixture.instantiate();
-      element = queryAndAssert<GrMessagesList>(
-        commentApiWrapper,
-        '#messagesList'
-      );
+      element = basicFixture.instantiate();
       await element.getCommentsModel().reloadComments(0 as NumericChangeId);
       element.messages = messages;
       await flush();
@@ -507,8 +488,6 @@
     let element: GrMessagesList;
     let messages: ChangeMessageInfo[];
 
-    let commentApiWrapper: any;
-
     setup(() => {
       stubRestApi('getLoggedIn').returns(Promise.resolve(false));
       stubRestApi('getDiffComments').returns(Promise.resolve({}));
@@ -529,13 +508,7 @@
         }),
       ];
 
-      // Element must be wrapped in an element with direct access to the
-      // comment API.
-      commentApiWrapper = basicFixture.instantiate();
-      element = queryAndAssert<GrMessagesList>(
-        commentApiWrapper,
-        '#messagesList'
-      );
+      element = basicFixture.instantiate();
       element.messages = messages;
       flush();
     });
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
index 6740977..d5ce654 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
@@ -29,12 +29,7 @@
   LabelInfo,
 } from '../../../types/common';
 import {hasOwnProperty} from '../../../utils/common-util';
-import {getAppContext} from '../../../services/app-context';
-import {
-  getApprovalInfo,
-  getCodeReviewLabel,
-  showNewSubmitRequirements,
-} from '../../../utils/label-util';
+import {getApprovalInfo, getCodeReviewLabel} from '../../../utils/label-util';
 import {sortReviewers} from '../../../utils/attention-set-util';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {css} from 'lit';
@@ -68,8 +63,6 @@
 
   @state() showAllReviewers = false;
 
-  private readonly flagsService = getAppContext().flagsService;
-
   static override get styles() {
     return [
       sharedStyles,
@@ -166,14 +159,12 @@
         .vote=${this.computeVote(reviewer)}
         .label=${this.computeCodeReviewLabel()}
       >
-        ${showNewSubmitRequirements(this.flagsService, this.change)
-          ? html`<gr-vote-chip
-              slot="vote-chip"
-              .vote=${this.computeVote(reviewer)}
-              .label=${this.computeCodeReviewLabel()}
-              circle-shape
-            ></gr-vote-chip>`
-          : nothing}
+        <gr-vote-chip
+          slot="vote-chip"
+          .vote=${this.computeVote(reviewer)}
+          .label=${this.computeCodeReviewLabel()}
+          circle-shape
+        ></gr-vote-chip>
       </gr-account-chip>
     `;
   }
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
index dc501c8..2e48771 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
@@ -20,7 +20,6 @@
 import '../../shared/revision-info/revision-info';
 import './gr-patch-range-select';
 import {GrPatchRangeSelect} from './gr-patch-range-select';
-import '../../../test/mocks/comment-api';
 import {RevisionInfo as RevisionInfoClass} from '../../shared/revision-info/revision-info';
 import {ChangeComments} from '../gr-comment-api/gr-comment-api';
 import {stubRestApi} from '../../../test/test-utils';
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
index 48d6998..3fecd63 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
@@ -167,9 +167,6 @@
   @property({type: Array})
   removableValues?: AccountInput[];
 
-  @property({type: Number})
-  maxCount = 0;
-
   /**
    * Returns suggestion items
    */
@@ -203,7 +200,7 @@
       .group {
         --account-label-suffix: ' (group)';
       }
-      .pending-add {
+      .pendingAdd {
         font-style: italic;
       }
       .list {
@@ -234,8 +231,7 @@
       </div>
       <gr-account-entry
         borderless=""
-        ?hidden=${(this.maxCount && this.maxCount <= this.accounts.length) ||
-        this.readonly}
+        ?hidden=${this.readonly}
         id="entry"
         .placeholder=${this.placeholder}
         @add=${this.handleAdd}
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
index 7b3a93d..26566a3 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
@@ -400,16 +400,6 @@
     assert.equal(element.accounts.length, 1);
   });
 
-  test('max-count', async () => {
-    element.maxCount = 1;
-    const acct = makeAccount();
-    handleAdd({account: acct, count: 1});
-    await element.updateComplete;
-    assert.isTrue(
-      queryAndAssert<GrAccountEntry>(element, '#entry').hasAttribute('hidden')
-    );
-  });
-
   test('enter text calls suggestions provider', async () => {
     const suggestions: Suggestion[] = [
       {
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
index b1d3914..ee3bec7 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
@@ -18,11 +18,9 @@
 import '../../../styles/gr-voting-styles';
 import '../../../styles/shared-styles';
 import '../gr-vote-chip/gr-vote-chip';
-import '../gr-account-label/gr-account-label';
 import '../gr-account-chip/gr-account-chip';
 import '../gr-button/gr-button';
 import '../gr-icons/gr-icons';
-import '../gr-label/gr-label';
 import '../gr-tooltip-content/gr-tooltip-content';
 import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
 import {
@@ -30,9 +28,7 @@
   LabelInfo,
   ApprovalInfo,
   AccountId,
-  isQuickLabelInfo,
   isDetailedLabelInfo,
-  LabelNameToInfoMap,
 } from '../../../types/common';
 import {LitElement, css, html} from 'lit';
 import {customElement, property} from 'lit/decorators';
@@ -40,10 +36,8 @@
 import {
   canVote,
   getApprovalInfo,
-  getVotingRangeOrDefault,
   hasNeutralStatus,
   hasVoted,
-  showNewSubmitRequirements,
   valueString,
 } from '../../../utils/label-util';
 import {getAppContext} from '../../../services/app-context';
@@ -61,19 +55,6 @@
   }
 }
 
-enum LabelClassName {
-  NEGATIVE = 'negative',
-  POSITIVE = 'positive',
-  MIN = 'min',
-  MAX = 'max',
-}
-
-interface FormattedLabel {
-  className?: LabelClassName;
-  account: ApprovalInfo | AccountInfo;
-  value: string;
-}
-
 @customElement('gr-label-info')
 export class GrLabelInfo extends LitElement {
   @property({type: Object})
@@ -107,8 +88,6 @@
 
   private readonly reporting = getAppContext().reportingService;
 
-  private readonly flagsService = getAppContext().flagsService;
-
   // TODO(TS): not used, remove later
   _xhrPromise?: Promise<void>;
 
@@ -118,9 +97,6 @@
       fontStyles,
       votingStyles,
       css`
-        .placeholder {
-          color: var(--deemphasized-text-color);
-        }
         .hidden {
           display: none;
         }
@@ -132,33 +108,6 @@
           margin-right: var(--spacing-s);
           padding: 1px;
         }
-        .max {
-          background-color: var(--vote-color-approved);
-        }
-        .min {
-          background-color: var(--vote-color-rejected);
-        }
-        .positive {
-          background-color: var(--vote-color-recommended);
-          border-radius: 12px;
-          border: 1px solid var(--vote-outline-recommended);
-          color: var(--chip-color);
-        }
-        .negative {
-          background-color: var(--vote-color-disliked);
-          border-radius: 12px;
-          border: 1px solid var(--vote-outline-disliked);
-          color: var(--chip-color);
-        }
-        .hidden {
-          display: none;
-        }
-        td {
-          vertical-align: top;
-        }
-        tr {
-          min-height: var(--line-height-normal);
-        }
         gr-tooltip-content {
           display: block;
         }
@@ -173,17 +122,10 @@
         gr-button[disabled] iron-icon {
           color: var(--border-color);
         }
-        gr-account-label {
-          --account-max-length: 100px;
-          margin-right: var(--spacing-xs);
-        }
         iron-icon {
           height: calc(var(--line-height-normal) - 2px);
           width: calc(var(--line-height-normal) - 2px);
         }
-        .labelValueContainer:not(:first-of-type) td {
-          padding-top: var(--spacing-s);
-        }
         .reviewer-row {
           padding-top: var(--spacing-s);
         }
@@ -208,14 +150,6 @@
   }
 
   override render() {
-    if (showNewSubmitRequirements(this.flagsService, this.change)) {
-      return this.renderNewSubmitRequirements();
-    } else {
-      return this.renderOldSubmitRequirements();
-    }
-  }
-
-  private renderNewSubmitRequirements() {
     const labelInfo = this.labelInfo;
     if (!labelInfo) return;
     const reviewers = (this.change?.reviewers['REVIEWER'] ?? [])
@@ -238,23 +172,6 @@
     </div>`;
   }
 
-  private renderOldSubmitRequirements() {
-    const labelInfo = this.labelInfo;
-    return html` <p
-        class="placeholder ${this.computeShowPlaceholder(
-          labelInfo,
-          this.change?.labels
-        )}"
-      >
-        No votes
-      </p>
-      <table>
-        ${this.mapLabelInfo(labelInfo, this.account, this.change?.labels).map(
-          mappedLabel => this.renderLabel(mappedLabel)
-        )}
-      </table>`;
-  }
-
   renderReviewerVote(reviewer: AccountInfo) {
     const labelInfo = this.labelInfo;
     if (!labelInfo) return;
@@ -285,30 +202,6 @@
     </div>`;
   }
 
-  renderLabel(mappedLabel: FormattedLabel) {
-    const {labelInfo, change} = this;
-    return html` <tr class="labelValueContainer">
-      <td>
-        <gr-tooltip-content
-          has-tooltip
-          title=${this._computeValueTooltip(labelInfo, mappedLabel.value)}
-        >
-          <gr-label class="${mappedLabel.className} voteChip font-small">
-            ${mappedLabel.value}
-          </gr-label>
-        </gr-tooltip-content>
-      </td>
-      <td>
-        <gr-account-label
-          clickable
-          .account=${mappedLabel.account}
-          .change=${change}
-        ></gr-account-label>
-      </td>
-      <td>${this.renderRemoveVote(mappedLabel.account)}</td>
-    </tr>`;
-  }
-
   private renderVoteAbility(reviewer: AccountInfo) {
     if (this.labelInfo && isDetailedLabelInfo(this.labelInfo)) {
       const approvalInfo = getApprovalInfo(this.labelInfo, reviewer);
@@ -341,83 +234,6 @@
   }
 
   /**
-   * This method also listens on change.labels.*,
-   * to trigger computation when a label is removed from the change.
-   *
-   * The third parameter is just for *triggering* computation.
-   */
-  private mapLabelInfo(
-    labelInfo?: LabelInfo,
-    account?: AccountInfo,
-    _?: LabelNameToInfoMap
-  ): FormattedLabel[] {
-    const result: FormattedLabel[] = [];
-    if (!labelInfo) {
-      return result;
-    }
-    if (!isDetailedLabelInfo(labelInfo)) {
-      if (
-        isQuickLabelInfo(labelInfo) &&
-        (labelInfo.rejected || labelInfo.approved)
-      ) {
-        const ok = labelInfo.approved || !labelInfo.rejected;
-        return [
-          {
-            value: ok ? '👍️' : '👎️',
-            className: ok ? LabelClassName.POSITIVE : LabelClassName.NEGATIVE,
-            // executed only if approved or rejected is not undefined
-            account: ok ? labelInfo.approved! : labelInfo.rejected!,
-          },
-        ];
-      }
-      return result;
-    }
-
-    // Sort votes by positivity.
-    // TODO(TS): maybe mark value as required if always present
-    const votes = (labelInfo.all || []).sort(
-      (a, b) => (a.value || 0) - (b.value || 0)
-    );
-    const votingRange = getVotingRangeOrDefault(labelInfo);
-    for (const label of votes) {
-      if (
-        label.value &&
-        (!isQuickLabelInfo(labelInfo) ||
-          label.value !== labelInfo.default_value)
-      ) {
-        let labelClassName;
-        let labelValPrefix = '';
-        if (label.value > 0) {
-          labelValPrefix = '+';
-          if (label.value === votingRange.max) {
-            labelClassName = LabelClassName.MAX;
-          } else {
-            labelClassName = LabelClassName.POSITIVE;
-          }
-        } else if (label.value < 0) {
-          if (label.value === votingRange.min) {
-            labelClassName = LabelClassName.MIN;
-          } else {
-            labelClassName = LabelClassName.NEGATIVE;
-          }
-        }
-        const formattedLabel: FormattedLabel = {
-          value: `${labelValPrefix}${label.value}`,
-          className: labelClassName,
-          account: label,
-        };
-        if (label._account_id === account?._account_id) {
-          // Put self-votes at the top.
-          result.unshift(formattedLabel);
-        } else {
-          result.push(formattedLabel);
-        }
-      }
-    }
-    return result;
-  }
-
-  /**
    * A user is able to delete a vote iff the mutable property is true and the
    * reviewer that left the vote exists in the list of removable_reviewers
    * received from the backend.
@@ -488,39 +304,4 @@
     }
     return labelInfo.values[score];
   }
-
-  /**
-   * This method also listens change.labels.* in
-   * order to trigger computation when a label is removed from the change.
-   *
-   * The second parameter is just for *triggering* computation.
-   */
-  private computeShowPlaceholder(
-    labelInfo?: LabelInfo,
-    _?: LabelNameToInfoMap
-  ) {
-    if (!labelInfo) {
-      return '';
-    }
-    if (
-      !isDetailedLabelInfo(labelInfo) &&
-      isQuickLabelInfo(labelInfo) &&
-      (labelInfo.rejected || labelInfo.approved)
-    ) {
-      return 'hidden';
-    }
-
-    if (isDetailedLabelInfo(labelInfo) && labelInfo.all) {
-      for (const label of labelInfo.all) {
-        if (
-          label.value &&
-          (!isQuickLabelInfo(labelInfo) ||
-            label.value !== labelInfo.default_value)
-        ) {
-          return 'hidden';
-        }
-      }
-    }
-    return '';
-  }
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
index 0ac49a7..f1336b4 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
@@ -20,20 +20,18 @@
 import {
   isHidden,
   mockPromise,
-  queryAll,
   queryAndAssert,
   stubRestApi,
 } from '../../../test/test-utils';
 import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
 import {GrLabelInfo} from './gr-label-info';
 import {GrButton} from '../gr-button/gr-button';
-import {GrLabel} from '../gr-label/gr-label';
 import {
   createAccountWithIdNameAndEmail,
+  createDetailedLabelInfo,
   createParsedChange,
 } from '../../../test/test-data-generators';
-import {LabelInfo} from '../../../types/common';
-import {GrAccountLabel} from '../gr-account-label/gr-account-label';
+import {ApprovalInfo, LabelInfo} from '../../../types/common';
 
 const basicFixture = fixtureFromElement('gr-label-info');
 
@@ -41,12 +39,51 @@
   let element: GrLabelInfo;
   const account = createAccountWithIdNameAndEmail(5);
 
-  setup(() => {
+  setup(async () => {
     element = basicFixture.instantiate();
 
     // Needed to trigger computed bindings.
     element.account = {};
-    element.change = {...createParsedChange(), labels: {}};
+    element.change = {
+      ...createParsedChange(),
+      labels: {},
+      reviewers: {
+        REVIEWER: [account],
+        CC: [],
+      },
+    };
+    const approval: ApprovalInfo = {
+      value: 2,
+      _account_id: account._account_id,
+    };
+    element.labelInfo = {
+      ...createDetailedLabelInfo(),
+      all: [approval],
+    };
+    await element.updateComplete;
+  });
+
+  test('renders', () => {
+    expect(element).shadowDom.to.equal(/* HTML */ `<div>
+      <div class="reviewer-row">
+        <gr-account-chip>
+          <gr-vote-chip circle-shape="" slot="vote-chip"> </gr-vote-chip>
+        </gr-account-chip>
+        <gr-tooltip-content has-tooltip="" title="Remove vote">
+          <gr-button
+            aria-disabled="false"
+            aria-label="Remove vote"
+            class="deleteBtn hidden"
+            data-account-id="5"
+            link=""
+            role="button"
+            tabindex="0"
+          >
+            <iron-icon icon="gr-icons:delete"> </iron-icon>
+          </gr-button>
+        </gr-tooltip-content>
+      </div>
+    </div>`);
   });
 
   suite('remove reviewer votes', () => {
@@ -62,6 +99,10 @@
       element.change = {
         ...createParsedChange(),
         labels: {'Code-Review': label},
+        reviewers: {
+          REVIEWER: [account],
+          CC: [],
+        },
       };
       element.labelInfo = label;
       element.label = 'Code-Review';
@@ -108,101 +149,6 @@
     });
   });
 
-  suite('label color and order', () => {
-    test('valueless label rejected', async () => {
-      element.labelInfo = {rejected: {name: 'someone'}};
-      await element.updateComplete;
-      const labels = queryAll<GrLabel>(element, 'gr-label');
-      assert.isTrue(labels[0].classList.contains('negative'));
-    });
-
-    test('valueless label approved', async () => {
-      element.labelInfo = {approved: {name: 'someone'}};
-      await element.updateComplete;
-      const labels = queryAll<GrLabel>(element, 'gr-label');
-      assert.isTrue(labels[0].classList.contains('positive'));
-    });
-
-    test('-2 to +2', async () => {
-      element.labelInfo = {
-        all: [
-          {value: 2, name: 'user 2'},
-          {value: 1, name: 'user 1'},
-          {value: -1, name: 'user 3'},
-          {value: -2, name: 'user 4'},
-        ],
-        values: {
-          '-2': 'Awful',
-          '-1': "Don't submit as-is",
-          ' 0': 'No score',
-          '+1': 'Looks good to me',
-          '+2': 'Ready to submit',
-        },
-      };
-      await element.updateComplete;
-      const labels = queryAll<GrLabel>(element, 'gr-label');
-      assert.isTrue(labels[0].classList.contains('max'));
-      assert.isTrue(labels[1].classList.contains('positive'));
-      assert.isTrue(labels[2].classList.contains('negative'));
-      assert.isTrue(labels[3].classList.contains('min'));
-    });
-
-    test('-1 to +1', async () => {
-      element.labelInfo = {
-        all: [
-          {value: 1, name: 'user 1'},
-          {value: -1, name: 'user 2'},
-        ],
-        values: {
-          '-1': "Don't submit as-is",
-          ' 0': 'No score',
-          '+1': 'Looks good to me',
-        },
-      };
-      await element.updateComplete;
-      const labels = queryAll<GrLabel>(element, 'gr-label');
-      assert.isTrue(labels[0].classList.contains('max'));
-      assert.isTrue(labels[1].classList.contains('min'));
-    });
-
-    test('0 to +2', async () => {
-      element.labelInfo = {
-        all: [
-          {value: 1, name: 'user 2'},
-          {value: 2, name: 'user '},
-        ],
-        values: {
-          ' 0': "Don't submit as-is",
-          '+1': 'No score',
-          '+2': 'Looks good to me',
-        },
-      };
-      await element.updateComplete;
-      const labels = queryAll<GrLabel>(element, 'gr-label');
-      assert.isTrue(labels[0].classList.contains('max'));
-      assert.isTrue(labels[1].classList.contains('positive'));
-    });
-
-    test('self votes at top', async () => {
-      const otherAccount = createAccountWithIdNameAndEmail(8);
-      element.account = account;
-      element.labelInfo = {
-        all: [
-          {...otherAccount, value: 1},
-          {...account, value: -1},
-        ],
-        values: {
-          '-1': "Don't submit as-is",
-          ' 0': 'No score',
-          '+1': 'Looks good to me',
-        },
-      };
-      await element.updateComplete;
-      const chips = queryAll<GrAccountLabel>(element, 'gr-account-label');
-      assert.equal(chips[0].account!._account_id, element.account._account_id);
-    });
-  });
-
   test('_computeValueTooltip', () => {
     // Existing label.
     let labelInfo: LabelInfo = {values: {0: 'Baz'}};
@@ -218,49 +164,4 @@
     score = '0';
     assert.equal(element._computeValueTooltip(labelInfo, score), '');
   });
-
-  test('placeholder', async () => {
-    const values = {
-      '0': 'No score',
-      '+1': 'good',
-      '+2': 'excellent',
-      '-1': 'bad',
-      '-2': 'terrible',
-    };
-    element.labelInfo = {};
-    await element.updateComplete;
-    assert.isFalse(
-      isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
-    );
-    element.labelInfo = {all: [], values};
-    await element.updateComplete;
-    assert.isFalse(
-      isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
-    );
-    element.labelInfo = {all: [{value: 1}], values};
-    await element.updateComplete;
-    assert.isTrue(
-      isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
-    );
-    element.labelInfo = {rejected: account};
-    await element.updateComplete;
-    assert.isTrue(
-      isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
-    );
-    element.labelInfo = {rejected: account, all: [{value: 1}], values};
-    await element.updateComplete;
-    assert.isTrue(
-      isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
-    );
-    element.labelInfo = {approved: account};
-    await element.updateComplete;
-    assert.isTrue(
-      isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
-    );
-    element.labelInfo = {approved: account, all: [{value: 1}], values};
-    await element.updateComplete;
-    assert.isTrue(
-      isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
-    );
-  });
 });
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label.ts b/polygerrit-ui/app/elements/shared/gr-label/gr-label.ts
deleted file mode 100644
index 842b35e..0000000
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/**
- * @fileoverview Consider removing this element as
- * its functionality seems to be duplicated with gr-tooltip and only
- * used in gr-label-info.
- */
-
-import {html, LitElement} from 'lit';
-import {customElement} from 'lit/decorators';
-
-declare global {
-  interface HTMLElementTagNameMap {
-    'gr-label': GrLabel;
-  }
-}
-
-@customElement('gr-label')
-export class GrLabel extends LitElement {
-  static override get styles() {
-    return [];
-  }
-
-  override render() {
-    return html` <slot></slot> `;
-  }
-}
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.ts b/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.ts
deleted file mode 100644
index 94196df..0000000
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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 {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html` <slot></slot> `;
diff --git a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
index c276f79..c80a1cf 100644
--- a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
+++ b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
@@ -10,13 +10,18 @@
   ChangeStatus,
   ReviewerState,
   AccountInfo,
+  AccountId,
 } from '../../api/rest-api';
 import {Model} from '../model';
 import {Finalizable} from '../../services/registry';
 import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
 import {define} from '../dependency';
 import {select} from '../../utils/observable-util';
-import {ReviewInput, ReviewerInput} from '../../types/common';
+import {
+  ReviewInput,
+  ReviewerInput,
+  AttentionSetInput,
+} from '../../types/common';
 
 export const bulkActionsModelToken =
   define<BulkActionsModel>('bulk-actions-model');
@@ -154,14 +159,15 @@
   }
 
   addReviewers(
-    changedReviewers: Map<ReviewerState, AccountInfo[]>
+    changedReviewers: Map<ReviewerState, AccountInfo[]>,
+    reason: string
   ): Promise<Response>[] {
     const current = this.subject$.getValue();
     const changes = current.selectedChangeNums.map(
       changeNum => current.allChanges.get(changeNum)!
     );
     return changes.map(change => {
-      const reviewersNewToChange = [
+      const reviewersNewToChange: ReviewerInput[] = [
         ReviewerState.REVIEWER,
         ReviewerState.CC,
       ].flatMap(state =>
@@ -170,8 +176,20 @@
       if (reviewersNewToChange.length === 0) {
         return Promise.resolve(new Response());
       }
+      const attentionSetUpdates: AttentionSetInput[] = reviewersNewToChange
+        .filter(reviewerInput => reviewerInput.state === ReviewerState.REVIEWER)
+        .map(reviewerInput => {
+          return {
+            // TODO: Once Groups are supported, filter them out and only add
+            // Accounts to the attention set, just like gr-reply-dialog.
+            user: reviewerInput.reviewer as AccountId,
+            reason,
+          };
+        });
       const reviewInput: ReviewInput = {
         reviewers: reviewersNewToChange,
+        ignore_automatic_attention_set_rules: true,
+        add_to_attention_set: attentionSetUpdates,
       };
       return this.restApiService.saveChangeReview(
         change._number,
diff --git a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
index 5347b41..aaf7123 100644
--- a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
+++ b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
@@ -236,7 +236,8 @@
         new Map([
           [ReviewerState.REVIEWER, [accounts[0]]],
           [ReviewerState.CC, [accounts[1]]],
-        ])
+        ]),
+        '<GERRIT_ACCOUNT_12345> replied on the change'
       );
 
       // changes[0] is not updated since it already has the reviewer & CC
@@ -249,6 +250,13 @@
             {reviewer: accounts[0]._account_id, state: ReviewerState.REVIEWER},
             {reviewer: accounts[1]._account_id, state: ReviewerState.CC},
           ],
+          ignore_automatic_attention_set_rules: true,
+          add_to_attention_set: [
+            {
+              reason: '<GERRIT_ACCOUNT_12345> replied on the change',
+              user: accounts[0]._account_id,
+            },
+          ],
         },
       ]);
     });
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts b/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
index 5f77e8a..028b2af 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
@@ -62,7 +62,7 @@
 
   static CREDS_EXPIRED_MSG = 'Credentials expired.';
 
-  private authCheckPromise?: Promise<Response>;
+  private authCheckPromise?: Promise<boolean>;
 
   private _last_auth_check_time: number = Date.now();
 
@@ -100,37 +100,37 @@
       Date.now() - this._last_auth_check_time > MAX_AUTH_CHECK_WAIT_TIME_MS
     ) {
       // Refetch after last check expired
-      this.authCheckPromise = fetch(`${this.baseUrl}/auth-check`);
+      this.authCheckPromise = fetch(`${this.baseUrl}/auth-check`)
+        .then(res => {
+          // Make a call that requires loading the body of the request. This makes it so that the browser
+          // can close the request even though callers of this method might only ever read headers.
+          // See https://stackoverflow.com/questions/45816743/how-to-solve-this-caution-request-is-not-finished-yet-in-chrome
+          try {
+            res.clone().text();
+          } catch {
+            // Ignore error
+          }
+
+          // auth-check will return 204 if authed
+          // treat the rest as unauthed
+          if (res.status === 204) {
+            this._setStatus(Auth.STATUS.AUTHED);
+            return true;
+          } else {
+            this._setStatus(Auth.STATUS.NOT_AUTHED);
+            return false;
+          }
+        })
+        .catch(() => {
+          this._setStatus(AuthStatus.ERROR);
+          // Reset authCheckPromise to avoid caching the failed promise
+          this.authCheckPromise = undefined;
+          return false;
+        });
       this._last_auth_check_time = Date.now();
     }
 
-    return this.authCheckPromise
-      .then(res => {
-        // Make a call that requires loading the body of the request. This makes it so that the browser
-        // can close the request even though callers of this method might only ever read headers.
-        // See https://stackoverflow.com/questions/45816743/how-to-solve-this-caution-request-is-not-finished-yet-in-chrome
-        try {
-          res.clone().text();
-        } catch {
-          // Ignore error
-        }
-
-        // auth-check will return 204 if authed
-        // treat the rest as unauthed
-        if (res.status === 204) {
-          this._setStatus(Auth.STATUS.AUTHED);
-          return true;
-        } else {
-          this._setStatus(Auth.STATUS.NOT_AUTHED);
-          return false;
-        }
-      })
-      .catch(() => {
-        this._setStatus(AuthStatus.ERROR);
-        // Reset authCheckPromise to avoid caching the failed promise
-        this.authCheckPromise = undefined;
-        return false;
-      });
+    return this.authCheckPromise;
   }
 
   clearCache() {
diff --git a/polygerrit-ui/app/test/mocks/comment-api.js b/polygerrit-ui/app/test/mocks/comment-api.js
deleted file mode 100644
index fc4599d..0000000
--- a/polygerrit-ui/app/test/mocks/comment-api.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 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 {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-
-/**
- * This is an "abstract" class for tests. The descendant must define a template
- * for this element and a tagName - see createCommentApiMockWithTemplateElement below
- */
-class CommentApiMock extends LegacyElementMixin(PolymerElement) {
-  static get properties() {
-    return {
-      _changeComments: Object,
-    };
-  }
-}
-
-/**
- * Creates a new element which is descendant of CommentApiMock with specified
- * template. Additionally, the method registers a tagName for this element.
- *
- * Each tagName must be a unique accross all tests.
- */
-export function createCommentApiMockWithTemplateElement(tagName, template) {
-  const elementClass = class extends CommentApiMock {
-    static get is() { return tagName; }
-
-    static get template() { return template; }
-  };
-  customElements.define(tagName, elementClass);
-  return elementClass;
-}
diff --git a/polygerrit-ui/app/utils/common-util.ts b/polygerrit-ui/app/utils/common-util.ts
index 6ccf770..95b753c 100644
--- a/polygerrit-ui/app/utils/common-util.ts
+++ b/polygerrit-ui/app/utils/common-util.ts
@@ -159,7 +159,8 @@
 
 /**
  * Returns the elements that are present in every sub-array. If a compareBy
- * predicate is passed in, it will be used instead of strict equality.
+ * predicate is passed in, it will be used instead of strict equality. A new
+ * array is always returned even if there is already just a single array.
  */
 export function intersection<T>(
   arrays: T[][],
@@ -171,6 +172,9 @@
   if (arrays.length === 0) {
     return [];
   }
+  if (arrays.length === 1) {
+    return [...arrays[0]];
+  }
   return arrays.reduce((result, array) =>
     result.filter(t => array.find(u => compareBy(t, u)))
   );
diff --git a/polygerrit-ui/app/utils/common-util_test.ts b/polygerrit-ui/app/utils/common-util_test.ts
index 8cc523a..0adfaa6 100644
--- a/polygerrit-ui/app/utils/common-util_test.ts
+++ b/polygerrit-ui/app/utils/common-util_test.ts
@@ -75,8 +75,11 @@
   });
 
   test('intersections', () => {
+    const arrayWithValues = [1, 2, 3];
     assert.sameDeepMembers(intersection([]), []);
-    assert.sameDeepMembers(intersection([[1, 2, 3]]), [1, 2, 3]);
+    assert.sameDeepMembers(intersection([arrayWithValues]), arrayWithValues);
+    // a new array is returned even if a single array is provided.
+    assert.notStrictEqual(intersection([arrayWithValues]), arrayWithValues);
     assert.sameDeepMembers(
       intersection([
         [1, 2, 3],