Upstream the PolyGerrit plugin for Checks.

This is just a basic buildable and runnable copy of the experimentally
devloped plugin. There are some obvious remaining TODOs:

* Rename build-result to checks everywhere.
* Integrate PolyGerrit plugin into the backend plugin jar.
* Hook up with the new Rest API endpoints for checks.

Change-Id: Ic6f24c434ad48f694d68b32e87023d60afecc6fa
diff --git a/gr-checks/BUILD b/gr-checks/BUILD
new file mode 100644
index 0000000..9711ad1
--- /dev/null
+++ b/gr-checks/BUILD
@@ -0,0 +1,19 @@
+package_group(
+    name = "visibility",
+    packages = ["//plugins/checks/..."],
+)
+
+package(default_visibility = [":visibility"])
+
+load("//tools/bzl:js.bzl", "bundle_assets", "polygerrit_plugin")
+
+polygerrit_plugin(
+    name = "build_results",
+    app = "build-results.html",
+    plugin_name = "build_results",
+    externs = ["externs.js"],
+    srcs = glob([
+            "*.html",
+            "*.js",
+        ]),
+)
diff --git a/gr-checks/all-statuses.js b/gr-checks/all-statuses.js
new file mode 100644
index 0000000..d318830
--- /dev/null
+++ b/gr-checks/all-statuses.js
@@ -0,0 +1,72 @@
+(function() {
+'use strict';
+
+window.Gerrit = (window.Gerrit || {});
+window.Gerrit.BuildResults = (window.Gerrit.BuildResults || {});
+
+// Prevent redefinition.
+if (window.Gerrit.BuildResults.Statuses) return;
+
+const Statuses = {
+  // non-terminal statuses
+  STATUS_UNKNOWN: 'STATUS_UNKNOWN',
+  QUEUING: 'QUEUING',
+  QUEUED: 'QUEUED',
+  WORKING: 'WORKING',
+
+  // terminal statuses
+  SUCCESS: 'SUCCESS',
+  FAILURE: 'FAILURE',
+  INTERNAL_ERROR: 'INTERNAL_ERROR',
+  TIMEOUT: 'TIMEOUT',
+  CANCELLED: 'CANCELLED',
+};
+
+
+function isStatus(status, includedStatuses) {
+  return includedStatuses.includes(status);
+}
+
+
+function isUnevaluated(status) {
+  return isStatus(status, [Statuses.STATUS_UNKNOWN, Statuses.CANCELLED]);
+}
+
+function isInProgress(status) {
+  return isStatus(
+      status, [Statuses.QUEUING, Statuses.QUEUED, Statuses.WORKING]);
+}
+
+function isSuccessful(status) {
+  return isStatus(status, [Statuses.SUCCESS]);
+}
+
+function isFailed(status) {
+  return isStatus(
+      status, [Statuses.FAILURE, Statuses.INTERNAL_ERROR, Statuses.TIMEOUT]);
+}
+
+
+function statusClass(status) {
+  if (isUnevaluated(status)) {
+    return 'unevaluated';
+  }
+  if (isInProgress(status)) {
+    return 'in-progress';
+  }
+  if (isSuccessful(status)) {
+    return 'successful';
+  }
+  if (isFailed(status)) {
+    return 'failed';
+  }
+  return 'unevaluated';
+}
+
+window.Gerrit.BuildResults.Statuses = Statuses;
+window.Gerrit.BuildResults.isUnevaluated = isUnevaluated;
+window.Gerrit.BuildResults.isInProgress = isInProgress;
+window.Gerrit.BuildResults.isSuccessful = isSuccessful;
+window.Gerrit.BuildResults.isFailed = isFailed;
+window.Gerrit.BuildResults.statusClass = statusClass;
+})();
diff --git a/gr-checks/build-result-item.html b/gr-checks/build-result-item.html
new file mode 100644
index 0000000..c134205
--- /dev/null
+++ b/gr-checks/build-result-item.html
@@ -0,0 +1,46 @@
+<dom-module id="build-result-item">
+  <template>
+      <style>
+        :host {
+          border-top: 1px solid #ddd;
+        }
+
+        td:first-child {
+          padding-left: 1rem;
+        }
+
+        td {
+          padding: 1px 32px 1px 0;
+          white-space: nowrap;
+        }
+
+        a.log {
+          margin-right: 16px;
+          display: inline-block;
+        }
+      </style>
+
+      <td>[[_triggerDescription]]</td>
+      <td><!-- required for merge--></td>
+      <td>
+        <build-result-status show-text status="[[build.status]]"></build-result-status>
+      </td>
+      <td>Google Cloud Build</td>
+      <td>[[_startTime]]</td>
+      <td>[[_duration]]</td>
+      <td>
+        <a href$="[[build.logUrl]]" target="_blank" class="log">
+          <gr-button link no-uppercase>
+            View log
+          </gr-button>
+        </a>
+        <gr-button
+          link
+          no-uppercase
+          on-tap="handleClick">
+          Re-run
+        </gr-button>
+      </td>
+  </template>
+  <script src="build-result-item.js"></script>
+</dom-module>
diff --git a/gr-checks/build-result-item.js b/gr-checks/build-result-item.js
new file mode 100644
index 0000000..5405359
--- /dev/null
+++ b/gr-checks/build-result-item.js
@@ -0,0 +1,121 @@
+(function() {
+'use strict';
+
+const Defs = {};
+/**
+ * @typedef {{
+ *   id: string,
+ *   projectId: string,
+ *   buildTriggerId: string,
+ *   startTime: string,
+ *   finishTime: string,
+ * }}
+ */
+Defs.Build;
+
+Polymer({
+  is: 'build-result-item',
+
+  properties: {
+    build: Object,
+    /** @type {function(string): !Promise<!Object>} */
+    getTrigger: Function,
+    /** @type {function(string): !Promise<!Object>} */
+    retryBuild: Function,
+    _triggerDescription: String,
+    _startTime: {
+      type: String,
+      computed: '_computeStartTime(build)',
+    },
+    _duration: {
+      type: String,
+      computed: '_computeDuration(build)',
+    },
+  },
+
+  observers: [
+    '_updateTriggerName(build, getTrigger)',
+  ],
+
+  /**
+   * @param {!Defs.Build} build
+   * @param {function(string): !Promise<!Object>} getTrigger
+   */
+  _updateTriggerName(build, getTrigger) {
+    const triggerId = build.buildTriggerId;
+    getTrigger(triggerId)
+        .then(
+            trigger => trigger && trigger.description || triggerId,
+            () => triggerId)
+        .then(triggerDescription => {
+          this.set('_triggerDescription', triggerDescription);
+        });
+  },
+
+  /**
+   * @param {!Defs.Build} build
+   * @return {string}
+   */
+  _computeStartTime(build) {
+    return moment(build.startTime).format('l');
+  },
+
+  /**
+   * @param {!Defs.Build} build
+   * @return {string}
+   */
+  _computeDuration(build) {
+    const startTime = moment(build.startTime);
+    const finishTime = moment(build.finishTime);
+    return generateDurationString(moment.duration(finishTime.diff(startTime)));
+  },
+
+  handleClick(event) {
+    event.preventDefault();
+    this.retryBuild(this.build.id);
+  }
+});
+
+const ZERO_SECONDS = '0 sec';
+
+/**
+ * @param {!Moment.Duration} duration a moment object
+ * @return {string}
+ */
+function generateDurationString(duration) {
+  if (duration.asSeconds() === 0) {
+    return ZERO_SECONDS;
+  }
+
+  const durationSegments = [];
+  if (duration.seconds()) {
+    durationSegments.push(`${duration.seconds()} sec`);
+  }
+  if (duration.minutes()) {
+    durationSegments.push(`${duration.minutes()} min`);
+  }
+  if (duration.hours()) {
+    const hours = pluralize(duration.hours(), 'hour', 'hours');
+    durationSegments.push(`${duration.hours()} ${hours}`);
+  }
+  if (duration.days()) {
+    const days = pluralize(duration.days(), 'day', 'days');
+    durationSegments.push(`${duration.days()} ${days}`);
+  }
+  if (duration.months()) {
+    const months = pluralize(duration.months(), 'month', 'months');
+    durationSegments.push(`${duration.months()} ${months}`);
+  }
+  return durationSegments.join(' ');
+}
+
+/**
+ * @param {number} unit
+ * @param {string} singular
+ * @param {string} plural
+ * @return {string}
+ */
+function pluralize(unit, singular, plural) {
+  return unit === 1 ? singular : plural;
+}
+})();
diff --git a/gr-checks/build-result-item_test.html b/gr-checks/build-result-item_test.html
new file mode 100644
index 0000000..cbf9309
--- /dev/null
+++ b/gr-checks/build-result-item_test.html
@@ -0,0 +1,157 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<script src="imports.js"></script>
+<script src="webcomponentsjs/webcomponents-lite.js"></script>
+<link rel="import" href="polymer/polymer.html">
+
+<title>build-result-item</title>
+<link rel="import" href="webcomponent_lib/build-result-item.html">
+
+<test-fixture id="basic">
+  <template is="dom-template">
+    <build-result-item
+      build="{{build}}"
+      get-trigger="[[getTrigger]]"
+      retry-build="[[retryBuild]]">
+    </build-result-item>
+  </template>
+</test-fixture>
+
+<script>
+  suite('build-result-item tests', () => {
+    let element;
+    let sandbox;
+    let getTriggerSpy;
+    let resolveGetTrigger;
+    let rejectGetTrigger;
+    let retryBuildSpy;
+
+    setup((done) => {
+      sandbox = sinon.sandbox.create();
+      const getTriggerPromise = new Promise((resolve, reject) => {
+        resolveGetTrigger = resolve;
+        rejectGetTrigger = reject;
+      });
+      getTriggerSpy = sinon.stub();
+      getTriggerSpy.returns(getTriggerPromise);
+      retryBuildSpy = sinon.stub();
+      retryBuildSpy.returns(Promise.resolve());
+
+      element = fixture('basic', {
+        getTrigger: getTriggerSpy,
+        retryBuild: retryBuildSpy,
+        build: {
+          buildTriggerId: 'test-build-trigger-id',
+          logUrl: 'http://example.com/test-log-url',
+          startTime: "2019-02-06T22:25:19.269Z",
+          finishTime: "2019-02-06T22:25:44.574Z",
+        },
+      });
+      flush(done);
+    });
+
+    teardown(() => { sandbox.restore(); });
+
+    suite('trigger name', () => {
+      test('calls getTrigger', () => {
+        assert.isTrue(getTriggerSpy.called);
+        assert.isTrue(getTriggerSpy.calledWith('test-build-trigger-id'));
+      });
+
+      function assertTriggerText(text) {
+        const name = element.$$('td:nth-child(1)');
+        assert.equal(name.textContent.trim(), text);
+      }
+
+      suite('on success', () => {
+        setup(done => {
+          resolveGetTrigger({description: 'test trigger name'});
+          flush(done);
+        });
+
+        test('renders the trigger name on success', () => {
+          assertTriggerText('test trigger name');
+        });
+      });
+
+      suite('on empty response', () => {
+        setup(done => {
+          resolveGetTrigger();
+          flush(done);
+        });
+
+        test('renders the id when there is no description', () => {
+          assertTriggerText('test-build-trigger-id');
+        });
+      });
+
+      suite('on error', () => {
+        setup(done => {
+          rejectGetTrigger(new Error('broken'));
+          flush(done);
+        });
+
+        test('renders the id when the call fails', () => {
+          assertTriggerText('test-build-trigger-id');
+        });
+      });
+    });
+
+    test('renders the status', () => {
+      const status = element.$$('td:nth-child(3) > build-result-status');
+      assert.exists(status);
+    });
+
+    test('renders the checking system', () => {
+      const checkingSystem = element.$$('td:nth-child(4)');
+      assert.equal(checkingSystem .textContent.trim(), "Google Cloud Build");
+    });
+
+    test('renders the run date', () => {
+      const name = element.$$('td:nth-child(5)');
+      assert.equal(name .textContent.trim(), "2/6/2019");
+    });
+
+    suite('duration', () => {
+      test('renders the run duration', () => {
+        const name = element.$$('td:nth-child(6)');
+        assert.equal(name .textContent.trim(), "25 sec");
+      });
+
+      test('renders 0 sec when the start and end time are the same', () => {
+        element.build = {
+          buildTriggerId: 'test-build-trigger-id',
+          logUrl: 'http://example.com/test-log-url',
+          startTime: "2019-02-06T22:25:19.269Z",
+          finishTime: "2019-02-06T22:25:19.269Z",
+        };
+        const name = element.$$('td:nth-child(6)');
+        assert.equal(name .textContent.trim(), "0 sec");
+      });
+    });
+
+    test('renders a link to the log', () => {
+      const logLink = element.$$('td:nth-child(7) > a');
+      assert.equal(logLink.getAttribute('href'), "http://example.com/test-log-url");
+      assert.equal(logLink.textContent.trim(), "View log");
+    });
+
+    suite('retryBuild', () => {
+      let retryBuildLink;
+
+      setup(() => {
+        retryBuildLink = element.$$('td:nth-child(7) > gr-button');
+      });
+
+      test('shows a link to the log url', () => {
+        assert.equal(retryBuildLink.textContent.trim(), "Re-run");
+      });
+
+      test('clicking on the link calls the retryBuild property', () => {
+        assert.isFalse(retryBuildSpy.called);
+        retryBuildLink.click();
+        assert.isTrue(retryBuildSpy.called);
+      });
+    });
+  });
+</script>
diff --git a/gr-checks/build-result-status.html b/gr-checks/build-result-status.html
new file mode 100644
index 0000000..ca39add
--- /dev/null
+++ b/gr-checks/build-result-status.html
@@ -0,0 +1,67 @@
+<dom-module id="build-result-status">
+  <template>
+      <style>
+        :host {
+          display: inline-block;
+        }
+        i {
+          border-radius: 50%;
+          color: white;
+          display: inline-block;
+          font-style: normal;
+          height: 16px;
+          margin-right: 4px;
+          text-align: center;
+          width: 16px;
+        }
+        .successful > i {
+          background-color: #00C752;
+        }
+        .failed > i {
+          background-color: #DA4236;
+        }
+        .in-progress > i {
+          background-color: #ddd;
+        }
+        .unevaluated > i {
+          background-color: black;
+        }
+      </style>
+    <span class$="[[_className]]">
+      <template is="dom-if" if="[[_isUnevaluated(status)]]">
+        <i>⏹</i>
+        <template is="dom-if" if="[[showText]]">
+          <span>
+            Unevaluated
+          </span>
+        </template>
+      </template>
+      <template is="dom-if" if="[[_isInProgress(status)]]">
+        <i>…</i>
+        <template is="dom-if" if="[[showText]]">
+          <span>
+            In progress
+          </span>
+        </template>
+      </template>
+      <template is="dom-if" if="[[_isSuccessful(status)]]">
+        <i>✓</i>
+        <template is="dom-if" if="[[showText]]">
+          <span>
+            Successful
+          </span>
+        </template>
+      </template>
+      <template is="dom-if" if="[[_isFailed(status)]]">
+        <i>!</i>
+        <template is="dom-if" if="[[showText]]">
+          <span>
+            Failed
+          </span>
+        </template>
+      </template>
+    </span>
+  </template>
+  <script src="all-statuses.js"></script>
+  <script src="build-result-status.js"></script>
+</dom-module>
diff --git a/gr-checks/build-result-status.js b/gr-checks/build-result-status.js
new file mode 100644
index 0000000..f7f94fd
--- /dev/null
+++ b/gr-checks/build-result-status.js
@@ -0,0 +1,37 @@
+(function() {
+'use strict';
+
+Polymer({
+  is: 'build-result-status',
+
+  properties: {
+    showText: {
+      type: Boolean,
+      value: false,
+      reflectToAttribute: true,
+    },
+    status: String,
+    _className: {type: String, computed: '_computeClassName(status)'},
+  },
+
+  _isUnevaluated(status) {
+    return window.Gerrit.BuildResults.isUnevaluated(status);
+  },
+
+  _isInProgress(status) {
+    return window.Gerrit.BuildResults.isInProgress(status);
+  },
+
+  _isSuccessful(status) {
+    return window.Gerrit.BuildResults.isSuccessful(status);
+  },
+
+  _isFailed(status) {
+    return window.Gerrit.BuildResults.isFailed(status);
+  },
+
+  _computeClassName(status) {
+    return window.Gerrit.BuildResults.statusClass(status);
+  },
+});
+})();
diff --git a/gr-checks/build-result-status_test.html b/gr-checks/build-result-status_test.html
new file mode 100644
index 0000000..6592e85
--- /dev/null
+++ b/gr-checks/build-result-status_test.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<script src="imports.js"></script>
+<script src="webcomponentsjs/webcomponents-lite.js"></script>
+<link rel="import" href="polymer/polymer.html">
+
+<title>build-result-item</title>
+<link rel="import" href="webcomponent_lib/build-result-status.html">
+
+<test-fixture id="basic">
+  <template is="dom-template">
+    <build-result-status show-text="{{showText}}" status="{{status}}"></build-result-status>
+  </template>
+</test-fixture>
+
+<script>
+  suite('build-result-status tests', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+    });
+
+    teardown(() => { sandbox.restore(); });
+
+    function testStatus(expectedStatusClass, statusText, statusesToTest) {
+      suite(expectedStatusClass, () => {
+
+        for(let statusToTest of statusesToTest) {
+          test(`renders ${expectedStatusClass} for ${statusToTest}`, (done) => {
+            const element = fixture('basic', {
+              status: statusToTest,
+              showText: false,
+            });
+
+            flush(() => {
+              const icon = element.$$(`span.${expectedStatusClass} i`);
+              assert.exists(icon);
+              done();
+            });
+          });
+          test(`renders ${expectedStatusClass} for ${statusToTest} rendering text ${statusText}`, (done) => {
+            const element = fixture('basic', {
+              status: statusToTest,
+              showText: true,
+            });
+
+            flush(() => {
+              const text = element.$$(`span.${expectedStatusClass} span`);
+              assert.equal(text.textContent.trim(), statusText);
+              done();
+            });
+          });
+        }
+      });
+    }
+
+    testStatus('successful', 'Successful', ['SUCCESS']);
+    testStatus('failed', 'Failed', ['FAILURE', 'INTERNAL_ERROR', 'TIMEOUT']);
+    testStatus('in-progress', 'In progress', ['QUEUING', 'QUEUED', 'WORKING']);
+    testStatus('unevaluated', 'Unevaluated', ['STATUS_UNKNOWN', 'CANCELLED']);
+  });
+</script>
diff --git a/gr-checks/build-results-configure-link.html b/gr-checks/build-results-configure-link.html
new file mode 100644
index 0000000..8a83e3e
--- /dev/null
+++ b/gr-checks/build-results-configure-link.html
@@ -0,0 +1,18 @@
+<dom-module id="build-results-configure-link">
+  <template>
+    <style>
+      iron-icon {
+        height: 1.2rem;
+        margin-right: 4px;
+        width: 1.2rem;
+      }
+    </style>
+    <a href$="[[configurePath]]">
+      <gr-button link no-uppercase>
+        <iron-icon icon="gr-icons:settings"></iron-icon>
+        Configure checks
+      </gr-button>
+    </a>
+  </template>
+  <script src="build-results-configure-link.js"></script>
+</dom-module>
diff --git a/gr-checks/build-results-configure-link.js b/gr-checks/build-results-configure-link.js
new file mode 100644
index 0000000..c17b329
--- /dev/null
+++ b/gr-checks/build-results-configure-link.js
@@ -0,0 +1,11 @@
+(function() {
+'use strict';
+
+Polymer({
+  is: 'build-results-configure-link',
+
+  properties: {
+    configurePath: String,
+  },
+});
+})();
diff --git a/gr-checks/build-results-configure-link_test.html b/gr-checks/build-results-configure-link_test.html
new file mode 100644
index 0000000..179ada2
--- /dev/null
+++ b/gr-checks/build-results-configure-link_test.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<script src="imports.js"></script>
+<script src="webcomponentsjs/webcomponents-lite.js"></script>
+<link rel="import" href="polymer/polymer.html">
+
+<title>build-results-configure-link</title>
+<link rel="import" href="webcomponent_lib/build-results-configure-link.html">
+
+<test-fixture id="basic">
+  <template is="dom-template">
+    <build-results-configure-link configure-path="[[configurePath]]"></build-results-configure-link>
+  </template>
+</test-fixture>
+
+<script>
+  suite('build-results-configure-link tests', () => {
+    let element;
+    let sandbox;
+
+    setup((done) => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic', {
+        configurePath: 'http://example.com/test-configure',
+      });
+      flush(done);
+    });
+
+    teardown(() => { sandbox.restore(); });
+
+    test('renders the link to configure a build', () => {
+      const configureLink = element.$$('a');
+      assert.equal(configureLink.getAttribute('href'), 'http://example.com/test-configure');
+      assert.equal(configureLink.textContent.trim(), 'Configure checks');
+    });
+  });
+</script>
diff --git a/gr-checks/build-results-view.html b/gr-checks/build-results-view.html
new file mode 100644
index 0000000..9f30421
--- /dev/null
+++ b/gr-checks/build-results-view.html
@@ -0,0 +1,116 @@
+<dom-module id="build-results-view">
+  <template>
+    <style>
+      :host {
+        display: block;
+        width: 100%;
+      }
+
+      table {
+        width: 100%;
+      }
+
+      build-result-item {
+        display: table-row;
+      }
+
+      .headerRow {
+        border-bottom: 1px solid #ddd;
+      }
+
+      .topHeader {
+        padding-bottom: 4px;
+        text-align: left;
+        white-space: nowrap;
+      }
+
+      th.topHeader:last-child {
+        width: 100%;
+      }
+
+      h2 {
+        font-size: 1.17em;
+        font-weight: 500;
+      }
+
+      h3 {
+        flex: 1;
+        font-size: 1.17em;
+        font-weight: 500;
+        margin-bottom: 1.5rem;
+      }
+
+      header {
+        display: flex;
+        margin: 1rem 1rem 0;
+      }
+
+      table {
+        margin-bottom: 16px;
+      }
+
+      th:first-child {
+        padding-left: 1rem;
+      }
+
+      .no-content {
+        min-height: 106px;
+        padding: 24px 0;
+        text-align: center;
+      }
+    </style>
+
+    <template is="dom-if" if="[[_isLoading(_status)]]">
+      <div class="no-content">
+        <p>Loading...</p>
+      </div>
+    </template>
+
+    <template is="dom-if" if="[[_isEmpty(_status)]]">
+      <div class="no-content">
+        <h2>No checks ran for this code review</h2>
+        <p>Configure code review checks to view the results here.</p>
+        <build-results-configure-link configure-path="[[configurePath]]">
+        </build-results-configure-link>
+      </div>
+    </template>
+
+    <template is="dom-if" if="[[_isNotConfigured(_status)]]">
+      <div class="no-content">
+        <h2>Code review checks not configured</h2>
+        <p>Configure code review checks to view the results here.</p>
+        <build-results-configure-link configure-path="[[configurePath]]">
+        </build-results-configure-link>
+      </div>
+    </template>
+
+    <template is="dom-if" if="[[_hasResults(_status)]]">
+      <header>
+        <h3>Latest checks for Patchset [[revision._number]]</h3>
+        <build-results-configure-link configure-path="[[configurePath]]">
+        </build-results-configure-link>
+      </header>
+
+      <table>
+        <thead>
+          <tr class="headerRow">
+            <th class="topHeader">Name</th>
+            <th class="topHeader"><!-- required for merge --></th>
+            <th class="topHeader">Status</th>
+            <th class="topHeader">Checking system</th>
+            <th class="topHeader">Started</th>
+            <th class="topHeader">Duration</th>
+            <th class="topHeader"><!-- actions --></th>
+          </tr>
+        </thead>
+        <tbody>
+          <template is="dom-repeat" items="[[_buildResults]]" as="build">
+            <build-result-item build="[[build]]" retry-build="[[retryBuild]]" get-trigger="[[getTrigger]]"></build-result-item>
+          </template>
+        </tbody>
+      </table>
+    </template>
+  </template>
+
+  <script src="build-results-view.js"></script>
+</dom-module>
diff --git a/gr-checks/build-results-view.js b/gr-checks/build-results-view.js
new file mode 100644
index 0000000..ca51876
--- /dev/null
+++ b/gr-checks/build-results-view.js
@@ -0,0 +1,103 @@
+(function() {
+'use strict';
+
+const Defs = {};
+/**
+ * @typedef {{
+ *   revisions: !Object<string, !Object>,
+ * }}
+ */
+Defs.Change;
+
+/**
+ * @param {!Defs.Change} change The current CL.
+ * @param {!Object} revision The current patchset.
+ * @return {string|undefined}
+ */
+function currentRevisionSha(change, revision) {
+  return Object.keys(change.revisions)
+      .find(sha => change.revisions[sha] === revision);
+}
+
+const LoadingStatus = {
+  LOADING: 0,
+  EMPTY: 1,
+  RESULTS: 2,
+  NOT_CONFIGURED: 3,
+};
+
+Polymer({
+  is: 'build-results-view',
+
+  properties: {
+    revision: Object,
+    change: Object,
+    // TODO(brohlfs): Implement getBuildResults based on Checks Rest API.
+    /** @type {function(string, (string|undefined)): !Promise<!Object>} */
+    getBuildResults: Function,
+    // TODO(brohlfs): Implement isConfigured based on Checks Rest API.
+    /** @type {function(string): !Promise<!Object>} */
+    isConfigured: Function,
+    // TODO(brohlfs): Implement getTrigger based on Checks Rest API.
+    /** @type {function(string, string): !Promise<!Object>} */
+    getTrigger: Function,
+    // TODO(brohlfs): Implement retryBuild based on Checks Rest API.
+    /** @type {function(string, string): !Promise<!Object>} */
+    retryBuild: Function,
+    // TODO(brohlfs): Implement configurePath based on Checks Rest API.
+    // The url path to configure code review triggers.
+    configurePath: String,
+    _buildResults: Object,
+    _status: {
+      type: Object,
+      value: LoadingStatus.LOADING,
+    },
+  },
+
+  observers: [
+    '_fetchBuildResults(change, revision, getBuildResults)',
+  ],
+
+  /**
+   * @param {!Defs.Change} change The current CL.
+   * @param {!Object} revision The current patchset.
+   * @param {function(string, (string|undefined)): !Promise<!Object>}
+   *     getBuildResults function to get build results.
+   */
+  _fetchBuildResults(change, revision, getBuildResults) {
+    const repository = change['project'];
+    const gitSha = currentRevisionSha(change, revision);
+
+    getBuildResults(repository, gitSha).then(buildResults => {
+      if (buildResults && buildResults.length) {
+        this.set('_buildResults', buildResults);
+        this.set('_status', LoadingStatus.RESULTS);
+      } else {
+        this._checkConfigured();
+      }
+    });
+  },
+
+  _checkConfigured() {
+    const repository = this.change['project'];
+    this.isConfigured(repository).then(configured => {
+      const status =
+          configured ? LoadingStatus.EMPTY : LoadingStatus.NOT_CONFIGURED;
+      this.set('_status', status);
+    });
+  },
+
+  _isLoading(status) {
+    return status === LoadingStatus.LOADING;
+  },
+  _isEmpty(status) {
+    return status === LoadingStatus.EMPTY;
+  },
+  _hasResults(status) {
+    return status === LoadingStatus.RESULTS;
+  },
+  _isNotConfigured(status) {
+    return status === LoadingStatus.NOT_CONFIGURED;
+  },
+});
+})();
diff --git a/gr-checks/build-results-view_test.html b/gr-checks/build-results-view_test.html
new file mode 100644
index 0000000..79851a0
--- /dev/null
+++ b/gr-checks/build-results-view_test.html
@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<script src="imports.js"></script>
+<script src="webcomponentsjs/webcomponents-lite.js"></script>
+<link rel="import" href="polymer/polymer.html">
+
+<title>build-result-item</title>
+<link rel="import" href="webcomponent_lib/build-results-view.html">
+
+<test-fixture id="basic">
+  <template is="dom-template">
+    <build-results-view
+      change="[[change]]"
+      revision="[[revision]]"
+      configure-path="[[configurePath]]"
+      get-build-results="[[getBuildResults]]"
+      is-configured="[[isConfigured]]"
+      get-trigger="[[getTrigger]]"
+      retry-build="[[retryBuild]]">
+    </build-results-view>
+  </template>
+</test-fixture>
+
+<script>
+  const BUILD1 = {
+    buildTriggerId: 'test-build-trigger-id',
+    logUrl: 'http://example.com/test-log-url',
+    startTime: "2019-02-06T22:25:19.269Z",
+    finishTime: "2019-02-06T22:25:44.574Z",
+  };
+const REVISION = {
+  "kind": "REWORK",
+  "_number": 3,
+  "created": "2018-05-15 21:56:13.000000000",
+  "uploader": {
+    "_account_id": 1000000,
+  },
+  "ref": "refs/changes/00/1000/1",
+  "commit": {
+    "parents": [],
+    "subject": "added test file",
+    "message": "added test file\n\nChange-Id: I8df212a28ae23cc239afd10ee4f506887e03ab70\n",
+    "commit": "1c9a1dfd38ea51dc7880f3ddf669100710f0c91b"
+  },
+};
+
+suite('build-results-view tests', () => {
+  let element;
+  let sandbox;
+  let getBuildResultsSpy;
+  let getBuildResultsResolve;
+  let getTriggerSpy;
+  let retryBuildSpy;
+  let isConfiguredSpy;
+  let isConfiguredResolve;
+
+  setup((done) => {
+    sandbox = sinon.sandbox.create();
+
+    getBuildResultsSpy = sinon.stub();
+    const getBuildResultsPromise = new Promise((resolve, reject) => {
+      getBuildResultsResolve = resolve;
+    });
+    getBuildResultsSpy.returns(getBuildResultsPromise);
+
+    isConfiguredSpy = sinon.stub();
+    const isConfiguredPromise = new Promise((resolve, reject) => {
+      isConfiguredResolve = resolve;
+    });
+    isConfiguredSpy.returns(isConfiguredPromise);
+
+    getTriggerSpy = sinon.stub();
+    getTriggerSpy.returns(Promise.resolve({description: 'test trigger name'}));
+    retryBuildSpy = sinon.stub();
+    retryBuildSpy.returns(Promise.resolve());
+
+    element = fixture('basic', {
+      getTrigger: getTriggerSpy,
+      retryBuild: retryBuildSpy,
+      getBuildResults: getBuildResultsSpy,
+      isConfigured: isConfiguredSpy,
+      configurePath: 'http://example.com/test-configure',
+      change: {
+        'project': 'test-repository',
+        'revisions': {
+          'first-sha': "test-revision",
+          'second-sha': REVISION,
+        }
+      },
+      revision: REVISION,
+    });
+    flush(done);
+  });
+
+  teardown(() => { sandbox.restore(); });
+
+  test('renders loading', () => {
+    assert.equal(element.textContent.trim(), 'Loading...');
+  });
+
+  test('queries the builds', () => {
+    assert.isTrue(getBuildResultsSpy.called);
+    assert.isTrue(getBuildResultsSpy.calledWith('test-repository', 'second-sha'));
+  });
+
+  suite('no builds returned', () => {
+    setup((done) => {
+      getBuildResultsResolve([]);
+      flush(done);
+    });
+
+    test('it calls to check if the builds are configured', () => {
+      assert.isTrue(isConfiguredSpy.called);
+      assert.isTrue(isConfiguredSpy.calledWith('test-repository'));
+    });
+
+    suite('not configured', () => {
+      setup((done) => {
+        isConfiguredResolve(false);
+        flush(done);
+      });
+
+      test('renders checks not configured', () => {
+        const header = element.$$('h2');
+        assert.equal(header.textContent.trim(), 'Code review checks not configured');
+      });
+
+      test('renders the link to configure a build', () => {
+        const configureLink = element.$$('build-results-configure-link');
+        assert.exists(configureLink);
+      });
+    });
+
+    suite('no checks ran', () => {
+      setup((done) => {
+        isConfiguredResolve(true);
+        flush(done);
+      });
+
+      test('renders checks not configured', () => {
+        const header = element.$$('h2');
+        assert.equal(header.textContent.trim(), 'No checks ran for this code review');
+      });
+
+      test('renders the link to configure a build', () => {
+        const configureLink = element.$$('build-results-configure-link');
+        assert.exists(configureLink);
+      });
+    });
+  });
+
+  suite('with build results', () => {
+    setup(done => {
+      getBuildResultsResolve([BUILD1, BUILD1, BUILD1]);
+      flush(done);
+    });
+
+    test('it calls to check if the builds are configured', () => {
+      assert.isFalse(isConfiguredSpy.called);
+    });
+
+    test('renders the header', () => {
+      const header = element.$$('header > h3');
+      assert.equal(header.textContent.trim(), 'Latest checks for Patchset 3');
+    });
+
+    test('renders the link to configure a build', () => {
+      const configureLink = element.$$('header > build-results-configure-link');
+      assert.exists(configureLink);
+    });
+
+    test('renders a table of all the builds', () => {
+      const tbody = element.$$('table > tbody');
+      assert.lengthOf(tbody.querySelectorAll('build-result-item'), 3)
+    });
+  });
+});
+</script>
diff --git a/gr-checks/build-results.html b/gr-checks/build-results.html
new file mode 100644
index 0000000..c47ec37
--- /dev/null
+++ b/gr-checks/build-results.html
@@ -0,0 +1,28 @@
+<link rel="import" href="build-results-view.html">
+<link rel="import" href="build-result-item.html">
+<link rel="import" href="build-result-status.html">
+<link rel="import" href="builds-chip-view.html">
+<link rel="import" href="builds-results-change-list-item-cell-view.html">
+<link rel="import" href="builds-results-change-list-header-view.html">
+<link rel="import" href="builds-results-change-view-tab-header-view.html">
+<link rel="import" href="build-results-configure-link.html">
+
+<dom-module id="build-results">
+  <script>
+    Gerrit.install(plugin => {
+      plugin.registerDynamicCustomComponent(
+          'change-list-header',
+          'builds-results-change-list-header-view');
+      plugin.registerDynamicCustomComponent(
+          'change-list-item-cell',
+          'builds-results-change-list-item-cell-view');
+      plugin.registerDynamicCustomComponent(
+          'change-view-tab-header',
+          'builds-results-change-view-tab-header-view');
+      plugin.registerDynamicCustomComponent(
+          'change-view-tab-content',
+          'build-results-view').onAttached(
+          buildResultsView => console.log('build-results-view attached'));
+    });
+  </script>
+</dom-module>
diff --git a/gr-checks/builds-chip-view.html b/gr-checks/builds-chip-view.html
new file mode 100644
index 0000000..110d26b
--- /dev/null
+++ b/gr-checks/builds-chip-view.html
@@ -0,0 +1,31 @@
+<dom-module id="builds-chip-view">
+  <template>
+    <style>
+
+      :host {
+        display: inline-block;
+      }
+
+      .chip {
+        border-color: #D0D0D0;
+        border-radius: 4px;
+        border-style: solid;
+        border-width: 1px;
+        padding: 4px 8px;
+      }
+      .chip.failed {
+        border-color: #DA4236;
+      }
+    </style>
+    <template is="dom-if" if="[[_hasBuilds]]">
+      Checks:
+      <span class$="[[_chipClasses]]">
+        <build-result-status status="[[_status]]"></build-result-status>
+        [[_statusString]]
+      </span>
+    </template>
+  </template>
+
+  <script src="all-statuses.js"></script>
+  <script src="builds-chip-view.js"></script>
+</dom-module>
diff --git a/gr-checks/builds-chip-view.js b/gr-checks/builds-chip-view.js
new file mode 100644
index 0000000..2fff5bf
--- /dev/null
+++ b/gr-checks/builds-chip-view.js
@@ -0,0 +1,124 @@
+(function() {
+'use strict';
+const Statuses = window.Gerrit.BuildResults.Statuses;
+
+const StatusPriorityOrder = [
+  Statuses.INTERNAL_ERROR, Statuses.TIMEOUT, Statuses.FAILURE,
+  Statuses.STATUS_UNKNOWN, Statuses.CANCELLED, Statuses.QUEUED,
+  Statuses.QUEUING, Statuses.WORKING, Statuses.SUCCESS
+];
+
+const HumanizedStatuses = {
+  // non-terminal statuses
+  STATUS_UNKNOWN: 'unevaluated',
+  QUEUING: 'in progress',
+  QUEUED: 'in progress',
+  WORKING: 'in progress',
+
+  // terminal statuses
+  SUCCESS: 'successful',
+  FAILURE: 'failed',
+  INTERNAL_ERROR: 'failed',
+  TIMEOUT: 'failed',
+  CANCELLED: 'unevaluated',
+};
+
+
+const Defs = {};
+/**
+ * @typedef {{
+ *   revisions: !Object<string, !Object>,
+ * }}
+ */
+Defs.Change;
+
+/**
+ * @param {!Defs.Change} change The current CL.
+ * @param {!Object} revision The current patchset.
+ * @return {string|undefined}
+ */
+function currentRevisionSha(change, revision) {
+  return Object.keys(change.revisions)
+      .find(sha => change.revisions[sha] === revision);
+}
+
+function computeBuildResultStatuses(buildResults) {
+  return buildResults.reduce((accum, buildResult) => {
+    accum[buildResult.status] || (accum[buildResult.status] = 0);
+    accum[buildResult.status]++;
+    return accum;
+  }, {total: buildResults.length});
+}
+
+Polymer({
+  is: 'builds-chip-view',
+
+  properties: {
+    revision: Object,
+    change: Object,
+    // TODO(brohlfs): Implement getBuildResults based on new Rest APIs.
+    /** @type {function(string, (string|undefined)): !Promise<!Object>} */
+    getBuildResults: Function,
+    _buildResultStatuses: Object,
+    _hasBuilds: Boolean,
+    _status: {type: String, computed: '_computeStatus(_buildResultStatuses)'},
+    _statusString: {
+      type: String,
+      computed: '_computeStatusString(_status, _buildResultStatuses)'
+    },
+    _chipClasses: {type: String, computed: '_computeChipClass(_status)'},
+  },
+
+  observers: [
+    '_fetchBuildResults(change, revision, getBuildResults)',
+  ],
+
+  /**
+   * @param {!Defs.Change} change The current CL.
+   * @param {!Object} revision The current patchset.
+   * @param {function(string, (string|undefined)): !Promise<!Object>}
+   *     getBuildResults function to get build results.
+   */
+  _fetchBuildResults(change, revision, getBuildResults) {
+    const repository = change['project'];
+    const gitSha = currentRevisionSha(change, revision);
+
+    getBuildResults(repository, gitSha).then(buildResults => {
+      this.set('_hasBuilds', buildResults.length > 0);
+      if (buildResults.length > 0) {
+        this.set(
+            '_buildResultStatuses', computeBuildResultStatuses(buildResults));
+      }
+    });
+  },
+
+  /**
+   * @param {!Object} buildResultStatuses The number of builds in each status.
+   * @return {string}
+   */
+  _computeStatus(buildResultStatuses) {
+    return StatusPriorityOrder.find(
+               status => buildResultStatuses[status] > 0) ||
+        Statuses.STATUS_UNKNOWN;
+  },
+
+  /**
+   * @param {string} status The overall status of the build results.
+   * @param {!Object} buildResultStatuses The number of builds in each status.
+   * @return {string}
+   */
+  _computeStatusString(status, buildResultStatuses) {
+    if (buildResultStatuses.total === 0) return 'No builds';
+    return `${buildResultStatuses[status]} of ${
+        buildResultStatuses.total} builds ${HumanizedStatuses[status]}`;
+  },
+
+  /**
+   * @param {string} status The overall status of the build results.
+   * @return {string}
+   */
+  _computeChipClass(status) {
+    return `chip ${window.Gerrit.BuildResults.statusClass(status)}`;
+  },
+});
+})();
diff --git a/gr-checks/builds-chip-view_test.html b/gr-checks/builds-chip-view_test.html
new file mode 100644
index 0000000..2f0feb5
--- /dev/null
+++ b/gr-checks/builds-chip-view_test.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<script src="imports.js"></script>
+<script src="webcomponentsjs/webcomponents-lite.js"></script>
+<link rel="import" href="polymer/polymer.html">
+
+<title>builds-chip-view</title>
+<link rel="import" href="webcomponent_lib/builds-chip-view.html">
+
+<test-fixture id="basic">
+  <template is="dom-template">
+    <builds-chip-view change="[[change]]" revision="[[revision]]" get-build-results="[[getBuildResults]]"></builds-chip-view>
+  </template>
+</test-fixture>
+
+<script>
+  const BUILD1 = {
+    buildTriggerId: 'test-build-trigger-id',
+    logUrl: 'http://example.com/test-log-url',
+    startTime: "2019-02-06T22:25:19.269Z",
+    finishTime: "2019-02-06T22:25:44.574Z",
+    status: 'SUCCESS',
+  };
+
+  const BUILD2 = {
+    buildTriggerId: 'test-build-trigger-id-2',
+    logUrl: 'http://example.com/test-log-url',
+    startTime: "2019-02-06T22:25:19.269Z",
+    finishTime: "2019-02-06T22:25:44.574Z",
+    status: 'FAILURE',
+  };
+
+  suite('build-chip-view tests', () => {
+    let element;
+    let sandbox;
+    let getBuildResultsSpy;
+
+    setup((done) => {
+      sandbox = sinon.sandbox.create();
+
+      getBuildResultsSpy = sinon.stub();
+      getBuildResultsSpy.returns(Promise.resolve([BUILD1, BUILD2, BUILD1]));
+
+      element = fixture('basic', {
+        getBuildResults: getBuildResultsSpy,
+        change: {
+          'project': 'test-repository',
+          'revisions': {
+            'first-sha': "test-revision",
+            'second-sha': "test-revision2",
+          }
+        },
+        revision: 'test-revision2',
+      });
+      flush(done);
+    });
+
+    teardown(() => { sandbox.restore(); });
+
+    test('renders the checks prefix', () => {
+      assert.include(element.textContent.trim(), 'Checks:');
+    });
+
+    suite('builds chip contents', () => {
+      test('queries the builds', () => {
+        assert.isTrue(getBuildResultsSpy.called);
+        assert.isTrue(getBuildResultsSpy.calledWith('test-repository', 'second-sha'));
+      });
+
+      test('renders the text of failed builds', () => {
+        const chip = element.$$('.chip');
+        assert.equal(chip.textContent.trim(), '1 of 3 builds failed');
+      });
+    });
+  });
+</script>
diff --git a/gr-checks/builds-results-change-list-header-view.html b/gr-checks/builds-results-change-list-header-view.html
new file mode 100644
index 0000000..596adfe
--- /dev/null
+++ b/gr-checks/builds-results-change-list-header-view.html
@@ -0,0 +1,13 @@
+<dom-module id="builds-results-change-list-header-view">
+  <style>
+    :host {
+      display: table-cell;
+      padding: 0 3px;
+    }
+  </style>
+  <template>
+    Checks
+  </template>
+
+  <script src="builds-results-change-list-header-view.js"></script>
+</dom-module>
diff --git a/gr-checks/builds-results-change-list-header-view.js b/gr-checks/builds-results-change-list-header-view.js
new file mode 100644
index 0000000..19aad70
--- /dev/null
+++ b/gr-checks/builds-results-change-list-header-view.js
@@ -0,0 +1,7 @@
+(function() {
+'use strict';
+
+Polymer({
+  is: 'builds-results-change-list-header-view',
+});
+})();
diff --git a/gr-checks/builds-results-change-list-header-view_test.html b/gr-checks/builds-results-change-list-header-view_test.html
new file mode 100644
index 0000000..b380930
--- /dev/null
+++ b/gr-checks/builds-results-change-list-header-view_test.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<script src="imports.js"></script>
+<script src="webcomponentsjs/webcomponents-lite.js"></script>
+<link rel="import" href="polymer/polymer.html">
+
+<title>builds-results-change-list-header-view</title>
+<link rel="import" href="webcomponent_lib/builds-results-change-list-header-view.html">
+
+<test-fixture id="basic">
+  <template is="dom-template">
+    <builds-results-change-list-header-view></builds-results-change-list-header-view>
+  </template>
+</test-fixture>
+
+<script>
+
+  suite('builds-results-change-list-header-view tests', () => {
+    let element;
+    let sandbox;
+
+    setup((done) => {
+      sandbox = sinon.sandbox.create();
+
+
+      element = fixture('basic');
+      flush(done);
+    });
+
+    teardown(() => { sandbox.restore(); });
+
+    test('renders the header', () => {
+      assert.include(element.textContent.trim(), 'Checks');
+    });
+  });
+</script>
diff --git a/gr-checks/builds-results-change-list-item-cell-view.html b/gr-checks/builds-results-change-list-item-cell-view.html
new file mode 100644
index 0000000..a919c3d
--- /dev/null
+++ b/gr-checks/builds-results-change-list-item-cell-view.html
@@ -0,0 +1,13 @@
+<dom-module id="builds-results-change-list-item-cell-view">
+  <style>
+    :host {
+      display: inline-block;
+      text-align: center;
+      width: 100%;
+    }
+  </style>
+  <template>
+  </template>
+
+  <script src="builds-results-change-list-item-cell-view.js"></script>
+</dom-module>
diff --git a/gr-checks/builds-results-change-list-item-cell-view.js b/gr-checks/builds-results-change-list-item-cell-view.js
new file mode 100644
index 0000000..ff8b556
--- /dev/null
+++ b/gr-checks/builds-results-change-list-item-cell-view.js
@@ -0,0 +1,11 @@
+(function() {
+'use strict';
+
+Polymer({
+  is: 'builds-results-change-list-item-cell-view',
+
+  properties: {
+    change: Object,
+  },
+});
+})();
diff --git a/gr-checks/builds-results-change-list-item-cell-view_test.html b/gr-checks/builds-results-change-list-item-cell-view_test.html
new file mode 100644
index 0000000..f1047fe
--- /dev/null
+++ b/gr-checks/builds-results-change-list-item-cell-view_test.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<script src="imports.js"></script>
+<script src="webcomponentsjs/webcomponents-lite.js"></script>
+<link rel="import" href="polymer/polymer.html">
+
+<title>builds-results-change-list-item-cell-view</title>
+<link rel="import" href="webcomponent_lib/builds-results-change-list-item-cell-view.html">
+
+<test-fixture id="basic">
+  <template is="dom-template">
+    <builds-results-change-list-item-cell-view change="[[change]]"></builds-results-change-list-item-cell-view>
+  </template>
+</test-fixture>
+
+<script>
+
+  suite('builds-results-change-list-item-cell-view tests', () => {
+    let element;
+    let sandbox;
+
+    setup((done) => {
+      sandbox = sinon.sandbox.create();
+
+
+      element = fixture('basic', {
+        change: {
+          'project': 'test-repository',
+          'revisions': {
+            'first-sha': "test-revision",
+            'second-sha': "test-revision2",
+          }
+        },
+      });
+      flush(done);
+    });
+
+    teardown(() => { sandbox.restore(); });
+
+    test('renders the element', () => {
+      assert.exists(element);
+    });
+  });
+</script>
diff --git a/gr-checks/builds-results-change-view-tab-header-view.html b/gr-checks/builds-results-change-view-tab-header-view.html
new file mode 100644
index 0000000..2982f70
--- /dev/null
+++ b/gr-checks/builds-results-change-view-tab-header-view.html
@@ -0,0 +1,7 @@
+<dom-module id="builds-results-change-view-tab-header-view">
+  <template>
+    Checks
+  </template>
+
+  <script src="builds-results-change-view-tab-header-view.js"></script>
+</dom-module>
diff --git a/gr-checks/builds-results-change-view-tab-header-view.js b/gr-checks/builds-results-change-view-tab-header-view.js
new file mode 100644
index 0000000..27ccf83
--- /dev/null
+++ b/gr-checks/builds-results-change-view-tab-header-view.js
@@ -0,0 +1,7 @@
+(function() {
+'use strict';
+
+Polymer({
+  is: 'builds-results-change-view-tab-header-view',
+});
+})();
diff --git a/gr-checks/builds-results-change-view-tab-header-view_test.html b/gr-checks/builds-results-change-view-tab-header-view_test.html
new file mode 100644
index 0000000..b71371b
--- /dev/null
+++ b/gr-checks/builds-results-change-view-tab-header-view_test.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<script src="imports.js"></script>
+<script src="webcomponentsjs/webcomponents-lite.js"></script>
+<link rel="import" href="polymer/polymer.html">
+
+<title>builds-results-change-view-tab-header-view</title>
+<link rel="import" href="webcomponent_lib/builds-results-change-view-tab-header-view.html">
+
+<test-fixture id="basic">
+  <template is="dom-template">
+    <builds-results-change-view-tab-header-view></builds-results-change-view-tab-header-view>
+  </template>
+</test-fixture>
+
+<script>
+  suite('builds-results-change-view-tab-header-view tests', () => {
+    let element;
+    let sandbox;
+
+    setup((done) => {
+      sandbox = sinon.sandbox.create();
+
+
+      element = fixture('basic');
+      flush(done);
+    });
+
+    teardown(() => { sandbox.restore(); });
+
+    test('renders the header', () => {
+      assert.include(element.textContent.trim(), 'Checks');
+    });
+  });
+</script>
diff --git a/gr-checks/externs.js b/gr-checks/externs.js
new file mode 100644
index 0000000..f14959b
--- /dev/null
+++ b/gr-checks/externs.js
@@ -0,0 +1,6 @@
+/**
+ * @fileoverview externs for moment.js
+ * @externs
+ */
+
+function moment() {};