Merge "convert path-list-util_test to Typescript"
diff --git a/plugins/replication b/plugins/replication
index dc9bb2e..46cfb7d 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit dc9bb2e946e4c6c31e8a4665f30eca6d00017523
+Subproject commit 46cfb7dd5b6891f991cfe66e72c08953487c1c81
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index fcf1cf4..4d0bb1f 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -109,7 +109,6 @@
     "elements/checks/gr-hovercard-run_html.ts",
     "elements/core/gr-main-header/gr-main-header_html.ts",
     "elements/core/gr-search-bar/gr-search-bar_html.ts",
-    "elements/core/gr-smart-search/gr-smart-search_html.ts",
     "elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.ts",
     "elements/diff/gr-diff-builder/gr-diff-builder-element_html.ts",
     "elements/diff/gr-diff-host/gr-diff-host_html.ts",
@@ -123,17 +122,14 @@
     "elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.ts",
     "elements/shared/gr-account-list/gr-account-list_html.ts",
     "elements/shared/gr-autocomplete/gr-autocomplete_html.ts",
-    "elements/shared/gr-change-status/gr-change-status_html.ts",
     "elements/shared/gr-comment-thread/gr-comment-thread_html.ts",
     "elements/shared/gr-comment/gr-comment_html.ts",
     "elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.ts",
-    "elements/shared/gr-copy-clipboard/gr-copy-clipboard_html.ts",
     "elements/shared/gr-dialog/gr-dialog_html.ts",
     "elements/shared/gr-diff-preferences/gr-diff-preferences_html.ts",
     "elements/shared/gr-download-commands/gr-download-commands_html.ts",
     "elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts",
     "elements/shared/gr-dropdown/gr-dropdown_html.ts",
-    "elements/shared/gr-editable-content/gr-editable-content_html.ts",
     "elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts",
     "elements/shared/gr-label-info/gr-label-info_html.ts",
     "elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.ts",
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
index aa7e2e0..7419713 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
@@ -32,6 +32,12 @@
 const SELF_EXPRESSION = 'self';
 const ME_EXPRESSION = 'me';
 
+declare global {
+  interface HTMLElementEventMap {
+    'handle-search': CustomEvent<SearchBarHandleSearchDetail>;
+  }
+}
+
 @customElement('gr-smart-search')
 export class GrSmartSearch extends PolymerElement {
   static get template() {
@@ -39,7 +45,7 @@
   }
 
   @property({type: String})
-  searchQuery?: string;
+  searchQuery = '';
 
   @property({type: Object})
   _config?: ServerInfo;
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.js
deleted file mode 100644
index f3a9965..0000000
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.js
+++ /dev/null
@@ -1,137 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-smart-search.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-smart-search');
-
-suite('gr-smart-search tests', () => {
-  let element;
-
-  setup(() => {
-    element = basicFixture.instantiate();
-  });
-
-  test('Autocompletes accounts', () => {
-    stubRestApi('getSuggestedAccounts').callsFake(() =>
-      Promise.resolve([
-        {
-          name: 'fred',
-          email: 'fred@goog.co',
-        },
-      ])
-    );
-    return element._fetchAccounts('owner', 'fr').then(s => {
-      assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
-    });
-  });
-
-  test('Inserts self as option when valid', () => {
-    stubRestApi('getSuggestedAccounts').callsFake( () =>
-      Promise.resolve([
-        {
-          name: 'fred',
-          email: 'fred@goog.co',
-        },
-      ])
-    );
-    element._fetchAccounts('owner', 's')
-        .then(s => {
-          assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
-          assert.deepEqual(s[1], {text: 'owner:self'});
-        })
-        .then(() => element._fetchAccounts('owner', 'selfs'))
-        .then(s => {
-          assert.notEqual(s[0], {text: 'owner:self'});
-        });
-  });
-
-  test('Inserts me as option when valid', () => {
-    stubRestApi('getSuggestedAccounts').callsFake( () =>
-      Promise.resolve([
-        {
-          name: 'fred',
-          email: 'fred@goog.co',
-        },
-      ])
-    );
-    return element._fetchAccounts('owner', 'm')
-        .then(s => {
-          assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
-          assert.deepEqual(s[1], {text: 'owner:me'});
-        })
-        .then(() => element._fetchAccounts('owner', 'meme'))
-        .then(s => {
-          assert.notEqual(s[0], {text: 'owner:me'});
-        });
-  });
-
-  test('Autocompletes groups', () => {
-    stubRestApi('getSuggestedGroups').callsFake( () =>
-      Promise.resolve({
-        Polygerrit: 0,
-        gerrit: 0,
-        gerrittest: 0,
-      })
-    );
-    return element._fetchGroups('ownerin', 'pol').then(s => {
-      assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'});
-    });
-  });
-
-  test('Autocompletes projects', () => {
-    stubRestApi('getSuggestedProjects').callsFake( () =>
-      Promise.resolve({Polygerrit: 0}));
-    return element._fetchProjects('project', 'pol').then(s => {
-      assert.deepEqual(s[0], {text: 'project:Polygerrit'});
-    });
-  });
-
-  test('Autocomplete doesnt override exact matches to input', () => {
-    stubRestApi('getSuggestedGroups').callsFake( () =>
-      Promise.resolve({
-        Polygerrit: 0,
-        gerrit: 0,
-        gerrittest: 0,
-      })
-    );
-    return element._fetchGroups('ownerin', 'gerrit').then(s => {
-      assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'});
-      assert.deepEqual(s[1], {text: 'ownerin:gerrit'});
-      assert.deepEqual(s[2], {text: 'ownerin:gerrittest'});
-    });
-  });
-
-  test('Autocompletes accounts with no email', () => {
-    stubRestApi('getSuggestedAccounts').callsFake( () =>
-      Promise.resolve([{name: 'fred'}]));
-    return element._fetchAccounts('owner', 'fr').then(s => {
-      assert.deepEqual(s[0], {text: 'owner:"fred"', label: 'fred'});
-    });
-  });
-
-  test('Autocompletes accounts with email', () => {
-    stubRestApi('getSuggestedAccounts').callsFake( () =>
-      Promise.resolve([{email: 'fred@goog.co'}]));
-    return element._fetchAccounts('owner', 'fr').then(s => {
-      assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: ''});
-    });
-  });
-});
-
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts
new file mode 100644
index 0000000..0218a8f
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts
@@ -0,0 +1,143 @@
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-smart-search';
+import {GrSmartSearch} from './gr-smart-search';
+import {stubRestApi} from '../../../test/test-utils';
+import {EmailAddress, GroupId, UrlEncodedRepoName} from '../../../types/common';
+
+const basicFixture = fixtureFromElement('gr-smart-search');
+
+suite('gr-smart-search tests', () => {
+  let element: GrSmartSearch;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('Autocompletes accounts', () => {
+    stubRestApi('getSuggestedAccounts').callsFake(() =>
+      Promise.resolve([
+        {
+          name: 'fred',
+          email: 'fred@goog.co' as EmailAddress,
+        },
+      ])
+    );
+    return element._fetchAccounts('owner', 'fr').then(s => {
+      assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
+    });
+  });
+
+  test('Inserts self as option when valid', () => {
+    stubRestApi('getSuggestedAccounts').callsFake(() =>
+      Promise.resolve([
+        {
+          name: 'fred',
+          email: 'fred@goog.co' as EmailAddress,
+        },
+      ])
+    );
+    element
+      ._fetchAccounts('owner', 's')
+      .then(s => {
+        assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
+        assert.deepEqual(s[1], {text: 'owner:self'});
+      })
+      .then(() => element._fetchAccounts('owner', 'selfs'))
+      .then(s => {
+        assert.notEqual(s[0], {text: 'owner:self'});
+      });
+  });
+
+  test('Inserts me as option when valid', () => {
+    stubRestApi('getSuggestedAccounts').callsFake(() =>
+      Promise.resolve([
+        {
+          name: 'fred',
+          email: 'fred@goog.co' as EmailAddress,
+        },
+      ])
+    );
+    return element
+      ._fetchAccounts('owner', 'm')
+      .then(s => {
+        assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
+        assert.deepEqual(s[1], {text: 'owner:me'});
+      })
+      .then(() => element._fetchAccounts('owner', 'meme'))
+      .then(s => {
+        assert.notEqual(s[0], {text: 'owner:me'});
+      });
+  });
+
+  test('Autocompletes groups', () => {
+    stubRestApi('getSuggestedGroups').callsFake(() =>
+      Promise.resolve({
+        Polygerrit: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f' as GroupId},
+        gerrit: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f' as GroupId},
+        gerrittest: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f' as GroupId},
+      })
+    );
+    return element._fetchGroups('ownerin', 'pol').then(s => {
+      assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'});
+    });
+  });
+
+  test('Autocompletes projects', () => {
+    stubRestApi('getSuggestedProjects').callsFake(() =>
+      Promise.resolve({Polygerrit: {id: 'test' as UrlEncodedRepoName}})
+    );
+    return element._fetchProjects('project', 'pol').then(s => {
+      assert.deepEqual(s[0], {text: 'project:Polygerrit'});
+    });
+  });
+
+  test('Autocomplete doesnt override exact matches to input', () => {
+    stubRestApi('getSuggestedGroups').callsFake(() =>
+      Promise.resolve({
+        Polygerrit: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f' as GroupId},
+        gerrit: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f' as GroupId},
+        gerrittest: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f' as GroupId},
+      })
+    );
+    return element._fetchGroups('ownerin', 'gerrit').then(s => {
+      assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'});
+      assert.deepEqual(s[1], {text: 'ownerin:gerrit'});
+      assert.deepEqual(s[2], {text: 'ownerin:gerrittest'});
+    });
+  });
+
+  test('Autocompletes accounts with no email', () => {
+    stubRestApi('getSuggestedAccounts').callsFake(() =>
+      Promise.resolve([{name: 'fred'}])
+    );
+    return element._fetchAccounts('owner', 'fr').then(s => {
+      assert.deepEqual(s[0], {text: 'owner:"fred"', label: 'fred'});
+    });
+  });
+
+  test('Autocompletes accounts with email', () => {
+    stubRestApi('getSuggestedAccounts').callsFake(() =>
+      Promise.resolve([{email: 'fred@goog.co' as EmailAddress}])
+    );
+    return element._fetchAccounts('owner', 'fr').then(s => {
+      assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: ''});
+    });
+  });
+});
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
index eb50914..864fd79 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
@@ -514,9 +514,7 @@
     if (beforeNumber !== 'FILE' && beforeNumber !== 'LOST') {
       const responsiveMode = getResponsiveMode(this._prefs, this._renderPrefs);
       const lineLimit =
-        responsiveMode !== 'FULL_RESPONSIVE'
-          ? this._prefs.line_length
-          : Infinity;
+        responsiveMode === 'NONE' ? this._prefs.line_length : Infinity;
       const contentText = this._formatText(
         line.text,
         this._prefs.tab_size,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
index c8b3901..f418cfa 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
@@ -23,7 +23,7 @@
   lineNumberToNumber,
 } from '../gr-diff/gr-diff-utils';
 
-const tokenMatcher = new RegExp(/[a-zA-Z0-9_-]+/g);
+const tokenMatcher = new RegExp(/[\w]+/g);
 
 /** CSS class for all tokens. */
 const CSS_TOKEN = 'token';
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
index 65e8e9f..0bd02d5 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
@@ -77,11 +77,11 @@
   @property({type: Object})
   resolveWeblinks?: GeneratedWebLink[] = [];
 
-  _computeStatusString(status: ChangeStates) {
+  _computeStatusString(status?: ChangeStates) {
     if (status === ChangeStates.WIP && !this.flat) {
       return 'Work in Progress';
     }
-    return status;
+    return status ?? '';
   }
 
   _toClassName(str?: ChangeStates) {
@@ -107,14 +107,14 @@
     revertedChange?: ChangeInfo,
     resolveWeblinks?: GeneratedWebLink[],
     status?: ChangeStates
-  ): string | undefined {
+  ): string {
     if (revertedChange) {
       return GerritNav.getUrlForSearchQuery(`${revertedChange._number}`);
     }
     if (status === ChangeStates.MERGE_CONFLICT && resolveWeblinks?.length) {
-      return resolveWeblinks[0].url;
+      return resolveWeblinks[0].url ?? '';
     }
-    return undefined;
+    return '';
   }
 
   showResolveIcon(
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
index 4a2bcee..36cffb7 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
@@ -20,6 +20,7 @@
 import {IronIconElement} from '@polymer/iron-icon';
 import {assertIsDefined, queryAndAssert} from '../../../utils/common-util';
 import {classMap} from 'lit-html/directives/class-map';
+import {ifDefined} from 'lit-html/directives/if-defined';
 import {css, customElement, html, property} from 'lit-element';
 import {GrLitElement} from '../../lit/gr-lit-element';
 import {GrButton} from '../gr-button/gr-button';
@@ -126,7 +127,7 @@
           link=""
           ?has-tooltip=${this.hasTooltip}
           class="copyToClipboard"
-          title="${this.buttonTitle}"
+          title="${ifDefined(this.buttonTitle)}"
           @click="${this._copyToClipboard}"
           aria-label="Click to copy to clipboard"
         >
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.js b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
similarity index 73%
rename from polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.js
rename to polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
index 45847d7..ef62fe9 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
@@ -15,14 +15,16 @@
  * limitations under the License.
  */
 
-import '../../../test/common-test-setup-karma.js';
-import './gr-copy-clipboard.js';
-import {queryAndAssert} from '../../../test/test-utils.js';
+import '../../../test/common-test-setup-karma';
+import './gr-copy-clipboard';
+import {GrCopyClipboard} from './gr-copy-clipboard';
+import {queryAndAssert} from '../../../test/test-utils';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
 
 const basicFixture = fixtureFromElement('gr-copy-clipboard');
 
 suite('gr-copy-clipboard tests', () => {
-  let element;
+  let element: GrCopyClipboard;
 
   setup(async () => {
     element = basicFixture.instantiate();
@@ -33,35 +35,34 @@
 
   test('copy to clipboard', () => {
     const clipboardSpy = sinon.spy(navigator.clipboard, 'writeText');
-    const copyBtn = element.shadowRoot
-        .querySelector('.copyToClipboard');
+    const copyBtn = queryAndAssert(element, '.copyToClipboard');
     MockInteractions.click(copyBtn);
     assert.isTrue(clipboardSpy.called);
   });
 
   test('focusOnCopy', () => {
     element.focusOnCopy();
-    const activeElement = element.shadowRoot.activeElement;
-    const button = element.shadowRoot.querySelector('.copyToClipboard');
+    const activeElement = element.shadowRoot!.activeElement;
+    const button = queryAndAssert(element, '.copyToClipboard');
     assert.deepEqual(activeElement, button);
   });
 
   test('_handleInputClick', () => {
     // iron-input as parent should never be hidden as copy won't work
     // on nested hidden elements
-    const ironInputElement = element.shadowRoot.querySelector('iron-input');
+    const ironInputElement = queryAndAssert(element, 'iron-input');
     assert.notEqual(getComputedStyle(ironInputElement).display, 'none');
 
-    const inputElement = element.shadowRoot.querySelector('input');
+    const inputElement = queryAndAssert(element, 'input') as HTMLInputElement;
     MockInteractions.tap(inputElement);
     assert.equal(inputElement.selectionStart, 0);
-    assert.equal(inputElement.selectionEnd, element.text.length - 1);
+    assert.equal(inputElement.selectionEnd, element.text!.length! - 1);
   });
 
   test('hideInput', async () => {
     // iron-input as parent should never be hidden as copy won't work
     // on nested hidden elements
-    const ironInputElement = element.shadowRoot.querySelector('iron-input');
+    const ironInputElement = queryAndAssert(element, 'iron-input');
     assert.notEqual(getComputedStyle(ironInputElement).display, 'none');
 
     const input = queryAndAssert(element, 'input');
@@ -76,10 +77,8 @@
     divParent.appendChild(element);
     const clickStub = sinon.stub();
     divParent.addEventListener('click', clickStub);
-    element.stopPropagation = true;
-    const copyBtn = element.shadowRoot.querySelector('.copyToClipboard');
+    const copyBtn = queryAndAssert(element, '.copyToClipboard');
     MockInteractions.tap(copyBtn);
     assert.isFalse(clickStub.called);
   });
 });
-
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
index 83cd380..592efba 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
@@ -106,13 +106,14 @@
   _saveDisabled!: boolean;
 
   @property({type: String, observer: '_newContentChanged'})
-  _newContent?: string;
+  _newContent = '';
 
   private readonly storage = appContext.storageService;
 
   private readonly reporting = appContext.reportingService;
 
-  private storeTask?: DelayedTask;
+  // Tests use this so needs to be non private
+  storeTask?: DelayedTask;
 
   /** @override */
   ready() {
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
index c6ff903..7877a1f 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
@@ -69,7 +69,7 @@
       box-shadow: var(--elevation-level-1);
       /* slightly up to cover rounded corner of the commit msg */
       margin-top: calc(-1 * var(--spacing-xs));
-      /* To make this bar pop over editor, since editor has relative position. 
+      /* To make this bar pop over editor, since editor has relative position.
       */
       position: relative;
     }
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
similarity index 73%
rename from polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.js
rename to polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
index 94a7b96..074678e 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
@@ -15,13 +15,17 @@
  * limitations under the License.
  */
 
-import '../../../test/common-test-setup-karma.js';
-import './gr-editable-content.js';
+import '../../../test/common-test-setup-karma';
+import './gr-editable-content';
+import {GrEditableContent} from './gr-editable-content';
+import {queryAndAssert, stubStorage} from '../../../test/test-utils';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+import {GrButton} from '../gr-button/gr-button';
 
 const basicFixture = fixtureFromElement('gr-editable-content');
 
 suite('gr-editable-content tests', () => {
-  let element;
+  let element: GrEditableContent;
 
   setup(() => {
     element = basicFixture.instantiate();
@@ -33,8 +37,7 @@
     const handler = sinon.spy();
     element.addEventListener('editable-content-save', handler);
 
-    MockInteractions.tap(element.shadowRoot
-        .querySelector('gr-button[primary]'));
+    MockInteractions.tap(queryAndAssert(element, 'gr-button[primary]'));
 
     assert.isTrue(handler.called);
     assert.equal(handler.lastCall.args[0].detail.content, 'foo');
@@ -44,8 +47,7 @@
     const handler = sinon.spy();
     element.addEventListener('editable-content-cancel', handler);
 
-    MockInteractions.tap(element.shadowRoot
-        .querySelector('gr-button.cancel-button'));
+    MockInteractions.tap(queryAndAssert(element, 'gr-button.cancel-button'));
 
     assert.isTrue(handler.called);
   });
@@ -79,19 +81,22 @@
     });
 
     test('save button is disabled initially', () => {
-      assert.isTrue(element.shadowRoot
-          .querySelector('gr-button[primary]').disabled);
+      assert.isTrue(
+        queryAndAssert<GrButton>(element, 'gr-button[primary]').disabled
+      );
     });
 
     test('save button is enabled when content changes', () => {
       element._newContent = 'new content';
-      assert.isFalse(element.shadowRoot
-          .querySelector('gr-button[primary]').disabled);
+      assert.isFalse(
+        queryAndAssert<GrButton>(element, 'gr-button[primary]').disabled
+      );
     });
   });
 
   suite('storageKey and related behavior', () => {
-    let dispatchSpy;
+    let dispatchSpy: sinon.SinonSpy;
+
     setup(() => {
       element.content = 'current content';
       element.storageKey = 'test';
@@ -99,8 +104,10 @@
     });
 
     test('editing toggled to true, has stored data', () => {
-      sinon.stub(element.storage, 'getEditableContentItem')
-          .returns({message: 'stored content'});
+      stubStorage('getEditableContentItem').returns({
+        message: 'stored content',
+        updated: 0,
+      });
       element.editing = true;
 
       assert.equal(element._newContent, 'stored content');
@@ -109,8 +116,7 @@
     });
 
     test('editing toggled to true, has no stored data', () => {
-      sinon.stub(element.storage, 'getEditableContentItem')
-          .returns({});
+      stubStorage('getEditableContentItem').returns(null);
       element.editing = true;
 
       assert.equal(element._newContent, 'current content');
@@ -118,28 +124,26 @@
     });
 
     test('edits are cached', () => {
-      const storeStub =
-          sinon.stub(element.storage, 'setEditableContentItem');
-      const eraseStub =
-          sinon.stub(element.storage, 'eraseEditableContentItem');
+      const storeStub = stubStorage('setEditableContentItem');
+      const eraseStub = stubStorage('eraseEditableContentItem');
       element.editing = true;
 
       element._newContent = 'new content';
       flush();
-      element.storeTask.flush();
+      element.storeTask?.flush();
 
       assert.isTrue(storeStub.called);
       assert.deepEqual(
-          [element.storageKey, element._newContent],
-          storeStub.lastCall.args);
+        [element.storageKey, element._newContent],
+        storeStub.lastCall.args
+      );
 
       element._newContent = '';
       flush();
-      element.storeTask.flush();
+      element.storeTask?.flush();
 
       assert.isTrue(eraseStub.called);
       assert.deepEqual([element.storageKey], eraseStub.lastCall.args);
     });
   });
 });
-