[gold] Add lit-html component byblameentry-sk, to be used by the "by blame" lit-html page.
Bug: skia:9525
Change-Id: I839733356218ac9f4546032ccdc23f22dd6ec206
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/255089
Commit-Queue: Leandro Lovisolo <lovisolo@google.com>
Reviewed-by: Kevin Lubick <kjlubick@google.com>
diff --git a/golden/modules/byblameentry-sk/byblameentry-sk-demo.html b/golden/modules/byblameentry-sk/byblameentry-sk-demo.html
new file mode 100644
index 0000000..874b0a3
--- /dev/null
+++ b/golden/modules/byblameentry-sk/byblameentry-sk-demo.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>byblameentry-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">
+</head>
+<body>
+<gold-scaffold-sk app_title="Skia Public" testing_offline>
+ <!-- byblameentry-sk elements will be inserted here dynamically. -->
+</gold-scaffold-sk>
+</body>
+</html>
diff --git a/golden/modules/byblameentry-sk/byblameentry-sk-demo.js b/golden/modules/byblameentry-sk/byblameentry-sk-demo.js
new file mode 100644
index 0000000..1d7d05c
--- /dev/null
+++ b/golden/modules/byblameentry-sk/byblameentry-sk-demo.js
@@ -0,0 +1,11 @@
+import './index.js'
+import '../gold-scaffold-sk'
+import { $$ } from 'common-sk/modules/dom'
+import { byBlameEntry, gitLog } from './test_data'
+
+const entry = document.createElement('byblameentry-sk');
+entry.byBlameEntry = byBlameEntry;
+entry.gitLog = gitLog;
+entry.baseRepoUrl = 'https://skia.googlesource.com/skia.git';
+entry.corpus = 'gm';
+$$('gold-scaffold-sk').appendChild(entry);
diff --git a/golden/modules/byblameentry-sk/byblameentry-sk.js b/golden/modules/byblameentry-sk/byblameentry-sk.js
new file mode 100644
index 0000000..58565c4
--- /dev/null
+++ b/golden/modules/byblameentry-sk/byblameentry-sk.js
@@ -0,0 +1,162 @@
+/**
+ * @module modules/byblame-page-sk/byblameentry-sk
+ * @description <h2><code>byblameentry-sk</code></h2>
+ *
+ * Displays a
+ * [ByBlameEntry]{@link https://github.com/google/skia-buildbot/blob/0e14ae66aa226821e981bbd4c63dc8d07776997a/golden/go/web/web.go#L318},
+ * that is, a blame group of untriaged digests.
+ */
+
+import { define } from 'elements-sk/define'
+import { ElementSk } from '../../../infra-sk/modules/ElementSk'
+import { html } from 'lit-html'
+import { diffDate } from '../../../common-sk/modules/human';
+
+const template = (el) => html`
+<div class=blame>
+ <p>
+ <a href=${el._blameHref()} class=triage target=_blank rel=noopener>
+ ${el.byBlameEntry.nDigests === 1
+ ? '1 untriaged digest'
+ : `${el.byBlameEntry.nDigests} untriaged digests`}
+ </a>
+ </p>
+
+ ${!el.byBlameEntry.commits || el.byBlameEntry.commits.length === 0
+ ? html`<p class=no-blamelist>No blamelist.</p>`
+ : blameListTemplate(el)}
+
+ <h3>Tests affected</h3>
+ <p class=num-tests-affected>
+ ${el.byBlameEntry.nTests === 1
+ ? '1 test affected.'
+ : `${el.byBlameEntry.nTests} tests affected.`}
+ </p>
+
+ ${affectedTestsTemplate(el.byBlameEntry.affectedTests)}
+</div>
+`;
+
+const blameListTemplate = (el) => html`
+<h3>Blame</h3>
+
+<ul class=blames>
+ ${el.byBlameEntry.commits.map(
+ (commit) => html`
+ <li>
+ <a href=${el._commitHref(commit.hash)}
+ target=_blank
+ rel=noopener>
+ ${commit.hash.slice(0, 7)}
+ </a>
+ <span class=commit-message>
+ ${el._commitMessage(commit.hash)}
+ </span>
+ <br/>
+ <small>
+ <span class=author>${commit.author}</span>,
+ <span class=age>
+ ${diffDate(commit.commit_time * 1000)}
+ </span> ago.
+ </small>
+ </li>`)}
+</ul>`;
+
+const affectedTestsTemplate =
+ (affectedTests) =>
+ !affectedTests || affectedTests.length === 0
+ ? ''
+ : html`
+<table class=affected-tests>
+ <thead>
+ <tr>
+ <th>Test</th>
+ <th># Digests</th>
+ <th>Example</th>
+ </tr>
+ </thead>
+ <tbody>
+ ${affectedTests.map(
+ (test) => html`
+ <tr>
+ <td class=test>${test.test}</td>
+ <td class=num-digests>${test.num}</td>
+ <td>
+ <a href=${detailHref(test)}
+ class=example-link
+ target=_blank
+ rel=noopener>
+ ${test.sample_digest}
+ </a>
+ </td>
+ </tr>`)}
+ </tbody>
+</table>`;
+
+const detailHref =
+ (test) => `/detail?test=${test.test}&digest=${test.sample_digest}`;
+
+define('byblameentry-sk', class extends ElementSk {
+ constructor() {
+ super(template);
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this._render();
+ }
+
+ /**
+ * @prop byBlameEntry {Object} A ByBlameEntry object returned by the
+ * /json/byblame RPC endpoint.
+ */
+ get byBlameEntry() { return this._byBlameEntry; }
+ set byBlameEntry(v) {
+ this._byBlameEntry = v;
+ this._render();
+ }
+
+ /**
+ * @prop gitLog {Object} A gitLog object fetched from /json/gitlog containing
+ * commit messages for the commits referenced by the ByBlameEntry object.
+ */
+ get gitLog() { return this._gitLog; }
+ set gitLog(v) {
+ this._gitLog = v;
+ this._render();
+ }
+
+ /** @prop corpus {string} The corpus corresponding to this blame group. */
+ get corpus() { return this._corpus; }
+ set corpus(v) {
+ this._corpus = v;
+ this._render();
+ }
+
+ /** @prop baseRepoUrl {string} Base repository URL. */
+ get baseRepoUrl() { return this._baseRepoUrl; }
+ set baseRepoUrl(v) {
+ this._baseRepoUrl = v;
+ this._render();
+ }
+
+ _blameHref() {
+ const query = encodeURIComponent(`source_type=${this.corpus}`);
+ const groupID = this.byBlameEntry.groupID;
+ return `/search?blame=${groupID}&unt=true&head=true&query=${query}`;
+ }
+
+ _commitHref(hash) {
+ if (!hash || !this.baseRepoUrl) {
+ return '';
+ }
+ const path = this.baseRepoUrl.indexOf('github.com') !== -1 ? 'commit' :'+';
+ return `${this.baseRepoUrl}/${path}/${hash}`;
+ }
+
+ _commitMessage(hash) {
+ const commitInfo =
+ this.gitLog.log.find((commitInfo) => commitInfo.commit === hash);
+ return commitInfo ? commitInfo.message : '';
+ }
+});
diff --git a/golden/modules/byblameentry-sk/byblameentry-sk.scss b/golden/modules/byblameentry-sk/byblameentry-sk.scss
new file mode 100644
index 0000000..d799a6f
--- /dev/null
+++ b/golden/modules/byblameentry-sk/byblameentry-sk.scss
@@ -0,0 +1,19 @@
+@import '~elements-sk/colors';
+
+byblameentry-sk {
+ .blame {
+ border: 1px solid var(--light-gray);
+ margin: 10px;
+ padding: 10px;
+ }
+
+ table.affected-tests {
+ td {
+ padding: 5px;
+ }
+ }
+
+ ul.blames li {
+ margin-bottom: 1em;
+ }
+}
\ No newline at end of file
diff --git a/golden/modules/byblameentry-sk/byblameentry-sk_test.js b/golden/modules/byblameentry-sk/byblameentry-sk_test.js
new file mode 100644
index 0000000..ccba35f
--- /dev/null
+++ b/golden/modules/byblameentry-sk/byblameentry-sk_test.js
@@ -0,0 +1,274 @@
+import './index.js'
+import { $, $$ } from 'common-sk/modules/dom'
+import { deepCopy } from 'common-sk/modules/object'
+import { byBlameEntry, gitLog } from './test_data'
+
+describe('byblameentry-sk', () => {
+ let byBlameEntrySk;
+
+ function newByBlameEntrySk(
+ byBlameEntry,
+ {
+ testGitLog = gitLog,
+ baseRepoUrl = 'https://skia.googlesource.com/skia.git',
+ corpus = 'gm'
+ } = {}) {
+ byBlameEntrySk = document.createElement('byblameentry-sk');
+ byBlameEntrySk.byBlameEntry = byBlameEntry;
+ byBlameEntrySk.gitLog = testGitLog;
+ byBlameEntrySk.baseRepoUrl = baseRepoUrl;
+ byBlameEntrySk.corpus = corpus;
+ document.body.appendChild(byBlameEntrySk);
+ }
+
+ let clock;
+
+ beforeEach(() => {
+ // This is necessary to make commit ages deterministic, and is set to 50
+ // seconds after Elisa's commit in the 'full example' test case below.
+ clock = sinon.useFakeTimers(1573149864000); // Nov 7, 2019 6:04:24 PM GMT
+ });
+
+ afterEach(() => {
+ // Remove the stale instance under test.
+ if (byBlameEntrySk) {
+ document.body.removeChild(byBlameEntrySk);
+ byBlameEntrySk = null;
+ }
+ clock.restore();
+ });
+
+ describe('full example', () => {
+ it('renders correctly', async () => {
+ // This is a comprehensive example of a blame group with multiple
+ // untriaged digests that could have originated from two different
+ // commits, and includes a list of affected tests along with links to an
+ // example untriaged digest for each affected test.
+
+ await newByBlameEntrySk(byBlameEntry);
+ expectTriageLinkEquals(
+ '112 untriaged digests',
+ // This server-generated blame ID is a colon-separated list of the
+ // commit hashes blamed for these untriaged digests.
+ '/search?blame=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb&unt=true&head=true&query=source_type%3Dgm');
+ expectBlamesListEquals([{
+ linkText: 'bbbbbbb',
+ linkHref: 'https://skia.googlesource.com/skia.git/+/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
+ commitMessage: 'One glyph() to rule them all!!!',
+ author: 'Elisa (elisa@example.com)',
+ age: '50s'
+ }, {
+ linkText: 'aaaaaaa',
+ linkHref: 'https://skia.googlesource.com/skia.git/+/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+ commitMessage: 'flesh out blendmodes through Screen',
+ author: 'Joe (joe@example.com)',
+ age: '5m'
+ }]);
+ expectNumTestsAffectedEquals('7 tests affected.');
+ expectAffectedTestsTableEquals([{
+ test: 'aarectmodes',
+ numDigests: 50,
+ exampleLinkText: 'c6476baec94eb6a5071606575318e4df',
+ exampleLinkHref: '/detail?test=aarectmodes&digest=c6476baec94eb6a5071606575318e4df',
+ }, {
+ test: 'aaxfermodes',
+ numDigests: 32,
+ exampleLinkText: '4acfd6b3a3943cc5d75cd22e966ae6f1',
+ exampleLinkHref: '/detail?test=aaxfermodes&digest=4acfd6b3a3943cc5d75cd22e966ae6f1',
+ }, {
+ test: 'hairmodes',
+ numDigests: 21,
+ exampleLinkText: 'f9e20c63b5ce3b58d9b6a90fa3e7224c',
+ exampleLinkHref: '/detail?test=hairmodes&digest=f9e20c63b5ce3b58d9b6a90fa3e7224c',
+ }, {
+ test: 'imagefilters_xfermodes',
+ numDigests: 5,
+ exampleLinkText: '47644613317040264fea6fa815af32e8',
+ exampleLinkHref: '/detail?test=imagefilters_xfermodes&digest=47644613317040264fea6fa815af32e8',
+ }, {
+ test: 'lattice2',
+ numDigests: 2,
+ exampleLinkText: '16e41798ecd59b1523322a57b49cc17f',
+ exampleLinkHref: '/detail?test=lattice2&digest=16e41798ecd59b1523322a57b49cc17f',
+ }, {
+ test: 'xfermodes',
+ numDigests: 1,
+ exampleLinkText: '8fbee03f794c455c4e4842ec2736b744',
+ exampleLinkHref: '/detail?test=xfermodes&digest=8fbee03f794c455c4e4842ec2736b744',
+ }, {
+ test: 'xfermodes3',
+ numDigests: 1,
+ exampleLinkText: 'fed2ff29abe371fc0ec1b2c65dfb3949',
+ exampleLinkHref: '/detail?test=xfermodes3&digest=fed2ff29abe371fc0ec1b2c65dfb3949',
+ }]);
+ });
+ });
+
+ describe('triage link', () => {
+ it('renders link text in singular if there is just 1 digest', async () => {
+ const testByBlameEntry = deepCopy(byBlameEntry);
+ testByBlameEntry.nDigests = 1;
+ await newByBlameEntrySk(testByBlameEntry);
+ expectTriageLinkEquals(
+ '1 untriaged digest',
+ '/search?blame=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb&unt=true&head=true&query=source_type%3Dgm');
+ });
+
+ it('includes the right corpus in the link href', async () => {
+ await newByBlameEntrySk(byBlameEntry, {corpus: 'svg'});
+ expectTriageLinkEquals(
+ '112 untriaged digests',
+ '/search?blame=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb&unt=true&head=true&query=source_type%3Dsvg');
+ })
+ });
+
+ describe('blamelist', () => {
+ it('shows "No blamelist" message if there are 0 blames', async () => {
+ const testByBlameEntry = deepCopy(byBlameEntry);
+ testByBlameEntry.commits = [];
+ await newByBlameEntrySk(testByBlameEntry);
+ expectBlamesListEquals([]);
+ });
+
+ it('points commit links to GitHub if repo is hosted there', async () => {
+ await newByBlameEntrySk(
+ byBlameEntry,
+ {baseRepoUrl: 'https://github.com/google/skia'});
+ expectBlamesListEquals([{
+ linkText: 'bbbbbbb',
+ linkHref: 'https://github.com/google/skia/commit/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
+ commitMessage: 'One glyph() to rule them all!!!',
+ author: 'Elisa (elisa@example.com)',
+ age: '50s'
+ }, {
+ linkText: 'aaaaaaa',
+ linkHref: 'https://github.com/google/skia/commit/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+ commitMessage: 'flesh out blendmodes through Screen',
+ author: 'Joe (joe@example.com)',
+ age: '5m'
+ }]);
+ });
+
+ it('shows empty commit messages if Git log is empty/missing', async () => {
+ await newByBlameEntrySk(byBlameEntry, {testGitLog: {log: []}});
+ expectBlamesListEquals([{
+ linkText: 'bbbbbbb',
+ linkHref: 'https://skia.googlesource.com/skia.git/+/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
+ commitMessage: '',
+ author: 'Elisa (elisa@example.com)',
+ age: '50s'
+ }, {
+ linkText: 'aaaaaaa',
+ linkHref: 'https://skia.googlesource.com/skia.git/+/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+ commitMessage: '',
+ author: 'Joe (joe@example.com)',
+ age: '5m'
+ }]);
+ })
+ });
+
+ describe('affected tests', () => {
+ it('renders correctly with nTests = 0', async () => {
+ const testByBlameEntry = deepCopy(byBlameEntry);
+ testByBlameEntry.nTests = 0;
+ testByBlameEntry.affectedTests = [];
+ await newByBlameEntrySk(testByBlameEntry);
+ expectNumTestsAffectedEquals('0 tests affected.');
+ expectAffectedTestsTableEquals([]);
+ });
+
+ it('renders correctly with nTests = 1 and 0 affected tests', async () => {
+ const testByBlameEntry = deepCopy(byBlameEntry);
+ testByBlameEntry.nTests = 1;
+ testByBlameEntry.affectedTests = [];
+ await newByBlameEntrySk(testByBlameEntry);
+ expectNumTestsAffectedEquals('1 test affected.');
+ expectAffectedTestsTableEquals([]);
+ });
+
+ it('renders correctly with nTests = 2 and 0 affected tests', async () => {
+ const testByBlameEntry = deepCopy(byBlameEntry);
+ testByBlameEntry.nTests = 2;
+ testByBlameEntry.affectedTests = [];
+ await newByBlameEntrySk(testByBlameEntry);
+ expectNumTestsAffectedEquals('2 tests affected.');
+ expectAffectedTestsTableEquals([]);
+ });
+
+ it('renders correctly with nTests = 2 and one affected test', async () => {
+ const testByBlameEntry = deepCopy(byBlameEntry);
+ testByBlameEntry.nTests = 2;
+ testByBlameEntry.affectedTests = [{
+ "test": "aarectmodes",
+ "num": 5,
+ "sample_digest": "c6476baec94eb6a5071606575318e4df",
+ }];
+ await newByBlameEntrySk(testByBlameEntry);
+ expectNumTestsAffectedEquals('2 tests affected.');
+ expectAffectedTestsTableEquals([{
+ test: 'aarectmodes',
+ numDigests: 5,
+ exampleLinkText: 'c6476baec94eb6a5071606575318e4df',
+ exampleLinkHref:
+ '/detail?test=aarectmodes&digest=c6476baec94eb6a5071606575318e4df',
+ }]);
+ });
+ });
+
+ const expectTriageLinkEquals = (text, href) => {
+ const triageLink = $$('a.triage', byBlameEntrySk);
+ expect(triageLink.innerText).to.contain(text);
+ expect(triageLink.href).to.have.string(href);
+ };
+
+ const expectBlamesListEquals = (expectedBlames) => {
+ const noBlameList = $$('p.no-blamelist', byBlameEntrySk);
+ const blames = $('ul.blames li', byBlameEntrySk);
+
+ if (!expectedBlames.length) {
+ expect(noBlameList.innerText).to.contain('No blamelist.');
+ expect(blames).to.be.empty;
+ } else {
+ expect(noBlameList).to.be.null;
+ expect(blames).to.have.length(expectedBlames.length);
+
+ for (let i = 0; i < expectedBlames.length; i++) {
+ const linkText = $$('a', blames[i]).innerText;
+ const linkHref = $$('a', blames[i]).href;
+ const commitMessage =
+ $$('.commit-message', blames[i]).innerText;
+ const author = $$('.author', blames[i]).innerText;
+ const age = $$('.age', blames[i]).innerText;
+
+ expect(linkText).to.contain(expectedBlames[i].linkText);
+ expect(linkHref).to.equal(expectedBlames[i].linkHref);
+ expect(commitMessage).to.contain(expectedBlames[i].commitMessage);
+ expect(author).to.contain(expectedBlames[i].author);
+ expect(age).to.contain(expectedBlames[i].age);
+ }
+ }
+ };
+
+ const expectNumTestsAffectedEquals =
+ (numTestsAffected) =>
+ expect($$('.num-tests-affected', byBlameEntrySk).innerText)
+ .to.contain(numTestsAffected);
+
+ const expectAffectedTestsTableEquals = (expectedRows) => {
+ const actualRows = $('.affected-tests tbody tr');
+ expect(actualRows.length).to.equal(expectedRows.length);
+
+ for (let i = 0; i < expectedRows.length; i++) {
+ const test = $$('.test', actualRows[i]).innerText;
+ const numDigests =
+ +$$('.num-digests', actualRows[i]).innerText;
+ const exampleLinkText = $$('a.example-link', actualRows[i]).innerText;
+ const exampleLinkHref = $$('a.example-link', actualRows[i]).href;
+
+ expect(test).to.contain(expectedRows[i].test);
+ expect(numDigests).to.equal(expectedRows[i].numDigests);
+ expect(exampleLinkText).to.contain(expectedRows[i].exampleLinkText);
+ expect(exampleLinkHref).to.contain(expectedRows[i].exampleLinkHref);
+ }
+ };
+});
diff --git a/golden/modules/byblameentry-sk/index.js b/golden/modules/byblameentry-sk/index.js
new file mode 100644
index 0000000..43cb8c9
--- /dev/null
+++ b/golden/modules/byblameentry-sk/index.js
@@ -0,0 +1,2 @@
+import './byblameentry-sk.js'
+import './byblameentry-sk.scss'
diff --git a/golden/modules/byblameentry-sk/test_data.js b/golden/modules/byblameentry-sk/test_data.js
new file mode 100644
index 0000000..7dfc6e6
--- /dev/null
+++ b/golden/modules/byblameentry-sk/test_data.js
@@ -0,0 +1,53 @@
+export const byBlameEntry = {
+ "groupID": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "nDigests": 112,
+ "nTests": 7,
+ "affectedTests": [{
+ "test": "aarectmodes",
+ "num": 50,
+ "sample_digest": "c6476baec94eb6a5071606575318e4df"
+ }, {
+ "test": "aaxfermodes",
+ "num": 32,
+ "sample_digest": "4acfd6b3a3943cc5d75cd22e966ae6f1"
+ }, {
+ "test": "hairmodes",
+ "num": 21,
+ "sample_digest": "f9e20c63b5ce3b58d9b6a90fa3e7224c"
+ }, {
+ "test": "imagefilters_xfermodes",
+ "num": 5,
+ "sample_digest": "47644613317040264fea6fa815af32e8"
+ }, {
+ "test": "lattice2",
+ "num": 2,
+ "sample_digest": "16e41798ecd59b1523322a57b49cc17f"
+ }, {
+ "test": "xfermodes",
+ "num": 1,
+ "sample_digest": "8fbee03f794c455c4e4842ec2736b744"
+ }, {
+ "test": "xfermodes3",
+ "num": 1,
+ "sample_digest": "fed2ff29abe371fc0ec1b2c65dfb3949"
+ }],
+ "commits": [{
+ "commit_time": 1573149814,
+ "hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "author": "Elisa (elisa@example.com)"
+ }, {
+ "commit_time": 1573149564,
+ "hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "author": "Joe (joe@example.com)"
+ }]
+};
+
+export const gitLog = {
+ log: [{
+ "commit": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "message": "One glyph() to rule them all!!!"
+ }, {
+ "commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "message": "flesh out blendmodes through Screen"
+ }]
+};