[gold] Add click handlers to cluster-digests-sk
Bug: skia:9525
Change-Id: Ie5b694fc276629e2000cd8a4aca7867aff83b568
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/301545
Reviewed-by: Leandro Lovisolo <lovisolo@google.com>
diff --git a/golden/modules/cluster-digests-sk/cluster-digests-sk.js b/golden/modules/cluster-digests-sk/cluster-digests-sk.js
index b52486e3..c8ccc5f 100644
--- a/golden/modules/cluster-digests-sk/cluster-digests-sk.js
+++ b/golden/modules/cluster-digests-sk/cluster-digests-sk.js
@@ -13,6 +13,9 @@
*
* @evt layout-complete; fired when the force layout has stabilized (i.e. finished rendering).
*
+ * @evt selection-changed; fired when a digest is clicked on (or the selection is cleared). Detail
+ * contains a list of digests that are selected.
+ *
*/
import { define } from 'elements-sk/define';
import { html } from 'lit-html';
@@ -40,6 +43,9 @@
// has been laid out as.
this._width = 400;
this._height = 400;
+
+ // An array of digests (strings) that correspond to the currently selected digests (if any).
+ this._selectedDigests = [];
}
connectedCallback() {
@@ -48,7 +54,7 @@
}
/**
- * Recomputes the positions of the digest nodes given the value of links. It expects all the svg
+ * Recomputes the positions of the digest nodes given the value of links. It expects all the SVG
* elements (e.g. circles, lines) to already be created; this function will simply update the
* X and Y values accordingly.
*/
@@ -71,7 +77,7 @@
const linkForce = d3Force.forceLink(this._links)
.distance((d) => d.value / this._linkTightness);
- // This force keeps the diagram centered in the svg.
+ // This force keeps the diagram centered in the SVG.
// See https://github.com/d3/d3-force#centering
const centerForce = d3Force.forceCenter(this._width / 2, this._height / 2);
@@ -107,10 +113,19 @@
});
}
+ _getNodeCSSClass(d) {
+ let base = `node ${d.status}`;
+ if (this._selectedDigests.indexOf(d.name) >= 0) {
+ base += ' selected';
+ }
+ return base;
+ }
+
/**
* Sets the new data to render in a cluster view.
*
- * @param nodes Array<Object>: contains Strings for keys digest and status.
+ * @param nodes Array<Object>: contains Strings for keys digest (called name for historical
+ * reasons) and status.
* @param links Array<Object>: contains Numbers for keys source, target, and value. source and
* target refer to the index of the nodes array. value represents how far apart those two
* nodes should be.
@@ -120,13 +135,13 @@
this._links = links;
this._render();
- // For reasons unknown, after render, we don't always see the svg element rendered in our
- // DOM, so we schedule the drawing for the next animation frame (when we *do* see the svg
+ // For reasons unknown, after render, we don't always see the SVG element rendered in our
+ // DOM, so we schedule the drawing for the next animation frame (when we *do* see the SVG
// in the DOM).
window.requestAnimationFrame(() => {
const clusterSk = $$('svg', this);
- // Delete existing svg elements
+ // Delete existing SVG elements
d3Select.select(clusterSk)
.selectAll('.link,.node')
.remove();
@@ -149,11 +164,53 @@
.data(this._nodes)
.enter()
.append('circle')
- .attr('class', (d) => `node ${d.status}`)
+ .attr('class', (d) => this._getNodeCSSClass(d))
.attr('r', 12)
- .attr('stroke', 'black');
+ .attr('stroke', 'black')
+ .attr('data-digest', (d) => d.name)
+ .on('click tap', (d) => {
+ // Capture this event (prevent it from propagating to the SVG).
+ const evt = d3Select.event;
+ evt.preventDefault();
+ evt.stopPropagation();
+
+ const digest = d.name;
+ if (this._selectedDigests.indexOf(digest) >= 0) {
+ return; // It's already selected, do nothing.
+ }
+ if (evt.shiftKey || evt.ctrlKey || evt.metaKey) {
+ // Support multiselection if shift, control or meta is held.
+ this._selectedDigests.push(digest);
+ } else {
+ // Clear the existing selection and replace it with this digest.
+ this._selectedDigests = [digest];
+ }
+ this._updateSelection();
+ });
+
+ d3Select.select(clusterSk).on('click tap', () => {
+ // Capture this event (prevent it from propagating outside the SVG).
+ const evt = d3Select.event;
+ evt.preventDefault();
+ evt.stopPropagation();
+
+ this._selectedDigests = [];
+ this._updateSelection();
+ });
this._layout();
});
}
+
+ _updateSelection() {
+ d3Select.select($$('svg', this))
+ .selectAll('circle.node')
+ .data(this._nodes)
+ .attr('class', (d) => this._getNodeCSSClass(d));
+
+ this.dispatchEvent(new CustomEvent('selection-changed', {
+ bubbles: true,
+ detail: this._selectedDigests,
+ }));
+ }
});
diff --git a/golden/modules/cluster-digests-sk/cluster-digests-sk_puppeteer_test.ts b/golden/modules/cluster-digests-sk/cluster-digests-sk_puppeteer_test.ts
index a0cb128..77f42dc 100644
--- a/golden/modules/cluster-digests-sk/cluster-digests-sk_puppeteer_test.ts
+++ b/golden/modules/cluster-digests-sk/cluster-digests-sk_puppeteer_test.ts
@@ -1,6 +1,9 @@
import { expect } from 'chai';
-import { addEventListenersToPuppeteerPage, takeScreenshot, TestBed } from '../../../puppeteer-tests/util';
+import { addEventListenersToPuppeteerPage, EventName,
+ takeScreenshot, TestBed } from '../../../puppeteer-tests/util';
import { loadGoldWebpack } from '../common_puppeteer_test/common_puppeteer_test';
+import { ElementHandle } from 'puppeteer';
+import { positiveDigest, negativeDigest, untriagedDigest } from './test_data';
describe('cluster-digests-sk', () => {
let testBed: TestBed;
@@ -8,12 +11,16 @@
testBed = await loadGoldWebpack();
});
+ let promiseFactory: <T>(eventName: EventName) => Promise<T>;
+ let clusterDigestsSk: ElementHandle;
+
beforeEach(async () => {
- const eventPromise = await addEventListenersToPuppeteerPage(testBed.page,
- ['layout-complete']);
- const loaded = eventPromise('layout-complete'); // Emitted when layout stabilizes.
+ promiseFactory = await addEventListenersToPuppeteerPage(testBed.page,
+ ['layout-complete', 'selection-changed']);
+ const loaded = promiseFactory('layout-complete'); // Emitted when layout stabilizes.
await testBed.page.goto(`${testBed.baseUrl}/dist/cluster-digests-sk.html`);
await loaded;
+ clusterDigestsSk = (await testBed.page.$('#cluster svg'))!;
});
it('should render the demo page', async () => {
@@ -22,7 +29,65 @@
});
it('should take a screenshot', async () => {
- const clusterDigestsSk = await testBed.page.$('#cluster svg');
- await takeScreenshot(clusterDigestsSk!, 'gold', 'cluster-digests-sk');
+ await takeScreenshot(clusterDigestsSk, 'gold', 'cluster-digests-sk');
});
+
+ it('supports single digest selection via clicking', async () => {
+ await clickNodeAndExpectSelectionChangedEvent(positiveDigest, [positiveDigest]);
+
+ await takeScreenshot(clusterDigestsSk, 'gold', 'cluster-digests-sk_one-positive-selected');
+
+ await clickNodeAndExpectSelectionChangedEvent(untriagedDigest, [untriagedDigest]);
+
+ await takeScreenshot(clusterDigestsSk, 'gold',
+ 'cluster-digests-sk_one-untriaged-selected');
+ });
+
+ it('supports multiple digest selection via shift clicking', async () => {
+ await clickNodeAndExpectSelectionChangedEvent(negativeDigest, [negativeDigest]);
+
+ await shiftClickNodeAndExpectSelectionChangedEvent(positiveDigest,
+ [negativeDigest, positiveDigest]);
+
+ await takeScreenshot(clusterDigestsSk, 'gold', 'cluster-digests-sk_two-digests-selected');
+
+ await shiftClickNodeAndExpectSelectionChangedEvent(untriagedDigest,
+ [negativeDigest, positiveDigest, untriagedDigest]);
+
+ await takeScreenshot(clusterDigestsSk, 'gold',
+ 'cluster-digests-sk_three-digests-selected');
+ });
+
+ it('clears selection by clicking anywhere on the svg that is not on a node', async () => {
+ await clickNodeAndExpectSelectionChangedEvent(negativeDigest, [negativeDigest]);
+
+ const clickEvent = promiseFactory<Array<string>>('selection-changed');
+ await clusterDigestsSk.click();
+ const evt = await clickEvent;
+ expect(evt).to.deep.equal([]);
+ });
+
+ async function clickNodeWithDigest(digest: string) {
+ await testBed.page.click(`circle.node[data-digest="${digest}"]`);
+ }
+
+ async function shiftClickNodeWithDigest(digest: string) {
+ await testBed.page.keyboard.down('Shift');
+ await clickNodeWithDigest(digest);
+ await testBed.page.keyboard.up('Shift');
+ }
+
+ async function clickNodeAndExpectSelectionChangedEvent(digest: string, expectedSelection: string[]) {
+ const clickEvent = promiseFactory<Array<string>>('selection-changed');
+ await clickNodeWithDigest(digest);
+ const evt = await clickEvent;
+ expect(evt).to.deep.equal(expectedSelection);
+ }
+
+ async function shiftClickNodeAndExpectSelectionChangedEvent(digest: string, expectedSelection: string[]) {
+ const clickEvent = promiseFactory<Array<string>>('selection-changed');
+ await shiftClickNodeWithDigest(digest);
+ const evt = await clickEvent;
+ expect(evt).to.deep.equal(expectedSelection);
+ }
});
diff --git a/golden/modules/cluster-digests-sk/test_data.js b/golden/modules/cluster-digests-sk/test_data.js
index 54c4fa5..7bbca74 100644
--- a/golden/modules/cluster-digests-sk/test_data.js
+++ b/golden/modules/cluster-digests-sk/test_data.js
@@ -1,17 +1,21 @@
+export const positiveDigest = '99c58c7002073346ff55f446d47d6311';
+export const negativeDigest = 'ec3b8f27397d99581e06eaa46d6d5837';
+export const untriagedDigest = '6246b773851984c726cb2e1cb13510c2';
+
// This is the data returned from Gold's /clusterdiff RPC. Not all of it is used in
// cluster-digests-sk.
export const clusterDiffJSON = {
nodes: [
{
- name: '99c58c7002073346ff55f446d47d6311',
+ name: positiveDigest,
status: 'positive',
},
{
- name: '6246b773851984c726cb2e1cb13510c2',
+ name: untriagedDigest,
status: 'untriaged',
},
{
- name: 'ec3b8f27397d99581e06eaa46d6d5837',
+ name: negativeDigest,
status: 'negative',
},
],