[gold] Add checkbox and delete rule interaction to ignore page.

Change-Id: I1f239d44c56e57a766402e08ab0c679ba6e322f2
Bug: skia:9525
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/263710
Commit-Queue: Kevin Lubick <kjlubick@google.com>
Reviewed-by: Leandro Lovisolo <lovisolo@google.com>
diff --git a/golden/modules/ignores-page-sk/ignores-page-sk-demo.js b/golden/modules/ignores-page-sk/ignores-page-sk-demo.js
index 5f7224d..ed63be6 100644
--- a/golden/modules/ignores-page-sk/ignores-page-sk-demo.js
+++ b/golden/modules/ignores-page-sk/ignores-page-sk-demo.js
@@ -9,4 +9,5 @@
 Date.now = () => fakeNow;
 
 fetchMock.get('/json/ignores?counts=1', delay(ignoreRules_10, 300));
+fetchMock.post('glob:/json/ignores/del/*', delay({}, 600));
 fetchMock.catch(404);
diff --git a/golden/modules/ignores-page-sk/ignores-page-sk.js b/golden/modules/ignores-page-sk/ignores-page-sk.js
index 52b824f..b98bbae 100644
--- a/golden/modules/ignores-page-sk/ignores-page-sk.js
+++ b/golden/modules/ignores-page-sk/ignores-page-sk.js
@@ -5,19 +5,22 @@
  * Page to view/edit/delete ignore rules.
  */
 
-import * as human from 'common-sk/modules/human'
+import * as human from 'common-sk/modules/human';
 
+import { $$ } from 'common-sk/modules/dom';
 import { classMap } from 'lit-html/directives/class-map.js';
-import { define } from 'elements-sk/define'
-import { ElementSk } from '../../../infra-sk/modules/ElementSk'
-import { html } from 'lit-html'
-import { jsonOrThrow } from "../../../common-sk/modules/jsonOrThrow";
+import { define } from 'elements-sk/define';
+import { ElementSk } from '../../../infra-sk/modules/ElementSk';
+import { html } from 'lit-html';
+import { jsonOrThrow } from '../../../common-sk/modules/jsonOrThrow';
+import { stateReflector } from 'common-sk/modules/stateReflector';
 
-import 'elements-sk/checkbox-sk'
-import 'elements-sk/icon/delete-icon-sk'
-import 'elements-sk/icon/info-outline-icon-sk'
-import 'elements-sk/icon/mode-edit-icon-sk'
-import 'elements-sk/styles/buttons'
+import 'elements-sk/checkbox-sk';
+import 'elements-sk/icon/delete-icon-sk';
+import 'elements-sk/icon/info-outline-icon-sk';
+import 'elements-sk/icon/mode-edit-icon-sk';
+import 'elements-sk/styles/buttons';
+import '../../../infra-sk/modules/confirm-dialog-sk';
 
 const template = (ele) => html`
 <div class=controls>
@@ -27,6 +30,8 @@
   <button @click=${ele._newIgnoreRule}>Create new ignore rule</button>
 </div>
 
+<confirm-dialog-sk></confirm-dialog-sk>
+
 <table>
   <thead>
     <tr>
@@ -61,8 +66,10 @@
   <td class=query><a href=${'/list?include=true&query=' + encodeURIComponent(r.query)}
     >${humanReadableQuery(r.query)}</a></td>
   <td>${r.note || '--'}</td>
-  <!--TODO(kjlubick) change this to use the All varients with the checkbox -->
-  <td title="these counts are recomputed every few minutes">${r.exclusiveCount} / ${r.count}</td>
+  <td class=matches title="these counts are recomputed every few minutes">
+    ${ele._countAllTraces ? r.exclusiveCountAll : r.exclusiveCount} /
+    ${ele._countAllTraces ? r.countAll: r.count}
+  </td>
   <td class=${classMap({expired: isExpired})}>
     ${isExpired ? 'Expired': human.diffDate(r.expires)}
   </td>
@@ -90,8 +97,23 @@
 
     this._rules = [];
     this._countAllTraces = false;
-    // TODO(kjlubick) state reflector
 
+    this._stateChanged = stateReflector(
+        /*getState*/() => {
+          return {
+            // provide empty values
+            'count_all': this._countAllTraces,
+          }
+        }, /*setState*/(newState) => {
+          if (!this._connected) {
+            return;
+          }
+
+          // default values if not specified.
+          this._countAllTraces = newState.count_all || false;
+          this._fetch();
+          this._render();
+        });
     // Allows us to abort fetches if we fetch again.
     this._fetchController = null;
   }
@@ -99,17 +121,20 @@
   connectedCallback() {
     super.connectedCallback();
     this._render();
-
-    // Run this on the next microtask to allow for the demo.js to be set up.
-    // TODO(kjlubick) remove after state reflector is added.
-    setTimeout(()=> {
-      this._fetch()
-    });
   }
 
   _delete(rule) {
-    // TODO(kjlubick)
-    console.log('delete', rule);
+    const dialog = $$('confirm-dialog-sk', this);
+    dialog.open('Are you sure you want to delete '+
+                'this ignore rule?').then(() => {
+      this._sendBusy();
+      fetch('/json/ignores/del/'+rule.id, {
+        method: 'POST',
+      }).then(jsonOrThrow).then(() => {
+        this._fetch();
+        this._sendDone();
+      }).catch((e) => this._sendFetchError(e, 'deleting ignore'));
+    });
   }
 
   _edit(rule) {
@@ -166,6 +191,7 @@
   _toggleCountAll(e) {
     e.preventDefault();
     this._countAllTraces = !this._countAllTraces;
+    this._stateChanged();
     this._render();
   }
-});
\ No newline at end of file
+});
diff --git a/golden/modules/ignores-page-sk/ignores-page-sk_test.js b/golden/modules/ignores-page-sk/ignores-page-sk_test.js
index 7ae7055..3099068 100644
--- a/golden/modules/ignores-page-sk/ignores-page-sk_test.js
+++ b/golden/modules/ignores-page-sk/ignores-page-sk_test.js
@@ -1,15 +1,10 @@
 import './index.js';
 
 import { $, $$ } from 'common-sk/modules/dom';
-import { eventPromise, expectNoUnmatchedCalls } from "../test_util";
+import { eventPromise, expectNoUnmatchedCalls, expectQueryStringToEqual } from '../test_util';
 import { fakeNow, ignoreRules_10 } from './test_data';
 import { fetchMock }  from 'fetch-mock';
 
-function setQueryString(q) {
-  history.pushState(
-    null, '', window.location.origin + window.location.pathname + q);
-}
-
 describe('ignores-page-sk', () => {
   const regularNow = Date.now;
   let ignoresSk;
@@ -55,7 +50,6 @@
     it('creates links to test the filter', () => {
       const rows = $('table tbody tr', ignoresSk);
       const queryLink = $$('.query a', rows[9]);
-      expect(queryLink).to.not.be.null;
       expect(queryLink.href).to.contain('include=true&query=config%3Dglmsaa4%26cpu_or_gpu_value%3DTegraX1%26name%3Drg1024_green_grapes.svg');
       expect(queryLink.textContent).to.equal(`config=glmsaa4\ncpu_or_gpu_value=TegraX1\nname=rg1024_green_grapes.svg`);
     });
@@ -65,7 +59,7 @@
       const firstRow = rows[0];
       expect(firstRow.className).to.contain('expired');
       let timeBox = $$('.expired', firstRow);
-      expect(timeBox.textContent).to.contain('Expired');
+      expect(timeBox.innerText).to.contain('Expired');
 
       const fourthRow = rows[4];
       expect(fourthRow.className).to.not.contain('expired');
@@ -73,4 +67,117 @@
       expect(timeBox).to.be.null;
     });
   }); // end describe('html layout')
-});
\ No newline at end of file
+
+  describe('interaction', () => {
+    it('toggles between counting traces with untriaged digests and all traces', () => {
+      let checkbox = findUntriagedDigestsCheckbox(ignoresSk);
+      expect(checkbox.checked).to.be.true;
+      expect(findMatchesTextForRow(2, ignoresSk)).to.contain('0 / 4');
+      expectQueryStringToEqual('');
+
+      clickUntriagedDigestsCheckbox(ignoresSk);
+
+      checkbox = findUntriagedDigestsCheckbox(ignoresSk);
+      expect(checkbox.checked).to.be.false;
+      expect(findMatchesTextForRow(2, ignoresSk)).to.contain('6 / 10');
+      expectQueryStringToEqual('?count_all=true');
+
+      clickUntriagedDigestsCheckbox(ignoresSk);
+
+      checkbox = findUntriagedDigestsCheckbox(ignoresSk);
+      expect(checkbox.checked).to.be.true;
+      expect(findMatchesTextForRow(2, ignoresSk)).to.contain('0 / 4');
+      expectQueryStringToEqual('');
+    });
+
+    it('responds to back and forward browser buttons', async () => {
+      // Create some mock history so we can use the back button.
+      setQueryString('?count_all=true');
+      setQueryString('');
+
+      // We should go back to the count_all=true setting
+      await goBack();
+      let checkbox = findUntriagedDigestsCheckbox(ignoresSk);
+      expect(checkbox.checked).to.be.false;
+
+      // And now return to the default view.
+      await goForward();
+      checkbox = findUntriagedDigestsCheckbox(ignoresSk);
+      expect(checkbox.checked).to.be.true;
+    });
+
+    it('prompts "are you sure" before deleting an ignore rule', () => {
+      let dialog = findDialog(ignoresSk);
+      expect(dialog.hasAttribute('open')).to.be.false;
+
+      const del = findDeleteForRow(2);
+      del.click();
+
+      expect(dialog.hasAttribute('open')).to.be.true;
+      const msg = $$('.message', dialog);
+      expect(msg.innerText).to.contain('Are you sure you want to delete');
+    });
+
+    it('fires a post request to delete an ignore rule', async () => {
+      const idOfThirdRule = '7589748925671328782';
+      const del = findDeleteForRow(2);
+      del.click();
+
+      fetchMock.post(`/json/ignores/del/${idOfThirdRule}`, '{"deleted": "true"}');
+      const p = eventPromise('end-task');
+      clickOkDialogButton(ignoresSk);
+      await p;
+    });
+  });
+});
+
+function setQueryString(q) {
+  history.pushState(
+    null, '', window.location.origin + window.location.pathname + q);
+}
+
+function findUntriagedDigestsCheckbox(ele) {
+  return $$('.controls checkbox-sk', ele);
+}
+
+function findMatchesTextForRow(n, ele) {
+  const row = $('table tbody tr', ele)[n];
+  const cell = $$('td.matches', row);
+  // condense all whitespace and then trim to avoid the formatting of
+  // the html from impacting the tests too much (e.g. extraneous \n)
+  return cell.innerText;
+}
+
+function findDeleteForRow(n, ele) {
+  const row = $('table tbody tr', ele)[n];
+  return $$('.mutate-icons delete-icon-sk', row);
+}
+
+function findDialog(ele) {
+  return $$('confirm-dialog-sk dialog', ele);
+}
+
+function clickUntriagedDigestsCheckbox(ele) {
+  // We need to click on the input element to accurately mimic a user event. This is
+  // because the checkbox-sk element listens for the click event created by the
+  // internal input event.
+  const input = $$('input[type="checkbox"]', findUntriagedDigestsCheckbox(ele));
+  input.click();
+}
+
+function clickOkDialogButton(ele) {
+  const ok = $$('button.confirm', findDialog(ele));
+  ok.click();
+}
+
+function goBack() {
+  const event = eventPromise('end-task');
+  history.back();
+  return event;
+}
+
+function goForward() {
+  const event = eventPromise('end-task');
+  history.forward();
+  return event;
+}
\ No newline at end of file