Merge "Replace "RESET VIEW" in results panel by "UNSELECT ALL" in runs panel"
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index f6b3aa0..8e1ffef 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -302,6 +302,15 @@
   buttonType: ContextButtonType;
 }
 
+/**
+ * Details to be externally accessed when hovering context
+ * expansion buttons
+ */
+export declare interface DiffContextButtonHoveredDetail {
+  linesToExpand: number;
+  buttonType: ContextButtonType;
+}
+
 export declare type ImageDiffAction =
   | {type: 'overview-image-clicked'}
   | {type: 'overview-frame-dragged'}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
index ea5339e..2086270 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
@@ -108,6 +108,8 @@
 
   private readonly restApiService = appContext.restApiService;
 
+  private reporting = appContext.reportingService;
+
   constructor() {
     super();
     this.addEventListener('next-page', () => this._handleNextPage());
@@ -272,6 +274,9 @@
   }
 
   _handleToggleStar(e: CustomEvent<ChangeStarToggleStarDetail>) {
+    if (e.detail.starred) {
+      this.reporting.reportInteraction('change-starred-from-change-list');
+    }
     this.restApiService.saveChangeStarred(
       e.detail.change._number,
       e.detail.starred
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
index bde7d77..e93b293 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
@@ -372,6 +372,9 @@
       e.detail.change._number,
       e.detail.starred
     );
+    if (e.detail.starred) {
+      this.reporting.reportInteraction('change-starred-from-dashboard');
+    }
     // When a change is updated the same change may appear elsewhere in the
     // dashboard (but is not the same object), so we must update other
     // occurrences of the same change.
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 f05d665..a11b142 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
@@ -2527,6 +2527,7 @@
 
   _handleToggleStar(e: CustomEvent<{change: ChangeInfo; starred: boolean}>) {
     if (e.detail.starred) {
+      this.reporting.reportInteraction('change-starred-from-change-view');
       this.lastStarredTimestamp = Date.now();
     } else {
       if (
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index e98ab61..a4ac01d 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -165,8 +165,8 @@
         tr.collapsed td .summary-cell .actions {
           display: none;
         }
-        tr.collapsed:hover .summary-cell .tags,
-        tr.collapsed:hover .summary-cell .label {
+        tr.collapsed:hover .summary-cell .hoverHide.tags,
+        tr.collapsed:hover .summary-cell .hoverHide.label {
           display: none;
         }
         td .summary-cell .tags .tag {
@@ -255,6 +255,7 @@
           <div>
             <span>${this.result.checkName}</span>
             <gr-checks-attempt .run="${this.result}"></gr-checks-attempt>
+            <gr-hovercard-run .run="${this.result}"></gr-hovercard-run>
           </div>
         </td>
         <td class="summaryCol">
@@ -264,7 +265,7 @@
             <div class="message" @click="${this.toggleExpanded}">
               ${this.isExpanded ? '' : this.result.message}
             </div>
-            <div class="tags">
+            <div class="tags ${this.hasLinksOrActions() ? 'hoverHide' : ''}">
               ${(this.result.tags ?? []).map(t => this.renderTag(t))}
             </div>
             ${this.renderLabel()} ${this.renderLinks()} ${this.renderActions()}
@@ -294,6 +295,13 @@
     `;
   }
 
+  private hasLinksOrActions() {
+    const linkCount = this.result?.links?.length ?? 0;
+    const actionCount = this.result?.actions?.length ?? 0;
+    // The primary link is rendered somewhere else, so it does not count here.
+    return linkCount > 1 || actionCount > 0;
+  }
+
   private renderExpanded() {
     if (!this.isExpanded) return;
     return html`<gr-result-expanded
@@ -323,7 +331,11 @@
   renderLabel() {
     const label = this.result?.labelName;
     if (!label) return;
-    return html`<div class="label">${label}</div>`;
+    return html`
+      <div class="label ${this.hasLinksOrActions() ? 'hoverHide' : ''}">
+        ${label}
+      </div>
+    `;
   }
 
   renderLinks() {
@@ -455,6 +467,21 @@
 SHOW_ALL_THRESHOLDS.set(Category.INFO, 5);
 SHOW_ALL_THRESHOLDS.set(Category.SUCCESS, 5);
 
+const CATEGORY_TOOLTIPS: Map<Category, string> = new Map();
+CATEGORY_TOOLTIPS.set(Category.ERROR, 'Must be fixed and is blocking submit');
+CATEGORY_TOOLTIPS.set(
+  Category.WARNING,
+  'Should be checked but is not blocking submit'
+);
+CATEGORY_TOOLTIPS.set(
+  Category.INFO,
+  'Does not have to be checked, for your information only'
+);
+CATEGORY_TOOLTIPS.set(
+  Category.SUCCESS,
+  'Successful runs without results and individual successful results'
+);
+
 @customElement('gr-checks-results')
 export class GrChecksResults extends GrLitElement {
   @query('#filterInput')
@@ -601,6 +628,9 @@
           height: var(--line-height-h3);
           margin-right: var(--spacing-s);
         }
+        .categoryHeader .statusIconWrapper {
+          display: inline-block;
+        }
         .categoryHeader .statusIcon {
           position: relative;
           top: 2px;
@@ -867,11 +897,16 @@
           @click="${() => this.toggleExpanded(category)}"
         >
           <iron-icon class="expandIcon" icon="${icon}"></iron-icon>
-          <iron-icon
-            icon="gr-icons:${iconForCategory(category)}"
-            class="statusIcon ${catString}"
-          ></iron-icon>
-          <span class="title">${catString}</span>
+          <div class="statusIconWrapper">
+            <iron-icon
+              icon="gr-icons:${iconForCategory(category)}"
+              class="statusIcon ${catString}"
+            ></iron-icon>
+            <span class="title">${catString}</span>
+            <paper-tooltip offset="5"
+              >${CATEGORY_TOOLTIPS.get(category)}</paper-tooltip
+            >
+          </div>
           <span class="count"
             >${this.renderCount(all, selected, filtered)}</span
           >
diff --git a/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls.ts b/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls.ts
index 41a448e..dec5111 100644
--- a/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls.ts
+++ b/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls.ts
@@ -23,6 +23,8 @@
 import '@polymer/paper-item/paper-item';
 import '@polymer/paper-listbox/paper-listbox';
 import '@polymer/paper-tooltip/paper-tooltip.js';
+import {of, EMPTY, Subject} from 'rxjs';
+import {switchMap, delay, takeUntil} from 'rxjs/operators';
 
 import '../../shared/gr-button/gr-button';
 import {pluralize} from '../../../utils/string-util';
@@ -40,12 +42,19 @@
 
 import {
   ContextButtonType,
+  DiffContextButtonHoveredDetail,
   RenderPreferences,
   SyntaxBlock,
 } from '../../../api/diff';
 
 import {GrDiffGroup, hideInContextControl} from '../gr-diff/gr-diff-group';
 
+declare global {
+  interface HTMLElementEventMap {
+    'diff-context-button-hovered': CustomEvent<DiffContextButtonHoveredDetail>;
+  }
+}
+
 const PARTIAL_CONTEXT_AMOUNT = 10;
 
 /**
@@ -88,6 +97,14 @@
 
   @property({type: Boolean}) showBelow = false;
 
+  private expandButtonsHover = new Subject<{
+    eventType: 'enter' | 'leave';
+    buttonType: ContextButtonType;
+    linesToExpand: number;
+  }>();
+
+  private disconnected$ = new Subject();
+
   static styles = css`
     :host {
       display: flex;
@@ -106,6 +123,26 @@
       /* First button needs to claim width to display without text wrapping. */
       position: relative;
     }
+
+    paper-button {
+      text-transform: none;
+      align-items: center;
+      background-color: var(--background-color);
+      font-family: inherit;
+      margin: var(--margin, 0);
+      min-width: var(--border, 0);
+      color: var(--diff-context-control-color);
+      border: solid var(--border-color);
+      border-width: 1px;
+      border-radius: var(--border-radius);
+      padding: var(--spacing-s) var(--spacing-l);
+    }
+
+    paper-button:hover {
+      /* same as defined in gr-button */
+      background: rgba(0, 0, 0, 0.12);
+    }
+
     .centeredButton {
       /* Center over divider. */
       top: 50%;
@@ -124,74 +161,52 @@
     .aboveButton {
       /* Display over preceding content / background placeholder. */
       transform: translateY(-100%);
+      border-bottom-width: 1px;
+      border-bottom-right-radius: 0;
+      border-bottom-left-radius: 0;
+      padding: var(--spacing-xxs) var(--spacing-l);
     }
     .belowButton {
       top: calc(100% + var(--divider-border));
+      border-top-width: 0;
+      border-top-left-radius: 0;
+      border-top-right-radius: 0;
+      padding: var(--spacing-xxs) var(--spacing-l);
     }
     .breadcrumbTooltip {
       white-space: nowrap;
     }
   `;
 
-  // To pass CSS mixins for @apply to Polymer components, they need to be
-  // wrapped in a <custom-style>.
-  static customStyles = html`
-    <custom-style>
-      <style>
-        .centeredButton {
-          --gr-button: {
-            color: var(--diff-context-control-color);
-            border-style: solid;
-            border-color: var(--border-color);
-            border-top-width: 1px;
-            border-right-width: 1px;
-            border-bottom-width: 1px;
-            border-left-width: 1px;
+  connectedCallback() {
+    super.connectedCallback();
+    this.setupButtonHoverHandler();
+  }
 
-            border-top-left-radius: var(--border-radius);
-            border-top-right-radius: var(--border-radius);
-            border-bottom-right-radius: var(--border-radius);
-            border-bottom-left-radius: var(--border-radius);
-            padding: var(--spacing-s) var(--spacing-l);
-          }
-        }
-        .aboveButton {
-          --gr-button: {
-            color: var(--diff-context-control-color);
-            border-style: solid;
-            border-color: var(--border-color);
-            border-top-width: 1px;
-            border-right-width: 1px;
-            border-bottom-width: 0;
-            border-left-width: 1px;
+  disconnectedCallback() {
+    this.disconnected$.next();
+  }
 
-            border-top-left-radius: var(--border-radius);
-            border-top-right-radius: var(--border-radius);
-            border-bottom-right-radius: 0;
-            border-bottom-left-radius: var(--border-radius);
-            padding: var(--spacing-xxs) var(--spacing-l);
+  setupButtonHoverHandler() {
+    this.expandButtonsHover
+      .pipe(
+        switchMap(e => {
+          if (e.eventType === 'leave') {
+            // cancel any previous delay
+            // for mouse enter
+            return EMPTY;
           }
-        }
-        .belowButton {
-          --gr-button: {
-            color: var(--diff-context-control-color);
-            border-style: solid;
-            border-color: var(--border-color);
-            border-top-width: 0;
-            border-right-width: 1px;
-            border-bottom-width: 1px;
-            border-left-width: 1px;
-
-            border-top-left-radius: 0;
-            border-top-right-radius: 0;
-            border-bottom-right-radius: var(--border-radius);
-            border-bottom-left-radius: var(--border-radius);
-            padding: var(--spacing-xxs) var(--spacing-l);
-          }
-        }
-      </style>
-    </custom-style>
-  `;
+          return of(e).pipe(delay(500));
+        }),
+        takeUntil(this.disconnected$)
+      )
+      .subscribe(({buttonType, linesToExpand}) => {
+        fire(this, 'diff-context-button-hovered', {
+          buttonType,
+          linesToExpand,
+        });
+      });
+  }
 
   private numLines() {
     const {leftStart, leftEnd} = this.contextRange();
@@ -276,16 +291,24 @@
       groups
     );
 
-    const button = html` <gr-button
+    const mouseHandler = (eventType: 'enter' | 'leave') => {
+      this.expandButtonsHover.next({
+        eventType,
+        buttonType: type,
+        linesToExpand,
+      });
+    };
+
+    const button = html` <paper-button
       class="${classes}"
-      link="true"
-      no-uppercase="true"
       aria-label="${ariaLabel}"
       @click="${expandHandler}"
+      @mouseenter="${() => mouseHandler('enter')}"
+      @mouseleave="${() => mouseHandler('leave')}"
     >
       <span class="showContext">${text}</span>
       ${tooltip}
-    </gr-button>`;
+    </paper-button>`;
     return button;
   }
 
@@ -468,7 +491,7 @@
       return html`<p>invalid properties</p>`;
     }
     return html`
-      ${GrContextControls.customStyles} ${this.createExpandAllButtonContainer()}
+      ${this.createExpandAllButtonContainer()}
       ${this.createPartialExpansionButtons()}
       ${this.createBlockExpansionButtons()}
     `;
diff --git a/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls_test.ts b/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls_test.ts
index f59b19d..4e75c82 100644
--- a/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls_test.ts
@@ -60,7 +60,7 @@
     await flush();
 
     const buttons = element.shadowRoot!.querySelectorAll(
-      'gr-button.showContext'
+      'paper-button.showContext'
     );
     assert.equal(buttons.length, 1);
     assert.equal(buttons[0].textContent!.trim(), '+10 common lines');
@@ -73,7 +73,7 @@
     await flush();
 
     const buttons = element.shadowRoot!.querySelectorAll(
-      'gr-button.showContext'
+      'paper-button.showContext'
     );
 
     assert.equal(buttons.length, 2);
@@ -92,7 +92,7 @@
     await flush();
 
     const buttons = element.shadowRoot!.querySelectorAll(
-      'gr-button.showContext'
+      'paper-button.showContext'
     );
 
     assert.equal(buttons.length, 3);
@@ -112,7 +112,7 @@
     await flush();
 
     const buttons = element.shadowRoot!.querySelectorAll(
-      'gr-button.showContext'
+      'paper-button.showContext'
     );
 
     assert.equal(buttons.length, 2);
@@ -138,13 +138,13 @@
     await flush();
 
     const fullExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.fullExpansion gr-button'
+      '.fullExpansion paper-button'
     );
     const partialExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.partialExpansion gr-button'
+      '.partialExpansion paper-button'
     );
     const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.blockExpansion gr-button'
+      '.blockExpansion paper-button'
     );
     assert.equal(fullExpansionButtons.length, 1);
     assert.equal(partialExpansionButtons.length, 1);
@@ -168,13 +168,13 @@
     await flush();
 
     const fullExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.fullExpansion gr-button'
+      '.fullExpansion paper-button'
     );
     const partialExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.partialExpansion gr-button'
+      '.partialExpansion paper-button'
     );
     const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.blockExpansion gr-button'
+      '.blockExpansion paper-button'
     );
     assert.equal(fullExpansionButtons.length, 1);
     assert.equal(partialExpansionButtons.length, 2);
@@ -205,13 +205,13 @@
     await flush();
 
     const fullExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.fullExpansion gr-button'
+      '.fullExpansion paper-button'
     );
     const partialExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.partialExpansion gr-button'
+      '.partialExpansion paper-button'
     );
     const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.blockExpansion gr-button'
+      '.blockExpansion paper-button'
     );
     assert.equal(fullExpansionButtons.length, 1);
     assert.equal(partialExpansionButtons.length, 1);
@@ -246,7 +246,7 @@
     await flush();
 
     const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.blockExpansion gr-button'
+      '.blockExpansion paper-button'
     );
     assert.equal(
       blockExpansionButtons[0]
@@ -299,7 +299,7 @@
     await flush();
 
     const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.blockExpansion gr-button'
+      '.blockExpansion paper-button'
     );
     assert.equal(
       blockExpansionButtons[0]
@@ -345,7 +345,7 @@
     await flush();
 
     const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.blockExpansion gr-button'
+      '.blockExpansion paper-button'
     );
     assert.equal(
       blockExpansionButtons[0]
@@ -364,7 +364,7 @@
     await flush();
 
     const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.blockExpansion gr-button'
+      '.blockExpansion paper-button'
     );
     const tooltipAbove = blockExpansionButtons[0].querySelector(
       'paper-tooltip'