Migrate gr-hovercard-run to HovercardMixin LitElement

Google-Bug-Id: b/202457138
Change-Id: Ibd877ff9955766408abfd1254edef7cb349db56c
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
index 57eac3b..95b7157 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
@@ -14,14 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import './gr-checks-styles';
-import '../../styles/gr-font-styles';
-import '../../styles/gr-hovercard-styles';
-import '../../styles/shared-styles';
-import {HovercardBehaviorMixin} from '../shared/gr-hovercard/gr-hovercard-behavior';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-hovercard-run_html';
-import {customElement, property} from '@polymer/decorators';
+import {fontStyles} from '../../styles/gr-font-styles';
+import {customElement, property} from 'lit/decorators';
 import './gr-checks-action';
 import {CheckRun} from '../../services/checks/checks-model';
 import {
@@ -33,102 +27,346 @@
 import {durationString, fromNow} from '../../utils/date-util';
 import {RunStatus} from '../../api/checks';
 import {ordinal} from '../../utils/string-util';
+import {HovercardMixin} from '../../mixins/hovercard-mixin/hovercard-mixin';
+import {css, html, LitElement} from 'lit';
+import {checksStyles} from './gr-checks-styles';
 
 // This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = HovercardBehaviorMixin(PolymerElement);
+const base = HovercardMixin(LitElement);
 
 @customElement('gr-hovercard-run')
 export class GrHovercardRun extends base {
-  static get template() {
-    return htmlTemplate;
-  }
-
   @property({type: Object})
   run?: CheckRun;
 
-  computeIcon(run?: CheckRun) {
-    if (!run) return '';
-    const category = worstCategory(run);
+  static override get styles() {
+    return [
+      fontStyles,
+      checksStyles,
+      base.styles || [],
+      css`
+        #container {
+          min-width: 356px;
+          max-width: 356px;
+          padding: var(--spacing-xl) 0 var(--spacing-m) 0;
+        }
+        .row {
+          display: flex;
+          margin-top: var(--spacing-s);
+        }
+        .attempts.row {
+          flex-wrap: wrap;
+        }
+        .chipRow {
+          display: flex;
+          margin-top: var(--spacing-s);
+        }
+        .chip {
+          background: var(--gray-background);
+          color: var(--gray-foreground);
+          border-radius: 20px;
+          padding: var(--spacing-xs) var(--spacing-m) var(--spacing-xs)
+            var(--spacing-s);
+        }
+        .title {
+          color: var(--deemphasized-text-color);
+          margin-right: var(--spacing-m);
+        }
+        div.section {
+          margin: 0 var(--spacing-xl) var(--spacing-m) var(--spacing-xl);
+          display: flex;
+        }
+        div.sectionIcon {
+          flex: 0 0 30px;
+        }
+        div.chip iron-icon {
+          width: 16px;
+          height: 16px;
+          /* Positioning of a 16px icon in the middle of a 20px line. */
+          position: relative;
+          top: 2px;
+        }
+        div.sectionIcon iron-icon {
+          position: relative;
+          top: 2px;
+          width: 20px;
+          height: 20px;
+        }
+        div.sectionIcon iron-icon.small {
+          position: relative;
+          top: 6px;
+          width: 16px;
+          height: 16px;
+        }
+        div.sectionContent iron-icon.link {
+          color: var(--link-color);
+        }
+        div.sectionContent .attemptIcon iron-icon,
+        div.sectionContent iron-icon.small {
+          width: 16px;
+          height: 16px;
+          margin-right: var(--spacing-s);
+          /* Positioning of a 16px icon in the middle of a 20px line. */
+          position: relative;
+          top: 2px;
+        }
+        div.sectionContent .attemptIcon iron-icon {
+          margin-right: 0;
+        }
+        .attemptIcon,
+        .attemptNumber {
+          margin-right: var(--spacing-s);
+          color: var(--deemphasized-text-color);
+          text-align: center;
+          width: 24px;
+          font-size: var(--font-size-small);
+        }
+        div.action {
+          border-top: 1px solid var(--border-color);
+          margin-top: var(--spacing-m);
+          padding: var(--spacing-m) var(--spacing-xl) 0;
+        }
+      `,
+    ];
+  }
+
+  override render() {
+    if (!this.run) return '';
+    const icon = this.computeIcon();
+    return html`
+      <div id="container" role="tooltip" tabindex="-1">
+        <div class="section">
+          <div
+            ?hidden="${!this.run || this.run.status === RunStatus.RUNNABLE}"
+            class="chipRow"
+          >
+            <div class="chip">
+              <iron-icon icon="gr-icons:${this.computeChipIcon()}"></iron-icon>
+              <span>${this.run.status}</span>
+            </div>
+          </div>
+        </div>
+        <div class="section">
+          <div class="sectionIcon" ?hidden="${icon.length === 0}">
+            <iron-icon class="${icon}" icon="gr-icons:${icon}"></iron-icon>
+          </div>
+          <div class="sectionContent">
+            <h3 class="name heading-3">
+              <span>${this.run.checkName}</span>
+            </h3>
+          </div>
+        </div>
+        ${this.renderStatusSection()} ${this.renderAttemptSection()}
+        ${this.renderTimestampSection()} ${this.renderDescriptionSection()}
+        ${this.renderActions()}
+      </div>
+    `;
+  }
+
+  private renderStatusSection() {
+    if (!this.run || (!this.run.statusLink && !this.run.statusDescription))
+      return;
+
+    return html`
+      <div class="section">
+        <div class="sectionIcon">
+          <iron-icon class="small" icon="gr-icons:info-outline"></iron-icon>
+        </div>
+        <div class="sectionContent">
+          ${this.run.statusLink
+            ? html` <div class="row">
+                <div class="title">Status</div>
+                <div>
+                  <a href="${this.run.statusLink}" target="_blank"
+                    ><iron-icon
+                      aria-label="external link to check status"
+                      class="small link"
+                      icon="gr-icons:launch"
+                    ></iron-icon
+                    >${this.computeHostName(this.run.statusLink)}
+                  </a>
+                </div>
+              </div>`
+            : ''}
+          ${this.run.statusDescription
+            ? html` <div class="row">
+                <div class="title">Message</div>
+                <div>${this.run.statusDescription}</div>
+              </div>`
+            : ''}
+        </div>
+      </div>
+    `;
+  }
+
+  private renderAttemptSection() {
+    if (this.hideAttempts()) return;
+    const attempts = this.computeAttempts();
+    return html`
+      <div class="section">
+        <div class="sectionIcon">
+          <iron-icon class="small" icon="gr-icons:arrow-forward"></iron-icon>
+        </div>
+        <div class="sectionContent">
+          <div class="attempts row">
+            <div class="title">Attempt</div>
+            ${attempts.map(a => this.renderAttempt(a))}
+          </div>
+        </div>
+      </div>
+    `;
+  }
+
+  private renderAttempt(attempt: AttemptDetail) {
+    return html`
+      <div>
+        <div class="attemptIcon">
+          <iron-icon
+            class="${attempt.icon}"
+            icon="gr-icons:${attempt.icon}"
+          ></iron-icon>
+        </div>
+        <div class="attemptNumber">${ordinal(attempt.attempt)}</div>
+      </div>
+    `;
+  }
+
+  private renderTimestampSection() {
+    if (
+      !this.run ||
+      (!this.run.startedTimestamp &&
+        !this.run.scheduledTimestamp &&
+        !this.run.finishedTimestamp)
+    )
+      return;
+
+    return html`
+      <div class="section">
+        <div class="sectionIcon">
+          <iron-icon class="small" icon="gr-icons:schedule"></iron-icon>
+        </div>
+        <div class="sectionContent">
+          <div ?hidden="${this.hideScheduled()}" class="row">
+            <div class="title">Scheduled</div>
+            <div>${this.computeDuration(this.run.scheduledTimestamp)}</div>
+          </div>
+          <div ?hidden="${!this.run.startedTimestamp}" class="row">
+            <div class="title">Started</div>
+            <div>${this.computeDuration(this.run.startedTimestamp)}</div>
+          </div>
+          <div ?hidden="${!this.run.finishedTimestamp}" class="row">
+            <div class="title">Ended</div>
+            <div>${this.computeDuration(this.run.finishedTimestamp)}</div>
+          </div>
+          <div ?hidden="${this.hideCompletion()}" class="row">
+            <div class="title">Completion</div>
+            <div>${this.computeCompletionDuration()}</div>
+          </div>
+        </div>
+      </div>
+    `;
+  }
+
+  private renderDescriptionSection() {
+    if (!this.run || (!this.run.checkLink && !this.run.checkDescription))
+      return;
+    return html`
+      <div class="section">
+        <div class="sectionIcon">
+          <iron-icon class="small" icon="gr-icons:link"></iron-icon>
+        </div>
+        <div class="sectionContent">
+          ${this.run.checkDescription
+            ? html` <div class="row">
+                <div class="title">Description</div>
+                <div>${this.run.checkDescription}</div>
+              </div>`
+            : ''}
+          ${this.run.checkLink
+            ? html` <div class="row">
+                <div class="title">Documentation</div>
+                <div>
+                  <a href="${this.run.checkLink}" target="_blank"
+                    ><iron-icon
+                      aria-label="external link to check documentation"
+                      class="small link"
+                      icon="gr-icons:launch"
+                    ></iron-icon
+                    >${this.computeHostName(this.run.checkLink)}
+                  </a>
+                </div>
+              </div>`
+            : ''}
+        </div>
+      </div>
+    `;
+  }
+
+  private renderActions() {
+    const actions = runActions(this.run);
+    return actions.map(
+      action =>
+        html`
+          <div class="action">
+            <gr-checks-action
+              .eventTarget="${this._target}"
+              .action="${action}"
+            ></gr-checks-action>
+          </div>
+        `
+    );
+  }
+
+  computeIcon() {
+    if (!this.run) return '';
+    const category = worstCategory(this.run);
     if (category) return iconFor(category);
-    return run.status === RunStatus.COMPLETED
+    return this.run.status === RunStatus.COMPLETED
       ? iconFor(RunStatus.COMPLETED)
       : '';
   }
 
-  computeActions(run?: CheckRun) {
-    return runActions(run);
-  }
-
-  computeAttempt(attempt?: number) {
-    return ordinal(attempt);
-  }
-
-  computeAttempts(run?: CheckRun): AttemptDetail[] {
-    const details = run?.attemptDetails ?? [];
+  computeAttempts(): AttemptDetail[] {
+    const details = this.run?.attemptDetails ?? [];
     const more =
       details.length > 7 ? [{icon: 'more-horiz', attempt: undefined}] : [];
     return [...more, ...details.slice(-7)];
   }
 
-  computeChipIcon(run?: CheckRun) {
-    if (run?.status === RunStatus.COMPLETED) return 'check';
-    if (run?.status === RunStatus.RUNNING) return 'timelapse';
+  private computeChipIcon() {
+    if (this.run?.status === RunStatus.COMPLETED) return 'check';
+    if (this.run?.status === RunStatus.RUNNING) return 'timelapse';
     return '';
   }
 
-  computeCompletionDuration(run?: CheckRun) {
-    if (!run?.finishedTimestamp || !run?.startedTimestamp) return '';
-    return durationString(run.startedTimestamp, run.finishedTimestamp, true);
-  }
-
-  computeDuration(date?: Date) {
-    return date ? fromNow(date) : '';
-  }
-
-  computeHostName(link?: string) {
-    return link ? new URL(link).hostname : '';
-  }
-
-  hideChip(run?: CheckRun) {
-    return !run || run.status === RunStatus.RUNNABLE;
-  }
-
-  hideHeaderSectionIcon(run?: CheckRun) {
-    return this.computeIcon(run).length === 0;
-  }
-
-  hideStatusSection(run?: CheckRun) {
-    if (!run) return true;
-    return !run.statusLink && !run.statusDescription;
-  }
-
-  hideTimestampSection(run?: CheckRun) {
-    if (!run) return true;
-    return (
-      !run.startedTimestamp && !run.scheduledTimestamp && !run.finishedTimestamp
+  private computeCompletionDuration() {
+    if (!this.run?.finishedTimestamp || !this.run?.startedTimestamp) return '';
+    return durationString(
+      this.run.startedTimestamp,
+      this.run.finishedTimestamp,
+      true
     );
   }
 
-  hideAttempts(run?: CheckRun) {
-    const attemptCount = run?.attemptDetails?.length;
+  private computeDuration(date?: Date) {
+    return date ? fromNow(date) : '';
+  }
+
+  private computeHostName(link?: string) {
+    return link ? new URL(link).hostname : '';
+  }
+
+  private hideAttempts() {
+    const attemptCount = this.run?.attemptDetails?.length;
     return attemptCount === undefined || attemptCount < 2;
   }
 
-  hideScheduled(run?: CheckRun) {
-    return !run?.scheduledTimestamp || !!run?.startedTimestamp;
+  private hideScheduled() {
+    return !this.run?.scheduledTimestamp || !!this.run?.startedTimestamp;
   }
 
-  hideCompletion(run?: CheckRun) {
-    return !run?.startedTimestamp || !run?.finishedTimestamp;
-  }
-
-  hideDescriptionSection(run?: CheckRun) {
-    if (!run) return true;
-    return !run.checkLink && !run.checkDescription;
-  }
-
-  _convertUndefined(value?: string) {
-    return value ?? '';
+  private hideCompletion() {
+    return !this.run?.startedTimestamp || !this.run?.finishedTimestamp;
   }
 }
 
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run_html.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run_html.ts
deleted file mode 100644
index 52dbb9c..0000000
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run_html.ts
+++ /dev/null
@@ -1,235 +0,0 @@
-/**
- * @license
- * Copyright (C) 2021 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`
-  <style include="gr-font-styles">
-    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
-  </style>
-  <style include="gr-checks-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">
-    #container {
-      min-width: 356px;
-      max-width: 356px;
-      padding: var(--spacing-xl) 0 var(--spacing-m) 0;
-    }
-    .row {
-      display: flex;
-      margin-top: var(--spacing-s);
-    }
-    .attempts.row {
-      flex-wrap: wrap;
-    }
-    .chipRow {
-      display: flex;
-      margin-top: var(--spacing-s);
-    }
-    .chip {
-      background: var(--gray-background);
-      color: var(--gray-foreground);
-      border-radius: 20px;
-      padding: var(--spacing-xs) var(--spacing-m) var(--spacing-xs)
-        var(--spacing-s);
-    }
-    .title {
-      color: var(--deemphasized-text-color);
-      margin-right: var(--spacing-m);
-    }
-    div.section {
-      margin: 0 var(--spacing-xl) var(--spacing-m) var(--spacing-xl);
-      display: flex;
-    }
-    div.sectionIcon {
-      flex: 0 0 30px;
-    }
-    div.chip iron-icon {
-      width: 16px;
-      height: 16px;
-      /* Positioning of a 16px icon in the middle of a 20px line. */
-      position: relative;
-      top: 2px;
-    }
-    div.sectionIcon iron-icon {
-      position: relative;
-      top: 2px;
-      width: 20px;
-      height: 20px;
-    }
-    div.sectionIcon iron-icon.small {
-      position: relative;
-      top: 6px;
-      width: 16px;
-      height: 16px;
-    }
-    div.sectionContent iron-icon.link {
-      color: var(--link-color);
-    }
-    div.sectionContent .attemptIcon iron-icon,
-    div.sectionContent iron-icon.small {
-      width: 16px;
-      height: 16px;
-      margin-right: var(--spacing-s);
-      /* Positioning of a 16px icon in the middle of a 20px line. */
-      position: relative;
-      top: 2px;
-    }
-    div.sectionContent .attemptIcon iron-icon {
-      margin-right: 0;
-    }
-    .attemptIcon,
-    .attemptNumber {
-      margin-right: var(--spacing-s);
-      color: var(--deemphasized-text-color);
-      text-align: center;
-      width: 24px;
-      font-size: var(--font-size-small);
-    }
-    div.action {
-      border-top: 1px solid var(--border-color);
-      margin-top: var(--spacing-m);
-      padding: var(--spacing-m) var(--spacing-xl) 0;
-    }
-  </style>
-  <div id="container" role="tooltip" tabindex="-1">
-    <div class="section">
-      <div hidden$="[[hideChip(run)]]" class="chipRow">
-        <div class="chip">
-          <iron-icon icon="gr-icons:[[computeChipIcon(run)]]"></iron-icon>
-          <span>[[run.status]]</span>
-        </div>
-      </div>
-    </div>
-    <div class="section">
-      <div class="sectionIcon" hidden$="[[hideHeaderSectionIcon(run)]]">
-        <iron-icon
-          class$="[[computeIcon(run)]]"
-          icon="gr-icons:[[computeIcon(run)]]"
-        ></iron-icon>
-      </div>
-      <div class="sectionContent">
-        <h3 class="name heading-3">
-          <span>[[run.checkName]]</span>
-        </h3>
-      </div>
-    </div>
-    <div class="section" hidden$="[[hideStatusSection(run)]]">
-      <div class="sectionIcon">
-        <iron-icon class="small" icon="gr-icons:info-outline"></iron-icon>
-      </div>
-      <div class="sectionContent">
-        <div hidden$="[[!run.statusLink]]" class="row">
-          <div class="title">Status</div>
-          <div>
-            <a href="[[_convertUndefined(run.statusLink)]]" target="_blank"
-              ><iron-icon
-                aria-label="external link to check status"
-                class="small link"
-                icon="gr-icons:launch"
-              ></iron-icon
-              >[[computeHostName(run.statusLink)]]
-            </a>
-          </div>
-        </div>
-        <div hidden$="[[!run.statusDescription]]" class="row">
-          <div class="title">Message</div>
-          <div>[[run.statusDescription]]</div>
-        </div>
-      </div>
-    </div>
-    <div class="section" hidden$="[[hideAttempts(run)]]">
-      <div class="sectionIcon">
-        <iron-icon class="small" icon="gr-icons:arrow-forward"></iron-icon>
-      </div>
-      <div class="sectionContent">
-        <div hidden$="[[hideAttempts(run)]]" class="attempts row">
-          <div class="title">Attempt</div>
-          <template is="dom-repeat" items="[[computeAttempts(run)]]">
-            <div>
-              <div class="attemptIcon">
-                <iron-icon
-                  class$="[[item.icon]]"
-                  icon="gr-icons:[[item.icon]]"
-                ></iron-icon>
-              </div>
-              <div class="attemptNumber">[[computeAttempt(item.attempt)]]</div>
-            </div>
-          </template>
-        </div>
-      </div>
-    </div>
-    <div class="section" hidden$="[[hideTimestampSection(run)]]">
-      <div class="sectionIcon">
-        <iron-icon class="small" icon="gr-icons:schedule"></iron-icon>
-      </div>
-      <div class="sectionContent">
-        <div hidden$="[[hideScheduled(run)]]" class="row">
-          <div class="title">Scheduled</div>
-          <div>[[computeDuration(run.scheduledTimestamp)]]</div>
-        </div>
-        <div hidden$="[[!run.startedTimestamp]]" class="row">
-          <div class="title">Started</div>
-          <div>[[computeDuration(run.startedTimestamp)]]</div>
-        </div>
-        <div hidden$="[[!run.finishedTimestamp]]" class="row">
-          <div class="title">Ended</div>
-          <div>[[computeDuration(run.finishedTimestamp)]]</div>
-        </div>
-        <div hidden$="[[hideCompletion(run)]]" class="row">
-          <div class="title">Completion</div>
-          <div>[[computeCompletionDuration(run)]]</div>
-        </div>
-      </div>
-    </div>
-    <div class="section" hidden$="[[hideDescriptionSection(run)]]">
-      <div class="sectionIcon">
-        <iron-icon class="small" icon="gr-icons:link"></iron-icon>
-      </div>
-      <div class="sectionContent">
-        <div hidden$="[[!run.checkDescription]]" class="row">
-          <div class="title">Description</div>
-          <div>[[run.checkDescription]]</div>
-        </div>
-        <div hidden$="[[!run.checkLink]]" class="row">
-          <div class="title">Documentation</div>
-          <div>
-            <a href="[[_convertUndefined(run.checkLink)]]" target="_blank"
-              ><iron-icon
-                aria-label="external link to check documentation"
-                class="small link"
-                icon="gr-icons:launch"
-              ></iron-icon
-              >[[computeHostName(run.checkLink)]]
-            </a>
-          </div>
-        </div>
-      </div>
-    </div>
-    <template is="dom-repeat" items="[[computeActions(run)]]">
-      <div class="action">
-        <gr-checks-action
-          event-target="[[_target]]"
-          action="[[item]]"
-        ></gr-checks-action>
-      </div>
-    </template>
-  </div>
-`;
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts
index 67781f5..352219a 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts
@@ -33,7 +33,7 @@
   });
 
   teardown(() => {
-    element.hide();
+    element.hide(new MouseEvent('click'));
   });
 
   test('hovercard is shown', () => {