[gold] Port gold-scaffold-sk to TypeScript.

Bug: skia:10246
Change-Id: I0b813d5c59cbf9a0c7b0915312186c23ba236f51
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/404018
Commit-Queue: Kevin Lubick <kjlubick@google.com>
Reviewed-by: Kevin Lubick <kjlubick@google.com>
diff --git a/golden/modules/byblame-page-sk/byblame-page-sk-demo.ts b/golden/modules/byblame-page-sk/byblame-page-sk-demo.ts
index 6e28582..1ed9450 100644
--- a/golden/modules/byblame-page-sk/byblame-page-sk-demo.ts
+++ b/golden/modules/byblame-page-sk/byblame-page-sk-demo.ts
@@ -12,6 +12,7 @@
 import { delay } from '../demo_util';
 import { testOnlySetSettings } from '../settings';
 import { ByBlameResponse } from '../rpc_types';
+import { GoldScaffoldSk } from '../gold-scaffold-sk/gold-scaffold-sk';
 
 testOnlySetSettings({
   title: 'Skia Public',
@@ -51,8 +52,8 @@
 });
 
 // By adding these elements after all the fetches are mocked out, they should load ok.
-const newScaf = document.createElement('gold-scaffold-sk');
-newScaf.setAttribute('testing_offline', 'true');
+const newScaf = new GoldScaffoldSk();
+newScaf.testingOffline = true;
 const body = $$('body')!;
 body.insertBefore(newScaf, body.childNodes[0]); // Make it the first element in body.
 const page = document.createElement('byblame-page-sk');
diff --git a/golden/modules/changelists-page-sk/changelists-page-sk-demo.ts b/golden/modules/changelists-page-sk/changelists-page-sk-demo.ts
index a1864fb..434f928 100644
--- a/golden/modules/changelists-page-sk/changelists-page-sk-demo.ts
+++ b/golden/modules/changelists-page-sk/changelists-page-sk-demo.ts
@@ -7,6 +7,7 @@
 import { fakeNow, changelistSummaries_5, empty } from './test_data';
 import { testOnlySetSettings } from '../settings';
 import { exampleStatusData } from '../last-commit-sk/demo_data';
+import { GoldScaffoldSk } from '../gold-scaffold-sk/gold-scaffold-sk';
 
 testOnlySetSettings({
   title: 'Skia Public',
@@ -42,8 +43,8 @@
 fetchMock.get('/json/v1/trstatus', JSON.stringify(exampleStatusData));
 
 // By adding these elements after all the fetches are mocked out, they should load ok.
-const newScaf = document.createElement('gold-scaffold-sk');
-newScaf.setAttribute('testing_offline', 'true');
+const newScaf = new GoldScaffoldSk();
+newScaf.testingOffline = true;
 // Make it the first element in body.
 document.body.insertBefore(newScaf, document.body.childNodes[0]);
 const page = document.createElement('changelists-page-sk');
diff --git a/golden/modules/cluster-page-sk/cluster-page-sk-demo.js b/golden/modules/cluster-page-sk/cluster-page-sk-demo.js
index efe9c47..50ea7ae 100644
--- a/golden/modules/cluster-page-sk/cluster-page-sk-demo.js
+++ b/golden/modules/cluster-page-sk/cluster-page-sk-demo.js
@@ -9,6 +9,7 @@
 import { clusterDiffJSON } from './test_data';
 import { fakeNow, twoHundredCommits, typicalDetails } from '../digest-details-sk/test_data';
 import { exampleStatusData } from '../last-commit-sk/demo_data';
+import { GoldScaffoldSk } from '../gold-scaffold-sk/gold-scaffold-sk';
 
 testOnlySetSettings({
   title: 'Skia Demo',
@@ -50,8 +51,8 @@
 }, fakeRpcDelayMillis));
 
 // By adding these elements after all the fetches are mocked out, they should load ok.
-const newScaf = document.createElement('gold-scaffold-sk');
-newScaf.setAttribute('testing_offline', 'true');
+const newScaf = new GoldScaffoldSk();
+newScaf.testingOffline = true;
 const body = $$('body');
 body.insertBefore(newScaf, body.childNodes[0]); // Make it the first element in body.
 const page = document.createElement('cluster-page-sk');
diff --git a/golden/modules/common.ts b/golden/modules/common.ts
index 3b488c5..2c2534b 100644
--- a/golden/modules/common.ts
+++ b/golden/modules/common.ts
@@ -113,6 +113,12 @@
   ele.dispatchEvent(new CustomEvent('end-task', { bubbles: true }));
 }
 
+/** Detail of the fetch-error event. */
+export interface FetchErrorEventDetail {
+  error: any;
+  loading: string;
+};
+
 /**
  * Helper to tell gold-scaffold-sk that a fetch failed. This will pop up on the toast-sk.
  * @param ele Element from which to dispatch the 'fetch-error' custom element.
@@ -120,7 +126,7 @@
  * @param what Description of what was being fetched.
  */
 export function sendFetchError(ele: Element, e: any, what: string) {
-  ele.dispatchEvent(new CustomEvent('fetch-error', {
+  ele.dispatchEvent(new CustomEvent<FetchErrorEventDetail>('fetch-error', {
     detail: {
       error: e,
       loading: what,
diff --git a/golden/modules/details-page-sk/details-page-sk-demo.js b/golden/modules/details-page-sk/details-page-sk-demo.js
index c384aaf..3de55c3 100644
--- a/golden/modules/details-page-sk/details-page-sk-demo.js
+++ b/golden/modules/details-page-sk/details-page-sk-demo.js
@@ -8,6 +8,7 @@
 import { testOnlySetSettings } from '../settings';
 import { exampleStatusData } from '../last-commit-sk/demo_data';
 import fetchMock from 'fetch-mock';
+import { GoldScaffoldSk } from '../gold-scaffold-sk/gold-scaffold-sk';
 
 testOnlySetSettings({
   title: 'Skia Public',
@@ -62,8 +63,8 @@
 });
 
 // By adding these elements after all the fetches are mocked out, they should load ok.
-const newScaf = document.createElement('gold-scaffold-sk');
-newScaf.setAttribute('testing_offline', 'true');
+const newScaf = new GoldScaffoldSk();
+newScaf.testingOffline = true;
 const body = $$('body');
 body.insertBefore(newScaf, body.childNodes[0]); // Make it the first element in body.
 const page = document.createElement('details-page-sk');
diff --git a/golden/modules/diff-page-sk/diff-page-sk-demo.js b/golden/modules/diff-page-sk/diff-page-sk-demo.js
index 7ee1783..d82bd19 100644
--- a/golden/modules/diff-page-sk/diff-page-sk-demo.js
+++ b/golden/modules/diff-page-sk/diff-page-sk-demo.js
@@ -8,6 +8,7 @@
 import { testOnlySetSettings } from '../settings';
 import { exampleStatusData } from '../last-commit-sk/demo_data';
 import fetchMock from 'fetch-mock';
+import { GoldScaffoldSk } from '../gold-scaffold-sk/gold-scaffold-sk';
 
 testOnlySetSettings({
   title: 'Skia Public',
@@ -57,8 +58,8 @@
 });
 
 // By adding these elements after all the fetches are mocked out, they should load ok.
-const newScaf = document.createElement('gold-scaffold-sk');
-newScaf.setAttribute('testing_offline', 'true');
+const newScaf = new GoldScaffoldSk();
+newScaf.testingOffline = true;
 const body = $$('body');
 body.insertBefore(newScaf, body.childNodes[0]); // Make it the first element in body.
 const page = document.createElement('diff-page-sk');
diff --git a/golden/modules/gold-scaffold-sk/gold-scaffold-sk-demo.js b/golden/modules/gold-scaffold-sk/gold-scaffold-sk-demo.js
deleted file mode 100644
index 5838dd9..0000000
--- a/golden/modules/gold-scaffold-sk/gold-scaffold-sk-demo.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import './index';
-import { testOnlySetSettings } from '../settings';
-import { $$ } from 'common-sk/modules/dom';
-
-testOnlySetSettings({
-  title: 'Skia Public',
-});
-$$('gold-scaffold-sk')._render(); // pick up title from settings.
diff --git a/golden/modules/gold-scaffold-sk/gold-scaffold-sk-demo.ts b/golden/modules/gold-scaffold-sk/gold-scaffold-sk-demo.ts
new file mode 100644
index 0000000..ad7ddf6
--- /dev/null
+++ b/golden/modules/gold-scaffold-sk/gold-scaffold-sk-demo.ts
@@ -0,0 +1,15 @@
+import './index';
+import { testOnlySetSettings } from '../settings';
+import { $$ } from 'common-sk/modules/dom';
+import { GoldScaffoldSk } from './gold-scaffold-sk';
+
+testOnlySetSettings({
+  title: 'Skia Public',
+});
+
+// Remove from DOM and reattach to trigger a re-render. This ensures that the GoldScaffoldSk
+// instance will pick up the title from the above settings.
+const goldScaffoldSk = $$<GoldScaffoldSk>('gold-scaffold-sk')!;
+const parent = goldScaffoldSk.parentNode!;
+parent.removeChild(goldScaffoldSk);
+parent.appendChild(goldScaffoldSk);
diff --git a/golden/modules/gold-scaffold-sk/gold-scaffold-sk.js b/golden/modules/gold-scaffold-sk/gold-scaffold-sk.js
deleted file mode 100644
index 9b607ef..0000000
--- a/golden/modules/gold-scaffold-sk/gold-scaffold-sk.js
+++ /dev/null
@@ -1,213 +0,0 @@
-/**
- * @module modules/gold-scaffold-sk
- * @description <h2><code>gold-scaffold-sk</code></h2>
- *
- * Contains the title bar, side bar, and error-toast for all the Gold pages. The rest of
- * every Gold page should be a child of this element.
- *
- * Has a spinner-sk that can be activated when it hears "begin-task" events and keeps
- * spinning until it hears an equal number of "end-task" events.
- *
- * The error-toast element responds to fetch-error events and normal error-sk events.
- *
- * @attr {boolean} testing_offline - If we should operate entirely in offline mode.
- */
-import { define } from 'elements-sk/define';
-import { errorMessage } from 'elements-sk/errorMessage';
-import { html } from 'lit-html';
-import { ElementSk } from '../../../infra-sk/modules/ElementSk';
-import { title } from '../settings';
-
-import '../../../infra-sk/modules/app-sk';
-import '../../../infra-sk/modules/login-sk';
-
-import '../last-commit-sk';
-
-import 'elements-sk/error-toast-sk';
-import 'elements-sk/icon/find-in-page-icon-sk';
-import 'elements-sk/icon/folder-icon-sk';
-import 'elements-sk/icon/help-icon-sk';
-import 'elements-sk/icon/home-icon-sk';
-import 'elements-sk/icon/label-icon-sk';
-import 'elements-sk/icon/laptop-chromebook-icon-sk';
-import 'elements-sk/icon/list-icon-sk';
-import 'elements-sk/icon/search-icon-sk';
-import 'elements-sk/icon/sync-problem-icon-sk';
-import 'elements-sk/icon/view-day-icon-sk';
-import 'elements-sk/spinner-sk';
-
-const template = (ele) => html`
-<app-sk>
-  <header>
-    <h1>${title()}</h1>
-    <div class=spinner-spacer>
-      <spinner-sk></spinner-sk>
-    </div>
-    <div class=spacer></div>
-    <last-commit-sk></last-commit-sk>
-    <login-sk ?testing_offline=${ele.testingOffline} .loginHost=${window.location.host}></login-sk>
-  </header>
-
-  <aside>
-    <nav>
-      <a href="/" tab-index=0>
-        <home-icon-sk></home-icon-sk><span>Home</span>
-      </a>
-      <a href="/" tab-index=0>
-        <view-day-icon-sk></view-day-icon-sk><span>By Blame<span>
-      </a>
-      <a href="/list" tab-index=0>
-        <list-icon-sk></list-icon-sk><span>By Test</span>
-      </a>
-      <a href="/changelists" tab-index=0>
-        <laptop-chromebook-icon-sk></laptop-chromebook-icon-sk><span>By ChangeList</span>
-      </a>
-      <a href="/search" tab-index=0>
-        <search-icon-sk></search-icon-sk><span>Search</span>
-      </a>
-      <a href="/ignores" tab-index=0>
-        <label-icon-sk></label-icon-sk><span>Ignores</span>
-      </a>
-      <a href="/triagelog" tab-index=0>
-        <find-in-page-icon-sk></find-in-page-icon-sk><span>Triage Log</span>
-      </a>
-      <a href="/help" tab-index=0>
-        <help-icon-sk></help-icon-sk><span>Help</span>
-      </a>
-      <a href="https://github.com/google/skia-buildbot/tree/master/golden" tab-index=0>
-        <folder-icon-sk></folder-icon-sk><span>Code</span>
-      </a>
-    </nav>
-  </aside>
-
-  <main></main>
-
-  <footer><error-toast-sk></error-toast-sk></footer>
-</app-sk>
-`;
-
-/**
- * Moves the elements from one NodeList to another NodeList.
- *
- * @param {NodeList} from - The list we are moving from.
- * @param {NodeList} to - The list we are moving to.
- */
-function move(from, to) {
-  Array.prototype.slice.call(from).forEach((ele) => to.appendChild(ele));
-}
-
-define('gold-scaffold-sk', class extends ElementSk {
-  constructor() {
-    super(template);
-    this._main = null;
-    this._busyTaskCount = 0;
-    this._spinner = null;
-  }
-
-  connectedCallback() {
-    super.connectedCallback();
-    // Don't call more than once.
-    if (this._main) {
-      return;
-    }
-    this.addEventListener('begin-task', this._addBusyTask);
-    this.addEventListener('end-task', this._finishedTask);
-    this.addEventListener('fetch-error', this._fetchError);
-
-    // We aren't using shadow dom so we need to manually move the children of
-    // gold-scaffold-sk to be children of 'main'. We have to do this for the
-    // existing elements and for all future mutations.
-
-    // Create a temporary holding spot for elements we're moving.
-    const div = document.createElement('div');
-    move(this.children, div);
-
-    // Now that we've moved all the old children out of the way we can render
-    // the template.
-    this._render();
-
-    this._spinner = this.querySelector('header spinner-sk');
-
-    // Move the old children back under main.
-    this._main = this.querySelector('main');
-    move(div.children, this._main);
-
-    // Move all future children under main also.
-    const observer = new MutationObserver((mutList) => {
-      mutList.forEach((mut) => {
-        move(mut.addedNodes, this._main);
-      });
-    });
-    observer.observe(this, { childList: true });
-  }
-
-  disconnectedCallback() {
-    super.disconnectedCallback();
-    this.removeEventListener('begin-task', this._addBusyTask);
-    this.removeEventListener('end-task', this._finishedTask);
-    this.removeEventListener('fetch-error', this._fetchError);
-  }
-
-  /** @prop {boolean} busy Indicates if there any on-going tasks (e.g. RPCs).
-   *                  This also mirrors the status of the embedded spinner-sk.
-   *                  Read-only. */
-  get busy() { return !!this._busyTaskCount; }
-
-  /** @prop testingOffline {boolean} Reflects the testing_offline attribute for ease of use.
-   */
-  get testingOffline() { return this.hasAttribute('testing_offline'); }
-
-  set testingOffline(val) {
-    if (val) {
-      this.setAttribute('testing_offline', '');
-    } else {
-      this.removeAttribute('testing_offline');
-    }
-  }
-
-  /**
-   * Indicate there are some number of tasks (e.g. RPCs) the app is waiting on
-   * and should be in the "busy" state, if it isn't already.
-   *
-   */
-  _addBusyTask() {
-    this._busyTaskCount++;
-    if (this._spinner && this._busyTaskCount > 0) {
-      this._spinner.active = true;
-    }
-  }
-
-  /**
-   * Removes one task from the busy count. If there are no more tasks to wait
-   * for, the app will leave the "busy" state and emit the "busy-end" event.
-   *
-   */
-  _finishedTask() {
-    this._busyTaskCount--;
-    if (this._busyTaskCount <= 0) {
-      this._busyTaskCount = 0;
-      if (this._spinner) {
-        this._spinner.active = false;
-      }
-      this.dispatchEvent(new CustomEvent('busy-end', { bubbles: true }));
-    }
-  }
-
-  /** Handles a fetch error. Expects the detail of error to contain:
-   *  error: the error given to fetch.
-   *  loading: A string explaining what was being fetched.
-   */
-  _fetchError(e) {
-    const loadingWhat = e.detail.loading;
-    e = e.detail.error;
-    if (e.name !== 'AbortError') {
-      // We can ignore AbortError since they fire anytime an AbortController was canceled.
-      // Chrome and Firefox report a DOMException in this case:
-      // https://developer.mozilla.org/en-US/docs/Web/API/DOMException
-      console.error(e);
-      errorMessage(`Unexpected error loading ${loadingWhat}: ${e.message}`,
-        5000);
-    }
-    this._finishedTask();
-  }
-});
diff --git a/golden/modules/gold-scaffold-sk/gold-scaffold-sk.ts b/golden/modules/gold-scaffold-sk/gold-scaffold-sk.ts
new file mode 100644
index 0000000..29bb8cf
--- /dev/null
+++ b/golden/modules/gold-scaffold-sk/gold-scaffold-sk.ts
@@ -0,0 +1,211 @@
+/**
+ * @module modules/gold-scaffold-sk
+ * @description <h2><code>gold-scaffold-sk</code></h2>
+ *
+ * Contains the title bar, side bar, and error-toast for all the Gold pages. The rest of
+ * every Gold page should be a child of this element.
+ *
+ * Has a spinner-sk that can be activated when it hears "begin-task" events and keeps
+ * spinning until it hears an equal number of "end-task" events.
+ *
+ * The error-toast element responds to fetch-error events and normal error-sk events.
+ *
+ * @attr {boolean} testing_offline - If we should operate entirely in offline mode.
+ */
+import { define } from 'elements-sk/define';
+import { errorMessage } from 'elements-sk/errorMessage';
+import { html } from 'lit-html';
+import { ElementSk } from '../../../infra-sk/modules/ElementSk';
+import { title } from '../settings';
+import { SpinnerSk } from 'elements-sk/spinner-sk/spinner-sk';
+import { FetchErrorEventDetail } from '../common';
+
+import '../../../infra-sk/modules/app-sk';
+import '../../../infra-sk/modules/login-sk';
+
+import '../last-commit-sk';
+
+import 'elements-sk/error-toast-sk';
+import 'elements-sk/icon/find-in-page-icon-sk';
+import 'elements-sk/icon/folder-icon-sk';
+import 'elements-sk/icon/help-icon-sk';
+import 'elements-sk/icon/home-icon-sk';
+import 'elements-sk/icon/label-icon-sk';
+import 'elements-sk/icon/laptop-chromebook-icon-sk';
+import 'elements-sk/icon/list-icon-sk';
+import 'elements-sk/icon/search-icon-sk';
+import 'elements-sk/icon/sync-problem-icon-sk';
+import 'elements-sk/icon/view-day-icon-sk';
+import 'elements-sk/spinner-sk';
+
+/** Moves the elements in a NodeList or HTMLCollection as children of another element. */
+function move(from: HTMLCollection | NodeList, to: HTMLElement) {
+  Array.prototype.slice.call(from).forEach((ele) => to.appendChild(ele));
+}
+
+export class GoldScaffoldSk extends ElementSk {
+  private static template = (ele: GoldScaffoldSk) => html`
+    <app-sk>
+      <header>
+        <h1>${title()}</h1>
+        <div class=spinner-spacer>
+          <spinner-sk></spinner-sk>
+        </div>
+        <div class=spacer></div>
+        <last-commit-sk></last-commit-sk>
+        <login-sk ?testing_offline=${ele.testingOffline}
+                  .loginHost=${window.location.host}></login-sk>
+      </header>
+
+      <aside>
+        <nav>
+          <a href="/" tab-index=0>
+            <home-icon-sk></home-icon-sk><span>Home</span>
+          </a>
+          <a href="/" tab-index=0>
+            <view-day-icon-sk></view-day-icon-sk><span>By Blame<span>
+          </a>
+          <a href="/list" tab-index=0>
+            <list-icon-sk></list-icon-sk><span>By Test</span>
+          </a>
+          <a href="/changelists" tab-index=0>
+            <laptop-chromebook-icon-sk></laptop-chromebook-icon-sk><span>By ChangeList</span>
+          </a>
+          <a href="/search" tab-index=0>
+            <search-icon-sk></search-icon-sk><span>Search</span>
+          </a>
+          <a href="/ignores" tab-index=0>
+            <label-icon-sk></label-icon-sk><span>Ignores</span>
+          </a>
+          <a href="/triagelog" tab-index=0>
+            <find-in-page-icon-sk></find-in-page-icon-sk><span>Triage Log</span>
+          </a>
+          <a href="/help" tab-index=0>
+            <help-icon-sk></help-icon-sk><span>Help</span>
+          </a>
+          <a href="https://github.com/google/skia-buildbot/tree/master/golden" tab-index=0>
+            <folder-icon-sk></folder-icon-sk><span>Code</span>
+          </a>
+        </nav>
+      </aside>
+
+      <main></main>
+
+      <footer><error-toast-sk></error-toast-sk></footer>
+    </app-sk>
+  `;
+
+  private main: HTMLElement | null = null;
+  private busyTaskCount = 0;
+  private spinner: SpinnerSk | null = null;
+
+  constructor() {
+    super(GoldScaffoldSk.template);
+  }
+
+  connectedCallback() {
+    super.connectedCallback();
+    // Don't call more than once.
+    if (this.main) {
+      return;
+    }
+    this.addEventListener('begin-task', this.addBusyTask);
+    this.addEventListener('end-task', this.finishedTask);
+    this.addEventListener('fetch-error', this.fetchError);
+
+    // We aren't using shadow dom so we need to manually move the children of
+    // gold-scaffold-sk to be children of 'main'. We have to do this for the
+    // existing elements and for all future mutations.
+
+    // Create a temporary holding spot for elements we're moving.
+    const div = document.createElement('div');
+    move(this.children, div);
+
+    // Now that we've moved all the old children out of the way we can render
+    // the template.
+    this._render();
+
+    this.spinner = this.querySelector<SpinnerSk>('header spinner-sk');
+
+    // Move the old children back under main.
+    this.main = this.querySelector('main');
+    move(div.children, this.main!);
+
+    // Move all future children under main also.
+    const observer = new MutationObserver((mutList) => {
+      mutList.forEach((mut) => {
+        move(mut.addedNodes, this.main!);
+      });
+    });
+    observer.observe(this, { childList: true });
+  }
+
+  disconnectedCallback() {
+    super.disconnectedCallback();
+    this.removeEventListener('begin-task', this.addBusyTask);
+    this.removeEventListener('end-task', this.finishedTask);
+    this.removeEventListener('fetch-error', this.fetchError);
+  }
+
+  /**
+   * Indicates if there any on-going tasks (e.g. RPCs). This also mirrors the status of the
+   * embedded spinner-sk.
+   */
+  get busy() { return !!this.busyTaskCount; }
+
+  /** Reflects the testing_offline attribute for ease of use. */
+  get testingOffline() { return this.hasAttribute('testing_offline'); }
+
+  set testingOffline(val) {
+    if (val) {
+      this.setAttribute('testing_offline', '');
+    } else {
+      this.removeAttribute('testing_offline');
+    }
+  }
+
+  /**
+   * Indicate there are some number of tasks (e.g. RPCs) the app is waiting on and should be in the
+   * "busy" state, if it isn't already.
+   */
+  private addBusyTask() {
+    this.busyTaskCount++;
+    if (this.spinner && this.busyTaskCount > 0) {
+      this.spinner.active = true;
+    }
+  }
+
+  /**
+   * Removes one task from the busy count. If there are no more tasks to wait for, the app will
+   * leave the "busy" state and emit the "busy-end" event.
+   */
+  private finishedTask() {
+    this.busyTaskCount--;
+    if (this.busyTaskCount <= 0) {
+      this.busyTaskCount = 0;
+      if (this.spinner) {
+        this.spinner.active = false;
+      }
+      this.dispatchEvent(new CustomEvent('busy-end', { bubbles: true }));
+    }
+  }
+
+  /** Handles a fetch-error event. */
+  private fetchError(e: Event) {
+    // Method removeEventListener expects an Event, so we're forced to take an Event and cast it as
+    // a CustomEvent here.
+    const fetchErrorEvent = e as CustomEvent<FetchErrorEventDetail>;
+    const error = fetchErrorEvent.detail.error;
+    const loadingWhat = fetchErrorEvent.detail.loading;
+    if (fetchErrorEvent.detail.error.name !== 'AbortError') {
+      // We can ignore AbortError since they fire anytime an AbortController was canceled.
+      // Chrome and Firefox report a DOMException in this case:
+      // https://developer.mozilla.org/en-US/docs/Web/API/DOMException
+      console.error(error);
+      errorMessage(`Unexpected error loading ${loadingWhat}: ${error.message}`, 5000);
+    }
+    this.finishedTask();
+  }
+}
+
+define('gold-scaffold-sk', GoldScaffoldSk);
diff --git a/golden/modules/gold-scaffold-sk/gold-scaffold-sk_test.js b/golden/modules/gold-scaffold-sk/gold-scaffold-sk_test.ts
similarity index 82%
rename from golden/modules/gold-scaffold-sk/gold-scaffold-sk_test.js
rename to golden/modules/gold-scaffold-sk/gold-scaffold-sk_test.ts
index 5c75b37..85e4161 100644
--- a/golden/modules/gold-scaffold-sk/gold-scaffold-sk_test.js
+++ b/golden/modules/gold-scaffold-sk/gold-scaffold-sk_test.ts
@@ -6,11 +6,15 @@
 import fetchMock from 'fetch-mock';
 import { exampleStatusData } from '../last-commit-sk/demo_data';
 import { testOnlySetSettings } from '../settings';
+import { GoldScaffoldSk } from './gold-scaffold-sk';
+import { expect } from 'chai';
+import { SpinnerSk } from 'elements-sk/spinner-sk/spinner-sk';
 
 describe('gold-scaffold-sk', () => {
-  const newInstance = setUpElementUnderTest('gold-scaffold-sk');
+  const newInstance = setUpElementUnderTest<GoldScaffoldSk>('gold-scaffold-sk');
 
-  let goldScaffoldSk;
+  let goldScaffoldSk: GoldScaffoldSk;
+
   beforeEach(() => {
     testOnlySetSettings({
       title: 'Skia Public',
@@ -38,16 +42,16 @@
     it('adds a sidebar with links', () => {
       const nav = $$('aside nav', goldScaffoldSk);
       expect(nav).to.not.be.null;
-      const links = $('a', nav);
+      const links = $('a', nav!);
       expect(links.length).not.to.equal(0);
     });
 
     it('puts the content under <main>', () => {
-      const main = $$('main', goldScaffoldSk);
+      const main = $$<HTMLElement>('main', goldScaffoldSk);
       expect(main).to.not.be.null;
-      const content = $$('div', main);
+      const content = $$<HTMLDivElement>('div', main!);
       expect(content).to.not.be.null;
-      expect(content.textContent).to.equal('content');
+      expect(content!.textContent).to.equal('content');
     });
   });// end describe('html layout')
 
@@ -65,7 +69,7 @@
     });
 
     it('keeps spinner active while busy', () => {
-      const spinner = $$('header spinner-sk', goldScaffoldSk);
+      const spinner = $$<SpinnerSk>('header spinner-sk', goldScaffoldSk)!;
       expect(spinner.active).to.equal(false);
 
       sendBeginTask(goldScaffoldSk);
diff --git a/golden/modules/gold-scaffold-sk/index.js b/golden/modules/gold-scaffold-sk/index.ts
similarity index 100%
rename from golden/modules/gold-scaffold-sk/index.js
rename to golden/modules/gold-scaffold-sk/index.ts
diff --git a/golden/modules/ignores-page-sk/ignores-page-sk-demo.ts b/golden/modules/ignores-page-sk/ignores-page-sk-demo.ts
index 24f78ff..21fdd16 100644
--- a/golden/modules/ignores-page-sk/ignores-page-sk-demo.ts
+++ b/golden/modules/ignores-page-sk/ignores-page-sk-demo.ts
@@ -7,6 +7,7 @@
 import { testOnlySetSettings } from '../settings';
 import { exampleStatusData } from '../last-commit-sk/demo_data';
 import fetchMock from 'fetch-mock';
+import { GoldScaffoldSk } from '../gold-scaffold-sk/gold-scaffold-sk';
 
 Date.now = () => fakeNow;
 testOnlySetSettings({
@@ -22,8 +23,8 @@
 fetchMock.get('/json/v1/trstatus', JSON.stringify(exampleStatusData));
 
 // By adding these elements after all the fetches are mocked out, they should load ok.
-const newScaf = document.createElement('gold-scaffold-sk');
-newScaf.setAttribute('testing_offline', 'true');
+const newScaf = new GoldScaffoldSk();
+newScaf.testingOffline = true;
 // Make it the first element in body.
 document.body.insertBefore(newScaf, document.body.childNodes[0]);
 const page = document.createElement('ignores-page-sk');
diff --git a/golden/modules/list-page-sk/list-page-sk-demo.ts b/golden/modules/list-page-sk/list-page-sk-demo.ts
index aaf2f68..a5c6b26 100644
--- a/golden/modules/list-page-sk/list-page-sk-demo.ts
+++ b/golden/modules/list-page-sk/list-page-sk-demo.ts
@@ -7,6 +7,7 @@
 import { exampleStatusData } from '../last-commit-sk/demo_data';
 import fetchMock from 'fetch-mock';
 import { ListPageSk } from './list-page-sk';
+import { GoldScaffoldSk } from '../gold-scaffold-sk/gold-scaffold-sk';
 
 testOnlySetSettings({
   title: 'Testing Gold',
@@ -19,8 +20,8 @@
 fetchMock.get('/json/v1/trstatus', JSON.stringify(exampleStatusData));
 
 // By adding these elements after all the fetches are mocked out, they should load ok.
-const newScaf = document.createElement('gold-scaffold-sk');
-newScaf.setAttribute('testing_offline', 'true');
+const newScaf = new GoldScaffoldSk();
+newScaf.testingOffline = true;
 // Make it the first element in body.
 document.body.insertBefore(newScaf, document.body.childNodes[0]);
 newScaf.appendChild(new ListPageSk());
diff --git a/golden/modules/search-page-sk/search-page-sk-demo.ts b/golden/modules/search-page-sk/search-page-sk-demo.ts
index 091e763..c2930e1 100644
--- a/golden/modules/search-page-sk/search-page-sk-demo.ts
+++ b/golden/modules/search-page-sk/search-page-sk-demo.ts
@@ -8,6 +8,7 @@
 import fetchMock from 'fetch-mock';
 import { setImageEndpointsForDemos } from '../common';
 import { TriageRequest } from '../rpc_types';
+import { GoldScaffoldSk } from '../gold-scaffold-sk/gold-scaffold-sk';
 
 testOnlySetSettings({
   title: 'Skia Infra',
@@ -86,8 +87,8 @@
 
 setImageEndpointsForDemos();
 
-const newScaf = document.createElement('gold-scaffold-sk');
-newScaf.setAttribute('testing_offline', 'true');
+const newScaf = new GoldScaffoldSk();
+newScaf.testingOffline = true;
 const body = $$('body');
 body?.insertBefore(newScaf, body.childNodes[0]); // Make it the first element in body.
 const page = new SearchPageSk();
diff --git a/golden/modules/triagelog-page-sk/triagelog-page-sk-demo.ts b/golden/modules/triagelog-page-sk/triagelog-page-sk-demo.ts
index b561538..f3abba0 100644
--- a/golden/modules/triagelog-page-sk/triagelog-page-sk-demo.ts
+++ b/golden/modules/triagelog-page-sk/triagelog-page-sk-demo.ts
@@ -9,6 +9,7 @@
 import { testOnlySetSettings } from '../settings';
 import { exampleStatusData } from '../last-commit-sk/demo_data';
 import { TriageLogEntry, TriageLogResponse } from '../rpc_types';
+import { GoldScaffoldSk } from '../gold-scaffold-sk/gold-scaffold-sk';
 
 const fakeRpcDelayMillis = 300;
 
@@ -81,8 +82,8 @@
 fetchMock.get('/json/v1/trstatus', JSON.stringify(exampleStatusData));
 
 // By adding these elements after all the fetches are mocked out, they should load ok.
-const newScaf = document.createElement('gold-scaffold-sk');
-newScaf.setAttribute('testing_offline', 'true');
+const newScaf = new GoldScaffoldSk();
+newScaf.testingOffline = true;
 // Make it the first element in body.
 document.body.insertBefore(newScaf, document.body.childNodes[0]);
 const page = document.createElement('triagelog-page-sk');