Migrate gr-hovercard-account to LitElement

Google-Bug-Id: b/202457138
Change-Id: Iac3756d0df781b53014d7442f03e6dccb6765099
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
index 9897a9f..dabf761 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
@@ -199,9 +199,9 @@
         ${!this.hideHovercard
           ? html`<gr-hovercard-account
               for="hovercardTarget"
-              .account="${account}"
-              .change="${change}"
-              ?highlight-attention=${highlightAttention}
+              .account=${account}
+              .change=${change}
+              .highlightAttention=${highlightAttention}
               .voteableText=${this.voteableText}
             ></gr-hovercard-account>`
           : ''}
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
index 3988095..6a34fbb 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
@@ -16,17 +16,11 @@
  */
 
 import '@polymer/iron-icon/iron-icon';
-import '../../../styles/gr-font-styles';
-import '../../../styles/shared-styles';
-import '../../../styles/gr-hovercard-styles';
 import '../gr-avatar/gr-avatar';
 import '../gr-button/gr-button';
-import {HovercardBehaviorMixin} from '../gr-hovercard/gr-hovercard-behavior';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-hovercard-account_html';
 import {appContext} from '../../../services/app-context';
 import {accountKey, isSelf} from '../../../utils/account-util';
-import {customElement, property} from '@polymer/decorators';
+import {customElement, property} from 'lit/decorators';
 import {
   AccountInfo,
   ChangeInfo,
@@ -46,16 +40,15 @@
 import {CURRENT} from '../../../utils/patch-set-util';
 import {isInvolved, isRemovableReviewer} from '../../../utils/change-util';
 import {assertIsDefined} from '../../../utils/common-util';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {css, html, LitElement} from 'lit';
+import {HovercardMixin} from '../../../mixins/hovercard-mixin/hovercard-mixin';
 
 // This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = HovercardBehaviorMixin(PolymerElement);
+const base = HovercardMixin(LitElement);
 
 @customElement('gr-hovercard-account')
 export class GrHovercardAccount extends base {
-  static get template() {
-    return htmlTemplate;
-  }
-
   @property({type: Object})
   account!: AccountInfo;
 
@@ -107,9 +100,214 @@
     });
   }
 
-  _computeText(account?: AccountInfo, selfAccount?: AccountInfo) {
-    if (!account || !selfAccount) return '';
-    return isSelf(account, selfAccount) ? 'Your' : 'Their';
+  static override get styles() {
+    return [
+      fontStyles,
+      base.styles || [],
+      css`
+        .top,
+        .attention,
+        .status,
+        .voteable {
+          padding: var(--spacing-s) var(--spacing-l);
+        }
+        .top {
+          display: flex;
+          padding-top: var(--spacing-xl);
+          min-width: 300px;
+        }
+        gr-avatar {
+          height: 48px;
+          width: 48px;
+          margin-right: var(--spacing-l);
+        }
+        .title,
+        .email {
+          color: var(--deemphasized-text-color);
+        }
+        .action {
+          border-top: 1px solid var(--border-color);
+          padding: var(--spacing-s) var(--spacing-l);
+          --gr-button-padding: var(--spacing-s) var(--spacing-m);
+        }
+        .attention {
+          background-color: var(--emphasis-color);
+        }
+        .attention a {
+          text-decoration: none;
+        }
+        iron-icon {
+          vertical-align: top;
+        }
+        .status iron-icon {
+          width: 14px;
+          height: 14px;
+          position: relative;
+          top: 2px;
+        }
+        iron-icon.attentionIcon {
+          width: 14px;
+          height: 14px;
+          position: relative;
+          top: 3px;
+        }
+        .reason {
+          padding-top: var(--spacing-s);
+        }
+      `,
+    ];
+  }
+
+  override render() {
+    return html`
+      <div id="container" role="tooltip" tabindex="-1">
+        ${this.renderContent()}
+      </div>
+    `;
+  }
+
+  private renderContent() {
+    if (!this._isShowing) return;
+    return html`
+      <div class="top">
+        <div class="avatar">
+          <gr-avautar .account=${this.account} imageSize="56"></gr-avatar>
+        </div>
+        <div class="account">
+          <h3 class="name heading-3">${this.account.name}</h3>
+          <div class="email">${this.account.email}</div>
+        </div>
+      </div>
+      ${this.renderAccountStatus()}
+      ${
+        this.voteableText
+          ? html`
+              <div class="voteable">
+                <span class="title">Voteable:</span>
+                <span class="value">${this.voteableText}</span>
+              </div>
+            `
+          : ''
+      }
+      ${this.renderNeedsAttention()} ${this.renderAddToAttention()}
+      ${this.renderRemoveFromAttention()} ${this.renderReviewerOrCcActions()}
+    `;
+  }
+
+  private renderReviewerOrCcActions() {
+    if (!this._selfAccount || !isRemovableReviewer(this.change, this.account))
+      return;
+    return html`
+      <div class="action">
+        <gr-button
+          class="removeReviewerOrCC"
+          link=""
+          no-uppercase
+          @click="${this.handleRemoveReviewerOrCC}"
+        >
+          Remove ${this.computeReviewerOrCCText()}
+        </gr-button>
+      </div>
+      <div class="action">
+        <gr-button
+          class="changeReviewerOrCC"
+          link=""
+          no-uppercase
+          @click="${this.handleChangeReviewerOrCCStatus}"
+        >
+          ${this.computeChangeReviewerOrCCText()}
+        </gr-button>
+      </div>
+    `;
+  }
+
+  private renderAccountStatus() {
+    if (!this.account.status) return;
+    return html`
+      <div class="status">
+        <span class="title">
+          <iron-icon icon="gr-icons:calendar"></iron-icon>
+          Status:
+        </span>
+        <span class="value">${this.account.status}</span>
+      </div>
+    `;
+  }
+
+  private renderNeedsAttention() {
+    if (!(this.isAttentionEnabled && this.hasUserAttention)) return;
+    const lastUpdate = getLastUpdate(this.account, this.change);
+    return html`
+      <div class="attention">
+        <div>
+          <iron-icon
+            class="attentionIcon"
+            icon="gr-icons:attention"
+          ></iron-icon>
+          <span> ${this.computePronoun()} turn to take this action. </span>
+          <a
+            href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
+            target="_blank"
+          >
+            <iron-icon
+              icon="gr-icons:help-outline"
+              title="read documentation"
+            ></iron-icon>
+          </a>
+        </div>
+        <div class="reason">
+          <span class="title">Reason:</span>
+          <span class="value">
+            ${getReason(this._config, this.account, this.change)}
+          </span>
+          ${lastUpdate
+            ? html` (<gr-date-formatter
+                  withTooltip
+                  .dateStr="${lastUpdate}"
+                ></gr-date-formatter
+                >)`
+            : ''}
+        </div>
+      </div>
+    `;
+  }
+
+  private renderAddToAttention() {
+    if (!this.computeShowActionAddToAttentionSet()) return;
+    return html`
+      <div class="action">
+        <gr-button
+          class="addToAttentionSet"
+          link=""
+          no-uppercase
+          @click="${this.handleClickAddToAttentionSet}"
+        >
+          Add to attention set
+        </gr-button>
+      </div>
+    `;
+  }
+
+  private renderRemoveFromAttention() {
+    if (!this.computeShowActionRemoveFromAttentionSet()) return;
+    return html`
+      <div class="action">
+        <gr-button
+          class="removeFromAttentionSet"
+          link=""
+          no-uppercase
+          @click="${this.handleClickRemoveFromAttentionSet}"
+        >
+          Remove from attention set
+        </gr-button>
+      </div>
+    `;
+  }
+
+  // private but used by tests
+  computePronoun() {
+    if (!this.account || !this._selfAccount) return '';
+    return isSelf(this.account, this._selfAccount) ? 'Your' : 'Their';
   }
 
   get isAttentionEnabled() {
@@ -124,27 +322,11 @@
     return hasAttention(this.account, this.change);
   }
 
-  _computeReason(change?: ChangeInfo) {
-    return getReason(this._config, this.account, change);
-  }
-
-  _computeLastUpdate(change?: ChangeInfo) {
-    return getLastUpdate(this.account, change);
-  }
-
-  /** 3rd parameter is just for *triggering* re-computation. */
-  _showReviewerOrCCActions(
-    account?: AccountInfo,
-    change?: ChangeInfo,
-    _?: unknown
-  ) {
-    return !!this._selfAccount && isRemovableReviewer(change, account);
-  }
-
-  _getReviewerState(account: AccountInfo, change: ChangeInfo) {
+  private getReviewerState() {
     if (
-      change.reviewers[ReviewerState.REVIEWER]?.some(
-        (reviewer: AccountInfo) => reviewer._account_id === account._account_id
+      this.change!.reviewers[ReviewerState.REVIEWER]?.some(
+        (reviewer: AccountInfo) =>
+          reviewer._account_id === this.account._account_id
       )
     ) {
       return ReviewerState.REVIEWER;
@@ -152,21 +334,21 @@
     return ReviewerState.CC;
   }
 
-  _computeReviewerOrCCText(account?: AccountInfo, change?: ChangeInfo) {
-    if (!change || !account) return '';
-    return this._getReviewerState(account, change) === ReviewerState.REVIEWER
+  private computeReviewerOrCCText() {
+    if (!this.change || !this.account) return '';
+    return this.getReviewerState() === ReviewerState.REVIEWER
       ? 'Reviewer'
       : 'CC';
   }
 
-  _computeChangeReviewerOrCCText(account?: AccountInfo, change?: ChangeInfo) {
-    if (!change || !account) return '';
-    return this._getReviewerState(account, change) === ReviewerState.REVIEWER
+  private computeChangeReviewerOrCCText() {
+    if (!this.change || !this.account) return '';
+    return this.getReviewerState() === ReviewerState.REVIEWER
       ? 'Move Reviewer to CC'
       : 'Move CC to Reviewer';
   }
 
-  _handleChangeReviewerOrCCStatus() {
+  private handleChangeReviewerOrCCStatus() {
     assertIsDefined(this.change, 'change');
     // accountKey() throws an error if _account_id & email is not found, which
     // we want to check before showing reloading toast
@@ -179,7 +361,7 @@
       {
         reviewer: _accountKey,
         state:
-          this._getReviewerState(this.account, this.change) === ReviewerState.CC
+          this.getReviewerState() === ReviewerState.CC
             ? ReviewerState.REVIEWER
             : ReviewerState.CC,
       },
@@ -190,15 +372,14 @@
       .then(response => {
         if (!response || !response.ok) {
           throw new Error(
-            'something went wrong when toggling' +
-              this._getReviewerState(this.account, this.change!)
+            'something went wrong when toggling' + this.getReviewerState()
           );
         }
         this.dispatchEventThroughTarget('reload', {clearPatchset: true});
       });
   }
 
-  _handleRemoveReviewerOrCC() {
+  private handleRemoveReviewerOrCC() {
     if (!this.change || !(this.account?._account_id || this.account?.email))
       throw new Error('Missing change or account.');
     this.dispatchEventThroughTarget('show-alert', {
@@ -218,45 +399,21 @@
       });
   }
 
-  /** Parameters are just for *triggering* re-computation. */
-  _computeShowLabelNeedsAttention(
-    _1: unknown,
-    _2: unknown,
-    _3: unknown,
-    _4: unknown
-  ) {
-    return this.isAttentionEnabled && this.hasUserAttention;
-  }
-
-  /** Parameters are just for *triggering* re-computation. */
-  _computeShowActionAddToAttentionSet(
-    _1: unknown,
-    _2: unknown,
-    _3: unknown,
-    _4: unknown,
-    _5: unknown
-  ) {
+  private computeShowActionAddToAttentionSet() {
     const involvedOrSelf =
       isInvolved(this.change, this._selfAccount) ||
       isSelf(this.account, this._selfAccount);
     return involvedOrSelf && this.isAttentionEnabled && !this.hasUserAttention;
   }
 
-  /** Parameters are just for *triggering* re-computation. */
-  _computeShowActionRemoveFromAttentionSet(
-    _1: unknown,
-    _2: unknown,
-    _3: unknown,
-    _4: unknown,
-    _5: unknown
-  ) {
+  private computeShowActionRemoveFromAttentionSet() {
     const involvedOrSelf =
       isInvolved(this.change, this._selfAccount) ||
       isSelf(this.account, this._selfAccount);
     return involvedOrSelf && this.isAttentionEnabled && this.hasUserAttention;
   }
 
-  _handleClickAddToAttentionSet() {
+  private handleClickAddToAttentionSet(e: MouseEvent) {
     if (!this.change || !this.account._account_id) return;
     this.dispatchEventThroughTarget('show-alert', {
       message: 'Saving attention set update ...',
@@ -277,17 +434,17 @@
 
     this.reporting.reportInteraction(
       'attention-hovercard-add',
-      this._reportingDetails()
+      this.reportingDetails()
     );
     this.restApiService
       .addToAttentionSet(this.change._number, this.account._account_id, reason)
       .then(() => {
         this.dispatchEventThroughTarget('hide-alert');
       });
-    this.hide();
+    this.hide(e);
   }
 
-  _handleClickRemoveFromAttentionSet() {
+  private handleClickRemoveFromAttentionSet(e: MouseEvent) {
     if (!this.change || !this.account._account_id) return;
     this.dispatchEventThroughTarget('show-alert', {
       message: 'Saving attention set update ...',
@@ -304,7 +461,7 @@
 
     this.reporting.reportInteraction(
       'attention-hovercard-remove',
-      this._reportingDetails()
+      this.reportingDetails()
     );
     this.restApiService
       .removeFromAttentionSet(
@@ -315,10 +472,10 @@
       .then(() => {
         this.dispatchEventThroughTarget('hide-alert');
       });
-    this.hide();
+    this.hide(e);
   }
 
-  _reportingDetails() {
+  private reportingDetails() {
     const targetId = this.account._account_id;
     const ownerId =
       (this.change && this.change.owner && this.change.owner._account_id) || -1;
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts
deleted file mode 100644
index cba7293..0000000
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts
+++ /dev/null
@@ -1,197 +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 '../../../styles/gr-hovercard-styles';
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
-  <style include="gr-font-styles">
-    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
-  </style>
-  <style include="shared-styles">
-    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
-  </style>
-  <style include="gr-hovercard-styles">
-    .top,
-    .attention,
-    .status,
-    .voteable {
-      padding: var(--spacing-s) var(--spacing-l);
-    }
-    .top {
-      display: flex;
-      padding-top: var(--spacing-xl);
-      min-width: 300px;
-    }
-    gr-avatar {
-      height: 48px;
-      width: 48px;
-      margin-right: var(--spacing-l);
-    }
-    .title,
-    .email {
-      color: var(--deemphasized-text-color);
-    }
-    .action {
-      border-top: 1px solid var(--border-color);
-      padding: var(--spacing-s) var(--spacing-l);
-      --gr-button-padding: var(--spacing-s) var(--spacing-m);
-    }
-    .attention {
-      background-color: var(--emphasis-color);
-    }
-    .attention a {
-      text-decoration: none;
-    }
-    iron-icon {
-      vertical-align: top;
-    }
-    .status iron-icon {
-      width: 14px;
-      height: 14px;
-      position: relative;
-      top: 2px;
-    }
-    iron-icon.attentionIcon {
-      width: 14px;
-      height: 14px;
-      position: relative;
-      top: 3px;
-    }
-    .reason {
-      padding-top: var(--spacing-s);
-    }
-  </style>
-  <div id="container" role="tooltip" tabindex="-1">
-    <template is="dom-if" if="[[_isShowing]]">
-      <div class="top">
-        <div class="avatar">
-          <gr-avatar account="[[account]]" imageSize="56"></gr-avatar>
-        </div>
-        <div class="account">
-          <h3 class="name heading-3">[[account.name]]</h3>
-          <div class="email">[[account.email]]</div>
-        </div>
-      </div>
-      <template is="dom-if" if="[[account.status]]">
-        <div class="status">
-          <span class="title">
-            <iron-icon icon="gr-icons:calendar"></iron-icon>
-            Status:
-          </span>
-          <span class="value">[[account.status]]</span>
-        </div>
-      </template>
-      <template is="dom-if" if="[[voteableText]]">
-        <div class="voteable">
-          <span class="title">Voteable:</span>
-          <span class="value">[[voteableText]]</span>
-        </div>
-      </template>
-      <template
-        is="dom-if"
-        if="[[_computeShowLabelNeedsAttention(_config, highlightAttention, account, change)]]"
-      >
-        <div class="attention">
-          <div>
-            <iron-icon
-              class="attentionIcon"
-              icon="gr-icons:attention"
-            ></iron-icon>
-            <span>
-              [[_computeText(account, _selfAccount)]] turn to take action.
-            </span>
-            <a
-              href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
-              target="_blank"
-            >
-              <iron-icon
-                icon="gr-icons:help-outline"
-                title="read documentation"
-              ></iron-icon>
-            </a>
-          </div>
-          <div class="reason">
-            <span class="title">Reason:</span>
-            <span class="value">[[_computeReason(change)]]</span>
-            <template is="dom-if" if="[[_computeLastUpdate(change)]]">
-              (<gr-date-formatter
-                withTooltip
-                date-str="[[_computeLastUpdate(change)]]"
-              ></gr-date-formatter
-              >)
-            </template>
-          </div>
-        </div>
-      </template>
-      <template
-        is="dom-if"
-        if="[[_computeShowActionAddToAttentionSet(_config, highlightAttention, account, change, _selfAccount)]]"
-      >
-        <div class="action">
-          <gr-button
-            class="addToAttentionSet"
-            link=""
-            no-uppercase=""
-            on-click="_handleClickAddToAttentionSet"
-          >
-            Add to attention set
-          </gr-button>
-        </div>
-      </template>
-      <template
-        is="dom-if"
-        if="[[_computeShowActionRemoveFromAttentionSet(_config, highlightAttention, account, change, _selfAccount)]]"
-      >
-        <div class="action">
-          <gr-button
-            class="removeFromAttentionSet"
-            link=""
-            no-uppercase=""
-            on-click="_handleClickRemoveFromAttentionSet"
-          >
-            Remove from attention set
-          </gr-button>
-        </div>
-      </template>
-      <template
-        is="dom-if"
-        if="[[_showReviewerOrCCActions(account, change, _selfAccount)]]"
-      >
-        <div class="action">
-          <gr-button
-            class="removeReviewerOrCC"
-            link=""
-            no-uppercase=""
-            on-click="_handleRemoveReviewerOrCC"
-          >
-            Remove [[_computeReviewerOrCCText(account, change)]]
-          </gr-button>
-        </div>
-        <div class="action">
-          <gr-button
-            class="changeReviewerOrCC"
-            link=""
-            no-uppercase=""
-            on-click="_handleChangeReviewerOrCCStatus"
-          >
-            [[_computeChangeReviewerOrCCText(account, change)]]
-          </gr-button>
-        </div>
-      </template>
-    </template>
-  </div>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
index 82f64d0..5530d7c 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
@@ -19,7 +19,7 @@
 import './gr-hovercard-account.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 import {ReviewerState} from '../../../constants/constants.js';
-import {stubRestApi} from '../../../test/test-utils.js';
+import {mockPromise, stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromTemplate(html`
 <gr-hovercard-account class="hovered"></gr-hovercard-account>
@@ -57,33 +57,21 @@
         'Kermit The Frog');
   });
 
-  test('_computeLastUpdate', () => {
-    const last_update = '2019-07-17 19:39:02.000000000';
-    const change = {
-      attention_set: {
-        31415926535: {
-          last_update,
-        },
-      },
-    };
-    assert.equal(element._computeLastUpdate(change), last_update);
-  });
-
-  test('_computeText', () => {
-    let account = {_account_id: '1'};
-    const selfAccount = {_account_id: '1'};
-    assert.equal(element._computeText(account, selfAccount), 'Your');
-    account = {_account_id: '2'};
-    assert.equal(element._computeText(account, selfAccount), 'Their');
+  test('computePronoun', () => {
+    element.account = {_account_id: '1'};
+    element._selfAccount = {_account_id: '1'};
+    assert.equal(element.computePronoun(), 'Your');
+    element.account = {_account_id: '2'};
+    assert.equal(element.computePronoun(), 'Their');
   });
 
   test('account status is not shown if the property is not set', () => {
     assert.isNull(element.shadowRoot.querySelector('.status'));
   });
 
-  test('account status is displayed', () => {
+  test('account status is displayed', async () => {
     element.account = {status: 'OOO', ...ACCOUNT};
-    flush();
+    await element.updateComplete;
     assert.equal(element.shadowRoot.querySelector('.status .value').innerText,
         'OOO');
   });
@@ -92,9 +80,9 @@
     assert.isNull(element.shadowRoot.querySelector('.voteable'));
   });
 
-  test('voteable div is displayed', () => {
+  test('voteable div is displayed', async () => {
     element.voteableText = 'CodeReview: +2';
-    flush();
+    await element.updateComplete;
     assert.equal(element.shadowRoot.querySelector('.voteable .value').innerText,
         element.voteableText);
   });
@@ -106,15 +94,15 @@
         [ReviewerState.REVIEWER]: [ACCOUNT],
       },
     };
+    await element.updateComplete;
     stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
     const reloadListener = sinon.spy();
     element._target.addEventListener('reload', reloadListener);
-    await flush();
     const button = element.shadowRoot.querySelector('.removeReviewerOrCC');
     assert.isOk(button);
     assert.equal(button.innerText, 'Remove Reviewer');
     MockInteractions.tap(button);
-    await flush();
+    await element.updateComplete;
     assert.isTrue(reloadListener.called);
   });
 
@@ -125,6 +113,7 @@
         [ReviewerState.REVIEWER]: [ACCOUNT],
       },
     };
+    await element.updateComplete;
     const saveReviewStub = stubRestApi(
         'saveChangeReview').returns(
         Promise.resolve({ok: true}));
@@ -132,14 +121,12 @@
     const reloadListener = sinon.spy();
     element._target.addEventListener('reload', reloadListener);
 
-    await flush();
     const button = element.shadowRoot.querySelector('.changeReviewerOrCC');
 
     assert.isOk(button);
     assert.equal(button.innerText, 'Move Reviewer to CC');
     MockInteractions.tap(button);
-    await flush();
-
+    await element.updateComplete;
     assert.isTrue(saveReviewStub.called);
     assert.isTrue(reloadListener.called);
   });
@@ -151,20 +138,19 @@
         [ReviewerState.REVIEWER]: [],
       },
     };
+    await element.updateComplete;
     const saveReviewStub = stubRestApi(
         'saveChangeReview').returns(Promise.resolve({ok: true}));
     stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
     const reloadListener = sinon.spy();
     element._target.addEventListener('reload', reloadListener);
-    await flush();
 
     const button = element.shadowRoot.querySelector('.changeReviewerOrCC');
     assert.isOk(button);
     assert.equal(button.innerText, 'Move CC to Reviewer');
 
     MockInteractions.tap(button);
-    await flush();
-
+    await element.updateComplete;
     assert.isTrue(saveReviewStub.called);
     assert.isTrue(reloadListener.called);
   });
@@ -176,31 +162,26 @@
         [ReviewerState.REVIEWER]: [],
       },
     };
+    await element.updateComplete;
     stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
     const reloadListener = sinon.spy();
     element._target.addEventListener('reload', reloadListener);
 
-    await flush();
     const button = element.shadowRoot.querySelector('.removeReviewerOrCC');
 
     assert.equal(button.innerText, 'Remove CC');
     assert.isOk(button);
     MockInteractions.tap(button);
-
-    await flush();
-
+    await element.updateComplete;
     assert.isTrue(reloadListener.called);
   });
 
   test('add to attention set', async () => {
-    let apiResolve;
-    const apiPromise = new Promise(r => {
-      apiResolve = r;
-    });
+    const apiPromise = mockPromise();
     const apiSpy = stubRestApi('addToAttentionSet').returns(apiPromise);
     element.highlightAttention = true;
     element._target = document.createElement('div');
-    flush();
+    await element.updateComplete;
     const showAlertListener = sinon.spy();
     const hideAlertListener = sinon.spy();
     const updatedListener = sinon.spy();
@@ -224,8 +205,8 @@
     assert.isTrue(updatedListener.called, 'updatedListener was called');
     assert.isFalse(element._isShowing, 'hovercard is hidden');
 
-    apiResolve({});
-    await flush();
+    apiPromise.resolve({});
+    await element.updateComplete;
     assert.isTrue(apiSpy.calledOnce);
     assert.equal(apiSpy.lastCall.args[2],
         `Added by <GERRIT_ACCOUNT_${ACCOUNT._account_id}>`
@@ -234,10 +215,7 @@
   });
 
   test('remove from attention set', async () => {
-    let apiResolve;
-    const apiPromise = new Promise(r => {
-      apiResolve = r;
-    });
+    const apiPromise = mockPromise();
     const apiSpy = stubRestApi('removeFromAttentionSet').returns(apiPromise);
     element.highlightAttention = true;
     element.change = {
@@ -246,7 +224,7 @@
       owner: {...ACCOUNT},
     };
     element._target = document.createElement('div');
-    flush();
+    await element.updateComplete;
     const showAlertListener = sinon.spy();
     const hideAlertListener = sinon.spy();
     const updatedListener = sinon.spy();
@@ -264,8 +242,8 @@
     assert.isTrue(updatedListener.called, 'updatedListener was called');
     assert.isFalse(element._isShowing, 'hovercard is hidden');
 
-    apiResolve({});
-    await flush();
+    apiPromise.resolve({});
+    await element.updateComplete;
 
     assert.isTrue(apiSpy.calledOnce);
     assert.equal(apiSpy.lastCall.args[2],