[gold] Add triage-sk lit-html component.

Bug: skia:9525
Change-Id: I7ccecfb019a43a939c99f813f623381a6c0df0ae
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/265150
Commit-Queue: Leandro Lovisolo <lovisolo@google.com>
Reviewed-by: Kevin Lubick <kjlubick@google.com>
diff --git a/golden/modules/triage-sk/index.js b/golden/modules/triage-sk/index.js
new file mode 100644
index 0000000..f787c43
--- /dev/null
+++ b/golden/modules/triage-sk/index.js
@@ -0,0 +1,2 @@
+import './triage-sk.scss'
+import './triage-sk.js'
diff --git a/golden/modules/triage-sk/triage-sk-demo.html b/golden/modules/triage-sk/triage-sk-demo.html
new file mode 100644
index 0000000..545f35d
--- /dev/null
+++ b/golden/modules/triage-sk/triage-sk-demo.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>triage-sk</title>
+  <meta charset="utf-8"/>
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <style>
+  #event-log-container {
+    border: 1px solid lightgrey;
+    margin: 5em 1em 1em;
+    padding: 1em;
+  }
+  #event-log {
+    font-family: monospace;
+  }
+  </style>
+</head>
+<body>
+<div id=container>
+  <!-- A triage-sk element will be inserted here dynamically. -->
+</div>
+
+<div id=event-log-container>
+  <p><strong>Event log</strong></p>
+  <textarea id=event-log readonly rows=30 cols=120></textarea>
+</div>
+</body>
+</html>
diff --git a/golden/modules/triage-sk/triage-sk-demo.js b/golden/modules/triage-sk/triage-sk-demo.js
new file mode 100644
index 0000000..fc492c4
--- /dev/null
+++ b/golden/modules/triage-sk/triage-sk-demo.js
@@ -0,0 +1,19 @@
+import './index.js';
+import { $$ } from 'common-sk/modules/dom';
+import { isPuppeteerTest } from '../demo_util';
+
+const logEventDetail = (e) => {
+  const log = $$("#event-log");
+  const entry = `${new Date().toISOString()}\t${e.detail}\n`
+  log.value = entry + log.value;
+};
+
+const triageSk = document.createElement('triage-sk');
+triageSk.addEventListener('change', logEventDetail);
+$$('#container').append(triageSk);
+
+// Hide event log if we're within a Puppeteer test. We don't need the event log
+// to appear in any screenshots uploaded to Gold.
+if (isPuppeteerTest()) {
+  $$('#event-log-container').style.display = 'none';
+}
diff --git a/golden/modules/triage-sk/triage-sk.js b/golden/modules/triage-sk/triage-sk.js
new file mode 100644
index 0000000..4b23535
--- /dev/null
+++ b/golden/modules/triage-sk/triage-sk.js
@@ -0,0 +1,75 @@
+/**
+ * @module modules/triage-sk
+ * @description <h2><code>triage-sk</code></h2>
+ *
+ * A custom element that allows labeling a digest as positive, negative or
+ * untriaged.
+ *
+ * @evt change - Sent when any of the triage buttons are clicked. The new value
+ *     will be contained in event.detail (possible values are "untriaged",
+ *     "positive" or "negative").
+ */
+
+import 'elements-sk/styles/buttons';
+import 'elements-sk/icon/check-circle-icon-sk';
+import 'elements-sk/icon/cancel-icon-sk';
+import 'elements-sk/icon/help-icon-sk';
+import { define } from 'elements-sk/define';
+import { ElementSk } from '../../../infra-sk/modules/ElementSk';
+import { html } from 'lit-html';
+import { classMap } from 'lit-html/directives/class-map.js';
+
+const POSITIVE = 'positive';
+const NEGATIVE = 'negative';
+const UNTRIAGED = 'untriaged';
+
+const template = (el) => html`
+  <button class=${classMap({'positive': true,
+                            'selected': el.value === POSITIVE})}
+          @click=${() => el._buttonClicked(POSITIVE)}>
+    <check-circle-icon-sk></check-circle-icon-sk>
+  </button>
+  <button class=${classMap({'negative': true,
+                            'selected': el.value === NEGATIVE})}
+          @click=${() => el._buttonClicked(NEGATIVE)}>
+    <cancel-icon-sk></cancel-icon-sk>
+  </button>
+  <button class=${classMap({'untriaged': true,
+                            'selected': el.value === UNTRIAGED})}
+          @click=${() => el._buttonClicked(UNTRIAGED)}>
+    <help-icon-sk></help-icon-sk>
+  </button>
+`;
+
+define('triage-sk', class extends ElementSk {
+  constructor() {
+    super(template);
+    this._value = UNTRIAGED;
+  }
+
+  connectedCallback() {
+    super.connectedCallback();
+    this._render();
+  }
+
+  /** @prop value {string} One of "untriaged", "positive" or "negative". */
+  get value() {
+    return this._value;
+  }
+  set value(newValue) {
+    if (![POSITIVE, NEGATIVE, UNTRIAGED].includes(newValue)) {
+      throw new RangeError(`Invalid triage-sk value: "${newValue}".`);
+    }
+    this._value = newValue;
+    this._render();
+  }
+
+  _buttonClicked(newValue) {
+    if (this.value === newValue) {
+      return;
+    }
+    this.value = newValue;
+    this.dispatchEvent(
+        new CustomEvent('change', {detail: newValue, bubbles: true}));
+  }
+});
diff --git a/golden/modules/triage-sk/triage-sk.scss b/golden/modules/triage-sk/triage-sk.scss
new file mode 100644
index 0000000..b93b358
--- /dev/null
+++ b/golden/modules/triage-sk/triage-sk.scss
@@ -0,0 +1,32 @@
+triage-sk {
+  display: inline-block;
+
+  button {
+    min-width: 0;
+    padding: 0.6em;
+
+    &.selected {
+      background-color: var(--dark-white);
+      border: none;
+      box-shadow: none;
+    }
+
+    &.positive {
+      &.selected check-circle-icon-sk {
+        fill: #1B9E77;
+      }
+    }
+
+    &.negative {
+      &.selected cancel-icon-sk {
+        fill: #E7298A;
+      }
+    }
+
+    &.untriaged {
+      &.selected help-icon-sk {
+        fill: #A6761D;
+      }
+    }
+  }
+}
diff --git a/golden/modules/triage-sk/triage-sk_test.js b/golden/modules/triage-sk/triage-sk_test.js
new file mode 100644
index 0000000..c3b617e
--- /dev/null
+++ b/golden/modules/triage-sk/triage-sk_test.js
@@ -0,0 +1,85 @@
+import './index.js';
+import { eventPromise, noEventPromise } from '../test_util';
+import { $$ } from 'common-sk/modules/dom';
+
+describe('triage-sk', function() {
+  let triageSk;
+
+  beforeEach(() => {
+    triageSk = document.createElement('triage-sk');
+    document.body.appendChild(triageSk);
+  });
+
+  afterEach(() => {
+    // Remove the stale instance under test.
+    if (triageSk) {
+      document.body.removeChild(triageSk);
+      triageSk = null;
+    }
+  });
+
+  it('is untriaged by default', () => {
+    expectValueAndToggledButtonToBe(triageSk, 'untriaged');
+  });
+
+  describe('"value" property setter/getter', () => {
+    it('sets and gets value via property', () => {
+      triageSk.value = 'positive';
+      expectValueAndToggledButtonToBe(triageSk, 'positive');
+
+      triageSk.value = 'negative';
+      expectValueAndToggledButtonToBe(triageSk, 'negative');
+
+      triageSk.value = 'untriaged';
+      expectValueAndToggledButtonToBe(triageSk, 'untriaged');
+    });
+
+    it('does not emit event "change" when setting value via property',
+        async () => {
+      const noTriageEvent = noEventPromise('change');
+      triageSk.value = 'positive';
+      await noTriageEvent;
+    });
+
+    it('throws an exception upon an invalid value', () => {
+      expect(() => triageSk.value = 'hello world')
+          .to.throw(RangeError, 'Invalid triage-sk value: "hello world".');
+    });
+  });
+
+  describe('buttons', () => {
+    let changeEvent;
+    beforeEach(() => { changeEvent = eventPromise('change', 100); });
+
+    it('sets value to positive when clicking positive button', async () => {
+      $$('button.positive', triageSk).click();
+      expectValueAndToggledButtonToBe(triageSk, 'positive');
+      expect((await changeEvent).detail).to.equal('positive');
+    });
+
+    it('sets value to negative when clicking negative button', async () => {
+      $$('button.negative', triageSk).click();
+      expectValueAndToggledButtonToBe(triageSk, 'negative');
+      expect((await changeEvent).detail).to.equal('negative');
+    });
+
+    it('sets value to untriaged when clicking untriaged button', async () => {
+      triageSk.value = 'positive';  // Untriaged by default; change value first.
+      $$('button.untriaged', triageSk).click();
+      expectValueAndToggledButtonToBe(triageSk, 'untriaged');
+      expect((await changeEvent).detail).to.equal('untriaged');
+    });
+
+    it('does not emit event "change" when clicking button for current value',
+        async () => {
+      const noChangeEvent = noEventPromise('change');
+      $$('button.untriaged', triageSk).click();
+      await noChangeEvent;
+    });
+  });
+});
+
+const expectValueAndToggledButtonToBe = (triageSk, value) => {
+  expect(triageSk.value).to.equal(value);
+  expect($$(`button.${value}`, triageSk).className).to.contain('selected');
+};
diff --git a/golden/puppeteer-tests/test/triage-sk_puppeteer_test.js b/golden/puppeteer-tests/test/triage-sk_puppeteer_test.js
new file mode 100644
index 0000000..3096a1a
--- /dev/null
+++ b/golden/puppeteer-tests/test/triage-sk_puppeteer_test.js
@@ -0,0 +1,42 @@
+const expect = require('chai').expect;
+const setUpPuppeteerAndDemoPageServer = require('./util').setUpPuppeteerAndDemoPageServer;
+const takeScreenshot = require('./util').takeScreenshot;
+
+describe('triage-sk', function() {
+  setUpPuppeteerAndDemoPageServer();  // Sets up this.page and this.baseUrl.
+
+  beforeEach(async function() {
+    await this.page.goto(`${this.baseUrl}/dist/triage-sk.html`);
+  });
+
+  it('should render the demo page', async function() {
+    expect(await this.page.$$('triage-sk')).to.have.length(1);  // Smoke test.
+  });
+
+  describe('screenshots', async function() {
+    it('should be untriaged by default', async function() {
+      const triageSk = await this.page.$('triage-sk');
+      await takeScreenshot(triageSk, 'triage-sk_untriaged');
+    });
+
+    it('should be negative', async function() {
+      await this.page.click('triage-sk button.negative');
+      await this.page.click('body');  // Remove focus from button.
+      const triageSk = await this.page.$('triage-sk');
+      await takeScreenshot(triageSk, 'triage-sk_negative');
+    });
+
+    it('should be positive', async function() {
+      await this.page.click('triage-sk button.positive');
+      await this.page.click('body');  // Remove focus from button.
+      const triageSk = await this.page.$('triage-sk');
+      await takeScreenshot(triageSk, 'triage-sk_positive');
+    });
+
+    it('should be positive, with button focused', async function() {
+      await this.page.click('triage-sk button.positive');
+      const triageSk = await this.page.$('triage-sk');
+      await takeScreenshot(triageSk, 'triage-sk_positive-button-focused');
+    });
+  });
+});