[gold] Port changelists-page-sk to TypeScript.

Bug: skia:10246
Change-Id: I3f9c0ac21a271974542902a831ca9f371669bd5f
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/399536
Commit-Queue: Leandro Lovisolo <lovisolo@google.com>
Reviewed-by: Kevin Lubick <kjlubick@google.com>
diff --git a/golden/go/web/frontend/generate_typescript_rpc_types/main.go b/golden/go/web/frontend/generate_typescript_rpc_types/main.go
index bc2e1b3..f6c8778 100644
--- a/golden/go/web/frontend/generate_typescript_rpc_types/main.go
+++ b/golden/go/web/frontend/generate_typescript_rpc_types/main.go
@@ -61,6 +61,9 @@
 	// Response for the /json/v1/triagelog RPC endpoint.
 	generator.Add(frontend.TriageLogResponse{})
 
+	// Response for the /json/v1/changelists RPC endpoint.
+	generator.Add(frontend.ChangelistsResponse{})
+
 	generator.AddUnionWithName(expectations.AllLabel, "Label")
 	generator.AddUnionWithName(common.AllRefClosest, "RefClosest")
 }
diff --git a/golden/go/web/frontend/types.go b/golden/go/web/frontend/types.go
index 9bf4c96..a5d009a 100644
--- a/golden/go/web/frontend/types.go
+++ b/golden/go/web/frontend/types.go
@@ -41,6 +41,12 @@
 	URL      string    `json:"url"`
 }
 
+// ChangelistsResponse is the response for /json/v1/changelists.
+type ChangelistsResponse struct {
+	Changelists []Changelist `json:"changelists"`
+	httputils.ResponsePagination
+}
+
 // ConvertChangelist turns a code_review.Changelist into a Changelist for the frontend.
 func ConvertChangelist(cl code_review.Changelist, system, urlTempl string) Changelist {
 	return Changelist{
diff --git a/golden/go/web/helpers.go b/golden/go/web/helpers.go
index 84058a0..7378a4f 100644
--- a/golden/go/web/helpers.go
+++ b/golden/go/web/helpers.go
@@ -23,14 +23,6 @@
 	noSniffContent           = "nosniff"
 )
 
-// ResponseEnvelope wraps all responses. Some fields might be empty depending
-// on context or whether there was an error or not.
-type ResponseEnvelope struct {
-	Data       *interface{}                  `json:"data"`
-	Status     int                           `json:"status"`
-	Pagination *httputils.ResponsePagination `json:"pagination"`
-}
-
 // setJSONHeaders sets secure headers for JSON responses.
 func setJSONHeaders(w http.ResponseWriter) {
 	h := w.Header()
@@ -39,22 +31,6 @@
 	h.Set(contentTypeOptionsHeader, noSniffContent)
 }
 
-// sendResponseWithPagination wraps the data of a successful response in a response envelope
-// and sends it to the client.
-func sendResponseWithPagination(w http.ResponseWriter, data interface{}, pagination *httputils.ResponsePagination) {
-	resp := ResponseEnvelope{
-		Data:       &data,
-		Status:     http.StatusOK,
-		Pagination: pagination,
-	}
-	setJSONHeaders(w)
-	w.WriteHeader(http.StatusOK)
-	if err := json.NewEncoder(w).Encode(resp); err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-}
-
 // sendJSONResponse serializes resp to JSON. If an error occurs
 // a text based error code is send to the client.
 func sendJSONResponse(w http.ResponseWriter, resp interface{}) {
diff --git a/golden/go/web/web.go b/golden/go/web/web.go
index e7de13c..3816dd5 100644
--- a/golden/go/web/web.go
+++ b/golden/go/web/web.go
@@ -384,7 +384,12 @@
 		return
 	}
 
-	sendResponseWithPagination(w, cls, pagination)
+	response := frontend.ChangelistsResponse{
+		Changelists:        cls,
+		ResponsePagination: *pagination,
+	}
+
+	sendJSONResponse(w, response)
 }
 
 // getIngestedChangelists performs the core of the logic for ChangelistsHandler,
diff --git a/golden/go/web/web_test.go b/golden/go/web/web_test.go
index 3d32c11..de952e6 100644
--- a/golden/go/web/web_test.go
+++ b/golden/go/web/web_test.go
@@ -321,12 +321,16 @@
 	mcls := &mock_clstore.Store{}
 	defer mcls.AssertExpectations(t)
 
+	const offset = 0
+	const size = 50
+
 	mcls.On("GetChangelists", testutils.AnyContext, clstore.SearchOptions{
-		StartIdx: 0,
-		Limit:    50,
+		StartIdx: offset,
+		Limit:    size,
 	}).Return(makeCodeReviewCLs(), len(makeCodeReviewCLs()), nil)
 
 	wh := Handlers{
+		anonymousExpensiveQuota: rate.NewLimiter(rate.Inf, 1),
 		HandlersConfig: HandlersConfig{
 			ReviewSystems: []clstore.ReviewSystem{
 				{
@@ -338,19 +342,23 @@
 		},
 	}
 
-	cls, pagination, err := wh.getIngestedChangelists(context.Background(), 0, 50, false)
-	assert.NoError(t, err)
-	assert.Len(t, cls, 3)
-	assert.NotNil(t, pagination)
+	cls := makeWebCLs()
 
-	assert.Equal(t, &httputils.ResponsePagination{
-		Offset: 0,
-		Size:   50,
-		Total:  3,
-	}, pagination)
+	expectedResponse := frontend.ChangelistsResponse{
+		Changelists: cls,
+		ResponsePagination: httputils.ResponsePagination{
+			Offset: offset,
+			Size:   size,
+			Total:  len(cls),
+		},
+	}
 
-	expected := makeWebCLs()
-	assert.Equal(t, expected, cls)
+	w := httptest.NewRecorder()
+	r := httptest.NewRequest(http.MethodGet, "/json/v1/changelist?size=50", nil)
+	wh.ChangelistsHandler(w, r)
+	b, err := json.Marshal(expectedResponse)
+	require.NoError(t, err)
+	assertJSONResponseWas(t, http.StatusOK, string(b), w)
 }
 
 // TestGetIngestedChangelists_ActiveChangelists_SunnyDay_Success makes sure that we properly get
@@ -361,13 +369,17 @@
 	mcls := &mock_clstore.Store{}
 	defer mcls.AssertExpectations(t)
 
+	const offset = 20
+	const size = 30
+
 	mcls.On("GetChangelists", testutils.AnyContext, clstore.SearchOptions{
-		StartIdx:    20,
-		Limit:       30,
+		StartIdx:    offset,
+		Limit:       size,
 		OpenCLsOnly: true,
 	}).Return(makeCodeReviewCLs(), 3, nil)
 
 	wh := Handlers{
+		anonymousExpensiveQuota: rate.NewLimiter(rate.Inf, 1),
 		HandlersConfig: HandlersConfig{
 			ReviewSystems: []clstore.ReviewSystem{
 				{
@@ -379,19 +391,23 @@
 		},
 	}
 
-	cls, pagination, err := wh.getIngestedChangelists(context.Background(), 20, 30, true)
-	assert.NoError(t, err)
-	assert.Len(t, cls, 3)
-	assert.NotNil(t, pagination)
+	cls := makeWebCLs()
 
-	assert.Equal(t, &httputils.ResponsePagination{
-		Offset: 20,
-		Size:   30,
-		Total:  3,
-	}, pagination)
+	expectedResponse := frontend.ChangelistsResponse{
+		Changelists: cls,
+		ResponsePagination: httputils.ResponsePagination{
+			Offset: offset,
+			Size:   size,
+			Total:  len(cls),
+		},
+	}
 
-	expected := makeWebCLs()
-	assert.Equal(t, expected, cls)
+	w := httptest.NewRecorder()
+	r := httptest.NewRequest(http.MethodGet, "/json/v1/changelist?offset=20&size=30&active=true", nil)
+	wh.ChangelistsHandler(w, r)
+	b, err := json.Marshal(expectedResponse)
+	require.NoError(t, err)
+	assertJSONResponseWas(t, http.StatusOK, string(b), w)
 }
 
 func makeCodeReviewCLs() []code_review.Changelist {
diff --git a/golden/modules/changelists-page-sk/changelists-page-sk-demo.js b/golden/modules/changelists-page-sk/changelists-page-sk-demo.ts
similarity index 60%
rename from golden/modules/changelists-page-sk/changelists-page-sk-demo.js
rename to golden/modules/changelists-page-sk/changelists-page-sk-demo.ts
index cdaa767..a1864fb 100644
--- a/golden/modules/changelists-page-sk/changelists-page-sk-demo.js
+++ b/golden/modules/changelists-page-sk/changelists-page-sk-demo.ts
@@ -3,7 +3,6 @@
 
 import fetchMock from 'fetch-mock';
 import { deepCopy } from 'common-sk/modules/object';
-import { $$ } from 'common-sk/modules/dom';
 import { delay } from '../demo_util';
 import { fakeNow, changelistSummaries_5, empty } from './test_data';
 import { testOnlySetSettings } from '../settings';
@@ -17,40 +16,36 @@
 Date.now = () => fakeNow;
 
 const ten = deepCopy(changelistSummaries_5);
-ten.data.push(...ten.data);
-ten.pagination = {
-  offset: 0,
-  size: 10,
-  total: 2147483647,
-};
+ten.changelists!.push(...ten.changelists!);
+ten.offset = 0;
+ten.size = 10;
+ten.total = 2147483647;
 
-changelistSummaries_5.pagination = {
-  offset: 10,
-  size: 10,
-  total: 15,
-};
+changelistSummaries_5.offset = 10;
+changelistSummaries_5.size = 10;
+changelistSummaries_5.total = 15;
 
 const open = deepCopy(changelistSummaries_5);
-open.data = open.data.slice(0, 3);
-open.pagination = {
-  offset: 0,
-  size: 3,
-  total: 3,
-};
+open.changelists = open.changelists!.slice(0, 3);
+open.offset = 0;
+open.size = 3;
+open.total = 3;
 
 const fakeRpcDelayMillis = 300;
 
 fetchMock.get('/json/v1/changelists?offset=0&size=10', delay(ten, fakeRpcDelayMillis));
-fetchMock.get('/json/v1/changelists?offset=0&size=10&active=true', delay(open, fakeRpcDelayMillis));
-fetchMock.get('/json/v1/changelists?offset=10&size=10', delay(changelistSummaries_5, fakeRpcDelayMillis));
-fetchMock.get('glob:/json/v1/changelists*', delay(empty, fakeRpcDelayMillis));
+fetchMock.get(
+    '/json/v1/changelists?offset=0&size=10&active=true', delay(open, fakeRpcDelayMillis));
+fetchMock.get(
+    '/json/v1/changelists?offset=10&size=10', delay(changelistSummaries_5, fakeRpcDelayMillis));
+fetchMock.get('glob:/json/v1/changelists*', delay(empty(), fakeRpcDelayMillis));
 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 body = $$('body');
-body.insertBefore(newScaf, body.childNodes[0]); // Make it the first element in body.
+// Make it the first element in body.
+document.body.insertBefore(newScaf, document.body.childNodes[0]);
 const page = document.createElement('changelists-page-sk');
 page.setAttribute('page_size', '10');
 newScaf.appendChild(page);
diff --git a/golden/modules/changelists-page-sk/changelists-page-sk.js b/golden/modules/changelists-page-sk/changelists-page-sk.js
deleted file mode 100644
index 79fec2f..0000000
--- a/golden/modules/changelists-page-sk/changelists-page-sk.js
+++ /dev/null
@@ -1,165 +0,0 @@
-/**
- * @module modules/changelists-page-sk
- * @description <h2><code>changelists-page-sk</code></h2>
- *
- * Allows the user to page through the ChangeLists for which Gold has seen
- * data uploaded via TryJobs.
- *
- */
-import * as human from 'common-sk/modules/human';
-
-import { define } from 'elements-sk/define';
-import { html } from 'lit-html';
-import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow';
-import { stateReflector } from 'common-sk/modules/stateReflector';
-import { ElementSk } from '../../../infra-sk/modules/ElementSk';
-
-import 'elements-sk/checkbox-sk';
-import 'elements-sk/icon/block-icon-sk';
-import 'elements-sk/icon/cached-icon-sk';
-import 'elements-sk/icon/done-icon-sk';
-
-import '../pagination-sk';
-import { sendBeginTask, sendEndTask, sendFetchError } from '../common';
-
-const _statusIcon = (cl) => {
-  if (cl.status === 'Open') {
-    return html`<cached-icon-sk title="ChangeList is open"></cached-icon-sk>`;
-  } if (cl.status === 'Landed') {
-    return html`<done-icon-sk title="ChangeList was landed"></done-icon-sk>`;
-  }
-  return html`<block-icon-sk title="ChangeList was abandoned"></block-icon-sk>`;
-};
-
-const changelist = (cl) => html`
-<tr>
-  <td class=id>
-    <a title="See codereview in a new window" target=_blank rel=noopener href=${cl.url}
-      >${cl.id}</a>
-    ${_statusIcon(cl)}
-  </td>
-  <td>
-    <a href="/cl/${cl.system}/${cl.id}"
-       target="_blank" rel="noopener">Triage</a>
-  </td>
-  <td class=owner>${cl.owner}</td>
-  <td title=${cl.updated}>${human.diffDate(cl.updated)} ago</td>
-  <td>${cl.subject}</td>
-</tr>`;
-
-const template = (ele) => html`
-<div class=controls>
-  <pagination-sk page_size=${ele._page_size} offset=${ele._offset}
-                 total=${ele._total} @page-changed=${ele._pageChanged}>
-  </pagination-sk>
-
-  <checkbox-sk label="show all" ?checked=${ele._showAll}
-               @click=${ele._toggleShowAll}></checkbox-sk>
-</div>
-
-<table>
-  <thead>
-    <tr>
-      <th colspan=2>ChangeList</th>
-      <th>Owner</th>
-      <th>Updated</th>
-      <th>Subject</th>
-    </tr>
-  </thead>
-  <tbody>
-  ${ele._cls.map(changelist)}
-</tbody>`;
-
-define('changelists-page-sk', class extends ElementSk {
-  constructor() {
-    super(template);
-
-    // Set empty values to allow empty rendering while we wait for
-    // stateReflector (which triggers on DomReady). Additionally, these values
-    // help stateReflector with types.
-    this._cls = [];
-    this._offset = 0;
-    this._page_size = 0;
-    this._total = 0;
-    this._showAll = false;
-
-    this._stateChanged = stateReflector(
-      /* getState */() => ({
-        // provide empty values
-        offset: this._offset,
-        page_size: this._page_size,
-        show_all: this._showAll,
-      }), /* setState */(newState) => {
-        if (!this._connected) {
-          return;
-        }
-
-        // default values if not specified.
-        this._offset = newState.offset || 0;
-        this._page_size = newState.page_size || +this.getAttribute('page_size') || 50;
-        this._showAll = newState.show_all || false;
-        this._fetch();
-        this._render();
-      },
-    );
-
-    // Allows us to abort fetches if a user pages.
-    this._fetchController = null;
-  }
-
-  connectedCallback() {
-    super.connectedCallback();
-    this._render();
-  }
-
-  // Returns a promise that resolves when all outstanding requests resolve
-  // or null if none were made. This promise makes unit tests a little more concise.
-  _fetch() {
-    if (this._fetchController) {
-      // Kill any outstanding requests
-      this._fetchController.abort();
-    }
-
-    // Make a fresh abort controller for each set of fetches.
-    // They cannot be re-used once aborted.
-    this._fetchController = new AbortController();
-    const extra = {
-      signal: this._fetchController.signal,
-    };
-
-    sendBeginTask(this);
-    let u = `/json/v1/changelists?offset=${this._offset}&size=${this._page_size}`;
-    if (!this._showAll) {
-      u += '&active=true';
-    }
-    return fetch(u, extra)
-      .then(jsonOrThrow)
-      .then((json) => {
-        this._cls = json.data || [];
-        this._offset = json.pagination.offset;
-        this._total = json.pagination.total;
-        this._render();
-        sendEndTask(this);
-      })
-      .catch((e) => sendFetchError(this, e, 'changelists'));
-  }
-
-  _pageChanged(e) {
-    const d = e.detail;
-    this._offset += d.delta * this._page_size;
-    if (this._offset < 0) {
-      this._offset = 0;
-    }
-    this._stateChanged();
-    this._render();
-    this._fetch();
-  }
-
-  _toggleShowAll(e) {
-    e.preventDefault();
-    this._showAll = !this._showAll;
-    this._stateChanged();
-    this._render();
-    this._fetch();
-  }
-});
diff --git a/golden/modules/changelists-page-sk/changelists-page-sk.ts b/golden/modules/changelists-page-sk/changelists-page-sk.ts
new file mode 100644
index 0000000..380a8c9
--- /dev/null
+++ b/golden/modules/changelists-page-sk/changelists-page-sk.ts
@@ -0,0 +1,173 @@
+/**
+ * @module modules/changelists-page-sk
+ * @description <h2><code>changelists-page-sk</code></h2>
+ *
+ * Allows the user to page through the ChangeLists for which Gold has seen
+ * data uploaded via TryJobs.
+ *
+ */
+import * as human from 'common-sk/modules/human';
+
+import { define } from 'elements-sk/define';
+import { html } from 'lit-html';
+import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow';
+import { stateReflector } from 'common-sk/modules/stateReflector';
+import { ElementSk } from '../../../infra-sk/modules/ElementSk';
+
+import 'elements-sk/checkbox-sk';
+import 'elements-sk/icon/block-icon-sk';
+import 'elements-sk/icon/cached-icon-sk';
+import 'elements-sk/icon/done-icon-sk';
+
+import '../pagination-sk';
+import { sendBeginTask, sendEndTask, sendFetchError } from '../common';
+import { Changelist, ChangelistsResponse } from '../rpc_types';
+import { PaginationSkPageChangedEventDetail } from '../pagination-sk/pagination-sk';
+
+export class ChangelistsPageSk extends ElementSk {
+  private static template = (ele: ChangelistsPageSk) => html`
+    <div class=controls>
+      <pagination-sk page_size=${ele.pageSize} offset=${ele.offset}
+                     total=${ele.total} @page-changed=${ele.pageChanged}>
+      </pagination-sk>
+
+      <checkbox-sk label="show all" ?checked=${ele.showAll}
+                   @click=${ele.toggleShowAll}></checkbox-sk>
+    </div>
+
+    <table>
+      <thead>
+        <tr>
+          <th colspan=2>ChangeList</th>
+          <th>Owner</th>
+          <th>Updated</th>
+          <th>Subject</th>
+        </tr>
+      </thead>
+      <tbody>
+      ${ele.cls.map(ChangelistsPageSk.changelist)}
+    </tbody>
+  `;
+
+  private static changelist = (cl: Changelist) => html`
+    <tr>
+      <td class=id>
+        <a title="See codereview in a new window" target=_blank rel=noopener href=${cl.url}
+          >${cl.id}</a>
+        ${ChangelistsPageSk.statusIcon(cl)}
+      </td>
+      <td>
+        <a href="/cl/${cl.system}/${cl.id}"
+           target="_blank" rel="noopener">Triage</a>
+      </td>
+      <td class=owner>${cl.owner}</td>
+      <td title=${cl.updated}>${human.diffDate(cl.updated)} ago</td>
+      <td>${cl.subject}</td>
+    </tr>
+  `;
+
+  private static statusIcon = (cl: Changelist) => {
+    if (cl.status === 'Open') {
+      return html`<cached-icon-sk title="ChangeList is open"></cached-icon-sk>`;
+    } if (cl.status === 'Landed') {
+      return html`<done-icon-sk title="ChangeList was landed"></done-icon-sk>`;
+    }
+    return html`<block-icon-sk title="ChangeList was abandoned"></block-icon-sk>`;
+  };
+
+  // Set empty values to allow empty rendering while we wait for
+  // stateReflector (which triggers on DomReady). Additionally, these values
+  // help stateReflector with types.
+  private cls: Changelist[] = [];
+  private offset = 0;
+  private pageSize = 0;
+  private total = 0;
+  private showAll = false;
+
+  private readonly stateChanged: () => void;
+
+  // Allows us to abort fetches if a user pages.
+  private fetchController?: AbortController;
+
+  constructor() {
+    super(ChangelistsPageSk.template);
+    this.stateChanged = stateReflector(
+      /* getState */() => ({
+        // provide empty values
+        offset: this.offset,
+        page_size: this.pageSize,
+        show_all: this.showAll,
+      }), /* setState */(newState) => {
+        if (!this._connected) {
+          return;
+        }
+
+        // default values if not specified.
+        this.offset = newState.offset as number || 0;
+        this.pageSize =
+            newState.page_size as number || +this.getAttribute('page_size')! || 50;
+        this.showAll = newState.show_all as boolean || false;
+        this.fetch();
+        this._render();
+      },
+    );
+  }
+
+  connectedCallback() {
+    super.connectedCallback();
+    this._render();
+  }
+
+  // Returns a promise that resolves when all outstanding requests resolve
+  // or null if none were made. This promise makes unit tests a little more concise.
+  private fetch(): Promise<void> {
+    if (this.fetchController) {
+      // Kill any outstanding requests
+      this.fetchController.abort();
+    }
+
+    // Make a fresh abort controller for each set of fetches.
+    // They cannot be re-used once aborted.
+    this.fetchController = new AbortController();
+    const extra = {
+      signal: this.fetchController.signal,
+    };
+
+    sendBeginTask(this);
+    let u = `/json/v1/changelists?offset=${this.offset}&size=${this.pageSize}`;
+    if (!this.showAll) {
+      u += '&active=true';
+    }
+    return fetch(u, extra)
+      .then(jsonOrThrow)
+      .then((response: ChangelistsResponse) => {
+        this.cls = response.changelists || [];
+        this.offset = response.offset;
+        this.total = response.total;
+        this._render();
+        sendEndTask(this);
+      })
+      .catch((e) => sendFetchError(this, e, 'changelists'));
+  }
+
+  private pageChanged(e: CustomEvent<PaginationSkPageChangedEventDetail>) {
+    const d = e.detail;
+    this.offset += d.delta * this.pageSize;
+    if (this.offset < 0) {
+      this.offset = 0;
+    }
+    this.stateChanged();
+    this._render();
+    this.fetch();
+  }
+
+  private toggleShowAll(e: Event) {
+    e.preventDefault();
+    this.showAll = !this.showAll;
+    this.stateChanged();
+    this._render();
+    this.fetch();
+  }
+}
+
+define('changelists-page-sk', ChangelistsPageSk);
diff --git a/golden/modules/changelists-page-sk/changelists-page-sk_test.js b/golden/modules/changelists-page-sk/changelists-page-sk_test.ts
similarity index 65%
rename from golden/modules/changelists-page-sk/changelists-page-sk_test.js
rename to golden/modules/changelists-page-sk/changelists-page-sk_test.ts
index b8e0ed2..837342d 100644
--- a/golden/modules/changelists-page-sk/changelists-page-sk_test.js
+++ b/golden/modules/changelists-page-sk/changelists-page-sk_test.ts
@@ -14,9 +14,11 @@
   setQueryString,
   setUpElementUnderTest,
 } from '../../../infra-sk/modules/test_util';
+import { ChangelistsPageSk } from './changelists-page-sk';
+import { expect } from 'chai';
 
 describe('changelists-page-sk', () => {
-  const newInstance = setUpElementUnderTest('changelists-page-sk');
+  const newInstance = setUpElementUnderTest<ChangelistsPageSk>('changelists-page-sk');
 
   // Instantiate page; wait for RPCs to complete and for the page to render.
   const loadChangelistsPageSk = async () => {
@@ -29,20 +31,22 @@
   beforeEach(async () => {
     // Clear out any query params we might have to not mess with our current state.
     setQueryString('');
-
-    // These are the default offset/page_size params
-    fetchMock.get('/json/v1/changelists?offset=0&size=50&active=true', changelistSummaries_5);
   });
 
   afterEach(() => {
+    expect(fetchMock.done()).to.be.true; // All mock RPCs called at least once.
     // Completely remove the mocking which allows each test
     // to be able to mess with the mocked routes w/o impacting other tests.
     fetchMock.reset();
   });
 
   describe('html layout', () => {
-    let changelistsPageSk;
+    let changelistsPageSk: ChangelistsPageSk
+
     beforeEach(async () => {
+      // These are the default offset/page_size params
+      fetchMock.get('/json/v1/changelists?offset=0&size=50&active=true', changelistSummaries_5);
+
       changelistsPageSk = await loadChangelistsPageSk();
     });
 
@@ -68,48 +72,37 @@
   }); // end describe('html layout')
 
   describe('api calls', () => {
-    let changelistsPageSk;
-    beforeEach(async () => {
-      changelistsPageSk = await loadChangelistsPageSk();
-    });
-
     it('includes pagination params in request to changelists', async () => {
-      fetchMock.resetHistory();
-
-      fetchMock.get('/json/v1/changelists?offset=100&size=10', empty);
-      // pretend these were loaded in via stateReflector
-      changelistsPageSk._offset = 100;
-      changelistsPageSk._page_size = 10;
-      changelistsPageSk._showAll = true;
-
-      await changelistsPageSk._fetch();
+      setQueryString('?offset=100&page_size=10');
+      fetchMock.get('/json/v1/changelists?offset=100&size=10&active=true', empty);
+      await loadChangelistsPageSk()
     });
 
     it('includes the active params unless show_all is set', async () => {
-      fetchMock.resetHistory();
-
-      fetchMock.get('/json/v1/changelists?offset=100&size=10&active=true', empty);
-      // pretend these were loaded in via stateReflector
-      changelistsPageSk._offset = 100;
-      changelistsPageSk._page_size = 10;
-      changelistsPageSk._showAll = false;
-
-      await changelistsPageSk._fetch();
+      setQueryString('?offset=100&page_size=10&show_all=true');
+      fetchMock.get('/json/v1/changelists?offset=100&size=10', empty);
+      await loadChangelistsPageSk()
     });
   }); // end describe('api calls')
 
   describe('navigation', () => {
     it('responds to the browser back/forward buttons', async () => {
+      // These are the default offset/page_size params. This request will be made when we test the
+      // ?hello=world query string.
+      fetchMock.get('/json/v1/changelists?offset=0&size=50&active=true', changelistSummaries_5);
+
       // First page of results.
       fetchMock.get(
         '/json/v1/changelists?offset=0&size=5&active=true',
         changelistSummaries_5,
       );
+
       // Second page of results.
       fetchMock.get(
         '/json/v1/changelists?offset=5&size=5&active=true',
         changelistSummaries_5_offset5,
       );
+
       // Third page of results.
       fetchMock.get(
         '/json/v1/changelists?offset=10&size=5&active=true',
@@ -128,24 +121,24 @@
       // Instantiate component.
       const changelistsPageSk = await loadChangelistsPageSk();
       expectQueryStringToEqual('?page_size=5');
-      expectFirstPage();
+      expectFirstPage(changelistsPageSk);
 
       await goToNextPage(changelistsPageSk);
       expectQueryStringToEqual('?offset=5&page_size=5');
-      expectSecondPage();
+      expectSecondPage(changelistsPageSk);
 
       await goToNextPage(changelistsPageSk);
       expectQueryStringToEqual('?offset=10&page_size=5');
-      expectThirdPage();
+      expectThirdPage(changelistsPageSk);
 
       await goBack();
       expectQueryStringToEqual('?offset=5&page_size=5');
-      expectSecondPage();
+      expectSecondPage(changelistsPageSk);
 
       // State at component instantiation.
       await goBack();
       expectQueryStringToEqual('?page_size=5');
-      expectFirstPage();
+      expectFirstPage(changelistsPageSk);
 
       // State before the component was instantiated.
       await goBack();
@@ -153,48 +146,44 @@
 
       await goForward();
       expectQueryStringToEqual('?page_size=5');
-      expectFirstPage();
+      expectFirstPage(changelistsPageSk);
 
       await goForward();
       expectQueryStringToEqual('?offset=5&page_size=5');
-      expectSecondPage();
+      expectSecondPage(changelistsPageSk);
 
       await goForward();
       expectQueryStringToEqual('?offset=10&page_size=5');
-      expectThirdPage();
+      expectThirdPage(changelistsPageSk);
     });
   }); // end describe('navigation')
 
   describe('dynamic content', () => {
-    let changelistsPageSk;
-    beforeEach(async () => {
-      changelistsPageSk = await loadChangelistsPageSk();
-    });
+    it('responds to clicking the show all checkbox', async () => {
+      fetchMock.get('/json/v1/changelists?offset=0&size=5&active=true', empty);
+      fetchMock.get('/json/v1/changelists?offset=0&size=5', empty);
 
-    it('responds to clicking the show all checkbox', () => {
-      fetchMock.get('/json/v1/changelists?offset=0&size=50', empty);
+      setQueryString('?page_size=5');
+      const changelistsPageSk = await loadChangelistsPageSk();
+
       // click on the input inside the checkbox, otherwise, we see double
       // events, since checkbox-sk "re-throws" the click event.
-      const showAllBox = $$('.controls checkbox-sk input', changelistsPageSk);
+      const showAllBox = $$<HTMLInputElement>('.controls checkbox-sk input', changelistsPageSk)!;
       expect(showAllBox).to.not.be.null;
-      expect(changelistsPageSk._showAll).to.equal(false);
-      expectQueryStringToEqual('');
+      expectQueryStringToEqual('?page_size=5');
       showAllBox.click();
-      expect(changelistsPageSk._showAll).to.equal(true);
-      expectQueryStringToEqual('?page_size=50&show_all=true');
+      expectQueryStringToEqual('?page_size=5&show_all=true');
       showAllBox.click();
-      expect(changelistsPageSk._showAll).to.equal(false);
-      expectQueryStringToEqual('?page_size=50');
+      expectQueryStringToEqual('?page_size=5');
       showAllBox.click();
-      expect(changelistsPageSk._showAll).to.equal(true);
-      expectQueryStringToEqual('?page_size=50&show_all=true');
+      expectQueryStringToEqual('?page_size=5&show_all=true');
     });
   }); // end describe('dynamic content')
 });
 
-function goToNextPage(changelistsPageSk) {
+function goToNextPage(changelistsPageSk: ChangelistsPageSk) {
   const event = eventPromise('end-task');
-  $$('pagination-sk button.next', changelistsPageSk).click();
+  $$<HTMLButtonElement>('pagination-sk button.next', changelistsPageSk)!.click();
   return event;
 }
 
@@ -210,14 +199,17 @@
   return event;
 }
 
-function expectFirstPage(changelistsPageSk) {
-  expect($('td.owner', changelistsPageSk)[0].innerText).to.contain('alpha');
+function expectFirstPage(changelistsPageSk: ChangelistsPageSk) {
+  expect($<HTMLTableDataCellElement>('td.owner', changelistsPageSk)[0].innerText)
+      .to.contain('alpha');
 }
 
-function expectSecondPage(changelistsPageSk) {
-  expect($('td.owner', changelistsPageSk)[0].innerText).to.contain('zeta');
+function expectSecondPage(changelistsPageSk: ChangelistsPageSk) {
+  expect($<HTMLTableDataCellElement>('td.owner', changelistsPageSk)[0].innerText)
+      .to.contain('zeta');
 }
 
-function expectThirdPage(changelistsPageSk) {
-  expect($('td.owner', changelistsPageSk)[0].innerText).to.contain('lambda');
+function expectThirdPage(changelistsPageSk: ChangelistsPageSk) {
+  expect($<HTMLTableDataCellElement>('td.owner', changelistsPageSk)[0].innerText)
+      .to.contain('lambda');
 }
diff --git a/golden/modules/changelists-page-sk/test_data.js b/golden/modules/changelists-page-sk/test_data.ts
similarity index 83%
rename from golden/modules/changelists-page-sk/test_data.js
rename to golden/modules/changelists-page-sk/test_data.ts
index 5fd8263..533cf28 100644
--- a/golden/modules/changelists-page-sk/test_data.js
+++ b/golden/modules/changelists-page-sk/test_data.ts
@@ -1,7 +1,9 @@
+import { ChangelistsResponse } from '../rpc_types';
+
 export const fakeNow = Date.parse('2019-09-09T19:32:30Z');
 
-export const changelistSummaries_5 = {
-  data: [
+export const changelistSummaries_5: ChangelistsResponse = {
+  changelists: [
     {
       system: 'gerrit',
       id: '1788313',
@@ -25,7 +27,10 @@
       id: '1788259',
       owner: 'gamma@example.org',
       status: 'Open',
-      subject: "Implement deep content compliance and malware scans for uploads. This is a really long subject, like wow! I hope the web UI doesn't mishandle this massively long subject",
+      subject:
+          "Implement deep content compliance and malware scans for uploads. " +
+          "This is a really long subject, like wow! " +
+          "I hope the web UI doesn't mishandle this massively long subject",
       updated: '2019-09-09T19:28:54Z',
       url: 'https://chromium-review.googlesource.com/1788259',
     },
@@ -48,16 +53,13 @@
       url: 'https://chromium-review.googlesource.com/1790066',
     },
   ],
-  status: 200,
-  pagination: {
-    offset: 0,
-    size: 5,
-    total: 2147483647,
-  },
+  offset: 0,
+  size: 5,
+  total: 2147483647,
 };
 
 export const changelistSummaries_5_offset5 = {
-  data: [
+  changelists: [
     {
       system: 'gerrit',
       id: '1806853',
@@ -104,16 +106,13 @@
       url: 'https://chromium-review.googlesource.com/1804507',
     },
   ],
-  status: 200,
-  pagination: {
-    offset: 5,
-    size: 5,
-    total: 2147483647,
-  },
+  offset: 5,
+  size: 5,
+  total: 2147483647,
 };
 
-export const changelistSummaries_5_offset10 = {
-  data: [
+export const changelistSummaries_5_offset10: ChangelistsResponse = {
+  changelists: [
     {
       system: 'gerrit',
       id: '1793168',
@@ -160,19 +159,14 @@
       url: 'https://chromium-review.googlesource.com/1805646',
     },
   ],
-  status: 200,
-  pagination: {
-    offset: 10,
-    size: 5,
-    total: 2147483647,
-  },
+  offset: 10,
+  size: 5,
+  total: 2147483647,
 };
 
-export const empty = {
-  status: 200,
-  pagination: {
-    offset: 100,
-    size: 10,
-    total: 100,
-  },
-};
+export const empty = (partial?: Partial<ChangelistsResponse>): ChangelistsResponse => ({
+  changelists: null,
+  offset: partial?.offset || 0,
+  size: partial?.size || 5,
+  total: partial?.total || 2147483647,
+});
diff --git a/golden/modules/rpc_types.ts b/golden/modules/rpc_types.ts
index 4aa049d..2727cc5 100644
--- a/golden/modules/rpc_types.ts
+++ b/golden/modules/rpc_types.ts
@@ -170,6 +170,13 @@
 	total: number;
 }
 
+export interface ChangelistsResponse {
+	changelists: Changelist[] | null;
+	offset: number;
+	size: number;
+	total: number;
+}
+
 export type ParamSet = { [key: string]: string[] };
 
 export type ParamSetResponse = { [key: string]: string[] | null };