[gold] Port dots-legend-sk to TypeScript.

Bug: skia:10246
Change-Id: Ib562f48cadce8b7e34d26279b69c656a59eed6d7
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/397336
Commit-Queue: Leandro Lovisolo <lovisolo@google.com>
Reviewed-by: Kevin Lubick <kjlubick@google.com>
diff --git a/golden/modules/dots-legend-sk/dots-legend-sk-demo.js b/golden/modules/dots-legend-sk/dots-legend-sk-demo.ts
similarity index 73%
rename from golden/modules/dots-legend-sk/dots-legend-sk-demo.js
rename to golden/modules/dots-legend-sk/dots-legend-sk-demo.ts
index 9f2df5b..0e02717 100644
--- a/golden/modules/dots-legend-sk/dots-legend-sk-demo.js
+++ b/golden/modules/dots-legend-sk/dots-legend-sk-demo.ts
@@ -1,7 +1,8 @@
 import './index';
-import { $$ } from 'common-sk/modules/dom';
+import { DigestStatus } from '../rpc_types';
+import { DotsLegendSk } from './dots-legend-sk';
 
-const someDigests = [
+const someDigests: DigestStatus[] = [
   { digest: 'ce0a9d2b546b25e00e39a33860cb72b6', status: 'untriaged' },
   { digest: '34e87ca0f753cf4c884fa01af6c08be9', status: 'positive' },
   { digest: '8ee9a2c61e9f12e6243f07423302f26a', status: 'negative' },
@@ -9,7 +10,7 @@
   { digest: 'dcccd6998b47f60ab28dcff17ae57ed2', status: 'positive' },
 ];
 
-const tooManyDigests = [
+const tooManyDigests: DigestStatus[] = [
   ...someDigests,
   { digest: '92d9faf80a25750629118018716387df', status: 'positive' },
   { digest: '1bc4771dcee95d97b2758a1e1945cc40', status: 'untriaged' },
@@ -18,14 +19,15 @@
   { digest: 'b00cb97f0d4dd7b22fb9af5378918d9f', status: 'untriaged' },
 ];
 
-function newDotsLegendSk(parentSelector, id, digests, clID, test) {
-  const dotsLegendSk = document.createElement('dots-legend-sk');
+function newDotsLegendSk(
+    parentSelector: string, id: string, digests: DigestStatus[], clID: string, test: string) {
+  const dotsLegendSk = new DotsLegendSk();
   dotsLegendSk.id = id;
   dotsLegendSk.digests = digests;
   dotsLegendSk.changeListID = clID;
   dotsLegendSk.test = test;
   dotsLegendSk.totalDigests = digests.length;
-  $$(parentSelector).appendChild(dotsLegendSk);
+  document.querySelector(parentSelector)!.appendChild(dotsLegendSk);
 }
 
 newDotsLegendSk(
diff --git a/golden/modules/dots-legend-sk/dots-legend-sk.js b/golden/modules/dots-legend-sk/dots-legend-sk.js
deleted file mode 100644
index 1a80f46..0000000
--- a/golden/modules/dots-legend-sk/dots-legend-sk.js
+++ /dev/null
@@ -1,162 +0,0 @@
-/**
- * @module modules/dots-legend-sk
- * @description <h2><code>dots-legend-sk</code></h2>
- *
- * A legend for the dots-sk element.
- */
-
-import { define } from 'elements-sk/define';
-import { html } from 'lit-html';
-import { ElementSk } from '../../../infra-sk/modules/ElementSk';
-import {
-  DOT_STROKE_COLORS,
-  DOT_FILL_COLORS,
-  MAX_UNIQUE_DIGESTS,
-} from '../dots-sk/constants';
-import { detailHref, diffPageHref } from '../common';
-
-import 'elements-sk/icon/cancel-icon-sk';
-import 'elements-sk/icon/check-circle-icon-sk';
-import 'elements-sk/icon/help-icon-sk';
-
-const template = (el) => html`
-  ${el._digests
-    .slice(0, MAX_UNIQUE_DIGESTS - 1)
-    .map((digest, index) => digestTemplate(digest, index, el))}
-
-  ${lastDigest(el)}
-`;
-
-const digestTemplate = (digest, index, el) => html`
-  ${dotTemplate(index)}
-  <a target=_blank class=digest href="${el._digestDetailHref(index)}">
-    ${digest.digest}
-  </a>
-  ${statusIconTemplate(digest.status)}
-  ${index > 0
-    ? html`<a target=_blank class=diff href="${el._digestDiffHref(index)}">
-               diff
-             </a>`
-    : html`<span></span>`}
-`;
-
-const lastDigest = (el) => {
-  // If the API returns fewer digests than MAX_UNIQUE_DIGESTS, we should compare against
-  // the reported totalDigests to determine if we need to display nothing (no more digests),
-  // the last digest (if it exactly matches the maximum) or the message saying there were too
-  // many digests to display them all.
-  if (el.totalDigests < MAX_UNIQUE_DIGESTS) {
-    return '';
-  }
-  if (el.totalDigests === MAX_UNIQUE_DIGESTS) {
-    return digestTemplate(el.digests[MAX_UNIQUE_DIGESTS - 1], MAX_UNIQUE_DIGESTS - 1, el);
-  }
-  return oneOfManyOtherDigestsTemplate(el.totalDigests);
-};
-
-const oneOfManyOtherDigestsTemplate = (totalDigests) => html`
-  ${dotTemplate(MAX_UNIQUE_DIGESTS - 1)}
-  <span class=one-of-many-other-digests>
-    One of ${totalDigests - (MAX_UNIQUE_DIGESTS - 1)} other digests
-    (${totalDigests} in total).
-  </span>
-`;
-
-const dotTemplate = (index) => {
-  const style = `border-color: ${DOT_STROKE_COLORS[index]};`
-      + `background-color: ${DOT_FILL_COLORS[index]};`;
-  return html`<div class=dot style="${style}"></div>`;
-};
-
-const statusIconTemplate = (status) => {
-  switch (status) {
-    case 'negative':
-      return html`<cancel-icon-sk class=negative-icon></cancel-icon-sk>`;
-    case 'positive':
-      return html`
-        <check-circle-icon-sk class=positive-icon></check-circle-icon-sk>
-      `;
-    case 'untriaged':
-      return html`<help-icon-sk class=untriaged-icon></help-icon-sk>`;
-    default:
-      throw `Unknown status: "${status}"`;
-  }
-};
-
-define('dots-legend-sk', class extends ElementSk {
-  constructor() {
-    super(template);
-    this._digests = [];
-    this._changeListID = '';
-    this._crs = '';
-    this._test = '';
-    this._totalDigests = 0;
-  }
-
-  connectedCallback() {
-    super.connectedCallback();
-    this._render();
-  }
-
-  /**
-   * @prop digests {Array} An array of {digest: 'a4f32...', status: 'positive'}
-   *   objects.
-   */
-  get digests() { return this._digests; }
-
-  set digests(digests) {
-    this._digests = digests;
-    this._render();
-  }
-
-  /**
-   * @prop changeListID {string} The changelist id (or empty string if this is the master branch).
-   */
-  get changeListID() { return this._changeListID; }
-
-  set changeListID(id) {
-    this._changeListID = id;
-    this._render();
-  }
-
-  /**
-   * @prop crs {string} The Code Review System (e.g. "gerrit") if changeListID is set.
-   */
-  get crs() { return this._crs; }
-
-  set crs(c) {
-    this._crs = c;
-    this._render();
-  }
-
-  /**
-   * @prop test {string} Test name.
-   */
-  get test() { return this._test; }
-
-  set test(test) {
-    this._test = test;
-    this._render();
-  }
-
-  /**
-   * @prop totalDigests {Number} The total number of digests that were seen in this group of traces,
-   *   which can be more than digests.length, due to the fact that the backend limits the length
-   *   of digests when it sends it to us.
-   */
-  get totalDigests() { return this._totalDigests; }
-
-  set totalDigests(td) {
-    this._totalDigests = td;
-    this._render();
-  }
-
-  _digestDetailHref(index) {
-    return detailHref(this._test, this._digests[index].digest, this.changeListID, this.crs);
-  }
-
-  _digestDiffHref(index) {
-    return diffPageHref(this._test, this._digests[0].digest, this._digests[index].digest,
-      this.changeListID, this.crs);
-  }
-});
diff --git a/golden/modules/dots-legend-sk/dots-legend-sk.ts b/golden/modules/dots-legend-sk/dots-legend-sk.ts
new file mode 100644
index 0000000..dd6860d
--- /dev/null
+++ b/golden/modules/dots-legend-sk/dots-legend-sk.ts
@@ -0,0 +1,158 @@
+/**
+ * @module modules/dots-legend-sk
+ * @description <h2><code>dots-legend-sk</code></h2>
+ *
+ * A legend for the dots-sk element.
+ */
+
+import { define } from 'elements-sk/define';
+import { html } from 'lit-html';
+import { ElementSk } from '../../../infra-sk/modules/ElementSk';
+import {
+  DOT_STROKE_COLORS,
+  DOT_FILL_COLORS,
+  MAX_UNIQUE_DIGESTS,
+} from '../dots-sk/constants';
+import { detailHref, diffPageHref } from '../common';
+import { DigestStatus, Label } from '../rpc_types';
+
+import 'elements-sk/icon/cancel-icon-sk';
+import 'elements-sk/icon/check-circle-icon-sk';
+import 'elements-sk/icon/help-icon-sk';
+
+export class DotsLegendSk extends ElementSk {
+  private static template = (el: DotsLegendSk) => html`
+    ${el._digests
+        .slice(0, MAX_UNIQUE_DIGESTS - 1)
+        .map((digest, index) => DotsLegendSk.digestTemplate(el, digest, index))}
+
+    ${DotsLegendSk.lastDigest(el)}
+  `;
+
+  private static digestTemplate = (el: DotsLegendSk, digest: DigestStatus, index: number) => html`
+    ${DotsLegendSk.dotTemplate(index)}
+    <a target=_blank class=digest href="${el.digestDetailHref(index)}">
+      ${digest.digest}
+    </a>
+    ${DotsLegendSk.statusIconTemplate(digest.status)}
+    ${index > 0
+        ? html`<a target=_blank class=diff href="${el.digestDiffHref(index)}">
+                 diff
+               </a>`
+        : html`<span></span>`}
+  `;
+
+  private static lastDigest = (el: DotsLegendSk) => {
+    // If the API returns fewer digests than MAX_UNIQUE_DIGESTS, we should compare against
+    // the reported totalDigests to determine if we need to display nothing (no more digests),
+    // the last digest (if it exactly matches the maximum) or the message saying there were too
+    // many digests to display them all.
+    if (el.totalDigests < MAX_UNIQUE_DIGESTS) {
+      return '';
+    }
+    if (el.totalDigests === MAX_UNIQUE_DIGESTS) {
+      return DotsLegendSk.digestTemplate(
+          el, el.digests[MAX_UNIQUE_DIGESTS - 1], MAX_UNIQUE_DIGESTS - 1);
+    }
+    return DotsLegendSk.oneOfManyOtherDigestsTemplate(el.totalDigests);
+  };
+
+  private static oneOfManyOtherDigestsTemplate = (totalDigests: number) => html`
+    ${DotsLegendSk.dotTemplate(MAX_UNIQUE_DIGESTS - 1)}
+    <span class=one-of-many-other-digests>
+      One of ${totalDigests - (MAX_UNIQUE_DIGESTS - 1)} other digests
+      (${totalDigests} in total).
+    </span>
+  `;
+
+  private static dotTemplate = (index: number) => {
+    const style = `border-color: ${DOT_STROKE_COLORS[index]};`
+        + `background-color: ${DOT_FILL_COLORS[index]};`;
+    return html`<div class=dot style="${style}"></div>`;
+  };
+
+  private static statusIconTemplate = (status: Label) => {
+    switch (status) {
+      case 'negative':
+        return html`<cancel-icon-sk class=negative-icon></cancel-icon-sk>`;
+      case 'positive':
+        return html`
+        <check-circle-icon-sk class=positive-icon></check-circle-icon-sk>
+      `;
+      case 'untriaged':
+        return html`<help-icon-sk class=untriaged-icon></help-icon-sk>`;
+      default:
+        throw `Unknown status: "${status}"`;
+    }
+  };
+
+  private _digests: DigestStatus[] = [];
+  private _changeListID = '';
+  private _crs = '';
+  private _test = '';
+  private _totalDigests = 0;
+
+  constructor() {
+    super(DotsLegendSk.template);
+  }
+
+  connectedCallback() {
+    super.connectedCallback();
+    this._render();
+  }
+
+  /** The digests to show. */
+  get digests(): DigestStatus[] { return this._digests; }
+
+  set digests(digests: DigestStatus[]) {
+    this._digests = digests;
+    this._render();
+  }
+
+  /** The changelist ID (or empty string if this is the master branch). */
+  get changeListID(): string { return this._changeListID; }
+
+  set changeListID(id: string) {
+    this._changeListID = id;
+    this._render();
+  }
+
+  /** The Code Review System (e.g. "gerrit") if changeListID is set. */
+  get crs(): string { return this._crs; }
+
+  set crs(c: string) {
+    this._crs = c;
+    this._render();
+  }
+
+  /** Test name. */
+  get test(): string { return this._test; }
+
+  set test(test: string) {
+    this._test = test;
+    this._render();
+  }
+
+  /**
+   * The total number of digests that were seen in this group of traces, which can be more than
+   * digests.length, due to the fact that the backend limits the length of digests when it sends it
+   * to us.
+   */
+  get totalDigests(): number { return this._totalDigests; }
+
+  set totalDigests(td: number) {
+    this._totalDigests = td;
+    this._render();
+  }
+
+  private digestDetailHref(index: number): string {
+    return detailHref(this._test, this._digests[index].digest, this.changeListID, this.crs);
+  }
+
+  private digestDiffHref(index: number): string {
+    return diffPageHref(this._test, this._digests[0].digest, this._digests[index].digest,
+      this.changeListID, this.crs);
+  }
+}
+
+define('dots-legend-sk', DotsLegendSk);
diff --git a/golden/modules/dots-legend-sk/dots-legend-sk_test.js b/golden/modules/dots-legend-sk/dots-legend-sk_test.ts
similarity index 83%
rename from golden/modules/dots-legend-sk/dots-legend-sk_test.js
rename to golden/modules/dots-legend-sk/dots-legend-sk_test.ts
index 1a3b266..32dbb29 100644
--- a/golden/modules/dots-legend-sk/dots-legend-sk_test.js
+++ b/golden/modules/dots-legend-sk/dots-legend-sk_test.ts
@@ -6,11 +6,13 @@
   MAX_UNIQUE_DIGESTS,
 } from '../dots-sk/constants';
 import { setUpElementUnderTest } from '../../../infra-sk/modules/test_util';
+import { DotsLegendSk } from './dots-legend-sk';
+import { expect } from 'chai';
 
 describe('dots-legend-sk', () => {
-  const newInstance = setUpElementUnderTest('dots-legend-sk');
+  const newInstance = setUpElementUnderTest<DotsLegendSk>('dots-legend-sk');
 
-  let dotsLegendSk;
+  let dotsLegendSk: DotsLegendSk;
   beforeEach(() => dotsLegendSk = newInstance());
 
   describe('with less than MAX_UNIQUE_DIGESTS unique digests', () => {
@@ -50,7 +52,7 @@
     });
 
     it('renders digest links correctly', () => {
-      const digestLinkFor = (d) => `/detail?test=My%20Test&digest=${d}`;
+      const digestLinkFor = (d: string) => `/detail?test=My%20Test&digest=${d}`;
       expect(digestLinks(dotsLegendSk)).to.deep.equal([
         digestLinkFor('00000000000000000000000000000000'),
         digestLinkFor('11111111111111111111111111111111'),
@@ -71,8 +73,8 @@
     });
 
     it('renders diff links correctly', () => {
-      const diffLinkFor = (d) => '/diff?test=My%20Test&left=00000000000000000000000000000000'
-          + `&right=${d}`;
+      const diffLinkFor =
+          (d: string) => `/diff?test=My%20Test&left=00000000000000000000000000000000&right=${d}`;
       expect(diffLinks(dotsLegendSk)).to.deep.equal([
         diffLinkFor('11111111111111111111111111111111'),
         diffLinkFor('22222222222222222222222222222222'),
@@ -89,8 +91,8 @@
       });
 
       it('renders digest links correctly', () => {
-        const digestLinkFor = (d) => `/detail?test=My%20Test&digest=${d}`
-          + '&changelist_id=123456&crs=gerrit';
+        const digestLinkFor = (d:string) =>
+            `/detail?test=My%20Test&digest=${d}&changelist_id=123456&crs=gerrit`;
         expect(digestLinks(dotsLegendSk)).to.deep.equal([
           digestLinkFor('00000000000000000000000000000000'),
           digestLinkFor('11111111111111111111111111111111'),
@@ -101,8 +103,9 @@
       });
 
       it('renders diff links correctly', () => {
-        const diffLinkFor = (d) => '/diff?test=My%20Test&left=00000000000000000000000000000000'
-            + `&right=${d}&changelist_id=123456&crs=gerrit`;
+        const diffLinkFor = (d: string) =>
+            '/diff?test=My%20Test&left=00000000000000000000000000000000' +
+            `&right=${d}&changelist_id=123456&crs=gerrit`;
         expect(diffLinks(dotsLegendSk)).to.deep.equal([
           diffLinkFor('11111111111111111111111111111111'),
           diffLinkFor('22222222222222222222222222222222'),
@@ -175,8 +178,8 @@
     });
 
     it('renders diff links correctly', () => {
-      const diffLinkFor = (d) => '/diff?test=My%20Test&left=00000000000000000000000000000000'
-        + `&right=${d}`;
+      const diffLinkFor = (d: string) =>
+          `/diff?test=My%20Test&left=00000000000000000000000000000000&right=${d}`;
       expect(diffLinks(dotsLegendSk)).to.deep.equal([
         diffLinkFor('11111111111111111111111111111111'),
         diffLinkFor('22222222222222222222222222222222'),
@@ -255,8 +258,8 @@
     });
 
     it('renders diff links correctly', () => {
-      const diffLinkFor = (d) => '/diff?test=My%20Test&left=00000000000000000000000000000000'
-          + `&right=${d}`;
+      const diffLinkFor = (d: string) =>
+          `/diff?test=My%20Test&left=00000000000000000000000000000000&right=${d}`;
       expect(diffLinks(dotsLegendSk)).to.deep.equal([
         diffLinkFor('11111111111111111111111111111111'),
         diffLinkFor('22222222222222222222222222222222'),
@@ -272,29 +275,30 @@
 
 // Takes a color represented as an RGB string (e.g. "rgb(10, 187, 204)") and
 // returns the equivalent hex string (e.g. "#0ABBCC").
-const rgbToHex = (rgb) => `#${rgb.match(/rgb\((\d+), (\d+), (\d+)\)/)
+const rgbToHex = (rgb: string): string => `#${rgb.match(/rgb\((\d+), (\d+), (\d+)\)/)!
   .slice(1) // ['10', '187', '204'].
-  .map((x) => parseInt(x)) // [10, 187, 204]
-  .map((x) => x.toString(16)) // ['a', 'bb', 'cc']
-  .map((x) => x.padStart(2, '0')) // ['0a', 'bb', 'cc']
-  .map((x) => x.toUpperCase()) // ['0A', 'BB', 'CC']
+  .map((x: string) => parseInt(x)) // [10, 187, 204]
+  .map((x: number) => x.toString(16)) // ['a', 'bb', 'cc']
+  .map((x: string) => x.padStart(2, '0')) // ['0a', 'bb', 'cc']
+  .map((x: string) => x.toUpperCase()) // ['0A', 'BB', 'CC']
   .join('')}`; // '0ABBCC'
 
 // Returns the dot colors as an array of arrays of the form
 // ["stroke color", "fill color"], where the colors are represented as hex
 // strings (e.g. "#AABBCC").
-const dotColors = (dotsLegendSk) => $('div.dot', dotsLegendSk)
-  .map((dot) => [
-    rgbToHex(dot.style.borderColor),
-    rgbToHex(dot.style.backgroundColor),
-  ]);
+const dotColors = (dotsLegendSk: DotsLegendSk): [string, string][] =>
+    $<HTMLDivElement>('div.dot', dotsLegendSk).map((dot) => [
+      rgbToHex(dot.style.borderColor),
+      rgbToHex(dot.style.backgroundColor),
+    ]);
 
-const digests = (dotsLegendSk) => $('a.digest, span.one-of-many-other-digests', dotsLegendSk)
-  .map((a) => a.innerText.trim());
+const digests = (dotsLegendSk: DotsLegendSk): string[] =>
+    $<HTMLElement>('a.digest, span.one-of-many-other-digests', dotsLegendSk)
+        .map((a) => a.innerText.trim());
 
-// Returns the status icons  as an array of strings. Possible values are
+// Returns the status icons as an array of strings. Possible values are
 // are "negative", "positive", "untriaged".
-const statusIcons = (dotsLegendSk) => $([
+const statusIcons = (dotsLegendSk: DotsLegendSk): string[] => $([
   'cancel-icon-sk.negative-icon',
   'check-circle-icon-sk.positive-icon',
   'help-icon-sk.untriaged-icon',
@@ -303,11 +307,13 @@
 
 // Takes an URL string (e.g. "http://example.com/search?q=hello") and returns
 // only the path and query string (e.g. "/search?q=hello").
-const urlToPathAndQueryString = (urlStr) => {
+const urlToPathAndQueryString = (urlStr: string): string => {
   const url = new URL(urlStr);
   return url.pathname + url.search;
 };
 
-const digestLinks = (dotsLegendSk) => $('a.digest', dotsLegendSk).map((a) => urlToPathAndQueryString(a.href));
+const digestLinks = (dotsLegendSk: DotsLegendSk): string[] =>
+    $<HTMLAnchorElement>('a.digest', dotsLegendSk).map((a) => urlToPathAndQueryString(a.href));
 
-const diffLinks = (dotsLegendSk) => $('a.diff', dotsLegendSk).map((a) => urlToPathAndQueryString(a.href));
+const diffLinks = (dotsLegendSk: DotsLegendSk): string[] =>
+    $<HTMLAnchorElement>('a.diff', dotsLegendSk).map((a) => urlToPathAndQueryString(a.href));
diff --git a/golden/modules/dots-legend-sk/index.js b/golden/modules/dots-legend-sk/index.ts
similarity index 100%
rename from golden/modules/dots-legend-sk/index.js
rename to golden/modules/dots-legend-sk/index.ts