Merge "Re-arrange gr-repo-access to have buttons at the bottom"
diff --git a/package.json b/package.json
index 0f10b62..30dbb99 100644
--- a/package.json
+++ b/package.json
@@ -15,21 +15,19 @@
     "fried-twinkie": "^0.2.2",
     "polymer-cli": "^1.9.11",
     "prettier": "2.0.5",
-    "typescript": "3.8.2",
-    "web-component-tester": "^6.5.1"
+    "typescript": "3.8.2"
   },
   "scripts": {
     "clean": "git clean -fdx && bazel clean --expunge",
     "start": "polygerrit-ui/run-server.sh",
-    "test": "WCT_HEADLESS_MODE=1 WCT_ARGS='--verbose -l chrome' ./polygerrit-ui/app/run_test.sh",
+    "test": "./polygerrit-ui/app/run_test.sh",
     "safe_bazelisk": "if which bazelisk >/dev/null; then bazel_bin=bazelisk; else bazel_bin=bazel; fi && $bazel_bin",
     "eslint": "npm run safe_bazelisk test polygerrit-ui/app:lint_test",
     "eslintfix": "npm run safe_bazelisk run polygerrit-ui/app:lint_bin -- -- --fix $(pwd)/polygerrit-ui/app",
     "test-template": "./polygerrit-ui/app/run_template_test.sh",
     "polylint": "npm run safe_bazelisk test polygerrit-ui/app:polylint_test",
-    "test:karma:debug": "npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --browsers ChromeDev --no-single-run --testFiles",
-    "test:karma": "npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --testFiles",
-    "test:wct": "npm run test -- --test_tag_filters=wct"
+    "test:debug": "npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --browsers ChromeDev --no-single-run --testFiles",
+    "test:single": "npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --testFiles"
   },
   "repository": {
     "type": "git",
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 29ba857..545c238 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -148,160 +148,30 @@
 ## Running Tests
 
 For daily development you typically only want to run and debug individual tests.
-There are 2 types of fronted tests in gerrit:
-- Karma tests - all tests matches `*_test.js` pattern
-- web-component-tester(WCT) tests - all tests matches the `*_test.html` pattern.
+There are several ways to run tests.
 
-**Note:** WCT tests are deprecated. We are migrating to Karma tests now. If you are going to change
-something in a WCT test file, we strongly recommend to convert it to Karma tests before making
-any change. See [Converting WCT tests to Karma](#wct-to-karma).
-
-Our CI integration ensures that all tests are run when you upload a change to
-Gerrit, but you can also run all tests locally in headless mode:
-
+* Run all tests in headless mode:
 ```sh
-npm test
+npm run test
 ```
 
-### Running Karma tests
-There are several ways to run Karma tests:
-
-* Run all Karma tests in headless mode:
-```sh
-npm run test:karma
-```
-
-* Run all Karma tests in debug mode (the command opens Chrome browser with
+* Run all tests in debug mode (the command opens Chrome browser with
 the default Karma page; you should click the "Debug" button to start testing):
 ```sh
-npm run test:karma:debug
+npm run test:debug
 ```
 
 * Run a single test file:
 ```
 # Headless mode
-npm run test:karma async-foreach-behavior_test.js
+npm run test:single async-foreach-behavior_test.js
 # Debug mode
-npm run test:karma:debug async-foreach-behavior_test.js
+npm run test:debug async-foreach-behavior_test.js
 ```
 
 * You can run tests in IDE:
   - [IntelliJ: running unit tests on Karma](https://www.jetbrains.com/help/idea/running-unit-tests-on-karma.html#ws_karma_running)
 
-### Running WCT tests
-
-Run the local [Go proxy server](#go-server) and navigate for example to
-<http://localhost:8081/elements/shared/gr-account-entry/gr-account-entry_test.html>.
-Check "Disable cache" in the "Network" tab of Chrome's dev tools, so code
-changes are picked up on "reload".
-
-You can also run all WCT tests locally in headless mode:
-
-```sh
-npm test:wct
-```
-
-To allow the tests to run in Safari:
-
-* In the Advanced preferences tab, check "Show Develop menu in menu bar".
-* In the Develop menu, enable the "Allow Remote Automation" option.
-
-To run Chrome tests in headless mode:
-
-```sh
-WCT_HEADLESS_MODE=1 WCT_ARGS='--verbose -l chrome' ./polygerrit-ui/app/run_test.sh
-```
-
-### <a name="wct-to-karma"></a>Converting WCT tests to Karma
-
-If you are want to change a WCT test file (any `..._test.html` file), please convert the file to a
-Karma test file before making any changes. It is better to make a conversion in a separate change,
-so any conversion-related problems can be catch at this step.
-
-Usually, our WCT tests files have the following structure:
-```Html
-<!-- Test header: meta, title, wct scripts -->
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-account-link</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<!-- Templates for test fixtures (optional) -->
-<test-fixture id="basic">
-   <template>
-     <gr-account-link></gr-account-link>
-   </template>
- </test-fixture>
-
-<test-fixture id="other">
-   <template>
-     <gr-dialog>
-       <span>Hello!</span>
-     </gr-dialog>
-   </template>
- </test-fixture>
-
-<!-- Tests -->
-<script type="module">
-// One or more imports:
-import '../../../test/common-test-setup.js';
-import ...;
-
-// Tests - one or more suites
-suite(..., () => {
-   ...
-   // instantiate 'basic' template:
-   element = fixture('basic');
-
-   ...
-   // instantiate 'other' template:
-   otherElements = fixture('other');
-
-);
-</script>
-```
-
-A conversion requires the following changes:
-* Rename the `..._test.html` file to the `..._test.js` file.
-* Remove test header (see a WCT test example above)
-* Remove all `<script...>` and `</script>` tags, but preserve javascript code
-* Change imports - use `test/common-test-setup-karma.js` instead of `test/common-test-setup.js`.
-Ensure, that the `common-test-setup-karma.js` import is placed above any other imports.
-* If there are test fixtures in the html file, move them inside a `<script>` tag and use
-the `fixtureFromTemplate` or `fixtureFromElement` (if there is only one element in a template)
-to define a test fixture template.
-* Use `instantiate` method instead of `fixture` method.
-
-After conversion, the Karma test file for the example above can look like:
-```Javascript
-import '../../../test/common-test-setup-karma.js';
-// Other imports:
-import ...
-
-// Define test fixtures templates:
-const fixture =
-  fixtureFromElement('gr-account-link');
-const otherFixture = fixtureFromTemplate(html`<gr-dialog>
-  <span>Hello!</span>
-</gr-dialog>
-`);
-
-// Tests - one or more suites
-suite(..., () => {
-   ...
-   // instantiate 'basic' template:
-   element = fixture.instantiate();
-
-   ...
-   // instantiate 'other' template:
-   otherElements = otherFixture.instantiate();
-
-);
-```
-
 ## Style guide
 
 We follow the [Google JavaScript Style Guide](https://google.github.io/styleguide/javascriptguide.xml)
diff --git a/polygerrit-ui/app/.eslintrc.js b/polygerrit-ui/app/.eslintrc.js
index fc53dbf..8aa6e5f 100644
--- a/polygerrit-ui/app/.eslintrc.js
+++ b/polygerrit-ui/app/.eslintrc.js
@@ -228,7 +228,7 @@
       }
     },
     {
-      "files": ["test/functional/**/*.js", "wct.conf.js", "template_test.js"],
+      "files": ["test/functional/**/*.js", "template_test.js"],
       // Settings for functional tests. These scripts are node scripts.
       // Turn off "no-undef" to allow any global variable
       "env": {
@@ -241,12 +241,6 @@
       }
     },
     {
-      "files": "test/index.html",
-      "globals": {
-        "WCT": "readonly",
-      }
-    },
-    {
       "files": ["*_html.js", "gr-icons.js", "*-theme.js", "*-styles.js"],
       "rules": {
         "max-len": "off"
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index bab00b0..88e0834 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -1,4 +1,4 @@
-load(":rules.bzl", "polygerrit_bundle", "wct_suite")
+load(":rules.bzl", "polygerrit_bundle")
 load("//tools/js:eslint.bzl", "eslint")
 
 package(default_visibility = ["//visibility:public"])
@@ -45,7 +45,6 @@
         exclude = [
             "node_modules/**",
             "node_modules_licenses/**",
-            "**/*_test.html",
             "test/**",
             "samples/**",
             "**/*_test.js",
@@ -59,19 +58,12 @@
     srcs = [
         "test/common-test-setup.js",
         "test/common-test-setup-karma.js",
-        "test/index.html",
         ":pg_code",
         "@ui_dev_npm//:node_modules",
         "@ui_npm//:node_modules",
     ],
 )
 
-wct_suite(
-    name = "wct",
-    srcs = [":test-srcs-fg"],
-    split_count = 4,
-)
-
 # Define the eslinter for polygerrit-ui app
 # The eslint macro creates 2 rules: lint_test and lint_bin
 eslint(
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.js b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.js
index 44173323..3b8a9cb 100644
--- a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.js
+++ b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.js
@@ -19,7 +19,7 @@
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
 import {BaseUrlBehavior} from './base-url-behavior.js';
 
-const basicFixture = fixtureFromElement('test-element');
+const basicFixture = fixtureFromElement('base-url-behavior-test-element');
 
 suite('base-url-behavior tests', () => {
   let element;
@@ -30,7 +30,7 @@
     window.CANONICAL_PATH = '/r';
     // Define a Polymer element that uses this behavior.
     Polymer({
-      is: 'test-element',
+      is: 'base-url-behavior-test-element',
       behaviors: [
         BaseUrlBehavior,
       ],
diff --git a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.js
similarity index 62%
rename from polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
rename to polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.js
index 0efd80f..93beb52 100644
--- a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.js
@@ -1,36 +1,26 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-<!-- Polymer included for the html import polyfill. -->
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<title>docs-url-behavior</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <docs-url-behavior-element></docs-url-behavior-element>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../test/common-test-setup.js';
+import '../../test/common-test-setup-karma.js';
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
 import {DocsUrlBehavior} from './docs-url-behavior.js';
+
+const basicFixture = fixtureFromElement('docs-url-behavior-element');
+
 suite('docs-url-behavior tests', () => {
   let element;
 
@@ -43,7 +33,7 @@
   });
 
   setup(() => {
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element._clearDocsBaseUrlCache();
   });
 
@@ -96,4 +86,4 @@
         });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html
deleted file mode 100644
index 1e842c1..0000000
--- a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html
+++ /dev/null
@@ -1,72 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>dom-util-behavior</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<test-fixture id="nested-structure">
-  <template>
-    <test-element></test-element>
-    <div>
-      <div class="a">
-        <div class="b">
-          <div class="c"></div>
-        </div>
-      </div>
-    </div>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../test/common-test-setup.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {DomUtilBehavior} from './dom-util-behavior.js';
-suite('dom-util-behavior tests', () => {
-  let element;
-  let divs;
-
-  suiteSetup(() => {
-    // Define a Polymer element that uses this behavior.
-    Polymer({
-      is: 'test-element',
-      behaviors: [DomUtilBehavior],
-    });
-  });
-
-  setup(() => {
-    const testDom = fixture('nested-structure');
-    element = testDom[0];
-    divs = testDom[1];
-  });
-
-  test('descendedFromClass', () => {
-    // .c is a child of .a and not vice versa.
-    assert.isTrue(element.descendedFromClass(divs.querySelector('.c'), 'a'));
-    assert.isFalse(element.descendedFromClass(divs.querySelector('.a'), 'c'));
-
-    // Stops at stop element.
-    assert.isFalse(element.descendedFromClass(divs.querySelector('.c'), 'a',
-        divs.querySelector('.b')));
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.js b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.js
new file mode 100644
index 0000000..c110b34
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.js
@@ -0,0 +1,62 @@
+/**
+ * @license
+ * Copyright (C) 2018 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 {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+import {DomUtilBehavior} from './dom-util-behavior.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const nestedStructureFixture = fixtureFromTemplate(html`
+  <dom-util-behavior-test-element></dom-util-behavior-test-element>
+  <div>
+    <div class="a">
+      <div class="b">
+        <div class="c"></div>
+      </div>
+    </div>
+  </div>
+`);
+
+suite('dom-util-behavior tests', () => {
+  let element;
+  let divs;
+
+  suiteSetup(() => {
+    // Define a Polymer element that uses this behavior.
+    Polymer({
+      is: 'dom-util-behavior-test-element',
+      behaviors: [DomUtilBehavior],
+    });
+  });
+
+  setup(() => {
+    const testDom = nestedStructureFixture.instantiate();
+    element = testDom[0];
+    divs = testDom[1];
+  });
+
+  test('descendedFromClass', () => {
+    // .c is a child of .a and not vice versa.
+    assert.isTrue(element.descendedFromClass(divs.querySelector('.c'), 'a'));
+    assert.isFalse(element.descendedFromClass(divs.querySelector('.a'), 'c'));
+
+    // Stops at stop element.
+    assert.isFalse(element.descendedFromClass(divs.querySelector('.c'), 'a',
+        divs.querySelector('.b')));
+  });
+});
+
diff --git a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html
deleted file mode 100644
index c5f3f94..0000000
--- a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html
+++ /dev/null
@@ -1,72 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>keyboard-shortcut-behavior</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<test-fixture id="basic">
-  <template>
-    <test-element></test-element>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../test/common-test-setup.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {AccessBehavior} from './gr-access-behavior.js';
-suite('gr-access-behavior tests', () => {
-  let element;
-
-  suiteSetup(() => {
-    // Define a Polymer element that uses this behavior.
-    Polymer({
-      is: 'test-element',
-      behaviors: [AccessBehavior],
-    });
-  });
-
-  setup(() => {
-    element = fixture('basic');
-  });
-
-  test('toSortedArray', () => {
-    const rules = {
-      'global:Project-Owners': {
-        action: 'ALLOW', force: false,
-      },
-      '4c97682e6ce6b7247f3381b6f1789356666de7f': {
-        action: 'ALLOW', force: false,
-      },
-    };
-    const expectedResult = [
-      {id: '4c97682e6ce6b7247f3381b6f1789356666de7f', value: {
-        action: 'ALLOW', force: false,
-      }},
-      {id: 'global:Project-Owners', value: {
-        action: 'ALLOW', force: false,
-      }},
-    ];
-    assert.deepEqual(element.toSortedArray(rules), expectedResult);
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.js b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.js
new file mode 100644
index 0000000..b29505f
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.js
@@ -0,0 +1,59 @@
+/**
+ * @license
+ * Copyright (C) 2017 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 {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+import {AccessBehavior} from './gr-access-behavior.js';
+
+const basicFixture = fixtureFromElement('gr-access-behavior-test-element');
+
+suite('gr-access-behavior tests', () => {
+  let element;
+
+  suiteSetup(() => {
+    // Define a Polymer element that uses this behavior.
+    Polymer({
+      is: 'gr-access-behavior-test-element',
+      behaviors: [AccessBehavior],
+    });
+  });
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('toSortedArray', () => {
+    const rules = {
+      'global:Project-Owners': {
+        action: 'ALLOW', force: false,
+      },
+      '4c97682e6ce6b7247f3381b6f1789356666de7f': {
+        action: 'ALLOW', force: false,
+      },
+    };
+    const expectedResult = [
+      {id: '4c97682e6ce6b7247f3381b6f1789356666de7f', value: {
+        action: 'ALLOW', force: false,
+      }},
+      {id: 'global:Project-Owners', value: {
+        action: 'ALLOW', force: false,
+      }},
+    ];
+    assert.deepEqual(element.toSortedArray(rules), expectedResult);
+  });
+});
+
diff --git a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.js
similarity index 86%
rename from polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
rename to polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.js
index 3f58499..72109f2 100644
--- a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.js
@@ -1,49 +1,35 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2018 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>keyboard-shortcut-behavior</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<test-fixture id="basic">
-  <template>
-    <test-element></test-element>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../test/common-test-setup.js';
+import '../../test/common-test-setup-karma.js';
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
 import {AdminNavBehavior} from './gr-admin-nav-behavior.js';
+
+const basicFixture = fixtureFromElement('gr-admin-nav-behavior-test-element');
+
 suite('gr-admin-nav-behavior tests', () => {
   let element;
-  let sandbox;
   let capabilityStub;
   let menuLinkStub;
 
   suiteSetup(() => {
     // Define a Polymer element that uses this behavior.
     Polymer({
-      is: 'test-element',
+      is: 'gr-admin-nav-behavior-test-element',
       behaviors: [
         AdminNavBehavior,
       ],
@@ -51,16 +37,11 @@
   });
 
   setup(() => {
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
+    element = basicFixture.instantiate();
     capabilityStub = sinon.stub();
     menuLinkStub = sinon.stub().returns([]);
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   const testAdminLinks = (account, options, expected, done) => {
     element.getAdminLinks(account,
         capabilityStub,
@@ -366,4 +347,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
deleted file mode 100644
index 3c5aedd..0000000
--- a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
+++ /dev/null
@@ -1,130 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>keyboard-shortcut-behavior</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<test-fixture id="basic">
-  <template>
-    <test-element></test-element>
-  </template>
-</test-fixture>
-
-<test-fixture id="within-overlay">
-  <template>
-    <gr-overlay>
-      <test-element></test-element>
-    </gr-overlay>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../test/common-test-setup.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {ChangeTableBehavior} from './gr-change-table-behavior.js';
-suite('gr-change-table-behavior tests', () => {
-  let element;
-  // eslint-disable-next-line no-unused-vars
-  let overlay;
-
-  suiteSetup(() => {
-    // Define a Polymer element that uses this behavior.
-    Polymer({
-      is: 'test-element',
-      behaviors: [ChangeTableBehavior],
-    });
-  });
-
-  setup(() => {
-    element = fixture('basic');
-    overlay = fixture('within-overlay');
-  });
-
-  test('getComplementColumns', () => {
-    let columns = [
-      'Subject',
-      'Status',
-      'Owner',
-      'Assignee',
-      'Reviewers',
-      'Comments',
-      'Repo',
-      'Branch',
-      'Updated',
-      'Size',
-    ];
-    assert.deepEqual(element.getComplementColumns(columns), []);
-
-    columns = [
-      'Subject',
-      'Status',
-      'Assignee',
-      'Reviewers',
-      'Comments',
-      'Repo',
-      'Branch',
-      'Size',
-    ];
-    assert.deepEqual(element.getComplementColumns(columns),
-        ['Owner', 'Updated']);
-  });
-
-  test('isColumnHidden', () => {
-    const columnToCheck = 'Repo';
-    let columnsToDisplay = [
-      'Subject',
-      'Status',
-      'Owner',
-      'Assignee',
-      'Repo',
-      'Branch',
-      'Updated',
-      'Size',
-    ];
-    assert.isFalse(element.isColumnHidden(columnToCheck, columnsToDisplay));
-
-    columnsToDisplay = [
-      'Subject',
-      'Status',
-      'Owner',
-      'Assignee',
-      'Branch',
-      'Updated',
-      'Size',
-    ];
-    assert.isTrue(element.isColumnHidden(columnToCheck, columnsToDisplay));
-  });
-
-  test('getVisibleColumns maps Project to Repo', () => {
-    const columns = [
-      'Subject',
-      'Status',
-      'Owner',
-    ];
-    assert.deepEqual(element.getVisibleColumns(columns), columns.slice(0));
-    assert.deepEqual(
-        element.getVisibleColumns(columns.concat(['Project'])),
-        columns.slice(0).concat(['Repo']));
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.js b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.js
new file mode 100644
index 0000000..dfca358
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.js
@@ -0,0 +1,118 @@
+/**
+ * @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 {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+import {ChangeTableBehavior} from './gr-change-table-behavior.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromElement(
+    'gr-change-table-behavior-test-element');
+
+const withinOverlayFixture = fixtureFromTemplate(html`
+  <gr-overlay>
+    <gr-change-table-behavior-test-element>
+    </gr-change-table-behavior-test-element>
+  </gr-overlay>
+`);
+
+suite('gr-change-table-behavior tests', () => {
+  let element;
+  // eslint-disable-next-line no-unused-vars
+  let overlay;
+
+  suiteSetup(() => {
+    // Define a Polymer element that uses this behavior.
+    Polymer({
+      is: 'gr-change-table-behavior-test-element',
+      behaviors: [ChangeTableBehavior],
+    });
+  });
+
+  setup(() => {
+    element = basicFixture.instantiate();
+    overlay = withinOverlayFixture.instantiate();
+  });
+
+  test('getComplementColumns', () => {
+    let columns = [
+      'Subject',
+      'Status',
+      'Owner',
+      'Assignee',
+      'Reviewers',
+      'Comments',
+      'Repo',
+      'Branch',
+      'Updated',
+      'Size',
+    ];
+    assert.deepEqual(element.getComplementColumns(columns), []);
+
+    columns = [
+      'Subject',
+      'Status',
+      'Assignee',
+      'Reviewers',
+      'Comments',
+      'Repo',
+      'Branch',
+      'Size',
+    ];
+    assert.deepEqual(element.getComplementColumns(columns),
+        ['Owner', 'Updated']);
+  });
+
+  test('isColumnHidden', () => {
+    const columnToCheck = 'Repo';
+    let columnsToDisplay = [
+      'Subject',
+      'Status',
+      'Owner',
+      'Assignee',
+      'Repo',
+      'Branch',
+      'Updated',
+      'Size',
+    ];
+    assert.isFalse(element.isColumnHidden(columnToCheck, columnsToDisplay));
+
+    columnsToDisplay = [
+      'Subject',
+      'Status',
+      'Owner',
+      'Assignee',
+      'Branch',
+      'Updated',
+      'Size',
+    ];
+    assert.isTrue(element.isColumnHidden(columnToCheck, columnsToDisplay));
+  });
+
+  test('getVisibleColumns maps Project to Repo', () => {
+    const columns = [
+      'Subject',
+      'Status',
+      'Owner',
+    ];
+    assert.deepEqual(element.getVisibleColumns(columns), columns.slice(0));
+    assert.deepEqual(
+        element.getVisibleColumns(columns.concat(['Project'])),
+        columns.slice(0).concat(['Repo']));
+  });
+});
+
diff --git a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
deleted file mode 100644
index fa72c40..0000000
--- a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
+++ /dev/null
@@ -1,100 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-display-name-behavior</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<test-fixture id="basic">
-  <template>
-    <test-element-anon></test-element-anon>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../test/common-test-setup.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {DisplayNameBehavior} from './gr-display-name-behavior.js';
-suite('gr-display-name-behavior tests', () => {
-  let element;
-  // eslint-disable-next-line no-unused-vars
-  const config = {
-    user: {
-      anonymous_coward_name: 'Anonymous Coward',
-    },
-  };
-
-  suiteSetup(() => {
-    // Define a Polymer element that uses this behavior.
-    Polymer({
-      is: 'test-element-anon',
-      behaviors: [
-        DisplayNameBehavior,
-      ],
-    });
-  });
-
-  setup(() => {
-    element = fixture('basic');
-  });
-
-  test('getUserName name only', () => {
-    const account = {
-      name: 'test-name',
-    };
-    assert.equal(element.getUserName(config, account), 'test-name');
-  });
-
-  test('getUserName username only', () => {
-    const account = {
-      username: 'test-user',
-    };
-    assert.equal(element.getUserName(config, account), 'test-user');
-  });
-
-  test('getUserName email only', () => {
-    const account = {
-      email: 'test-user@test-url.com',
-    };
-    assert.equal(element.getUserName(config, account),
-        'test-user@test-url.com');
-  });
-
-  test('getUserName returns not Anonymous Coward as the anon name', () => {
-    assert.equal(element.getUserName(config, null), 'Anonymous');
-  });
-
-  test('getUserName for the config returning the anon name', () => {
-    const config = {
-      user: {
-        anonymous_coward_name: 'Test Anon',
-      },
-    };
-    assert.equal(element.getUserName(config, null), 'Test Anon');
-  });
-
-  test('getGroupDisplayName', () => {
-    assert.equal(element.getGroupDisplayName({name: 'Some user name'}),
-        'Some user name (group)');
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.js b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.js
new file mode 100644
index 0000000..82108429
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.js
@@ -0,0 +1,87 @@
+/**
+ * @license
+ * Copyright (C) 2017 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 {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+import {DisplayNameBehavior} from './gr-display-name-behavior.js';
+
+const basicFixture = fixtureFromElement('test-element-anon');
+
+suite('gr-display-name-behavior tests', () => {
+  let element;
+  // eslint-disable-next-line no-unused-vars
+  const config = {
+    user: {
+      anonymous_coward_name: 'Anonymous Coward',
+    },
+  };
+
+  suiteSetup(() => {
+    // Define a Polymer element that uses this behavior.
+    Polymer({
+      is: 'test-element-anon',
+      behaviors: [
+        DisplayNameBehavior,
+      ],
+    });
+  });
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('getUserName name only', () => {
+    const account = {
+      name: 'test-name',
+    };
+    assert.equal(element.getUserName(config, account), 'test-name');
+  });
+
+  test('getUserName username only', () => {
+    const account = {
+      username: 'test-user',
+    };
+    assert.equal(element.getUserName(config, account), 'test-user');
+  });
+
+  test('getUserName email only', () => {
+    const account = {
+      email: 'test-user@test-url.com',
+    };
+    assert.equal(element.getUserName(config, account),
+        'test-user@test-url.com');
+  });
+
+  test('getUserName returns not Anonymous Coward as the anon name', () => {
+    assert.equal(element.getUserName(config, null), 'Anonymous');
+  });
+
+  test('getUserName for the config returning the anon name', () => {
+    const config = {
+      user: {
+        anonymous_coward_name: 'Test Anon',
+      },
+    };
+    assert.equal(element.getUserName(config, null), 'Test Anon');
+  });
+
+  test('getGroupDisplayName', () => {
+    assert.equal(element.getGroupDisplayName({name: 'Some user name'}),
+        'Some user name (group)');
+  });
+});
+
diff --git a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html
deleted file mode 100644
index 80013bf..0000000
--- a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html
+++ /dev/null
@@ -1,93 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>keyboard-shortcut-behavior</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<test-fixture id="basic">
-  <template>
-    <test-element></test-element>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../test/common-test-setup.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {ListViewBehavior} from './gr-list-view-behavior.js';
-suite('gr-list-view-behavior tests', () => {
-  let element;
-  // eslint-disable-next-line no-unused-vars
-  let overlay;
-
-  suiteSetup(() => {
-    // Define a Polymer element that uses this behavior.
-    Polymer({
-      is: 'test-element',
-      behaviors: [ListViewBehavior],
-    });
-  });
-
-  setup(() => {
-    element = fixture('basic');
-  });
-
-  test('computeLoadingClass', () => {
-    assert.equal(element.computeLoadingClass(true), 'loading');
-    assert.equal(element.computeLoadingClass(false), '');
-  });
-
-  test('computeShownItems', () => {
-    const myArr = new Array(26);
-    assert.equal(element.computeShownItems(myArr).length, 25);
-  });
-
-  test('getUrl', () => {
-    assert.equal(element.getUrl('/path/to/something/', 'item'),
-        '/path/to/something/item');
-    assert.equal(element.getUrl('/path/to/something/', 'item%test'),
-        '/path/to/something/item%2525test');
-  });
-
-  test('getFilterValue', () => {
-    let params;
-    assert.equal(element.getFilterValue(params), '');
-
-    params = {filter: null};
-    assert.equal(element.getFilterValue(params), '');
-
-    params = {filter: 'test'};
-    assert.equal(element.getFilterValue(params), 'test');
-  });
-
-  test('getOffsetValue', () => {
-    let params;
-    assert.equal(element.getOffsetValue(params), 0);
-
-    params = {offset: null};
-    assert.equal(element.getOffsetValue(params), 0);
-
-    params = {offset: 1};
-    assert.equal(element.getOffsetValue(params), 1);
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.js b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.js
new file mode 100644
index 0000000..bb54672
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.js
@@ -0,0 +1,81 @@
+/**
+ * @license
+ * Copyright (C) 2017 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 {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+import {ListViewBehavior} from './gr-list-view-behavior.js';
+
+const basicFixture = fixtureFromElement(
+    'gr-list-view-behavior-test-element');
+
+suite('gr-list-view-behavior tests', () => {
+  let element;
+  // eslint-disable-next-line no-unused-vars
+  let overlay;
+
+  suiteSetup(() => {
+    // Define a Polymer element that uses this behavior.
+    Polymer({
+      is: 'gr-list-view-behavior-test-element',
+      behaviors: [ListViewBehavior],
+    });
+  });
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('computeLoadingClass', () => {
+    assert.equal(element.computeLoadingClass(true), 'loading');
+    assert.equal(element.computeLoadingClass(false), '');
+  });
+
+  test('computeShownItems', () => {
+    const myArr = new Array(26);
+    assert.equal(element.computeShownItems(myArr).length, 25);
+  });
+
+  test('getUrl', () => {
+    assert.equal(element.getUrl('/path/to/something/', 'item'),
+        '/path/to/something/item');
+    assert.equal(element.getUrl('/path/to/something/', 'item%test'),
+        '/path/to/something/item%2525test');
+  });
+
+  test('getFilterValue', () => {
+    let params;
+    assert.equal(element.getFilterValue(params), '');
+
+    params = {filter: null};
+    assert.equal(element.getFilterValue(params), '');
+
+    params = {filter: 'test'};
+    assert.equal(element.getFilterValue(params), 'test');
+  });
+
+  test('getOffsetValue', () => {
+    let params;
+    assert.equal(element.getOffsetValue(params), 0);
+
+    params = {offset: null};
+    assert.equal(element.getOffsetValue(params), 0);
+
+    params = {offset: 1};
+    assert.equal(element.getOffsetValue(params), 1);
+  });
+});
+
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.js
similarity index 89%
rename from polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
rename to polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.js
index f03e3ac..b14c0bd 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.js
@@ -1,28 +1,21 @@
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-<!-- Polymer included for the html import polyfill. -->
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<title>gr-patch-set-behavior</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script type="module">
-import '../../test/common-test-setup.js';
+import '../../test/common-test-setup-karma.js';
 import {PatchSetBehavior} from './gr-patch-set-behavior.js';
 suite('gr-patch-set-behavior tests', () => {
   test('getRevisionByPatchNum', () => {
@@ -321,4 +314,4 @@
     assert.equal(PatchSetBehavior.getParentIndex(-4), 4);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.js
similarity index 74%
rename from polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
rename to polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.js
index cf5105e..94e82e2 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.js
@@ -1,28 +1,21 @@
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-<!-- Polymer included for the html import polyfill. -->
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<title>gr-path-list-behavior</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script type="module">
-import '../../test/common-test-setup.js';
+import '../../test/common-test-setup-karma.js';
 import {PathListBehavior} from './gr-path-list-behavior.js';
 import {SpecialFilePath} from '../../constants/constants.js';
 
@@ -113,4 +106,4 @@
     assert.equal(shortenedPath, expectedPath);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.js
similarity index 66%
rename from polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
rename to polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.js
index 79c515e..1f144a0 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.js
@@ -1,41 +1,28 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<title>tooltip-behavior</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <tooltip-behavior-element></tooltip-behavior-element>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../test/common-test-setup.js';
+import '../../test/common-test-setup-karma.js';
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
 import {TooltipBehavior} from './gr-tooltip-behavior.js';
+
+const basicFixture = fixtureFromElement('tooltip-behavior-element');
+
 suite('gr-tooltip-behavior tests', () => {
   let element;
-  let sandbox;
 
   function makeTooltip(tooltipRect, parentRect) {
     return {
@@ -57,16 +44,11 @@
   });
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    element = basicFixture.instantiate();
   });
 
   test('normal position', () => {
-    sandbox.stub(element, 'getBoundingClientRect', () => {
+    sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
       return {top: 100, left: 100, width: 200};
     });
     const tooltip = makeTooltip(
@@ -80,7 +62,7 @@
   });
 
   test('left side position', () => {
-    sandbox.stub(element, 'getBoundingClientRect', () => {
+    sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
       return {top: 100, left: 10, width: 50};
     });
     const tooltip = makeTooltip(
@@ -97,7 +79,7 @@
   });
 
   test('right side position', () => {
-    sandbox.stub(element, 'getBoundingClientRect', () => {
+    sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
       return {top: 100, left: 950, width: 50};
     });
     const tooltip = makeTooltip(
@@ -114,7 +96,7 @@
   });
 
   test('position to bottom', () => {
-    sandbox.stub(element, 'getBoundingClientRect', () => {
+    sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
       return {top: 100, left: 950, width: 50, height: 50};
     });
     const tooltip = makeTooltip(
@@ -132,23 +114,23 @@
   });
 
   test('hides tooltip when detached', () => {
-    sandbox.stub(element, '_handleHideTooltip');
+    sinon.stub(element, '_handleHideTooltip');
     element.remove();
     flushAsynchronousOperations();
     assert.isTrue(element._handleHideTooltip.called);
   });
 
   test('sets up listeners when has-tooltip is changed', () => {
-    const addListenerStub = sandbox.stub(element, 'addEventListener');
+    const addListenerStub = sinon.stub(element, 'addEventListener');
     element.hasTooltip = true;
     assert.isTrue(addListenerStub.called);
   });
 
   test('clean up listeners when has-tooltip changed to false', () => {
-    const removeListenerStub = sandbox.stub(element, 'removeEventListener');
+    const removeListenerStub = sinon.stub(element, 'removeEventListener');
     element.hasTooltip = true;
     element.hasTooltip = false;
     assert.isTrue(removeListenerStub.called);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html
deleted file mode 100644
index d0a2cde..0000000
--- a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html
+++ /dev/null
@@ -1,92 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 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.
--->
-
-<title>gr-url-encoding-behavior</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <test-element></test-element>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../test/common-test-setup.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {URLEncodingBehavior} from './gr-url-encoding-behavior.js';
-suite('gr-url-encoding-behavior tests', () => {
-  let element;
-  let sandbox;
-
-  suiteSetup(() => {
-    // Define a Polymer element that uses this behavior.
-    Polymer({
-      is: 'test-element',
-      behaviors: [URLEncodingBehavior],
-    });
-  });
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  suite('encodeURL', () => {
-    test('double encodes', () => {
-      assert.equal(element.encodeURL('abc?123'), 'abc%253F123');
-      assert.equal(element.encodeURL('def/ghi'), 'def%252Fghi');
-      assert.equal(element.encodeURL('jkl'), 'jkl');
-      assert.equal(element.encodeURL(''), '');
-    });
-
-    test('does not convert colons', () => {
-      assert.equal(element.encodeURL('mno:pqr'), 'mno:pqr');
-    });
-
-    test('converts spaces to +', () => {
-      assert.equal(element.encodeURL('words with spaces'), 'words+with+spaces');
-    });
-
-    test('does not convert slashes when configured', () => {
-      assert.equal(element.encodeURL('stu/vwx', true), 'stu/vwx');
-    });
-
-    test('does not convert slashes when configured', () => {
-      assert.equal(element.encodeURL('stu/vwx', true), 'stu/vwx');
-    });
-  });
-
-  suite('singleDecodeUrl', () => {
-    test('single decodes', () => {
-      assert.equal(element.singleDecodeURL('abc%3Fdef'), 'abc?def');
-    });
-
-    test('converts + to space', () => {
-      assert.equal(element.singleDecodeURL('ghi+jkl'), 'ghi jkl');
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.js b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.js
new file mode 100644
index 0000000..1fe2874
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.js
@@ -0,0 +1,75 @@
+/**
+ * @license
+ * Copyright (C) 2018 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 {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+import {URLEncodingBehavior} from './gr-url-encoding-behavior.js';
+
+const basicFixture =
+    fixtureFromElement('gr-url-encoding-behavior-test-element');
+
+suite('gr-url-encoding-behavior tests', () => {
+  let element;
+
+  suiteSetup(() => {
+    // Define a Polymer element that uses this behavior.
+    Polymer({
+      is: 'gr-url-encoding-behavior-test-element',
+      behaviors: [URLEncodingBehavior],
+    });
+  });
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  suite('encodeURL', () => {
+    test('double encodes', () => {
+      assert.equal(element.encodeURL('abc?123'), 'abc%253F123');
+      assert.equal(element.encodeURL('def/ghi'), 'def%252Fghi');
+      assert.equal(element.encodeURL('jkl'), 'jkl');
+      assert.equal(element.encodeURL(''), '');
+    });
+
+    test('does not convert colons', () => {
+      assert.equal(element.encodeURL('mno:pqr'), 'mno:pqr');
+    });
+
+    test('converts spaces to +', () => {
+      assert.equal(element.encodeURL('words with spaces'), 'words+with+spaces');
+    });
+
+    test('does not convert slashes when configured', () => {
+      assert.equal(element.encodeURL('stu/vwx', true), 'stu/vwx');
+    });
+
+    test('does not convert slashes when configured', () => {
+      assert.equal(element.encodeURL('stu/vwx', true), 'stu/vwx');
+    });
+  });
+
+  suite('singleDecodeUrl', () => {
+    test('single decodes', () => {
+      assert.equal(element.singleDecodeURL('abc%3Fdef'), 'abc?def');
+    });
+
+    test('converts + to space', () => {
+      assert.equal(element.singleDecodeURL('ghi+jkl'), 'ghi jkl');
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
index 92ecb8d..2db1c09 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
@@ -746,6 +746,10 @@
   },
 };
 
+export function _testOnly_getShortcutManagerInstance() {
+  return shortcutManager;
+}
+
 // TODO(dmfilippov) Remove the following lines with assignments
 // Plugins can use the behavior because it was accessible with
 // the global Gerrit... variable. To avoid breaking changes in plugins
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.js
similarity index 84%
rename from polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
rename to polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.js
index fcb7b4f..30231ce 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.js
@@ -1,58 +1,45 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>keyboard-shortcut-behavior</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<test-fixture id="basic">
-  <template>
-    <test-element></test-element>
-  </template>
-</test-fixture>
-
-<test-fixture id="within-overlay">
-  <template>
-    <gr-overlay>
-      <test-element></test-element>
-    </gr-overlay>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../test/common-test-setup.js';
+import '../../test/common-test-setup-karma.js';
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
 import {KeyboardShortcutBehavior, KeyboardShortcutBinder} from './keyboard-shortcut-behavior.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture =
+    fixtureFromElement('keyboard-shortcut-behavior-test-element');
+
+const withinOverlayFixture = fixtureFromTemplate(html`
+<gr-overlay>
+  <keyboard-shortcut-behavior-test-element>      
+  </keyboard-shortcut-behavior-test-element>
+</gr-overlay>
+`);
+
 suite('keyboard-shortcut-behavior tests', () => {
   const kb = KeyboardShortcutBinder;
 
   let element;
   let overlay;
-  let sandbox;
 
   suiteSetup(() => {
     // Define a Polymer element that uses this behavior.
     Polymer({
-      is: 'test-element',
+      is: 'keyboard-shortcut-behavior-test-element',
       behaviors: [KeyboardShortcutBehavior],
       keyBindings: {
         k: '_handleKey',
@@ -63,13 +50,8 @@
   });
 
   setup(() => {
-    element = fixture('basic');
-    overlay = fixture('within-overlay');
-    sandbox = sinon.sandbox.create();
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    element = basicFixture.instantiate();
+    overlay = withinOverlayFixture.instantiate();
   });
 
   suite('ShortcutManager', () => {
@@ -326,7 +308,8 @@
 
   test('blocks kb shortcuts for anything in a gr-overlay', done => {
     const divEl = document.createElement('div');
-    const element = overlay.querySelector('test-element');
+    const element =
+        overlay.querySelector('keyboard-shortcut-behavior-test-element');
     element.appendChild(divEl);
     element._handleKey = e => {
       assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
@@ -337,7 +320,8 @@
 
   test('blocks enter shortcut on an anchor', done => {
     const anchorEl = document.createElement('a');
-    const element = overlay.querySelector('test-element');
+    const element =
+        overlay.querySelector('keyboard-shortcut-behavior-test-element');
     element.appendChild(anchorEl);
     element._handleKey = e => {
       assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
@@ -347,7 +331,7 @@
   });
 
   test('modifierPressed returns accurate values', () => {
-    const spy = sandbox.spy(element, 'modifierPressed');
+    const spy = sinon.spy(element, 'modifierPressed');
     element._handleKey = e => {
       element.modifierPressed(e);
     };
@@ -368,7 +352,7 @@
   });
 
   test('isModifierPressed returns accurate value', () => {
-    const spy = sandbox.spy(element, 'isModifierPressed');
+    const spy = sinon.spy(element, 'isModifierPressed');
     element._handleKey = e => {
       element.isModifierPressed(e, 'shiftKey');
     };
@@ -394,12 +378,12 @@
     setup(() => {
       element._shortcut_go_table.set('a', '_handleA');
       handlerStub = element._handleA = sinon.stub();
-      sandbox.stub(Date, 'now').returns(10000);
+      sinon.stub(Date, 'now').returns(10000);
     });
 
     test('success', () => {
       const e = {detail: {key: 'a'}, preventDefault: () => {}};
-      sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
       element._shortcut_go_key_last_pressed = 9000;
       element._handleGoAction(e);
       assert.isTrue(handlerStub.calledOnce);
@@ -408,7 +392,7 @@
 
     test('go key not pressed', () => {
       const e = {detail: {key: 'a'}, preventDefault: () => {}};
-      sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
       element._shortcut_go_key_last_pressed = null;
       element._handleGoAction(e);
       assert.isFalse(handlerStub.called);
@@ -416,7 +400,7 @@
 
     test('go key pressed too long ago', () => {
       const e = {detail: {key: 'a'}, preventDefault: () => {}};
-      sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
       element._shortcut_go_key_last_pressed = 3000;
       element._handleGoAction(e);
       assert.isFalse(handlerStub.called);
@@ -424,7 +408,7 @@
 
     test('should suppress', () => {
       const e = {detail: {key: 'a'}, preventDefault: () => {}};
-      sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(true);
+      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(true);
       element._shortcut_go_key_last_pressed = 9000;
       element._handleGoAction(e);
       assert.isFalse(handlerStub.called);
@@ -432,11 +416,11 @@
 
     test('unrecognized key', () => {
       const e = {detail: {key: 'f'}, preventDefault: () => {}};
-      sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
       element._shortcut_go_key_last_pressed = 9000;
       element._handleGoAction(e);
       assert.isFalse(handlerStub.called);
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.js
similarity index 77%
rename from polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
rename to polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.js
index 980bc8f..8f63a25 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.js
@@ -1,63 +1,46 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>keyboard-shortcut-behavior</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script type="module">
-import '../../test/common-test-setup.js';
-/** @type {string} */
-window.CANONICAL_PATH = '/r';
-</script>
-
-<test-fixture id="basic">
-  <template>
-    <test-element></test-element>
-  </template>
-</test-fixture>
-
-<test-fixture id="within-overlay">
-  <template>
-    <gr-overlay>
-      <test-element></test-element>
-    </gr-overlay>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../test/common-test-setup.js';
+import '../../test/common-test-setup-karma.js';
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
 import {BaseUrlBehavior} from '../base-url-behavior/base-url-behavior.js';
 import {RESTClientBehavior} from './rest-client-behavior.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromElement('rest-client-behavior-test-element');
+
+const withinOverlayFixture = fixtureFromTemplate(html`
+<gr-overlay>
+  <rest-client-behavior-test-element></rest-client-behavior-test-element>
+</gr-overlay>
+`);
+
 suite('rest-client-behavior tests', () => {
   let element;
   // eslint-disable-next-line no-unused-vars
   let overlay;
+  let originalCanonicalPath;
 
   suiteSetup(() => {
+    originalCanonicalPath = window.CANONICAL_PATH;
+    window.CANONICAL_PATH = '/r';
     // Define a Polymer element that uses this behavior.
     Polymer({
-      is: 'test-element',
+      is: 'rest-client-behavior-test-element',
       behaviors: [
         BaseUrlBehavior,
         RESTClientBehavior,
@@ -65,9 +48,13 @@
     });
   });
 
+  suiteTeardown(() => {
+    window.CANONICAL_PATH = originalCanonicalPath;
+  });
+
   setup(() => {
-    element = fixture('basic');
-    overlay = fixture('within-overlay');
+    element = basicFixture.instantiate();
+    overlay = withinOverlayFixture.instantiate();
   });
 
   test('changeBaseURL', () => {
@@ -234,4 +221,4 @@
     assert.equal(statusString, 'Merge Conflict, WIP, Private');
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.js
similarity index 64%
rename from polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html
rename to polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.js
index 6fe4460..0e0ff2e 100644
--- a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.js
@@ -1,41 +1,28 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2018 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.
+ */
 
-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.
--->
-
-<title>safe-types-behavior</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <safe-types-element></safe-types-element>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../test/common-test-setup.js';
+import '../../test/common-test-setup-karma.js';
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
 import {SafeTypes} from './safe-types-behavior.js';
+
+const basicFixture = fixtureFromElement('safe-types-element');
+
 suite('gr-tooltip-behavior tests', () => {
   let element;
-  let sandbox;
 
   suiteSetup(() => {
     Polymer({
@@ -45,12 +32,7 @@
   });
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    element = basicFixture.instantiate();
   });
 
   test('SafeUrl accepts valid urls', () => {
@@ -119,4 +101,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.js b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.js
index 2128e29..2d7096f 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.js
@@ -22,17 +22,11 @@
 
 suite('gr-access-section tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     element = fixture.instantiate();
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   suite('unit tests', () => {
     setup(() => {
       element.section = {
@@ -123,7 +117,7 @@
     });
 
     test('_computePermissions', () => {
-      sandbox.stub(element, 'toSortedArray').returns(
+      sinon.stub(element, 'toSortedArray').returns(
           [{
             id: 'push',
             value: {
@@ -463,7 +457,7 @@
 
       test('_handleValueChange', () => {
         // For an existing section.
-        const modifiedHandler = sandbox.stub();
+        const modifiedHandler = sinon.stub();
         element.section = {id: 'refs/for/bar', value: {permissions: {}}};
         assert.notOk(element.section.value.updatedId);
         element.section.id = 'refs/for/baz';
@@ -528,7 +522,7 @@
       });
 
       test('remove an added section', () => {
-        const removeStub = sandbox.stub();
+        const removeStub = sinon.stub();
         element.addEventListener('added-section-removed', removeStub);
         element.editing = true;
         element.section.value.added = true;
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.js
similarity index 70%
rename from polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
rename to polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.js
index c8c7f0c..546b8a8 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.js
@@ -1,41 +1,26 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-admin-group-list</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/page/page.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-admin-group-list></gr-admin-group-list>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-admin-group-list.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+
+const basicFixture = fixtureFromElement('gr-admin-group-list');
+
 let counter = 0;
 const groupGenerator = () => {
   return {
@@ -55,20 +40,15 @@
 suite('gr-admin-group-list tests', () => {
   let element;
   let groups;
-  let sandbox;
+
   let value;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    element = basicFixture.instantiate();
   });
 
   test('_computeGroupUrl', () => {
-    let urlStub = sandbox.stub(GerritNav, 'getUrlForGroup',
+    let urlStub = sinon.stub(GerritNav, 'getUrlForGroup').callsFake(
         () => '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5');
 
     let group = {
@@ -79,7 +59,7 @@
 
     urlStub.restore();
 
-    urlStub = sandbox.stub(GerritNav, 'getUrlForGroup',
+    urlStub = sinon.stub(GerritNav, 'getUrlForGroup').callsFake(
         () => '/admin/groups/user/test');
 
     group = {
@@ -117,7 +97,7 @@
     });
 
     test('_maybeOpenCreateOverlay', () => {
-      const overlayOpen = sandbox.stub(element.$.createOverlay, 'open');
+      const overlayOpen = sinon.stub(element.$.createOverlay, 'open');
       element._maybeOpenCreateOverlay();
       assert.isFalse(overlayOpen.called);
       const params = {};
@@ -149,10 +129,10 @@
 
   suite('filter', () => {
     test('_paramsChanged', done => {
-      sandbox.stub(
+      sinon.stub(
           element.$.restAPI,
-          'getGroups',
-          () => Promise.resolve(groups));
+          'getGroups')
+          .callsFake(() => Promise.resolve(groups));
       const value = {
         filter: 'test',
         offset: 25,
@@ -182,7 +162,7 @@
 
   suite('create new', () => {
     test('_handleCreateClicked called when create-click fired', () => {
-      sandbox.stub(element, '_handleCreateClicked');
+      sinon.stub(element, '_handleCreateClicked');
       element.shadowRoot
           .querySelector('gr-list-view').dispatchEvent(
               new CustomEvent('create-clicked', {
@@ -192,13 +172,13 @@
     });
 
     test('_handleCreateClicked opens modal', () => {
-      const openStub = sandbox.stub(element.$.createOverlay, 'open');
+      const openStub = sinon.stub(element.$.createOverlay, 'open');
       element._handleCreateClicked();
       assert.isTrue(openStub.called);
     });
 
     test('_handleCreateGroup called when confirm fired', () => {
-      sandbox.stub(element, '_handleCreateGroup');
+      sinon.stub(element, '_handleCreateGroup');
       element.$.createDialog.dispatchEvent(
           new CustomEvent('confirm', {
             composed: true, bubbles: true,
@@ -207,7 +187,7 @@
     });
 
     test('_handleCloseCreate called when cancel fired', () => {
-      sandbox.stub(element, '_handleCloseCreate');
+      sinon.stub(element, '_handleCloseCreate');
       element.$.createDialog.dispatchEvent(
           new CustomEvent('cancel', {
             composed: true, bubbles: true,
@@ -216,4 +196,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
similarity index 82%
rename from polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
rename to polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
index ec72bfd..25b14e2 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
@@ -1,64 +1,43 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-admin-view</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-admin-view></gr-admin-view>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-admin-view.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 
+const basicFixture = fixtureFromElement('gr-admin-view');
+
 suite('gr-admin-view tests', () => {
   let element;
-  let sandbox;
 
   setup(done => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     stub('gr-rest-api-interface', {
       getProjectConfig() {
         return Promise.resolve({});
       },
     });
     const pluginsLoaded = Promise.resolve();
-    sandbox.stub(pluginLoader, 'awaitPluginsLoaded').returns(pluginsLoaded);
+    sinon.stub(pluginLoader, 'awaitPluginsLoaded').returns(pluginsLoaded);
     pluginsLoaded.then(() => flush(done));
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('_computeURLHelper', () => {
     const path = '/test';
     const host = 'http://www.testsite.com';
@@ -71,7 +50,7 @@
         element._computeLinkURL({url: '/test', noBaseUrl: true}),
         '//' + window.location.host + '/test');
 
-    sandbox.stub(element, 'getBaseUrl').returns('/foo');
+    sinon.stub(element, 'getBaseUrl').returns('/foo');
     assert.equal(
         element._computeLinkURL({url: '/test', noBaseUrl: true}),
         '//' + window.location.host + '/foo/test');
@@ -103,18 +82,18 @@
   });
 
   test('_filteredLinks admin', done => {
-    sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
+    sinon.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
       name: 'test-user',
     }));
-    sandbox.stub(
+    sinon.stub(
         element.$.restAPI,
-        'getAccountCapabilities',
-        () => Promise.resolve({
+        'getAccountCapabilities')
+        .callsFake(() => Promise.resolve({
           createGroup: true,
           createProject: true,
           viewPlugins: true,
         })
-    );
+        );
     element.reload().then(() => {
       assert.equal(element._filteredLinks.length, 3);
 
@@ -131,14 +110,14 @@
   });
 
   test('_filteredLinks non admin authenticated', done => {
-    sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
+    sinon.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
       name: 'test-user',
     }));
-    sandbox.stub(
+    sinon.stub(
         element.$.restAPI,
-        'getAccountCapabilities',
-        () => Promise.resolve({})
-    );
+        'getAccountCapabilities')
+        .callsFake(() => Promise.resolve({})
+        );
     element.reload().then(() => {
       assert.equal(element._filteredLinks.length, 2);
 
@@ -162,7 +141,7 @@
   });
 
   test('_filteredLinks from plugin', () => {
-    sandbox.stub(element.$.jsAPI, 'getAdminMenuLinks').returns([
+    sinon.stub(element.$.jsAPI, 'getAdminMenuLinks').returns([
       {text: 'internal link text', url: '/internal/link/url'},
       {text: 'external link text', url: 'http://external/link/url'},
     ]);
@@ -191,13 +170,13 @@
 
   test('Repo shows up in nav', done => {
     element._repoName = 'Test Repo';
-    sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
+    sinon.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
       name: 'test-user',
     }));
-    sandbox.stub(
+    sinon.stub(
         element.$.restAPI,
-        'getAccountCapabilities',
-        () => Promise.resolve({
+        'getAccountCapabilities')
+        .callsFake(() => Promise.resolve({
           createGroup: true,
           createProject: true,
           viewPlugins: true,
@@ -222,13 +201,13 @@
     element._groupIsInternal = true;
     element._isAdmin = true;
     element._groupOwner = false;
-    sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
+    sinon.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
       name: 'test-user',
     }));
-    sandbox.stub(
+    sinon.stub(
         element.$.restAPI,
-        'getAccountCapabilities',
-        () => Promise.resolve({
+        'getAccountCapabilities')
+        .callsFake(() => Promise.resolve({
           createGroup: true,
           createProject: true,
           viewPlugins: true,
@@ -251,19 +230,19 @@
   });
 
   test('Nav is reloaded when repo changes', () => {
-    sandbox.stub(
+    sinon.stub(
         element.$.restAPI,
-        'getAccountCapabilities',
-        () => Promise.resolve({
+        'getAccountCapabilities')
+        .callsFake(() => Promise.resolve({
           createGroup: true,
           createProject: true,
           viewPlugins: true,
         }));
-    sandbox.stub(
+    sinon.stub(
         element.$.restAPI,
-        'getAccount',
-        () => Promise.resolve({_id: 1}));
-    sandbox.stub(element, 'reload');
+        'getAccount')
+        .callsFake(() => Promise.resolve({_id: 1}));
+    sinon.stub(element, 'reload');
     element.params = {repo: 'Test Repo', adminView: 'gr-repo'};
     assert.equal(element.reload.callCount, 1);
     element.params = {repo: 'Test Repo 2',
@@ -272,28 +251,28 @@
   });
 
   test('Nav is reloaded when group changes', () => {
-    sandbox.stub(element, '_computeGroupName');
-    sandbox.stub(
+    sinon.stub(element, '_computeGroupName');
+    sinon.stub(
         element.$.restAPI,
-        'getAccountCapabilities',
-        () => Promise.resolve({
+        'getAccountCapabilities')
+        .callsFake(() => Promise.resolve({
           createGroup: true,
           createProject: true,
           viewPlugins: true,
         }));
-    sandbox.stub(
+    sinon.stub(
         element.$.restAPI,
-        'getAccount',
-        () => Promise.resolve({_id: 1}));
-    sandbox.stub(element, 'reload');
+        'getAccount')
+        .callsFake(() => Promise.resolve({_id: 1}));
+    sinon.stub(element, 'reload');
     element.params = {groupId: '1', adminView: 'gr-group'};
     assert.equal(element.reload.callCount, 1);
   });
 
   test('Nav is reloaded when group name changes', done => {
     const newName = 'newName';
-    sandbox.stub(element, '_computeGroupName');
-    sandbox.stub(element, 'reload', () => {
+    sinon.stub(element, '_computeGroupName');
+    sinon.stub(element, 'reload').callsFake(() => {
       assert.equal(element._groupName, newName);
       assert.isTrue(element.reload.called);
       done();
@@ -340,18 +319,18 @@
       view: GerritNav.View.REPO,
       detail: GerritNav.RepoDetailView.ACCESS,
     };
-    sandbox.stub(
+    sinon.stub(
         element.$.restAPI,
-        'getAccountCapabilities',
-        () => Promise.resolve({
+        'getAccountCapabilities')
+        .callsFake(() => Promise.resolve({
           createGroup: true,
           createProject: true,
           viewPlugins: true,
         }));
-    sandbox.stub(
+    sinon.stub(
         element.$.restAPI,
-        'getAccount',
-        () => Promise.resolve({_id: 1}));
+        'getAccount')
+        .callsFake(() => Promise.resolve({_id: 1}));
     flushAsynchronousOperations();
     const expectedFilteredLinks = [
       {
@@ -464,9 +443,9 @@
         parent: 'my-repo',
       },
     ];
-    sandbox.stub(GerritNav, 'navigateToRelativeUrl');
-    sandbox.spy(element, '_selectedIsCurrentPage');
-    sandbox.spy(element, '_handleSubsectionChange');
+    sinon.stub(GerritNav, 'navigateToRelativeUrl');
+    sinon.spy(element, '_selectedIsCurrentPage');
+    sinon.spy(element, '_handleSubsectionChange');
     element.reload().then(() => {
       assert.deepEqual(element._filteredLinks, expectedFilteredLinks);
       assert.deepEqual(element._subsectionLinks, expectedSubsectionLinks);
@@ -503,18 +482,18 @@
 
   suite('_computeSelectedClass', () => {
     setup(() => {
-      sandbox.stub(
+      sinon.stub(
           element.$.restAPI,
-          'getAccountCapabilities',
-          () => Promise.resolve({
+          'getAccountCapabilities')
+          .callsFake(() => Promise.resolve({
             createGroup: true,
             createProject: true,
             viewPlugins: true,
           }));
-      sandbox.stub(
+      sinon.stub(
           element.$.restAPI,
-          'getAccount',
-          () => Promise.resolve({_id: 1}));
+          'getAccount')
+          .callsFake(() => Promise.resolve({_id: 1}));
 
       return element.reload();
     });
@@ -596,12 +575,12 @@
           _loadGroupDetails: () => {},
         });
 
-        sandbox.stub(element.$.restAPI, 'getGroupConfig')
+        sinon.stub(element.$.restAPI, 'getGroupConfig')
             .returns(Promise.resolve({
               name: 'foo',
               id: 'c0f83e941ce90caea30e6ad88f0d4ea0e841a7a9',
             }));
-        sandbox.stub(element.$.restAPI, 'getIsGroupOwner')
+        sinon.stub(element.$.restAPI, 'getIsGroupOwner')
             .returns(Promise.resolve(true));
         return element.reload();
       });
@@ -640,7 +619,7 @@
 
       test('external group', () => {
         element.$.restAPI.getGroupConfig.restore();
-        sandbox.stub(element.$.restAPI, 'getGroupConfig')
+        sinon.stub(element.$.restAPI, 'getGroupConfig')
             .returns(Promise.resolve({
               name: 'foo',
               id: 'external-id',
@@ -681,4 +660,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html
deleted file mode 100644
index 003edfb..0000000
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html
+++ /dev/null
@@ -1,90 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-confirm-delete-item-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-confirm-delete-item-dialog></gr-confirm-delete-item-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-confirm-delete-item-dialog.js';
-suite('gr-confirm-delete-item-dialog tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('_handleConfirmTap', () => {
-    const confirmHandler = sandbox.stub();
-    element.addEventListener('confirm', confirmHandler);
-    sandbox.spy(element, '_handleConfirmTap');
-    element.shadowRoot
-        .querySelector('gr-dialog').dispatchEvent(
-            new CustomEvent('confirm', {
-              composed: true, bubbles: true,
-            }));
-    assert.isTrue(confirmHandler.called);
-    assert.isTrue(confirmHandler.calledOnce);
-    assert.isTrue(element._handleConfirmTap.called);
-    assert.isTrue(element._handleConfirmTap.calledOnce);
-  });
-
-  test('_handleCancelTap', () => {
-    const cancelHandler = sandbox.stub();
-    element.addEventListener('cancel', cancelHandler);
-    sandbox.spy(element, '_handleCancelTap');
-    element.shadowRoot
-        .querySelector('gr-dialog').dispatchEvent(
-            new CustomEvent('cancel', {
-              composed: true, bubbles: true,
-            }));
-    assert.isTrue(cancelHandler.called);
-    assert.isTrue(cancelHandler.calledOnce);
-    assert.isTrue(element._handleCancelTap.called);
-    assert.isTrue(element._handleCancelTap.calledOnce);
-  });
-
-  test('_computeItemName function for branches', () => {
-    assert.deepEqual(element._computeItemName('branches'), 'Branch');
-    assert.notEqual(element._computeItemName('branches'), 'Tag');
-  });
-
-  test('_computeItemName function for tags', () => {
-    assert.deepEqual(element._computeItemName('tags'), 'Tag');
-    assert.notEqual(element._computeItemName('tags'), 'Branch');
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.js b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.js
new file mode 100644
index 0000000..485a48b
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.js
@@ -0,0 +1,70 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-confirm-delete-item-dialog.js';
+
+const basicFixture = fixtureFromElement('gr-confirm-delete-item-dialog');
+
+suite('gr-confirm-delete-item-dialog tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('_handleConfirmTap', () => {
+    const confirmHandler = sinon.stub();
+    element.addEventListener('confirm', confirmHandler);
+    sinon.spy(element, '_handleConfirmTap');
+    element.shadowRoot
+        .querySelector('gr-dialog').dispatchEvent(
+            new CustomEvent('confirm', {
+              composed: true, bubbles: true,
+            }));
+    assert.isTrue(confirmHandler.called);
+    assert.isTrue(confirmHandler.calledOnce);
+    assert.isTrue(element._handleConfirmTap.called);
+    assert.isTrue(element._handleConfirmTap.calledOnce);
+  });
+
+  test('_handleCancelTap', () => {
+    const cancelHandler = sinon.stub();
+    element.addEventListener('cancel', cancelHandler);
+    sinon.spy(element, '_handleCancelTap');
+    element.shadowRoot
+        .querySelector('gr-dialog').dispatchEvent(
+            new CustomEvent('cancel', {
+              composed: true, bubbles: true,
+            }));
+    assert.isTrue(cancelHandler.called);
+    assert.isTrue(cancelHandler.calledOnce);
+    assert.isTrue(element._handleCancelTap.called);
+    assert.isTrue(element._handleCancelTap.calledOnce);
+  });
+
+  test('_computeItemName function for branches', () => {
+    assert.deepEqual(element._computeItemName('branches'), 'Branch');
+    assert.notEqual(element._computeItemName('branches'), 'Tag');
+  });
+
+  test('_computeItemName function for tags', () => {
+    assert.deepEqual(element._computeItemName('tags'), 'Tag');
+    assert.notEqual(element._computeItemName('tags'), 'Branch');
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.js
similarity index 67%
rename from polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
rename to polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.js
index eaee4a4..07eee42 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.js
@@ -1,45 +1,29 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-create-change-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-create-change-dialog></gr-create-change-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-create-change-dialog.js';
+
+const basicFixture = fixtureFromElement('gr-create-change-dialog');
+
 suite('gr-create-change-dialog tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     stub('gr-rest-api-interface', {
       getLoggedIn() { return Promise.resolve(true); },
       getRepoBranches(input) {
@@ -56,7 +40,7 @@
         }
       },
     });
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.repoName = 'test-repo';
     element._repoConfig = {
       private_by_default: {
@@ -66,10 +50,6 @@
     };
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('new change created with default', done => {
     const configInputObj = {
       branch: 'test-branch',
@@ -79,8 +59,8 @@
       work_in_progress: true,
     };
 
-    const saveStub = sandbox.stub(element.$.restAPI,
-        'createChange', () => Promise.resolve({}));
+    const saveStub = sinon.stub(element.$.restAPI,
+        'createChange').callsFake(() => Promise.resolve({}));
 
     element.branch = 'test-branch';
     element.topic = 'test-topic';
@@ -106,7 +86,8 @@
       configured_value: 'TRUE',
       inherited_value: false,
     };
-    sandbox.stub(element, '_formatBooleanString', () => Promise.resolve(true));
+    sinon.stub(element, '_formatBooleanString')
+        .callsFake(() => Promise.resolve(true));
     flushAsynchronousOperations();
 
     const configInputObj = {
@@ -117,8 +98,8 @@
       work_in_progress: true,
     };
 
-    const saveStub = sandbox.stub(element.$.restAPI,
-        'createChange', () => Promise.resolve({}));
+    const saveStub = sinon.stub(element.$.restAPI,
+        'createChange').callsFake(() => Promise.resolve({}));
 
     element.branch = 'test-branch';
     element.topic = 'test-topic';
@@ -164,4 +145,4 @@
     assert.equal(element._computePrivateSectionClass(false), '');
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html
deleted file mode 100644
index 164db53..0000000
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html
+++ /dev/null
@@ -1,100 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-create-group-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/page/page.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-create-group-dialog></gr-create-group-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-create-group-dialog.js';
-import page from 'page/page.mjs';
-
-suite('gr-create-group-dialog tests', () => {
-  let element;
-  let sandbox;
-  const GROUP_NAME = 'test-group';
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    stub('gr-rest-api-interface', {
-      getLoggedIn() { return Promise.resolve(true); },
-    });
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('name is updated correctly', done => {
-    assert.isFalse(element.hasNewGroupName);
-
-    const inputEl = element.root.querySelector('iron-input');
-    inputEl.bindValue = GROUP_NAME;
-
-    setTimeout(() => {
-      assert.isTrue(element.hasNewGroupName);
-      assert.deepEqual(element._name, GROUP_NAME);
-      done();
-    });
-  });
-
-  test('test for redirecting to group on successful creation', done => {
-    sandbox.stub(element.$.restAPI, 'createGroup')
-        .returns(Promise.resolve({status: 201}));
-
-    sandbox.stub(element.$.restAPI, 'getGroupConfig')
-        .returns(Promise.resolve({group_id: 551}));
-
-    const showStub = sandbox.stub(page, 'show');
-    element.handleCreateGroup()
-        .then(() => {
-          assert.isTrue(showStub.calledWith('/admin/groups/551'));
-          done();
-        });
-  });
-
-  test('test for unsuccessful group creation', done => {
-    sandbox.stub(element.$.restAPI, 'createGroup')
-        .returns(Promise.resolve({status: 409}));
-
-    sandbox.stub(element.$.restAPI, 'getGroupConfig')
-        .returns(Promise.resolve({group_id: 551}));
-
-    const showStub = sandbox.stub(page, 'show');
-    element.handleCreateGroup()
-        .then(() => {
-          assert.isFalse(showStub.called);
-          done();
-        });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.js b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.js
new file mode 100644
index 0000000..d9bc500
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.js
@@ -0,0 +1,79 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-create-group-dialog.js';
+import page from 'page/page.mjs';
+
+const basicFixture = fixtureFromElement('gr-create-group-dialog');
+
+suite('gr-create-group-dialog tests', () => {
+  let element;
+
+  const GROUP_NAME = 'test-group';
+
+  setup(() => {
+    stub('gr-rest-api-interface', {
+      getLoggedIn() { return Promise.resolve(true); },
+    });
+    element = basicFixture.instantiate();
+  });
+
+  test('name is updated correctly', done => {
+    assert.isFalse(element.hasNewGroupName);
+
+    const inputEl = element.root.querySelector('iron-input');
+    inputEl.bindValue = GROUP_NAME;
+
+    setTimeout(() => {
+      assert.isTrue(element.hasNewGroupName);
+      assert.deepEqual(element._name, GROUP_NAME);
+      done();
+    });
+  });
+
+  test('test for redirecting to group on successful creation', done => {
+    sinon.stub(element.$.restAPI, 'createGroup')
+        .returns(Promise.resolve({status: 201}));
+
+    sinon.stub(element.$.restAPI, 'getGroupConfig')
+        .returns(Promise.resolve({group_id: 551}));
+
+    const showStub = sinon.stub(page, 'show');
+    element.handleCreateGroup()
+        .then(() => {
+          assert.isTrue(showStub.calledWith('/admin/groups/551'));
+          done();
+        });
+  });
+
+  test('test for unsuccessful group creation', done => {
+    sinon.stub(element.$.restAPI, 'createGroup')
+        .returns(Promise.resolve({status: 409}));
+
+    sinon.stub(element.$.restAPI, 'getGroupConfig')
+        .returns(Promise.resolve({group_id: 551}));
+
+    const showStub = sinon.stub(page, 'show');
+    element.handleCreateGroup()
+        .then(() => {
+          assert.isFalse(showStub.called);
+          done();
+        });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html
deleted file mode 100644
index 2778d40..0000000
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html
+++ /dev/null
@@ -1,135 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-create-pointer-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-create-pointer-dialog></gr-create-pointer-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-create-pointer-dialog.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-suite('gr-create-pointer-dialog tests', () => {
-  let element;
-  let sandbox;
-
-  const ironInput = function(element) {
-    return dom(element).querySelector('iron-input');
-  };
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    stub('gr-rest-api-interface', {
-      getLoggedIn() { return Promise.resolve(true); },
-    });
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('branch created', done => {
-    sandbox.stub(
-        element.$.restAPI,
-        'createRepoBranch',
-        () => Promise.resolve({}));
-
-    assert.isFalse(element.hasNewItemName);
-
-    element._itemName = 'test-branch';
-    element.itemDetail = 'branches';
-
-    ironInput(element.$.itemNameSection).bindValue = 'test-branch2';
-    ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
-
-    setTimeout(() => {
-      assert.isTrue(element.hasNewItemName);
-      assert.equal(element._itemName, 'test-branch2');
-      assert.equal(element._itemRevision, 'HEAD');
-      done();
-    });
-  });
-
-  test('tag created', done => {
-    sandbox.stub(
-        element.$.restAPI,
-        'createRepoTag',
-        () => Promise.resolve({}));
-
-    assert.isFalse(element.hasNewItemName);
-
-    element._itemName = 'test-tag';
-    element.itemDetail = 'tags';
-
-    ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
-    ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
-
-    setTimeout(() => {
-      assert.isTrue(element.hasNewItemName);
-      assert.equal(element._itemName, 'test-tag2');
-      assert.equal(element._itemRevision, 'HEAD');
-      done();
-    });
-  });
-
-  test('tag created with annotations', done => {
-    sandbox.stub(
-        element.$.restAPI,
-        'createRepoTag',
-        () => Promise.resolve({}));
-
-    assert.isFalse(element.hasNewItemName);
-
-    element._itemName = 'test-tag';
-    element._itemAnnotation = 'test-message';
-    element.itemDetail = 'tags';
-
-    ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
-    ironInput(element.$.itemAnnotationSection).bindValue = 'test-message2';
-    ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
-
-    setTimeout(() => {
-      assert.isTrue(element.hasNewItemName);
-      assert.equal(element._itemName, 'test-tag2');
-      assert.equal(element._itemAnnotation, 'test-message2');
-      assert.equal(element._itemRevision, 'HEAD');
-      done();
-    });
-  });
-
-  test('_computeHideItemClass returns hideItem if type is branches', () => {
-    assert.equal(element._computeHideItemClass('branches'), 'hideItem');
-  });
-
-  test('_computeHideItemClass returns strings if not branches', () => {
-    assert.equal(element._computeHideItemClass('tags'), '');
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.js b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.js
new file mode 100644
index 0000000..22f19a6
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.js
@@ -0,0 +1,115 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-create-pointer-dialog.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromElement('gr-create-pointer-dialog');
+
+suite('gr-create-pointer-dialog tests', () => {
+  let element;
+
+  const ironInput = function(element) {
+    return dom(element).querySelector('iron-input');
+  };
+
+  setup(() => {
+    stub('gr-rest-api-interface', {
+      getLoggedIn() { return Promise.resolve(true); },
+    });
+    element = basicFixture.instantiate();
+  });
+
+  test('branch created', done => {
+    sinon.stub(
+        element.$.restAPI,
+        'createRepoBranch')
+        .callsFake(() => Promise.resolve({}));
+
+    assert.isFalse(element.hasNewItemName);
+
+    element._itemName = 'test-branch';
+    element.itemDetail = 'branches';
+
+    ironInput(element.$.itemNameSection).bindValue = 'test-branch2';
+    ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
+
+    setTimeout(() => {
+      assert.isTrue(element.hasNewItemName);
+      assert.equal(element._itemName, 'test-branch2');
+      assert.equal(element._itemRevision, 'HEAD');
+      done();
+    });
+  });
+
+  test('tag created', done => {
+    sinon.stub(
+        element.$.restAPI,
+        'createRepoTag')
+        .callsFake(() => Promise.resolve({}));
+
+    assert.isFalse(element.hasNewItemName);
+
+    element._itemName = 'test-tag';
+    element.itemDetail = 'tags';
+
+    ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
+    ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
+
+    setTimeout(() => {
+      assert.isTrue(element.hasNewItemName);
+      assert.equal(element._itemName, 'test-tag2');
+      assert.equal(element._itemRevision, 'HEAD');
+      done();
+    });
+  });
+
+  test('tag created with annotations', done => {
+    sinon.stub(
+        element.$.restAPI,
+        'createRepoTag')
+        .callsFake(() => Promise.resolve({}));
+
+    assert.isFalse(element.hasNewItemName);
+
+    element._itemName = 'test-tag';
+    element._itemAnnotation = 'test-message';
+    element.itemDetail = 'tags';
+
+    ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
+    ironInput(element.$.itemAnnotationSection).bindValue = 'test-message2';
+    ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
+
+    setTimeout(() => {
+      assert.isTrue(element.hasNewItemName);
+      assert.equal(element._itemName, 'test-tag2');
+      assert.equal(element._itemAnnotation, 'test-message2');
+      assert.equal(element._itemRevision, 'HEAD');
+      done();
+    });
+  });
+
+  test('_computeHideItemClass returns hideItem if type is branches', () => {
+    assert.equal(element._computeHideItemClass('branches'), 'hideItem');
+  });
+
+  test('_computeHideItemClass returns strings if not branches', () => {
+    assert.equal(element._computeHideItemClass('tags'), '');
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html
deleted file mode 100644
index dfab4ac..0000000
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html
+++ /dev/null
@@ -1,105 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-create-repo-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-create-repo-dialog></gr-create-repo-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-create-repo-dialog.js';
-suite('gr-create-repo-dialog tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    stub('gr-rest-api-interface', {
-      getLoggedIn() { return Promise.resolve(true); },
-    });
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('default values are populated', () => {
-    assert.isTrue(element.$.initialCommit.bindValue);
-    assert.isFalse(element.$.parentRepo.bindValue);
-  });
-
-  test('repo created', done => {
-    const configInputObj = {
-      name: 'test-repo',
-      create_empty_commit: true,
-      parent: 'All-Project',
-      permissions_only: false,
-      owners: ['testId'],
-    };
-
-    const saveStub = sandbox.stub(element.$.restAPI,
-        'createRepo', () => Promise.resolve({}));
-
-    assert.isFalse(element.hasNewRepoName);
-
-    element._repoConfig = {
-      name: 'test-repo',
-      create_empty_commit: true,
-      parent: 'All-Project',
-      permissions_only: false,
-    };
-
-    element._repoOwner = 'test';
-    element._repoOwnerId = 'testId';
-
-    element.$.repoNameInput.bindValue = configInputObj.name;
-    element.$.rightsInheritFromInput.bindValue = configInputObj.parent;
-    element.$.ownerInput.text = configInputObj.owners[0];
-    element.$.initialCommit.bindValue =
-        configInputObj.create_empty_commit;
-    element.$.parentRepo.bindValue =
-        configInputObj.permissions_only;
-
-    assert.isTrue(element.hasNewRepoName);
-
-    assert.deepEqual(element._repoConfig, configInputObj);
-
-    element.handleCreateRepo().then(() => {
-      assert.isTrue(saveStub.lastCall.calledWithExactly(configInputObj));
-      done();
-    });
-  });
-
-  test('testing observer of _repoOwner', () => {
-    element._repoOwnerId = 'test-5';
-    assert.deepEqual(element._repoConfig.owners, ['test-5']);
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.js b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.js
new file mode 100644
index 0000000..1e1fb0e
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.js
@@ -0,0 +1,85 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-create-repo-dialog.js';
+
+const basicFixture = fixtureFromElement('gr-create-repo-dialog');
+
+suite('gr-create-repo-dialog tests', () => {
+  let element;
+
+  setup(() => {
+    stub('gr-rest-api-interface', {
+      getLoggedIn() { return Promise.resolve(true); },
+    });
+    element = basicFixture.instantiate();
+  });
+
+  test('default values are populated', () => {
+    assert.isTrue(element.$.initialCommit.bindValue);
+    assert.isFalse(element.$.parentRepo.bindValue);
+  });
+
+  test('repo created', done => {
+    const configInputObj = {
+      name: 'test-repo',
+      create_empty_commit: true,
+      parent: 'All-Project',
+      permissions_only: false,
+      owners: ['testId'],
+    };
+
+    const saveStub = sinon.stub(element.$.restAPI,
+        'createRepo').callsFake(() => Promise.resolve({}));
+
+    assert.isFalse(element.hasNewRepoName);
+
+    element._repoConfig = {
+      name: 'test-repo',
+      create_empty_commit: true,
+      parent: 'All-Project',
+      permissions_only: false,
+    };
+
+    element._repoOwner = 'test';
+    element._repoOwnerId = 'testId';
+
+    element.$.repoNameInput.bindValue = configInputObj.name;
+    element.$.rightsInheritFromInput.bindValue = configInputObj.parent;
+    element.$.ownerInput.text = configInputObj.owners[0];
+    element.$.initialCommit.bindValue =
+        configInputObj.create_empty_commit;
+    element.$.parentRepo.bindValue =
+        configInputObj.permissions_only;
+
+    assert.isTrue(element.hasNewRepoName);
+
+    assert.deepEqual(element._repoConfig, configInputObj);
+
+    element.handleCreateRepo().then(() => {
+      assert.isTrue(saveStub.lastCall.calledWithExactly(configInputObj));
+      done();
+    });
+  });
+
+  test('testing observer of _repoOwner', () => {
+    element._repoOwnerId = 'test-5';
+    assert.deepEqual(element._repoConfig.owners, ['test-5']);
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html
deleted file mode 100644
index 4590220..0000000
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html
+++ /dev/null
@@ -1,116 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-group-audit-log</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-group-audit-log></gr-group-audit-log>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-group-audit-log.js';
-suite('gr-group-audit-log tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  suite('members', () => {
-    test('test _getNameForGroup', () => {
-      let group = {
-        member: {
-          name: 'test-name',
-        },
-      };
-      assert.equal(element._getNameForGroup(group.member), 'test-name');
-
-      group = {
-        member: {
-          id: 'test-id',
-        },
-      };
-      assert.equal(element._getNameForGroup(group.member), 'test-id');
-    });
-
-    test('test _isGroupEvent', () => {
-      assert.isTrue(element._isGroupEvent('ADD_GROUP'));
-      assert.isTrue(element._isGroupEvent('REMOVE_GROUP'));
-
-      assert.isFalse(element._isGroupEvent('ADD_USER'));
-      assert.isFalse(element._isGroupEvent('REMOVE_USER'));
-    });
-  });
-
-  suite('users', () => {
-    test('test _getIdForUser', () => {
-      const account = {
-        user: {
-          username: 'test-user',
-          _account_id: 12,
-        },
-      };
-      assert.equal(element._getIdForUser(account.user), ' (12)');
-    });
-
-    test('test _account_id not present', () => {
-      const account = {
-        user: {
-          username: 'test-user',
-        },
-      };
-      assert.equal(element._getIdForUser(account.user), '');
-    });
-  });
-
-  suite('404', () => {
-    test('fires page-error', done => {
-      element.groupId = 1;
-
-      const response = {status: 404};
-      sandbox.stub(
-          element.$.restAPI, 'getGroupAuditLog', (group, errFn) => {
-            errFn(response);
-          });
-
-      element.addEventListener('page-error', e => {
-        assert.deepEqual(e.detail.response, response);
-        done();
-      });
-
-      element._getAuditLogs();
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.js b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.js
new file mode 100644
index 0000000..1bbfcae
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.js
@@ -0,0 +1,97 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-group-audit-log.js';
+
+const basicFixture = fixtureFromElement('gr-group-audit-log');
+
+suite('gr-group-audit-log tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  suite('members', () => {
+    test('test _getNameForGroup', () => {
+      let group = {
+        member: {
+          name: 'test-name',
+        },
+      };
+      assert.equal(element._getNameForGroup(group.member), 'test-name');
+
+      group = {
+        member: {
+          id: 'test-id',
+        },
+      };
+      assert.equal(element._getNameForGroup(group.member), 'test-id');
+    });
+
+    test('test _isGroupEvent', () => {
+      assert.isTrue(element._isGroupEvent('ADD_GROUP'));
+      assert.isTrue(element._isGroupEvent('REMOVE_GROUP'));
+
+      assert.isFalse(element._isGroupEvent('ADD_USER'));
+      assert.isFalse(element._isGroupEvent('REMOVE_USER'));
+    });
+  });
+
+  suite('users', () => {
+    test('test _getIdForUser', () => {
+      const account = {
+        user: {
+          username: 'test-user',
+          _account_id: 12,
+        },
+      };
+      assert.equal(element._getIdForUser(account.user), ' (12)');
+    });
+
+    test('test _account_id not present', () => {
+      const account = {
+        user: {
+          username: 'test-user',
+        },
+      };
+      assert.equal(element._getIdForUser(account.user), '');
+    });
+  });
+
+  suite('404', () => {
+    test('fires page-error', done => {
+      element.groupId = 1;
+
+      const response = {status: 404};
+      sinon.stub(
+          element.$.restAPI, 'getGroupAuditLog')
+          .callsFake((group, errFn) => {
+            errFn(response);
+          });
+
+      element.addEventListener('page-error', e => {
+        assert.deepEqual(e.detail.response, response);
+        done();
+      });
+
+      element._getAuditLogs();
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js
similarity index 82%
rename from polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
rename to polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js
index 3a5bfd8..f047cfe 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js
@@ -1,51 +1,35 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-group-members</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-group-members></gr-group-members>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-group-members.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromElement('gr-group-members');
+
 suite('gr-group-members tests', () => {
   let element;
-  let sandbox;
+
   let groups;
   let groupMembers;
   let includedGroups;
   let groupStub;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-
     groups = {
       name: 'Administrators',
       owner: 'Administrators',
@@ -150,20 +134,16 @@
         return Promise.resolve();
       },
     });
-    element = fixture('basic');
-    sandbox.stub(element, 'getBaseUrl').returns('https://test/site');
+    element = basicFixture.instantiate();
+    sinon.stub(element, 'getBaseUrl').returns('https://test/site');
     element.groupId = 1;
-    groupStub = sandbox.stub(
+    groupStub = sinon.stub(
         element.$.restAPI,
-        'getGroupConfig',
-        () => Promise.resolve(groups));
+        'getGroupConfig')
+        .callsFake(() => Promise.resolve(groups));
     return element._loadGroupDetails();
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('_includedGroups', () => {
     assert.equal(element._includedGroups.length, 3);
     assert.equal(dom(element.root)
@@ -181,8 +161,8 @@
 
     const memberName = 'test-admin';
 
-    const saveStub = sandbox.stub(element.$.restAPI, 'saveGroupMembers',
-        () => Promise.resolve({}));
+    const saveStub = sinon.stub(element.$.restAPI, 'saveGroupMembers')
+        .callsFake(() => Promise.resolve({}));
 
     const button = element.$.saveGroupMember;
 
@@ -206,8 +186,9 @@
 
     const includedGroupName = 'testName';
 
-    const saveIncludedGroupStub = sandbox.stub(
-        element.$.restAPI, 'saveIncludedGroup', () => Promise.resolve({}));
+    const saveIncludedGroupStub = sinon.stub(
+        element.$.restAPI, 'saveIncludedGroup')
+        .callsFake(() => Promise.resolve({}));
 
     const button = element.$.saveIncludedGroups;
 
@@ -230,11 +211,11 @@
     element._groupOwner = true;
 
     const memberName = 'bad-name';
-    const alertStub = sandbox.stub();
+    const alertStub = sinon.stub();
     element.addEventListener('show-alert', alertStub);
     const error = new Error('error');
     error.status = 404;
-    sandbox.stub(element.$.restAPI, 'saveGroupMembers',
+    sinon.stub(element.$.restAPI, 'saveGroupMembers').callsFake(
         () => Promise.reject(error));
 
     element.$.groupMemberSearchInput.text = memberName;
@@ -359,8 +340,9 @@
     element.groupId = 1;
 
     const response = {status: 404};
-    sandbox.stub(
-        element.$.restAPI, 'getGroupConfig', (group, errFn) => {
+    sinon.stub(
+        element.$.restAPI, 'getGroupConfig')
+        .callsFake((group, errFn) => {
           errFn(response);
         });
     element.addEventListener('page-error', e => {
@@ -371,4 +353,4 @@
     element._loadGroupDetails();
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
similarity index 71%
rename from polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
rename to polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
index 5621fff..c6054b2 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
@@ -1,42 +1,28 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-group</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-group></gr-group>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-group.js';
+
+const basicFixture = fixtureFromElement('gr-group');
+
 suite('gr-group tests', () => {
   let element;
-  let sandbox;
+
   let groupStub;
   const group = {
     id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
@@ -50,20 +36,14 @@
   };
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     stub('gr-rest-api-interface', {
       getLoggedIn() { return Promise.resolve(true); },
     });
-    element = fixture('basic');
-    groupStub = sandbox.stub(
+    element = basicFixture.instantiate();
+    groupStub = sinon.stub(
         element.$.restAPI,
-        'getGroupConfig',
-        () => Promise.resolve(group)
-    );
-  });
-
-  teardown(() => {
-    sandbox.restore();
+        'getGroupConfig')
+        .callsFake(() => Promise.resolve(group));
   });
 
   test('loading displays before group config is loaded', () => {
@@ -75,10 +55,10 @@
   });
 
   test('default values are populated with internal group', done => {
-    sandbox.stub(
+    sinon.stub(
         element.$.restAPI,
-        'getIsGroupOwner',
-        () => Promise.resolve(true));
+        'getIsGroupOwner')
+        .callsFake(() => Promise.resolve(true));
     element.groupId = 1;
     element._loadGroup().then(() => {
       assert.isTrue(element._groupIsInternal);
@@ -91,14 +71,14 @@
     const groupExternal = Object.assign({}, group);
     groupExternal.id = 'external-group-id';
     groupStub.restore();
-    groupStub = sandbox.stub(
+    groupStub = sinon.stub(
         element.$.restAPI,
-        'getGroupConfig',
-        () => Promise.resolve(groupExternal));
-    sandbox.stub(
+        'getGroupConfig')
+        .callsFake(() => Promise.resolve(groupExternal));
+    sinon.stub(
         element.$.restAPI,
-        'getIsGroupOwner',
-        () => Promise.resolve(true));
+        'getIsGroupOwner')
+        .callsFake(() => Promise.resolve(true));
     element.groupId = 1;
     element._loadGroup().then(() => {
       assert.isFalse(element._groupIsInternal);
@@ -116,15 +96,15 @@
     };
     element._groupName = groupName;
 
-    sandbox.stub(
+    sinon.stub(
         element.$.restAPI,
-        'getIsGroupOwner',
-        () => Promise.resolve(true));
+        'getIsGroupOwner')
+        .callsFake(() => Promise.resolve(true));
 
-    sandbox.stub(
+    sinon.stub(
         element.$.restAPI,
-        'saveGroupName',
-        () => Promise.resolve({status: 200}));
+        'saveGroupName')
+        .callsFake(() => Promise.resolve({status: 200}));
 
     const button = element.$.inputUpdateNameBtn;
 
@@ -155,10 +135,10 @@
     element._groupConfigOwner = 'testId';
     element._groupOwner = true;
 
-    sandbox.stub(
+    sinon.stub(
         element.$.restAPI,
-        'getIsGroupOwner',
-        () => Promise.resolve({status: 200}));
+        'getIsGroupOwner')
+        .callsFake(() => Promise.resolve({status: 200}));
 
     const button = element.$.inputUpdateOwnerBtn;
 
@@ -182,10 +162,10 @@
   test('test for undefined group name', done => {
     groupStub.restore();
 
-    sandbox.stub(
+    sinon.stub(
         element.$.restAPI,
-        'getGroupConfig',
-        () => Promise.resolve({}));
+        'getGroupConfig')
+        .callsFake(() => Promise.resolve({}));
 
     assert.isUndefined(element.groupId);
 
@@ -209,10 +189,10 @@
       name: 'test-group',
     };
 
-    sandbox.stub(element.$.restAPI, 'saveGroupName')
+    sinon.stub(element.$.restAPI, 'saveGroupName')
         .returns(Promise.resolve({status: 200}));
 
-    const showStub = sandbox.stub(element, 'dispatchEvent');
+    const showStub = sinon.stub(element, 'dispatchEvent');
     element._handleSaveName()
         .then(() => {
           assert.isTrue(showStub.called);
@@ -259,10 +239,10 @@
     element.groupId = 1;
 
     const response = {status: 404};
-    sandbox.stub(
-        element.$.restAPI, 'getGroupConfig', (group, errFn) => {
-          errFn(response);
-        });
+    sinon.stub(
+        element.$.restAPI, 'getGroupConfig').callsFake((group, errFn) => {
+      errFn(response);
+    });
 
     element.addEventListener('page-error', e => {
       assert.deepEqual(e.detail.response, response);
@@ -286,4 +266,4 @@
     assert.equal('user/group', element.$.uuid.text);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.js
similarity index 87%
rename from polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
rename to polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.js
index 1ce492e..835c90a 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.js
@@ -1,48 +1,31 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-permission</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/page/page.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-permission></gr-permission>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-permission.js';
+
+const basicFixture = fixtureFromElement('gr-permission');
+
 suite('gr-permission tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-    sandbox.stub(element.$.restAPI, 'getSuggestedGroups').returns(
+    element = basicFixture.instantiate();
+    sinon.stub(element.$.restAPI, 'getSuggestedGroups').returns(
         Promise.resolve({
           'Administrators': {
             id: '4c97682e6ce61b7247f3381b6f1789356666de7f',
@@ -53,10 +36,6 @@
         }));
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   suite('unit tests', () => {
     test('_sortPermission', () => {
       const permission = {
@@ -268,7 +247,7 @@
 
   suite('interactions', () => {
     setup(() => {
-      sandbox.spy(element, '_computeLabel');
+      sinon.spy(element, '_computeLabel');
       element.name = 'Priority';
       element.section = 'refs/*';
       element.labels = {
@@ -352,7 +331,7 @@
     });
 
     test('removing an added permission', () => {
-      const removeStub = sandbox.stub();
+      const removeStub = sinon.stub();
       element.addEventListener('added-permission-removed', removeStub);
       element.editing = true;
       element.name = 'Priority';
@@ -367,7 +346,7 @@
       element.name = 'Priority';
       element.section = 'refs/*';
 
-      const removeStub = sandbox.stub();
+      const removeStub = sinon.stub();
       element.addEventListener('added-permission-removed', removeStub);
 
       assert.isFalse(element.$.permission.classList.contains('deleted'));
@@ -399,7 +378,7 @@
     });
 
     test('_handleValueChange', () => {
-      const modifiedHandler = sandbox.stub();
+      const modifiedHandler = sinon.stub();
       element.permission = {value: {rules: {}}};
       element.addEventListener('access-modified', modifiedHandler);
       assert.isNotOk(element.permission.value.modified);
@@ -431,4 +410,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.js
similarity index 65%
rename from polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html
rename to polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.js
index 5eff42d..9e9eb1c 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.js
@@ -1,50 +1,35 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2018 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-plugin-config-array-editor</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-plugin-config-array-editor></gr-plugin-config-array-editor>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-plugin-config-array-editor.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromElement('gr-plugin-config-array-editor');
+
 suite('gr-plugin-config-array-editor tests', () => {
   let element;
-  let sandbox;
+
   let dispatchStub;
 
   const getAll = str => dom(element.root).querySelectorAll(str);
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.pluginOption = {
       _key: 'test-key',
       info: {
@@ -53,8 +38,6 @@
     };
   });
 
-  teardown(() => sandbox.restore());
-
   test('_computeShowInputRow', () => {
     assert.equal(element._computeShowInputRow(true), 'hide');
     assert.equal(element._computeShowInputRow(false), '');
@@ -72,7 +55,7 @@
 
   suite('adding', () => {
     setup(() => {
-      dispatchStub = sandbox.stub(element, '_dispatchChanged');
+      dispatchStub = sinon.stub(element, '_dispatchChanged');
     });
 
     test('with enter', () => {
@@ -107,7 +90,7 @@
   });
 
   test('deleting', () => {
-    dispatchStub = sandbox.stub(element, '_dispatchChanged');
+    dispatchStub = sinon.stub(element, '_dispatchChanged');
     element.pluginOption = {info: {values: ['test', 'test2']}};
     flushAsynchronousOperations();
 
@@ -131,7 +114,7 @@
   });
 
   test('_dispatchChanged', () => {
-    const eventStub = sandbox.stub(element, 'dispatchEvent');
+    const eventStub = sinon.stub(element, 'dispatchEvent');
     element._dispatchChanged(['new-test-value']);
 
     assert.isTrue(eventStub.called);
@@ -141,4 +124,4 @@
     assert.equal(detail.notifyPath, 'test-key.values');
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js
similarity index 70%
rename from polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html
rename to polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js
index e2c88a2..a73c7cf 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js
@@ -1,41 +1,26 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-plugin-list</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/page/page.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-plugin-list></gr-plugin-list>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-plugin-list.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromElement('gr-plugin-list');
+
 let counter;
 const pluginGenerator = () => {
   const plugin = {
@@ -53,19 +38,14 @@
 suite('gr-plugin-list tests', () => {
   let element;
   let plugins;
-  let sandbox;
+
   let value;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     counter = 0;
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   suite('list with plugins', () => {
     setup(done => {
       plugins = _.times(26, pluginGenerator);
@@ -125,10 +105,10 @@
 
   suite('filter', () => {
     test('_paramsChanged', done => {
-      sandbox.stub(
+      sinon.stub(
           element.$.restAPI,
-          'getPlugins',
-          () => Promise.resolve(plugins));
+          'getPlugins')
+          .callsFake(() => Promise.resolve(plugins));
       const value = {
         filter: 'test',
         offset: 25,
@@ -163,7 +143,7 @@
   suite('404', () => {
     test('fires page-error', done => {
       const response = {status: 404};
-      sandbox.stub(element.$.restAPI, 'getPlugins',
+      sinon.stub(element.$.restAPI, 'getPlugins').callsFake(
           (filter, pluginsPerPage, opt_offset, errFn) => {
             errFn(response);
           });
@@ -181,4 +161,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js
similarity index 92%
rename from polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
rename to polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js
index 7d66cb0..0248dfc 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js
@@ -1,46 +1,30 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-repo-access</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/page/page.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-repo-access></gr-repo-access>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-repo-access.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
+const basicFixture = fixtureFromElement('gr-repo-access');
+
 suite('gr-repo-access tests', () => {
   let element;
-  let sandbox;
+
   let repoStub;
 
   const accessRes = {
@@ -115,37 +99,32 @@
     },
   };
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     stub('gr-rest-api-interface', {
       getAccount() { return Promise.resolve(null); },
     });
-    repoStub = sandbox.stub(element.$.restAPI, 'getRepo').returns(
+    repoStub = sinon.stub(element.$.restAPI, 'getRepo').returns(
         Promise.resolve(repoRes));
     element._loading = false;
     element._ownerOf = [];
     element._canUpload = false;
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('_repoChanged called when repo name changes', () => {
-    sandbox.stub(element, '_repoChanged');
+    sinon.stub(element, '_repoChanged');
     element.repo = 'New Repo';
     assert.isTrue(element._repoChanged.called);
   });
 
   test('_repoChanged', done => {
-    const accessStub = sandbox.stub(element.$.restAPI,
+    const accessStub = sinon.stub(element.$.restAPI,
         'getRepoAccessRights');
 
     accessStub.withArgs('New Repo').returns(
         Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
     accessStub.withArgs('Another New Repo')
         .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2))));
-    const capabilitiesStub = sandbox.stub(element.$.restAPI,
+    const capabilitiesStub = sinon.stub(element.$.restAPI,
         'getCapabilities');
     capabilitiesStub.returns(Promise.resolve(capabilitiesRes));
 
@@ -180,9 +159,9 @@
         name: 'Access Database',
       },
     };
-    const accessStub = sandbox.stub(element.$.restAPI, 'getRepoAccessRights')
+    const accessStub = sinon.stub(element.$.restAPI, 'getRepoAccessRights')
         .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2))));
-    const capabilitiesStub = sandbox.stub(element.$.restAPI,
+    const capabilitiesStub = sinon.stub(element.$.restAPI,
         'getCapabilities').returns(Promise.resolve(capabilitiesRes));
 
     element._repoChanged().then(() => {
@@ -215,7 +194,7 @@
   test('inherit section', () => {
     element._local = {};
     element._ownerOf = [];
-    sandbox.stub(element, '_computeParentHref');
+    sinon.stub(element, '_computeParentHref');
     // Nothing should appear when no inherit from and not in edit mode.
     assert.equal(getComputedStyle(element.$.inheritsFrom).display, 'none');
     // The autocomplete should be hidden, and the link should be  displayed.
@@ -260,8 +239,9 @@
   test('fires page-error', done => {
     const response = {status: 404};
 
-    sandbox.stub(
-        element.$.restAPI, 'getRepoAccessRights', (repoName, errFn) => {
+    sinon.stub(
+        element.$.restAPI, 'getRepoAccessRights')
+        .callsFake((repoName, errFn) => {
           errFn(response);
         });
 
@@ -368,7 +348,7 @@
     });
 
     test('_handleAccessModified called with event fired', () => {
-      sandbox.spy(element, '_handleAccessModified');
+      sinon.spy(element, '_handleAccessModified');
       element.dispatchEvent(
           new CustomEvent('access-modified', {
             composed: true, bubbles: true,
@@ -386,7 +366,7 @@
             detail: {},
             composed: true, bubbles: true,
           }));
-      sandbox.spy(element, '_handleAccessModified');
+      sinon.spy(element, '_handleAccessModified');
       element.dispatchEvent(
           new CustomEvent('access-modified', {
             detail: {},
@@ -397,8 +377,8 @@
 
     test('_handleSaveForReview', () => {
       const saveStub =
-          sandbox.stub(element.$.restAPI, 'setRepoAccessRightsForReview');
-      sandbox.stub(element, '_computeAddAndRemove').returns({
+          sinon.stub(element.$.restAPI, 'setRepoAccessRightsForReview');
+      sinon.stub(element, '_computeAddAndRemove').returns({
         add: {},
         remove: {},
       });
@@ -1180,16 +1160,16 @@
           },
         },
       };
-      sandbox.stub(element.$.restAPI, 'getRepoAccessRights').returns(
+      sinon.stub(element.$.restAPI, 'getRepoAccessRights').returns(
           Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
-      sandbox.stub(GerritNav, 'navigateToChange');
+      sinon.stub(GerritNav, 'navigateToChange');
       let resolver;
-      const saveStub = sandbox.stub(element.$.restAPI,
+      const saveStub = sinon.stub(element.$.restAPI,
           'setRepoAccessRights')
           .returns(new Promise(r => resolver = r));
 
       element.repo = 'test-repo';
-      sandbox.stub(element, '_computeAddAndRemove').returns(repoAccessInput);
+      sinon.stub(element, '_computeAddAndRemove').returns(repoAccessInput);
 
       element._modified = true;
       MockInteractions.tap(element.$.saveBtn);
@@ -1227,16 +1207,16 @@
           },
         },
       };
-      sandbox.stub(element.$.restAPI, 'getRepoAccessRights').returns(
+      sinon.stub(element.$.restAPI, 'getRepoAccessRights').returns(
           Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
-      sandbox.stub(GerritNav, 'navigateToChange');
+      sinon.stub(GerritNav, 'navigateToChange');
       let resolver;
-      const saveForReviewStub = sandbox.stub(element.$.restAPI,
+      const saveForReviewStub = sinon.stub(element.$.restAPI,
           'setRepoAccessRightsForReview')
           .returns(new Promise(r => resolver = r));
 
       element.repo = 'test-repo';
-      sandbox.stub(element, '_computeAddAndRemove').returns(repoAccessInput);
+      sinon.stub(element, '_computeAddAndRemove').returns(repoAccessInput);
 
       element._modified = true;
       MockInteractions.tap(element.$.saveReviewBtn);
@@ -1251,4 +1231,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
deleted file mode 100644
index a52ab92..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
+++ /dev/null
@@ -1,153 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-repo-commands</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/page/page.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-repo-commands></gr-repo-commands>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-repo-commands.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-
-suite('gr-repo-commands tests', () => {
-  let element;
-  let sandbox;
-  let repoStub;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-    repoStub = sandbox.stub(
-        element.$.restAPI,
-        'getProjectConfig',
-        () => Promise.resolve({}));
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  suite('create new change dialog', () => {
-    test('_createNewChange opens modal', () => {
-      const openStub = sandbox.stub(element.$.createChangeOverlay, 'open');
-      element._createNewChange();
-      assert.isTrue(openStub.called);
-    });
-
-    test('_handleCreateChange called when confirm fired', () => {
-      sandbox.stub(element, '_handleCreateChange');
-      element.$.createChangeDialog.dispatchEvent(
-          new CustomEvent('confirm', {
-            composed: true, bubbles: true,
-          }));
-      assert.isTrue(element._handleCreateChange.called);
-    });
-
-    test('_handleCloseCreateChange called when cancel fired', () => {
-      sandbox.stub(element, '_handleCloseCreateChange');
-      element.$.createChangeDialog.dispatchEvent(
-          new CustomEvent('cancel', {
-            composed: true, bubbles: true,
-          }));
-      assert.isTrue(element._handleCloseCreateChange.called);
-    });
-  });
-
-  suite('edit repo config', () => {
-    let createChangeStub;
-    let urlStub;
-    let handleSpy;
-    let alertStub;
-
-    setup(() => {
-      createChangeStub = sandbox.stub(element.$.restAPI, 'createChange');
-      urlStub = sandbox.stub(GerritNav, 'getEditUrlForDiff');
-      sandbox.stub(GerritNav, 'navigateToRelativeUrl');
-      handleSpy = sandbox.spy(element, '_handleEditRepoConfig');
-      alertStub = sandbox.stub();
-      element.addEventListener('show-alert', alertStub);
-    });
-
-    test('successful creation of change', () => {
-      const change = {_number: '1'};
-      createChangeStub.returns(Promise.resolve(change));
-      MockInteractions.tap(element.$.editRepoConfig);
-      assert.isTrue(element.$.editRepoConfig.loading);
-      return handleSpy.lastCall.returnValue.then(() => {
-        flushAsynchronousOperations();
-
-        assert.isTrue(alertStub.called);
-        assert.equal(alertStub.lastCall.args[0].detail.message,
-            'Navigating to change');
-        assert.isTrue(urlStub.called);
-        assert.deepEqual(urlStub.lastCall.args,
-            [change, 'project.config', 1]);
-        assert.isFalse(element.$.editRepoConfig.loading);
-      });
-    });
-
-    test('unsuccessful creation of change', () => {
-      createChangeStub.returns(Promise.resolve(null));
-      MockInteractions.tap(element.$.editRepoConfig);
-      assert.isTrue(element.$.editRepoConfig.loading);
-      return handleSpy.lastCall.returnValue.then(() => {
-        flushAsynchronousOperations();
-
-        assert.isTrue(alertStub.called);
-        assert.equal(alertStub.lastCall.args[0].detail.message,
-            'Failed to create change.');
-        assert.isFalse(urlStub.called);
-        assert.isFalse(element.$.editRepoConfig.loading);
-      });
-    });
-  });
-
-  suite('404', () => {
-    test('fires page-error', done => {
-      repoStub.restore();
-
-      element.repo = 'test';
-
-      const response = {status: 404};
-      sandbox.stub(
-          element.$.restAPI, 'getProjectConfig', (repo, errFn) => {
-            errFn(response);
-          });
-      element.addEventListener('page-error', e => {
-        assert.deepEqual(e.detail.response, response);
-        done();
-      });
-
-      element._loadRepo();
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.js
new file mode 100644
index 0000000..0bb0c55
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.js
@@ -0,0 +1,133 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-repo-commands.js';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+
+const basicFixture = fixtureFromElement('gr-repo-commands');
+
+suite('gr-repo-commands tests', () => {
+  let element;
+
+  let repoStub;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+    repoStub = sinon.stub(
+        element.$.restAPI,
+        'getProjectConfig')
+        .callsFake(() => Promise.resolve({}));
+  });
+
+  suite('create new change dialog', () => {
+    test('_createNewChange opens modal', () => {
+      const openStub = sinon.stub(element.$.createChangeOverlay, 'open');
+      element._createNewChange();
+      assert.isTrue(openStub.called);
+    });
+
+    test('_handleCreateChange called when confirm fired', () => {
+      sinon.stub(element, '_handleCreateChange');
+      element.$.createChangeDialog.dispatchEvent(
+          new CustomEvent('confirm', {
+            composed: true, bubbles: true,
+          }));
+      assert.isTrue(element._handleCreateChange.called);
+    });
+
+    test('_handleCloseCreateChange called when cancel fired', () => {
+      sinon.stub(element, '_handleCloseCreateChange');
+      element.$.createChangeDialog.dispatchEvent(
+          new CustomEvent('cancel', {
+            composed: true, bubbles: true,
+          }));
+      assert.isTrue(element._handleCloseCreateChange.called);
+    });
+  });
+
+  suite('edit repo config', () => {
+    let createChangeStub;
+    let urlStub;
+    let handleSpy;
+    let alertStub;
+
+    setup(() => {
+      createChangeStub = sinon.stub(element.$.restAPI, 'createChange');
+      urlStub = sinon.stub(GerritNav, 'getEditUrlForDiff');
+      sinon.stub(GerritNav, 'navigateToRelativeUrl');
+      handleSpy = sinon.spy(element, '_handleEditRepoConfig');
+      alertStub = sinon.stub();
+      element.addEventListener('show-alert', alertStub);
+    });
+
+    test('successful creation of change', () => {
+      const change = {_number: '1'};
+      createChangeStub.returns(Promise.resolve(change));
+      MockInteractions.tap(element.$.editRepoConfig);
+      assert.isTrue(element.$.editRepoConfig.loading);
+      return handleSpy.lastCall.returnValue.then(() => {
+        flushAsynchronousOperations();
+
+        assert.isTrue(alertStub.called);
+        assert.equal(alertStub.lastCall.args[0].detail.message,
+            'Navigating to change');
+        assert.isTrue(urlStub.called);
+        assert.deepEqual(urlStub.lastCall.args,
+            [change, 'project.config', 1]);
+        assert.isFalse(element.$.editRepoConfig.loading);
+      });
+    });
+
+    test('unsuccessful creation of change', () => {
+      createChangeStub.returns(Promise.resolve(null));
+      MockInteractions.tap(element.$.editRepoConfig);
+      assert.isTrue(element.$.editRepoConfig.loading);
+      return handleSpy.lastCall.returnValue.then(() => {
+        flushAsynchronousOperations();
+
+        assert.isTrue(alertStub.called);
+        assert.equal(alertStub.lastCall.args[0].detail.message,
+            'Failed to create change.');
+        assert.isFalse(urlStub.called);
+        assert.isFalse(element.$.editRepoConfig.loading);
+      });
+    });
+  });
+
+  suite('404', () => {
+    test('fires page-error', done => {
+      repoStub.restore();
+
+      element.repo = 'test';
+
+      const response = {status: 404};
+      sinon.stub(
+          element.$.restAPI, 'getProjectConfig')
+          .callsFake((repo, errFn) => {
+            errFn(response);
+          });
+      element.addEventListener('page-error', e => {
+        assert.deepEqual(e.detail.response, response);
+        done();
+      });
+
+      element._loadRepo();
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.js
similarity index 67%
rename from polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html
rename to polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.js
index dc12eff..b4d3575 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.js
@@ -1,57 +1,36 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-repo-dashboards</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-repo-dashboards></gr-repo-dashboards>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-repo-dashboards.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
+const basicFixture = fixtureFromElement('gr-repo-dashboards');
+
 suite('gr-repo-dashboards tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    element = basicFixture.instantiate();
   });
 
   suite('dashboard table', () => {
     setup(() => {
-      sandbox.stub(element.$.restAPI, 'getRepoDashboards').returns(
+      sinon.stub(element.$.restAPI, 'getRepoDashboards').returns(
           Promise.resolve([
             {
               id: 'default:contributor',
@@ -132,7 +111,7 @@
 
   suite('test url', () => {
     test('_getUrl', () => {
-      sandbox.stub(GerritNav, 'getUrlForRepoDashboard',
+      sinon.stub(GerritNav, 'getUrlForRepoDashboard').callsFake(
           () => '/r/dashboard/test');
 
       assert.equal(element._getUrl('/dashboard/test', {}), '/r/dashboard/test');
@@ -144,8 +123,9 @@
   suite('404', () => {
     test('fires page-error', done => {
       const response = {status: 404};
-      sandbox.stub(
-          element.$.restAPI, 'getRepoDashboards', (repo, errFn) => {
+      sinon.stub(
+          element.$.restAPI, 'getRepoDashboards')
+          .callsFake((repo, errFn) => {
             errFn(response);
           });
 
@@ -158,4 +138,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.js
similarity index 84%
rename from polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
rename to polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.js
index 9d7bba4..7190218 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.js
@@ -1,42 +1,27 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-repo-detail-list</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/page/page.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-repo-detail-list></gr-repo-detail-list>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-repo-detail-list.js';
 import page from 'page/page.mjs';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromElement('gr-repo-detail-list');
+
 let counter;
 const branchGenerator = () => {
   return {
@@ -74,18 +59,12 @@
   suite('Branches', () => {
     let element;
     let branches;
-    let sandbox;
 
     setup(() => {
-      sandbox = sinon.sandbox.create();
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.detailType = 'branches';
       counter = 0;
-      sandbox.stub(page, 'show');
-    });
-
-    teardown(() => {
-      sandbox.restore();
+      sinon.stub(page, 'show');
     });
 
     suite('list of repo branches', () => {
@@ -137,8 +116,8 @@
       });
 
       test('Edit HEAD button not admin', done => {
-        sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
-        sandbox.stub(element.$.restAPI, 'getRepoAccess').returns(
+        sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
+        sinon.stub(element.$.restAPI, 'getRepoAccess').returns(
             Promise.resolve({
               test: {is_owner: false},
             }));
@@ -161,12 +140,12 @@
         const revisionWithEditing = dom(element.root)
             .querySelector('.revisionWithEditing');
 
-        sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
-        sandbox.stub(element.$.restAPI, 'getRepoAccess').returns(
+        sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
+        sinon.stub(element.$.restAPI, 'getRepoAccess').returns(
             Promise.resolve({
               test: {is_owner: true},
             }));
-        sandbox.stub(element, '_handleSaveRevision');
+        sinon.stub(element, '_handleSaveRevision');
         element._determineIfOwner('test').then(() => {
           assert.equal(element._isOwner, true);
           // The revision container for non-editing enabled row is not visible.
@@ -237,9 +216,9 @@
       });
 
       test('_handleSaveRevision with invalid rev', done => {
-        const event = {model: {set: sandbox.stub()}};
+        const event = {model: {set: sinon.stub()}};
         element._isEditing = true;
-        sandbox.stub(element.$.restAPI, 'setRepoHead').returns(
+        sinon.stub(element.$.restAPI, 'setRepoHead').returns(
             Promise.resolve({
               status: 400,
             })
@@ -253,9 +232,9 @@
       });
 
       test('_handleSaveRevision with valid rev', done => {
-        const event = {model: {set: sandbox.stub()}};
+        const event = {model: {set: sinon.stub()}};
         element._isEditing = true;
-        sandbox.stub(element.$.restAPI, 'setRepoHead').returns(
+        sinon.stub(element.$.restAPI, 'setRepoHead').returns(
             Promise.resolve({
               status: 200,
             })
@@ -299,10 +278,10 @@
 
     suite('filter', () => {
       test('_paramsChanged', done => {
-        sandbox.stub(
+        sinon.stub(
             element.$.restAPI,
-            'getRepoBranches',
-            () => Promise.resolve(branches));
+            'getRepoBranches')
+            .callsFake(() => Promise.resolve(branches));
         const params = {
           detail: 'branches',
           repo: 'test',
@@ -326,7 +305,7 @@
     suite('404', () => {
       test('fires page-error', done => {
         const response = {status: 404};
-        sandbox.stub(element.$.restAPI, 'getRepoBranches',
+        sinon.stub(element.$.restAPI, 'getRepoBranches').callsFake(
             (filter, repo, reposBranchesPerPage, opt_offset, errFn) => {
               errFn(response);
             });
@@ -350,18 +329,12 @@
   suite('Tags', () => {
     let element;
     let tags;
-    let sandbox;
 
     setup(() => {
-      sandbox = sinon.sandbox.create();
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.detailType = 'tags';
       counter = 0;
-      sandbox.stub(page, 'show');
-    });
-
-    teardown(() => {
-      sandbox.restore();
+      sinon.stub(page, 'show');
     });
 
     test('_computeMessage', () => {
@@ -483,10 +456,10 @@
 
     suite('filter', () => {
       test('_paramsChanged', done => {
-        sandbox.stub(
+        sinon.stub(
             element.$.restAPI,
-            'getRepoTags',
-            () => Promise.resolve(tags));
+            'getRepoTags')
+            .callsFake(() => Promise.resolve(tags));
         const params = {
           repo: 'test',
           detail: 'tags',
@@ -509,7 +482,7 @@
 
     suite('create new', () => {
       test('_handleCreateClicked called when create-click fired', () => {
-        sandbox.stub(element, '_handleCreateClicked');
+        sinon.stub(element, '_handleCreateClicked');
         element.shadowRoot
             .querySelector('gr-list-view').dispatchEvent(
                 new CustomEvent('create-clicked', {
@@ -519,13 +492,13 @@
       });
 
       test('_handleCreateClicked opens modal', () => {
-        const openStub = sandbox.stub(element.$.createOverlay, 'open');
+        const openStub = sinon.stub(element.$.createOverlay, 'open');
         element._handleCreateClicked();
         assert.isTrue(openStub.called);
       });
 
       test('_handleCreateItem called when confirm fired', () => {
-        sandbox.stub(element, '_handleCreateItem');
+        sinon.stub(element, '_handleCreateItem');
         element.$.createDialog.dispatchEvent(
             new CustomEvent('confirm', {
               composed: true, bubbles: true,
@@ -534,7 +507,7 @@
       });
 
       test('_handleCloseCreate called when cancel fired', () => {
-        sandbox.stub(element, '_handleCloseCreate');
+        sinon.stub(element, '_handleCloseCreate');
         element.$.createDialog.dispatchEvent(
             new CustomEvent('cancel', {
               composed: true, bubbles: true,
@@ -546,7 +519,7 @@
     suite('404', () => {
       test('fires page-error', done => {
         const response = {status: 404};
-        sandbox.stub(element.$.restAPI, 'getRepoTags',
+        sinon.stub(element.$.restAPI, 'getRepoTags').callsFake(
             (filter, repo, reposTagsPerPage, opt_offset, errFn) => {
               errFn(response);
             });
@@ -573,4 +546,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js
similarity index 69%
rename from polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html
rename to polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js
index 96cb9ff..b629cf4 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js
@@ -1,42 +1,26 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-repo-list</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/page/page.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-repo-list></gr-repo-list>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-repo-list.js';
 import page from 'page/page.mjs';
 
+const basicFixture = fixtureFromElement('gr-repo-list');
+
 let counter;
 const repoGenerator = () => {
   return {
@@ -54,20 +38,15 @@
 suite('gr-repo-list tests', () => {
   let element;
   let repos;
-  let sandbox;
+
   let value;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    sandbox.stub(page, 'show');
-    element = fixture('basic');
+    sinon.stub(page, 'show');
+    element = basicFixture.instantiate();
     counter = 0;
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   suite('list with repos', () => {
     setup(done => {
       repos = _.times(26, repoGenerator);
@@ -91,7 +70,7 @@
     });
 
     test('_maybeOpenCreateOverlay', () => {
-      const overlayOpen = sandbox.stub(element.$.createOverlay, 'open');
+      const overlayOpen = sinon.stub(element.$.createOverlay, 'open');
       element._maybeOpenCreateOverlay();
       assert.isFalse(overlayOpen.called);
       const params = {};
@@ -129,7 +108,8 @@
     });
 
     test('_paramsChanged', done => {
-      sandbox.stub(element.$.restAPI, 'getRepos', () => Promise.resolve(repos));
+      sinon.stub(element.$.restAPI, 'getRepos')
+          .callsFake( () => Promise.resolve(repos));
       const value = {
         filter: 'test',
         offset: 25,
@@ -142,7 +122,7 @@
     });
 
     test('latest repos requested are always set', done => {
-      const repoStub = sandbox.stub(element.$.restAPI, 'getRepos');
+      const repoStub = sinon.stub(element.$.restAPI, 'getRepos');
       repoStub.withArgs('test').returns(Promise.resolve(repos));
       repoStub.withArgs('filter').returns(Promise.resolve(reposFiltered));
       element._filter = 'test';
@@ -172,7 +152,7 @@
 
   suite('create new', () => {
     test('_handleCreateClicked called when create-click fired', () => {
-      sandbox.stub(element, '_handleCreateClicked');
+      sinon.stub(element, '_handleCreateClicked');
       element.shadowRoot
           .querySelector('gr-list-view').dispatchEvent(
               new CustomEvent('create-clicked', {
@@ -182,13 +162,13 @@
     });
 
     test('_handleCreateClicked opens modal', () => {
-      const openStub = sandbox.stub(element.$.createOverlay, 'open');
+      const openStub = sinon.stub(element.$.createOverlay, 'open');
       element._handleCreateClicked();
       assert.isTrue(openStub.called);
     });
 
     test('_handleCreateRepo called when confirm fired', () => {
-      sandbox.stub(element, '_handleCreateRepo');
+      sinon.stub(element, '_handleCreateRepo');
       element.$.createDialog.dispatchEvent(
           new CustomEvent('confirm', {
             composed: true, bubbles: true,
@@ -197,7 +177,7 @@
     });
 
     test('_handleCloseCreate called when cancel fired', () => {
-      sandbox.stub(element, '_handleCloseCreate');
+      sinon.stub(element, '_handleCloseCreate');
       element.$.createDialog.dispatchEvent(
           new CustomEvent('cancel', {
             composed: true, bubbles: true,
@@ -206,4 +186,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.js
similarity index 72%
rename from polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html
rename to polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.js
index a2370d9..1730839 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.js
@@ -1,50 +1,32 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2018 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-repo-plugin-config</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-repo-plugin-config></gr-repo-plugin-config>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-repo-plugin-config.js';
+
+const basicFixture = fixtureFromElement('gr-repo-plugin-config');
+
 suite('gr-repo-plugin-config tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+    element = basicFixture.instantiate();
   });
 
-  teardown(() => sandbox.restore());
-
   test('_computePluginConfigOptions', () => {
     assert.deepEqual(element._computePluginConfigOptions(), []);
     assert.deepEqual(element._computePluginConfigOptions({}), []);
@@ -62,7 +44,7 @@
   });
 
   test('_handleChange', () => {
-    const eventStub = sandbox.stub(element, 'dispatchEvent');
+    const eventStub = sinon.stub(element, 'dispatchEvent');
     element.pluginData = {
       name: 'testName',
       config: {plugin: {value: 'test'}},
@@ -86,8 +68,8 @@
     let buildStub;
 
     setup(() => {
-      changeStub = sandbox.stub(element, '_handleChange');
-      buildStub = sandbox.stub(element, '_buildConfigChangeInfo');
+      changeStub = sinon.stub(element, '_handleChange');
+      buildStub = sinon.stub(element, '_buildConfigChangeInfo');
     });
 
     test('ARRAY type option', () => {
@@ -178,4 +160,4 @@
     assert.equal(detail.notifyPath, 'plugin.value');
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.js
similarity index 83%
rename from polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
rename to polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.js
index 58b488a..3b42e3b 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.js
@@ -1,44 +1,30 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-repo</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-repo></gr-repo>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-repo.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+
+const basicFixture = fixtureFromElement('gr-repo');
+
 suite('gr-repo tests', () => {
   let element;
-  let sandbox;
+
   let repoStub;
   const repoConf = {
     description: 'Access inherited by all other projects.',
@@ -113,22 +99,17 @@
   }
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     stub('gr-rest-api-interface', {
       getLoggedIn() { return Promise.resolve(false); },
       getConfig() {
         return Promise.resolve({download: {}});
       },
     });
-    element = fixture('basic');
-    repoStub = sandbox.stub(
+    element = basicFixture.instantiate();
+    repoStub = sinon.stub(
         element.$.restAPI,
-        'getProjectConfig',
-        () => Promise.resolve(repoConf));
-  });
-
-  teardown(() => {
-    sandbox.restore();
+        'getProjectConfig')
+        .callsFake(() => Promise.resolve(repoConf));
   });
 
   test('_computePluginData', () => {
@@ -140,7 +121,7 @@
   });
 
   test('_handlePluginConfigChanged', () => {
-    const notifyStub = sandbox.stub(element, 'notifyPath');
+    const notifyStub = sinon.stub(element, 'notifyPath');
     element._repoConfig = {plugin_config: {}};
     element._handlePluginConfigChanged({detail: {
       name: 'test',
@@ -189,11 +170,11 @@
 
   test('form defaults to read only when logged in and not admin', done => {
     element.repo = REPO;
-    sandbox.stub(element, '_getLoggedIn', () => Promise.resolve(true));
-    sandbox.stub(
+    sinon.stub(element, '_getLoggedIn').callsFake(() => Promise.resolve(true));
+    sinon.stub(
         element.$.restAPI,
-        'getRepoAccess',
-        () => Promise.resolve({'test-repo': {}}));
+        'getRepoAccess')
+        .callsFake(() => Promise.resolve({'test-repo': {}}));
     element._loadRepo().then(() => {
       assert.isTrue(element._readOnly);
       done();
@@ -266,10 +247,10 @@
     element.repo = 'test';
 
     const response = {status: 404};
-    sandbox.stub(
-        element.$.restAPI, 'getProjectConfig', (repo, errFn) => {
-          errFn(response);
-        });
+    sinon.stub(
+        element.$.restAPI, 'getProjectConfig').callsFake((repo, errFn) => {
+      errFn(response);
+    });
     element.addEventListener('page-error', e => {
       assert.deepEqual(e.detail.response, response);
       done();
@@ -281,11 +262,12 @@
   suite('admin', () => {
     setup(() => {
       element.repo = REPO;
-      sandbox.stub(element, '_getLoggedIn', () => Promise.resolve(true));
-      sandbox.stub(
+      sinon.stub(element, '_getLoggedIn')
+          .callsFake(() => Promise.resolve(true));
+      sinon.stub(
           element.$.restAPI,
-          'getRepoAccess',
-          () => Promise.resolve({'test-repo': {is_owner: true}}));
+          'getRepoAccess')
+          .callsFake(() => Promise.resolve({'test-repo': {is_owner: true}}));
     });
 
     test('all form elements are enabled', done => {
@@ -341,8 +323,8 @@
         enable_reviewer_by_email: 'TRUE',
       };
 
-      const saveStub = sandbox.stub(element.$.restAPI, 'saveRepoConfig'
-          , () => Promise.resolve({}));
+      const saveStub = sinon.stub(element.$.restAPI, 'saveRepoConfig')
+          .callsFake(() => Promise.resolve({}));
 
       const button = dom(element.root).querySelector('gr-button');
 
@@ -397,4 +379,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.js
similarity index 91%
rename from polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
rename to polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.js
index f096eed..9364a50 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.js
@@ -1,52 +1,31 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-rule-editor</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/page/page.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-rule-editor></gr-rule-editor>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-rule-editor.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromElement('gr-rule-editor');
+
 suite('gr-rule-editor tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    element = basicFixture.instantiate();
   });
 
   suite('unit tests', () => {
@@ -148,7 +127,7 @@
     test('_setDefaultRuleValues', () => {
       element.rule = {id: 123};
       const defaultValue = {action: 'ALLOW'};
-      sandbox.stub(element, '_getDefaultRuleValues').returns(defaultValue);
+      sinon.stub(element, '_getDefaultRuleValues').returns(defaultValue);
       element._setDefaultRuleValues();
       assert.isTrue(element._getDefaultRuleValues.called);
       assert.equal(element.rule.value, defaultValue);
@@ -171,7 +150,7 @@
     });
 
     test('_handleValueChange', () => {
-      const modifiedHandler = sandbox.stub();
+      const modifiedHandler = sinon.stub();
       element.rule = {value: {}};
       element.addEventListener('access-modified', modifiedHandler);
       element._handleValueChange();
@@ -361,7 +340,7 @@
 
     test('remove value', () => {
       element.editing = true;
-      const removeStub = sandbox.stub();
+      const removeStub = sinon.stub();
       element.addEventListener('added-rule-removed', removeStub);
       MockInteractions.tap(element.$.removeBtn);
       flushAsynchronousOperations();
@@ -414,7 +393,7 @@
     });
 
     test('modify value', () => {
-      const removeStub = sandbox.stub();
+      const removeStub = sinon.stub();
       element.addEventListener('added-rule-removed', removeStub);
       assert.isNotOk(element.rule.value.modified);
       dom(element.root).querySelector('#labelMin').bindValue = 1;
@@ -429,7 +408,7 @@
 
   suite('new rule with labels', () => {
     setup(done => {
-      sandbox.spy(element, '_setDefaultRuleValues');
+      sinon.spy(element, '_setDefaultRuleValues');
       element.label = {values: [
         {value: -2, text: 'This shall not be merged'},
         {value: -1, text: 'I would prefer this is not merged as is'},
@@ -623,4 +602,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.js
similarity index 86%
rename from polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
rename to polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.js
index e426d84..4b9424b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.js
@@ -1,56 +1,37 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2015 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-change-list-item</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-change-list-item></gr-change-list-item>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-change-list-item.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
+const basicFixture = fixtureFromElement('gr-change-list-item');
+
 suite('gr-change-list-item tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     stub('gr-rest-api-interface', {
       getConfig() { return Promise.resolve({}); },
       getLoggedIn() { return Promise.resolve(false); },
     });
-    element = fixture('basic');
+    element = basicFixture.instantiate();
   });
 
-  teardown(() => { sandbox.restore(); });
-
   test('computed fields', () => {
     assert.equal(element._computeLabelClass({labels: {}}),
         'cell label u-gray-background');
@@ -276,7 +257,7 @@
   });
 
   test('change params passed to gr-navigation', () => {
-    sandbox.stub(GerritNav);
+    sinon.stub(GerritNav);
     const change = {
       internalHost: 'test-host',
       project: 'test-repo',
@@ -309,4 +290,4 @@
         '…/test/repo');
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js
similarity index 74%
rename from polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
rename to polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js
index 58ec4e1..f945476 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js
@@ -1,49 +1,32 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-change-list-view</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/page/page.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-change-list-view></gr-change-list-view>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-change-list-view.js';
 import page from 'page/page.mjs';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
+const basicFixture = fixtureFromElement('gr-change-list-view');
+
 const CHANGE_ID = 'IcA3dAB3edAB9f60B8dcdA6ef71A75980e4B7127';
 const COMMIT_HASH = '12345678';
 
 suite('gr-change-list-view tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
     stub('gr-rest-api-interface', {
@@ -54,13 +37,11 @@
       getAccountDetails() { return Promise.resolve({}); },
       getAccountStatus() { return Promise.resolve({}); },
     });
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
+    element = basicFixture.instantiate();
   });
 
   teardown(done => {
     flush(() => {
-      sandbox.restore();
       done();
     });
   });
@@ -80,7 +61,7 @@
   });
 
   test('_computeNavLink', () => {
-    const getUrlStub = sandbox.stub(GerritNav, 'getUrlForSearchQuery')
+    const getUrlStub = sinon.stub(GerritNav, 'getUrlForSearchQuery')
         .returns('');
     const query = 'status:open';
     let offset = 0;
@@ -126,7 +107,7 @@
   });
 
   test('_handleNextPage', () => {
-    const showStub = sandbox.stub(page, 'show');
+    const showStub = sinon.stub(page, 'show');
     element.$.nextArrow.hidden = true;
     element._handleNextPage();
     assert.isFalse(showStub.called);
@@ -136,7 +117,7 @@
   });
 
   test('_handlePreviousPage', () => {
-    const showStub = sandbox.stub(page, 'show');
+    const showStub = sinon.stub(page, 'show');
     element.$.prevArrow.hidden = true;
     element._handlePreviousPage();
     assert.isFalse(showStub.called);
@@ -202,16 +183,16 @@
 
     teardown(done => {
       flush(() => {
-        sandbox.restore();
+        sinon.restore();
         done();
       });
     });
 
     test('Searching for a change ID redirects to change', done => {
       const change = {_number: 1};
-      sandbox.stub(element, '_getChanges')
+      sinon.stub(element, '_getChanges')
           .returns(Promise.resolve([change]));
-      sandbox.stub(GerritNav, 'navigateToChange', url => {
+      sinon.stub(GerritNav, 'navigateToChange').callsFake( url => {
         assert.equal(url, change);
         done();
       });
@@ -221,9 +202,9 @@
 
     test('Searching for a change num redirects to change', done => {
       const change = {_number: 1};
-      sandbox.stub(element, '_getChanges')
+      sinon.stub(element, '_getChanges')
           .returns(Promise.resolve([change]));
-      sandbox.stub(GerritNav, 'navigateToChange', url => {
+      sinon.stub(GerritNav, 'navigateToChange').callsFake( url => {
         assert.equal(url, change);
         done();
       });
@@ -233,9 +214,9 @@
 
     test('Commit hash redirects to change', done => {
       const change = {_number: 1};
-      sandbox.stub(element, '_getChanges')
+      sinon.stub(element, '_getChanges')
           .returns(Promise.resolve([change]));
-      sandbox.stub(GerritNav, 'navigateToChange', url => {
+      sinon.stub(GerritNav, 'navigateToChange').callsFake( url => {
         assert.equal(url, change);
         done();
       });
@@ -244,9 +225,9 @@
     });
 
     test('Searching for an invalid change ID searches', () => {
-      sandbox.stub(element, '_getChanges')
+      sinon.stub(element, '_getChanges')
           .returns(Promise.resolve([]));
-      const stub = sandbox.stub(GerritNav, 'navigateToChange');
+      const stub = sinon.stub(GerritNav, 'navigateToChange');
 
       element.params = {view: GerritNav.View.SEARCH, query: CHANGE_ID};
       flushAsynchronousOperations();
@@ -255,9 +236,9 @@
     });
 
     test('Change ID with multiple search results searches', () => {
-      sandbox.stub(element, '_getChanges')
+      sinon.stub(element, '_getChanges')
           .returns(Promise.resolve([{}, {}]));
-      const stub = sandbox.stub(GerritNav, 'navigateToChange');
+      const stub = sinon.stub(GerritNav, 'navigateToChange');
 
       element.params = {view: GerritNav.View.SEARCH, query: CHANGE_ID};
       flushAsynchronousOperations();
@@ -266,4 +247,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
similarity index 85%
rename from polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
rename to polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
index 16abb10..78973df 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
@@ -1,76 +1,55 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2015 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-change-list</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script src="/node_modules/page/page.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-change-list></gr-change-list>
-  </template>
-</test-fixture>
-
-<test-fixture id="grouped">
-  <template>
-    <gr-change-list></gr-change-list>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-change-list.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.js';
-import {KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {TestKeyboardShortcutBinder} from '../../../test/test-utils.js';
+
+const basicFixture = fixtureFromElement('gr-change-list');
 
 suite('gr-change-list basic tests', () => {
-  // Define keybindings before attaching other fixtures.
-  const kb = KeyboardShortcutBinder;
-  kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_CHANGE, 'j');
-  kb.bindShortcut(kb.Shortcut.CURSOR_PREV_CHANGE, 'k');
-  kb.bindShortcut(kb.Shortcut.OPEN_CHANGE, 'o');
-  kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE_LIST, 'shift+r');
-  kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r');
-  kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's');
-  kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'n');
-  kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'p');
-
   let element;
-  let sandbox;
 
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+  suiteSetup(() => {
+    const kb = TestKeyboardShortcutBinder.push();
+    kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_CHANGE, 'j');
+    kb.bindShortcut(kb.Shortcut.CURSOR_PREV_CHANGE, 'k');
+    kb.bindShortcut(kb.Shortcut.OPEN_CHANGE, 'o');
+    kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE_LIST, 'shift+r');
+    kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r');
+    kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's');
+    kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'n');
+    kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'p');
   });
 
-  teardown(() => { sandbox.restore(); });
+  suiteTeardown(() => {
+    TestKeyboardShortcutBinder.pop();
+  });
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
 
   suite('test show change number not logged in', () => {
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.account = null;
       element.preferences = null;
       element._config = {};
@@ -83,7 +62,7 @@
 
   suite('test show change number preference enabled', () => {
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.preferences = {
         legacycid_in_change_table: true,
         time_format: 'HHMM_12',
@@ -101,7 +80,7 @@
 
   suite('test show change number preference disabled', () => {
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       // legacycid_in_change_table is not set when false.
       element.preferences = {
         time_format: 'HHMM_12',
@@ -170,7 +149,7 @@
   });
 
   test('keyboard shortcuts', done => {
-    sandbox.stub(element, '_computeLabelNames');
+    sinon.stub(element, '_computeLabelNames');
     element.sections = [
       {results: new Array(1)},
       {results: new Array(2)},
@@ -195,7 +174,7 @@
       assert.equal(element.selectedIndex, 2);
       assert.isTrue(elementItems[2].hasAttribute('selected'));
 
-      const navStub = sandbox.stub(GerritNav, 'navigateToChange');
+      const navStub = sinon.stub(GerritNav, 'navigateToChange');
       assert.equal(element.selectedIndex, 2);
       MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
       assert.deepEqual(navStub.lastCall.args[0], {_number: 2},
@@ -212,7 +191,7 @@
       MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
       assert.equal(element.selectedIndex, 0);
 
-      const reloadStub = sandbox.stub(element, '_reloadWindow');
+      const reloadStub = sinon.stub(element, '_reloadWindow');
       MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
       assert.isTrue(reloadStub.called);
 
@@ -340,7 +319,7 @@
     let element;
 
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.sections = [
         {results: [{}]},
       ];
@@ -371,7 +350,7 @@
     let element;
 
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.sections = [
         {results: [{}]},
       ];
@@ -409,7 +388,7 @@
     let element;
 
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.sections = [
         {results: [{}]},
       ];
@@ -453,7 +432,7 @@
     /* This would only exist if somebody manually updated the config
     file. */
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.account = {_account_id: 1001};
       element.preferences = {
         legacycid_in_change_table: true,
@@ -474,14 +453,12 @@
 
   suite('dashboard queries', () => {
     let element;
-    let sandbox;
 
     setup(() => {
-      sandbox = sinon.sandbox.create();
-      element = fixture('basic');
+      element = basicFixture.instantiate();
     });
 
-    teardown(() => { sandbox.restore(); });
+    teardown(() => { sinon.restore(); });
 
     test('query without age and limit unchanged', () => {
       const query = 'status:closed owner:me';
@@ -527,15 +504,11 @@
 
   suite('gr-change-list sections', () => {
     let element;
-    let sandbox;
 
     setup(() => {
-      sandbox = sinon.sandbox.create();
-      element = fixture('basic');
+      element = basicFixture.instantiate();
     });
 
-    teardown(() => { sandbox.restore(); });
-
     test('keyboard shortcuts', done => {
       element.selectedIndex = 0;
       element.sections = [
@@ -571,7 +544,7 @@
         assert.equal(element.selectedIndex, 1);
         MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
 
-        const navStub = sandbox.stub(GerritNav, 'navigateToChange');
+        const navStub = sinon.stub(GerritNav, 'navigateToChange');
         assert.equal(element.selectedIndex, 2);
 
         MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
@@ -640,7 +613,7 @@
     });
 
     test('_computeItemAbsoluteIndex', () => {
-      sandbox.stub(element, '_computeLabelNames');
+      sinon.stub(element, '_computeLabelNames');
       element.sections = [
         {results: new Array(1)},
         {results: new Array(2)},
@@ -659,4 +632,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html
deleted file mode 100644
index 9b8ed29..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html
+++ /dev/null
@@ -1,50 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-create-change-help</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-create-change-help></gr-create-change-help>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-create-change-help.js';
-suite('gr-create-change-help tests', () => {
-  let element;
-
-  setup(() => {
-    element = fixture('basic');
-  });
-
-  test('Create change tap', done => {
-    element.addEventListener('create-tap', () => done());
-    MockInteractions.tap(element.shadowRoot
-        .querySelector('gr-button'));
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.js b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.js
new file mode 100644
index 0000000..9dd0a35
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.js
@@ -0,0 +1,36 @@
+/**
+ * @license
+ * Copyright (C) 2018 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-create-change-help.js';
+
+const basicFixture = fixtureFromElement('gr-create-change-help');
+
+suite('gr-create-change-help tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('Create change tap', done => {
+    element.addEventListener('create-tap', () => done());
+    MockInteractions.tap(element.shadowRoot
+        .querySelector('gr-button'));
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html
deleted file mode 100644
index e6cd587..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html
+++ /dev/null
@@ -1,54 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-create-commands-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-create-commands-dialog></gr-create-commands-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-create-commands-dialog.js';
-suite('gr-create-commands-dialog tests', () => {
-  let element;
-
-  setup(() => {
-    element = fixture('basic');
-  });
-
-  test('_computePushCommand', () => {
-    element.branch = 'master';
-    assert.equal(element._pushCommand,
-        'git push origin HEAD:refs/for/master');
-
-    element.branch = 'stable-2.15';
-    assert.equal(element._pushCommand,
-        'git push origin HEAD:refs/for/stable-2.15');
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.js b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.js
new file mode 100644
index 0000000..9dbcd29
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.js
@@ -0,0 +1,40 @@
+/**
+ * @license
+ * Copyright (C) 2018 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-create-commands-dialog.js';
+
+const basicFixture = fixtureFromElement('gr-create-commands-dialog');
+
+suite('gr-create-commands-dialog tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('_computePushCommand', () => {
+    element.branch = 'master';
+    assert.equal(element._pushCommand,
+        'git push origin HEAD:refs/for/master');
+
+    element.branch = 'stable-2.15';
+    assert.equal(element._pushCommand,
+        'git push origin HEAD:refs/for/stable-2.15');
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
similarity index 78%
rename from polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
rename to polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
index 2fcf233..bdd374a 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
@@ -1,45 +1,30 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-dashboard-view</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-dashboard-view></gr-dashboard-view>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-dashboard-view.js';
 import {isHidden} from '../../../test/test-utils.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
+const basicFixture = fixtureFromElement('gr-dashboard-view');
+
 suite('gr-dashboard-view tests', () => {
   let element;
-  let sandbox;
+
   let paramsChangedPromise;
   let getChangesStub;
 
@@ -49,9 +34,9 @@
       getAccountDetails() { return Promise.resolve({}); },
       getAccountStatus() { return Promise.resolve(false); },
     });
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
-    getChangesStub = sandbox.stub(element.$.restAPI, 'getChanges',
+    element = basicFixture.instantiate();
+
+    getChangesStub = sinon.stub(element.$.restAPI, 'getChanges').callsFake(
         (_, qs) => Promise.resolve(qs.map(() => [])));
 
     let resolver;
@@ -59,15 +44,11 @@
       resolver = resolve;
     });
     const paramsChanged = element._paramsChanged.bind(element);
-    sandbox.stub(element, '_paramsChanged', params => {
+    sinon.stub(element, '_paramsChanged').callsFake( params => {
       paramsChanged(params).then(() => resolver());
     });
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   suite('drafts banner functionality', () => {
     suite('_maybeShowDraftsBanner', () => {
       test('not dashboard/self', () => {
@@ -86,7 +67,7 @@
       test('no drafts on open changes', () => {
         element.params = {user: 'self'};
         element._results = [{query: 'has:draft', results: [{status: '_'}]}];
-        sandbox.stub(element, 'changeIsOpen').returns(true);
+        sinon.stub(element, 'changeIsOpen').returns(true);
         element._maybeShowDraftsBanner();
         assert.isFalse(element._showDraftsBanner);
       });
@@ -94,7 +75,7 @@
       test('no drafts on open changes', () => {
         element.params = {user: 'self'};
         element._results = [{query: 'has:draft', results: [{status: '_'}]}];
-        sandbox.stub(element, 'changeIsOpen').returns(false);
+        sinon.stub(element, 'changeIsOpen').returns(false);
         element._maybeShowDraftsBanner();
         assert.isTrue(element._showDraftsBanner);
       });
@@ -113,7 +94,7 @@
     });
 
     test('delete tap opens dialog', () => {
-      sandbox.stub(element, '_handleOpenDeleteDialog');
+      sinon.stub(element, '_handleOpenDeleteDialog');
       element._showDraftsBanner = true;
       flushAsynchronousOperations();
 
@@ -123,15 +104,15 @@
     });
 
     test('delete comments flow', async () => {
-      sandbox.spy(element, '_handleConfirmDelete');
-      sandbox.stub(element, '_reload');
+      sinon.spy(element, '_handleConfirmDelete');
+      sinon.stub(element, '_reload');
 
       // Set up control over timing of when RPC resolves.
       let deleteDraftCommentsPromiseResolver;
       const deleteDraftCommentsPromise = new Promise(resolve => {
         deleteDraftCommentsPromiseResolver = resolve;
       });
-      sandbox.stub(element.$.restAPI, 'deleteDraftComments')
+      sinon.stub(element.$.restAPI, 'deleteDraftComments')
           .returns(deleteDraftCommentsPromise);
 
       // Open confirmation dialog and tap confirm button.
@@ -246,14 +227,15 @@
 
   suite('_getProjectDashboard', () => {
     test('dashboard with foreach', () => {
-      sandbox.stub(element.$.restAPI, 'getDashboard', () => Promise.resolve({
-        title: 'title',
-        foreach: 'foreach for ${project}',
-        sections: [
-          {name: 'section 1', query: 'query 1'},
-          {name: 'section 2', query: '${project} query 2'},
-        ],
-      }));
+      sinon.stub(element.$.restAPI, 'getDashboard')
+          .callsFake( () => Promise.resolve({
+            title: 'title',
+            foreach: 'foreach for ${project}',
+            sections: [
+              {name: 'section 1', query: 'query 1'},
+              {name: 'section 2', query: '${project} query 2'},
+            ],
+          }));
       return element._getProjectDashboard('project', '').then(dashboard => {
         assert.deepEqual(
             dashboard,
@@ -271,13 +253,14 @@
     });
 
     test('dashboard without foreach', () => {
-      sandbox.stub(element.$.restAPI, 'getDashboard', () => Promise.resolve({
-        title: 'title',
-        sections: [
-          {name: 'section 1', query: 'query 1'},
-          {name: 'section 2', query: '${project} query 2'},
-        ],
-      }));
+      sinon.stub(element.$.restAPI, 'getDashboard').callsFake(
+          () => Promise.resolve({
+            title: 'title',
+            sections: [
+              {name: 'section 1', query: 'query 1'},
+              {name: 'section 2', query: '${project} query 2'},
+            ],
+          }));
       return element._getProjectDashboard('project', '').then(dashboard => {
         assert.deepEqual(
             dashboard,
@@ -298,7 +281,7 @@
       {name: 'test2', query: 'test2', hideIfEmpty: true},
     ];
     getChangesStub.restore();
-    sandbox.stub(element.$.restAPI, 'getChanges')
+    sinon.stub(element.$.restAPI, 'getChanges')
         .returns(Promise.resolve([[], ['nonempty']]));
 
     return element._fetchDashboardChanges({sections}, false).then(() => {
@@ -313,7 +296,7 @@
       {name: 'test2', query: 'test2'},
     ];
     getChangesStub.restore();
-    sandbox.stub(element.$.restAPI, 'getChanges')
+    sinon.stub(element.$.restAPI, 'getChanges')
         .returns(Promise.resolve([[], []]));
 
     return element._fetchDashboardChanges({sections}, false).then(() => {
@@ -351,7 +334,7 @@
 
   test('404 page', done => {
     const response = {status: 404};
-    sandbox.stub(element.$.restAPI, 'getDashboard',
+    sinon.stub(element.$.restAPI, 'getDashboard').callsFake(
         async (project, dashboard, errFn) => {
           errFn(response);
         });
@@ -367,7 +350,7 @@
   });
 
   test('params change triggers dashboardDisplayed()', () => {
-    sandbox.stub(element.reporting, 'dashboardDisplayed');
+    sinon.stub(element.reporting, 'dashboardDisplayed');
     element.params = {
       view: GerritNav.View.DASHBOARD,
       project: 'project',
@@ -378,4 +361,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html
deleted file mode 100644
index 78c1f09..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html
+++ /dev/null
@@ -1,59 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-repo-header</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-repo-header></gr-repo-header>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-repo-header.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-
-suite('gr-repo-header tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => { sandbox.restore(); });
-
-  test('repoUrl reset once repo changed', () => {
-    sandbox.stub(GerritNav, 'getUrlForRepo',
-        repoName => `http://test.com/${repoName}`
-    );
-    assert.equal(element._repoUrl, undefined);
-    element.repo = 'test';
-    assert.equal(element._repoUrl, 'http://test.com/test');
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.js b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.js
new file mode 100644
index 0000000..4f93d54
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.js
@@ -0,0 +1,40 @@
+/**
+ * @license
+ * Copyright (C) 2018 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-repo-header.js';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+
+const basicFixture = fixtureFromElement('gr-repo-header');
+
+suite('gr-repo-header tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('repoUrl reset once repo changed', () => {
+    sinon.stub(GerritNav, 'getUrlForRepo').callsFake(
+        repoName => `http://test.com/${repoName}`
+    );
+    assert.equal(element._repoUrl, undefined);
+    element.repo = 'test';
+    assert.equal(element._repoUrl, 'http://test.com/test');
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
deleted file mode 100644
index 44eb96c..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
+++ /dev/null
@@ -1,81 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-user-header</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-user-header></gr-user-header>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-user-header.js';
-suite('gr-user-header tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => { sandbox.restore(); });
-
-  test('loads and clears account info', done => {
-    sandbox.stub(element.$.restAPI, 'getAccountDetails')
-        .returns(Promise.resolve({
-          name: 'foo',
-          email: 'bar',
-          registered_on: '2015-03-12 18:32:08.000000000',
-        }));
-    sandbox.stub(element.$.restAPI, 'getAccountStatus')
-        .returns(Promise.resolve('baz'));
-
-    element.userId = 'foo.bar@baz';
-    flush(() => {
-      assert.isOk(element._accountDetails);
-      assert.isOk(element._status);
-
-      element.userId = null;
-      flush(() => {
-        flushAsynchronousOperations();
-        assert.isNull(element._accountDetails);
-        assert.isNull(element._status);
-
-        done();
-      });
-    });
-  });
-
-  test('_computeDashboardLinkClass', () => {
-    assert.include(element._computeDashboardLinkClass(false, false), 'hide');
-    assert.include(element._computeDashboardLinkClass(true, false), 'hide');
-    assert.include(element._computeDashboardLinkClass(false, true), 'hide');
-    assert.notInclude(element._computeDashboardLinkClass(true, true), 'hide');
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.js
new file mode 100644
index 0000000..6baacef
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.js
@@ -0,0 +1,63 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-user-header.js';
+
+const basicFixture = fixtureFromElement('gr-user-header');
+
+suite('gr-user-header tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('loads and clears account info', done => {
+    sinon.stub(element.$.restAPI, 'getAccountDetails')
+        .returns(Promise.resolve({
+          name: 'foo',
+          email: 'bar',
+          registered_on: '2015-03-12 18:32:08.000000000',
+        }));
+    sinon.stub(element.$.restAPI, 'getAccountStatus')
+        .returns(Promise.resolve('baz'));
+
+    element.userId = 'foo.bar@baz';
+    flush(() => {
+      assert.isOk(element._accountDetails);
+      assert.isOk(element._status);
+
+      element.userId = null;
+      flush(() => {
+        flushAsynchronousOperations();
+        assert.isNull(element._accountDetails);
+        assert.isNull(element._status);
+
+        done();
+      });
+    });
+  });
+
+  test('_computeDashboardLinkClass', () => {
+    assert.include(element._computeDashboardLinkClass(false, false), 'hide');
+    assert.include(element._computeDashboardLinkClass(true, false), 'hide');
+    assert.include(element._computeDashboardLinkClass(false, true), 'hide');
+    assert.notInclude(element._computeDashboardLinkClass(true, true), 'hide');
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
similarity index 90%
rename from polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
rename to polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
index 5efeebd..2702af4 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
@@ -1,42 +1,28 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-change-actions</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-change-actions></gr-change-actions>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-change-actions.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+
+const basicFixture = fixtureFromElement('gr-change-actions');
+
 const CHERRY_PICK_TYPES = {
   SINGLE_CHANGE: 1,
   TOPIC: 2,
@@ -44,7 +30,6 @@
 // TODO(dhruvsri): remove use of _populateRevertMessage as it's private
 suite('gr-change-actions tests', () => {
   let element;
-  let sandbox;
 
   suite('basic tests', () => {
     setup(() => {
@@ -99,11 +84,10 @@
         getProjectConfig() { return Promise.resolve({}); },
       });
 
-      sandbox = sinon.sandbox.create();
-      sandbox.stub(pluginLoader, 'awaitPluginsLoaded')
+      sinon.stub(pluginLoader, 'awaitPluginsLoaded')
           .returns(Promise.resolve());
 
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.change = {};
       element.changeNum = '42';
       element.latestPatchNum = '2';
@@ -115,18 +99,14 @@
           enabled: true,
         },
       };
-      sandbox.stub(element.$.confirmCherrypick.$.restAPI,
+      sinon.stub(element.$.confirmCherrypick.$.restAPI,
           'getRepoBranches').returns(Promise.resolve([]));
-      sandbox.stub(element.$.confirmMove.$.restAPI,
+      sinon.stub(element.$.confirmMove.$.restAPI,
           'getRepoBranches').returns(Promise.resolve([]));
 
       return element.reload();
     });
 
-    teardown(() => {
-      sandbox.restore();
-    });
-
     test('show-revision-actions event should fire', done => {
       const spy = sinon.spy(element, '_sendShowRevisionActions');
       element.reload();
@@ -158,7 +138,7 @@
     });
 
     test('plugin revision actions', done => {
-      sandbox.stub(element.$.restAPI, 'getChangeActionURL').returns(
+      sinon.stub(element.$.restAPI, 'getChangeActionURL').returns(
           Promise.resolve('the-url'));
       element.revisionActions = {
         'plugin~action': {},
@@ -173,7 +153,7 @@
     });
 
     test('plugin change actions', done => {
-      sandbox.stub(element.$.restAPI, 'getChangeActionURL').returns(
+      sinon.stub(element.$.restAPI, 'getChangeActionURL').returns(
           Promise.resolve('the-url'));
       element.actions = {
         'plugin~action': {},
@@ -288,12 +268,12 @@
     });
 
     test('submit change', () => {
-      const showSpy = sandbox.spy(element, '_showActionDialog');
-      sandbox.stub(element.$.restAPI, 'getFromProjectLookup')
+      const showSpy = sinon.spy(element, '_showActionDialog');
+      sinon.stub(element.$.restAPI, 'getFromProjectLookup')
           .returns(Promise.resolve('test'));
-      sandbox.stub(element, 'fetchChangeUpdates',
+      sinon.stub(element, 'fetchChangeUpdates').callsFake(
           () => Promise.resolve({isLatest: true}));
-      sandbox.stub(element.$.overlay, 'open').returns(Promise.resolve());
+      sinon.stub(element.$.overlay, 'open').returns(Promise.resolve());
       element.change = {
         revisions: {
           rev1: {_number: 1},
@@ -312,12 +292,12 @@
     });
 
     test('submit change, tap on icon', done => {
-      sandbox.stub(element.$.confirmSubmitDialog, 'resetFocus', done);
-      sandbox.stub(element.$.restAPI, 'getFromProjectLookup')
+      sinon.stub(element.$.confirmSubmitDialog, 'resetFocus').callsFake( done);
+      sinon.stub(element.$.restAPI, 'getFromProjectLookup')
           .returns(Promise.resolve('test'));
-      sandbox.stub(element, 'fetchChangeUpdates',
+      sinon.stub(element, 'fetchChangeUpdates').callsFake(
           () => Promise.resolve({isLatest: true}));
-      sandbox.stub(element.$.overlay, 'open').returns(Promise.resolve());
+      sinon.stub(element.$.overlay, 'open').returns(Promise.resolve());
       element.change = {
         revisions: {
           rev1: {_number: 1},
@@ -334,8 +314,8 @@
     });
 
     test('_handleSubmitConfirm', () => {
-      const fireStub = sandbox.stub(element, '_fireAction');
-      sandbox.stub(element, '_canSubmitChange').returns(true);
+      const fireStub = sinon.stub(element, '_fireAction');
+      sinon.stub(element, '_canSubmitChange').returns(true);
       element._handleSubmitConfirm();
       assert.isTrue(fireStub.calledOnce);
       assert.deepEqual(fireStub.lastCall.args,
@@ -343,16 +323,16 @@
     });
 
     test('_handleSubmitConfirm when not able to submit', () => {
-      const fireStub = sandbox.stub(element, '_fireAction');
-      sandbox.stub(element, '_canSubmitChange').returns(false);
+      const fireStub = sinon.stub(element, '_fireAction');
+      sinon.stub(element, '_canSubmitChange').returns(false);
       element._handleSubmitConfirm();
       assert.isFalse(fireStub.called);
     });
 
     test('submit change with plugin hook', done => {
-      sandbox.stub(element, '_canSubmitChange',
+      sinon.stub(element, '_canSubmitChange').callsFake(
           () => false);
-      const fireActionStub = sandbox.stub(element, '_fireAction');
+      const fireActionStub = sinon.stub(element, '_fireAction');
       flush(() => {
         const submitButton = element.shadowRoot
             .querySelector('gr-button[data-action-key="submit"]');
@@ -392,8 +372,8 @@
     });
 
     test('rebase change', done => {
-      const fireActionStub = sandbox.stub(element, '_fireAction');
-      const fetchChangesStub = sandbox.stub(element.$.confirmRebase,
+      const fireActionStub = sinon.stub(element, '_fireAction');
+      const fetchChangesStub = sinon.stub(element.$.confirmRebase,
           'fetchRecentChanges').returns(Promise.resolve([]));
       element._hasKnownChainState = true;
       flush(() => {
@@ -418,8 +398,8 @@
     });
 
     test('rebase change calls navigateToChange', done => {
-      const navigateToChangeStub = sandbox.stub(GerritNav, 'navigateToChange');
-      sandbox.stub(element.$.restAPI, 'getResponseObject').returns(
+      const navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
+      sinon.stub(element.$.restAPI, 'getResponseObject').returns(
           Promise.resolve({}));
       element._handleResponse({__key: 'rebase'}, {});
       flush(() => {
@@ -429,7 +409,7 @@
     });
 
     test(`rebase dialog gets recent changes each time it's opened`, done => {
-      const fetchChangesStub = sandbox.stub(element.$.confirmRebase,
+      const fetchChangesStub = sinon.stub(element.$.confirmRebase,
           'fetchRecentChanges').returns(Promise.resolve([]));
       element._hasKnownChainState = true;
       const rebaseButton = element.shadowRoot
@@ -457,7 +437,7 @@
         MockInteractions.tap(rebaseButton);
         flushAsynchronousOperations();
         assert.isFalse(element.$.confirmRebase.hidden);
-        sandbox.stub(element.$.restAPI, 'getChanges')
+        sinon.stub(element.$.restAPI, 'getChanges')
             .returns(Promise.resolve([]));
         element._handleCherrypickTap();
         flush(() => {
@@ -469,7 +449,7 @@
     });
 
     test('fullscreen-overlay-opened hides content', () => {
-      sandbox.spy(element, '_handleHideBackgroundContent');
+      sinon.spy(element, '_handleHideBackgroundContent');
       element.$.overlay.dispatchEvent(
           new CustomEvent('fullscreen-overlay-opened', {
             composed: true, bubbles: true,
@@ -479,7 +459,7 @@
     });
 
     test('fullscreen-overlay-closed shows content', () => {
-      sandbox.spy(element, '_handleShowBackgroundContent');
+      sinon.spy(element, '_handleShowBackgroundContent');
       element.$.overlay.dispatchEvent(
           new CustomEvent('fullscreen-overlay-closed', {
             composed: true, bubbles: true,
@@ -491,8 +471,8 @@
     test('_setLabelValuesOnRevert', () => {
       const labels = {'Foo': 1, 'Bar-Baz': -2};
       const changeId = 1234;
-      sandbox.stub(element.$.jsAPI, 'getLabelValuesPostRevert').returns(labels);
-      const saveStub = sandbox.stub(element.$.restAPI, 'saveChangeReview')
+      sinon.stub(element.$.jsAPI, 'getLabelValuesPostRevert').returns(labels);
+      const saveStub = sinon.stub(element.$.restAPI, 'saveChangeReview')
           .returns(Promise.resolve());
       return element._setLabelValuesOnRevert(changeId).then(() => {
         assert.isTrue(saveStub.calledOnce);
@@ -525,7 +505,7 @@
         element.set('editMode', true);
         element.set('editPatchsetLoaded', true);
 
-        const fireActionStub = sandbox.stub(element, '_fireAction');
+        const fireActionStub = sinon.stub(element, '_fireAction');
         element._handleDeleteEditTap();
         assert.isFalse(element.$.confirmDeleteEditDialog.hidden);
         MockInteractions.tap(
@@ -658,8 +638,8 @@
       let fireActionStub;
 
       setup(() => {
-        fireActionStub = sandbox.stub(element, '_fireAction');
-        sandbox.stub(window, 'alert');
+        fireActionStub = sinon.stub(element, '_fireAction');
+        sinon.stub(window, 'alert');
       });
 
       test('works', () => {
@@ -767,7 +747,7 @@
           },
         ];
         setup(done => {
-          sandbox.stub(element.$.restAPI, 'getChanges')
+          sinon.stub(element.$.restAPI, 'getChanges')
               .returns(Promise.resolve(changes));
           element._handleCherrypickTap();
           flush(() => {
@@ -832,8 +812,8 @@
       let fireActionStub;
 
       setup(() => {
-        fireActionStub = sandbox.stub(element, '_fireAction');
-        sandbox.stub(window, 'alert');
+        fireActionStub = sinon.stub(element, '_fireAction');
+        sinon.stub(window, 'alert');
       });
 
       test('works', () => {
@@ -914,8 +894,8 @@
       let fireActionStub;
 
       setup(() => {
-        fireActionStub = sandbox.stub(element, '_fireAction');
-        alertStub = sandbox.stub(window, 'alert');
+        fireActionStub = sinon.stub(element, '_fireAction');
+        alertStub = sinon.stub(window, 'alert');
         element.actions = {
           abandon: {
             method: 'POST',
@@ -984,7 +964,7 @@
       let fireActionStub;
 
       setup(() => {
-        fireActionStub = sandbox.stub(element, '_fireAction');
+        fireActionStub = sinon.stub(element, '_fireAction');
         element.commitMessage = 'random commit message';
         element.change.current_revision = 'abcdef';
         element.actions = {
@@ -1000,18 +980,18 @@
 
       test('revert change with plugin hook', done => {
         const newRevertMsg = 'Modified revert msg';
-        sandbox.stub(element.$.confirmRevertDialog, '_modifyRevertMsg',
+        sinon.stub(element.$.confirmRevertDialog, '_modifyRevertMsg').callsFake(
             () => newRevertMsg);
         element.change = {
           current_revision: 'abc1234',
         };
-        sandbox.stub(element.$.restAPI, 'getChanges')
+        sinon.stub(element.$.restAPI, 'getChanges')
             .returns(Promise.resolve([
               {change_id: '12345678901234', topic: 'T', subject: 'random'},
               {change_id: '23456', topic: 'T', subject: 'a'.repeat(100)},
             ]));
-        sandbox.stub(element.$.confirmRevertDialog,
-            '_populateRevertSubmissionMessage', () => 'original msg');
+        sinon.stub(element.$.confirmRevertDialog,
+            '_populateRevertSubmissionMessage').callsFake(() => 'original msg');
         flush(() => {
           const revertButton = element.shadowRoot
               .querySelector('gr-button[data-action-key="revert"]');
@@ -1030,7 +1010,7 @@
             submission_id: '199 0',
             current_revision: '2000',
           };
-          getChangesStub = sandbox.stub(element.$.restAPI, 'getChanges')
+          getChangesStub = sinon.stub(element.$.restAPI, 'getChanges')
               .returns(Promise.resolve([
                 {change_id: '12345678901234', topic: 'T', subject: 'random'},
                 {change_id: '23456', topic: 'T', subject: 'a'.repeat(100)},
@@ -1077,7 +1057,7 @@
               .querySelector('gr-button[data-action-key="revert"]');
           const confirmRevertDialog = element.$.confirmRevertDialog;
           MockInteractions.tap(revertButton);
-          const fireStub = sandbox.stub(confirmRevertDialog, 'dispatchEvent');
+          const fireStub = sinon.stub(confirmRevertDialog, 'dispatchEvent');
           flush(() => {
             const confirmButton = element.$.confirmRevertDialog.shadowRoot
                 .querySelector('gr-dialog')
@@ -1140,7 +1120,7 @@
             submission_id: '199',
             current_revision: '2000',
           };
-          sandbox.stub(element.$.restAPI, 'getChanges')
+          sinon.stub(element.$.restAPI, 'getChanges')
               .returns(Promise.resolve([
                 {change_id: '12345678901234', topic: 'T', subject: 'random'},
               ]));
@@ -1151,7 +1131,7 @@
               .querySelector('gr-button[data-action-key="revert"]');
           const confirmRevertDialog = element.$.confirmRevertDialog;
           MockInteractions.tap(revertButton);
-          const fireStub = sandbox.stub(confirmRevertDialog, 'dispatchEvent');
+          const fireStub = sinon.stub(confirmRevertDialog, 'dispatchEvent');
           flush(() => {
             const confirmButton = element.$.confirmRevertDialog.shadowRoot
                 .querySelector('gr-dialog')
@@ -1303,7 +1283,7 @@
       let deleteAction;
 
       setup(() => {
-        fireActionStub = sandbox.stub(element, '_fireAction');
+        fireActionStub = sinon.stub(element, '_fireAction');
         element.change = {
           current_revision: 'abc1234',
         };
@@ -1352,7 +1332,7 @@
 
     suite('ignore change', () => {
       setup(done => {
-        sandbox.stub(element, '_fireAction');
+        sinon.stub(element, '_fireAction');
 
         const IgnoreAction = {
           __key: 'ignore',
@@ -1395,7 +1375,7 @@
 
     suite('unignore change', () => {
       setup(done => {
-        sandbox.stub(element, '_fireAction');
+        sinon.stub(element, '_fireAction');
 
         const UnignoreAction = {
           __key: 'unignore',
@@ -1438,7 +1418,7 @@
 
     suite('reviewed change', () => {
       setup(done => {
-        sandbox.stub(element, '_fireAction');
+        sinon.stub(element, '_fireAction');
 
         const ReviewedAction = {
           __key: 'reviewed',
@@ -1495,7 +1475,7 @@
 
     suite('unreviewed change', () => {
       setup(done => {
-        sandbox.stub(element, '_fireAction');
+        sinon.stub(element, '_fireAction');
 
         const UnreviewedAction = {
           __key: 'unreviewed',
@@ -1627,7 +1607,7 @@
       });
 
       test('approves when tapped', () => {
-        const fireActionStub = sandbox.stub(element, '_fireAction');
+        const fireActionStub = sinon.stub(element, '_fireAction');
         MockInteractions.tap(
             element.shadowRoot
                 .querySelector('gr-button[data-action-key=\'review\']'));
@@ -1731,7 +1711,7 @@
     });
 
     test('adds download revision action', () => {
-      const handler = sandbox.stub();
+      const handler = sinon.stub();
       element.addEventListener('download-tap', handler);
       assert.ok(element.revisionActions.download);
       element._handleDownloadTap();
@@ -1741,7 +1721,7 @@
     });
 
     test('changing changeNum or patchNum does not reload', () => {
-      const reloadStub = sandbox.stub(element, 'reload');
+      const reloadStub = sinon.stub(element, 'reload');
       element.changeNum = 123;
       assert.isFalse(reloadStub.called);
       element.latestPatchNum = 456;
@@ -1783,7 +1763,7 @@
 
       suite('_waitForChangeReachable', () => {
         setup(() => {
-          sandbox.stub(element, 'async', fn => fn());
+          sinon.stub(element, 'async').callsFake( fn => fn());
         });
 
         const makeGetChange = numTries => () => {
@@ -1796,14 +1776,16 @@
         };
 
         test('succeed', () => {
-          sandbox.stub(element.$.restAPI, 'getChange', makeGetChange(5));
+          sinon.stub(element.$.restAPI, 'getChange')
+              .callsFake( makeGetChange(5));
           return element._waitForChangeReachable(123).then(success => {
             assert.isTrue(success);
           });
         });
 
         test('fail', () => {
-          sandbox.stub(element.$.restAPI, 'getChange', makeGetChange(6));
+          sinon.stub(element.$.restAPI, 'getChange')
+              .callsFake( makeGetChange(6));
           return element._waitForChangeReachable(123).then(success => {
             assert.isFalse(success);
           });
@@ -1834,15 +1816,15 @@
       suite('happy path', () => {
         let sendStub;
         setup(() => {
-          sandbox.stub(element, 'fetchChangeUpdates')
+          sinon.stub(element, 'fetchChangeUpdates')
               .returns(Promise.resolve({isLatest: true}));
-          sendStub = sandbox.stub(element.$.restAPI, 'executeChangeAction')
+          sendStub = sinon.stub(element.$.restAPI, 'executeChangeAction')
               .returns(Promise.resolve({}));
-          getResponseObjectStub = sandbox.stub(element.$.restAPI,
+          getResponseObjectStub = sinon.stub(element.$.restAPI,
               'getResponseObject');
-          sandbox.stub(GerritNav,
+          sinon.stub(GerritNav,
               'navigateToChange').returns(Promise.resolve(true));
-          sandbox.stub(element, 'computeLatestPatchNum')
+          sinon.stub(element, 'computeLatestPatchNum')
               .returns(element.latestPatchNum);
         });
 
@@ -1862,7 +1844,7 @@
           setup(() => {
             element.change.submission_id = '199';
             element.change.current_revision = '2000';
-            sandbox.stub(element.$.restAPI, 'getChanges')
+            sinon.stub(element.$.restAPI, 'getChanges')
                 .returns(Promise.resolve([
                   {change_id: '12345678901234', topic: 'T', subject: 'random'},
                   {change_id: '23456', topic: 'T', subject: 'a'.repeat(100)},
@@ -1877,7 +1859,7 @@
               '23456: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
               '\n';
             const modifiedMsg = expectedMsg + 'abcd';
-            sandbox.stub(element.$.confirmRevertSubmissionDialog,
+            sinon.stub(element.$.confirmRevertSubmissionDialog,
                 '_modifyRevertSubmissionMsg').returns(modifiedMsg);
             element.showRevertSubmissionDialog();
             flush(() => {
@@ -1895,7 +1877,7 @@
                 .returns(Promise.resolve({revert_changes: [
                   {change_id: 12345},
                 ]}));
-            navigateToSearchQueryStub = sandbox.stub(GerritNav,
+            navigateToSearchQueryStub = sinon.stub(GerritNav,
                 'navigateToSearchQuery');
           });
 
@@ -1920,8 +1902,8 @@
                   {change_id: 12345, topic: 'T'},
                   {change_id: 23456, topic: 'T'},
                 ]}));
-            showActionDialogStub = sandbox.stub(element, '_showActionDialog');
-            navigateToSearchQueryStub = sandbox.stub(GerritNav,
+            showActionDialogStub = sinon.stub(element, '_showActionDialog');
+            navigateToSearchQueryStub = sinon.stub(GerritNav,
                 'navigateToSearchQuery');
           });
 
@@ -1954,9 +1936,9 @@
 
       suite('failure modes', () => {
         test('non-latest', () => {
-          sandbox.stub(element, 'fetchChangeUpdates')
+          sinon.stub(element, 'fetchChangeUpdates')
               .returns(Promise.resolve({isLatest: false}));
-          const sendStub = sandbox.stub(element.$.restAPI,
+          const sendStub = sinon.stub(element.$.restAPI,
               'executeChangeAction');
 
           return element._send('DELETE', payload, '/endpoint', true, cleanup)
@@ -1969,15 +1951,15 @@
         });
 
         test('send fails', () => {
-          sandbox.stub(element, 'fetchChangeUpdates')
+          sinon.stub(element, 'fetchChangeUpdates')
               .returns(Promise.resolve({isLatest: true}));
-          const sendStub = sandbox.stub(element.$.restAPI,
-              'executeChangeAction',
+          const sendStub = sinon.stub(element.$.restAPI,
+              'executeChangeAction').callsFake(
               (num, method, patchNum, endpoint, payload, onErr) => {
                 onErr();
                 return Promise.resolve(null);
               });
-          const handleErrorStub = sandbox.stub(element, '_handleResponseError');
+          const handleErrorStub = sinon.stub(element, '_handleResponseError');
 
           return element._send('DELETE', payload, '/endpoint', true, cleanup)
               .then(() => {
@@ -1991,8 +1973,8 @@
     });
 
     test('_handleAction reports', () => {
-      sandbox.stub(element, '_fireAction');
-      const reportStub = sandbox.stub(element.reporting, 'reportInteraction');
+      sinon.stub(element, '_fireAction');
+      const reportStub = sinon.stub(element.reporting, 'reportInteraction');
       element._handleAction('type', 'key');
       assert.isTrue(reportStub.called);
       assert.equal(reportStub.lastCall.args[0], 'type-key');
@@ -2001,7 +1983,7 @@
 
   suite('getChangeRevisionActions returns only some actions', () => {
     let element;
-    let sandbox;
+
     let changeRevisionActions;
 
     setup(() => {
@@ -2015,28 +1997,23 @@
         getProjectConfig() { return Promise.resolve({}); },
       });
 
-      sandbox = sinon.sandbox.create();
-      sandbox.stub(pluginLoader, 'awaitPluginsLoaded')
+      sinon.stub(pluginLoader, 'awaitPluginsLoaded')
           .returns(Promise.resolve());
 
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       // getChangeRevisionActions is not called without
       // set the following properies
       element.change = {};
       element.changeNum = '42';
       element.latestPatchNum = '2';
 
-      sandbox.stub(element.$.confirmCherrypick.$.restAPI,
+      sinon.stub(element.$.confirmCherrypick.$.restAPI,
           'getRepoBranches').returns(Promise.resolve([]));
-      sandbox.stub(element.$.confirmMove.$.restAPI,
+      sinon.stub(element.$.confirmMove.$.restAPI,
           'getRepoBranches').returns(Promise.resolve([]));
       return element.reload();
     });
 
-    teardown(() => {
-      sandbox.restore();
-    });
-
     test('confirmSubmitDialog and confirmRebase properties are changed', () => {
       changeRevisionActions = {};
       element.reload();
@@ -2064,4 +2041,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.js
index c3f6113..50c2355 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.js
@@ -54,7 +54,6 @@
 const pluginApi = _testOnly_initGerritPluginApi();
 
 suite('gr-change-metadata integration tests', () => {
-  let sandbox;
   let element;
 
   const sectionSelectors = [
@@ -89,7 +88,6 @@
   }
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     stub('gr-rest-api-interface', {
       getConfig() { return Promise.resolve({}); },
       getLoggedIn() { return Promise.resolve(false); },
@@ -98,7 +96,6 @@
   });
 
   teardown(() => {
-    sandbox.restore();
     resetPlugins();
   });
 
@@ -122,13 +119,19 @@
         plugin.registerStyleModule('change-metadata', 'my-plugin-style');
       }, undefined, 'http://test.com/plugins/style.js');
       element = createElement();
-      sandbox.stub(pluginEndpoints, 'importUrl', url => Promise.resolve());
+      sinon.stub(pluginEndpoints, 'importUrl')
+          .callsFake( url => Promise.resolve());
       pluginLoader.loadPlugins([]);
       pluginLoader.awaitPluginsLoaded().then(() => {
         flush(done);
       });
     });
 
+    teardown(() => {
+      document.body.querySelectorAll('custom-style')
+          .forEach(style => style.remove());
+    });
+
     for (const sectionSelector of sectionSelectors) {
       test('section.strategy may have display: none', () => {
         assert.equal(getStyle(sectionSelector, 'display'), 'none');
@@ -144,14 +147,19 @@
         plugin = p;
         plugin.registerStyleModule('change-metadata', 'my-plugin-style');
       }, undefined, 'http://test.com/plugins/style.js');
-      sandbox.stub(pluginLoader, 'arePluginsLoaded').returns(true);
+      sinon.stub(pluginLoader, 'arePluginsLoaded').returns(true);
       pluginLoader.loadPlugins([]);
       element = createElement();
     });
 
+    teardown(() => {
+      document.body.querySelectorAll('custom-style')
+          .forEach(style => style.remove());
+    });
+
     test('labels changed callback', done => {
       let callCount = 0;
-      const labelChangeSpy = sandbox.spy(arg => {
+      const labelChangeSpy = sinon.spy(arg => {
         callCount++;
         if (callCount === 1) {
           assert.deepEqual(arg, labels);
@@ -177,4 +185,4 @@
       plugin.changeMetadata().onLabelsChanged(labelChangeSpy);
     });
   });
-});
\ No newline at end of file
+});
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.js
index 27f9b85..26baf3c 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.js
@@ -29,21 +29,16 @@
 
 suite('gr-change-metadata tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     stub('gr-rest-api-interface', {
       getConfig() { return Promise.resolve({}); },
       getLoggedIn() { return Promise.resolve(false); },
     });
 
     element = basicFixture.instantiate();
-    sandbox.stub(pluginEndpoints, 'importUrl', url => Promise.resolve());
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    sinon.stub(pluginEndpoints, 'importUrl')
+        .callsFake( url => Promise.resolve());
   });
 
   test('computed fields', () => {
@@ -111,7 +106,7 @@
   });
 
   test('weblinks use GerritNav interface', () => {
-    const weblinksStub = sandbox.stub(GerritNav, '_generateWeblinks')
+    const weblinksStub = sinon.stub(GerritNav, '_generateWeblinks')
         .returns([{name: 'stubb', url: '#s'}]);
     element.commitInfo = {};
     element.serverConfig = {};
@@ -154,7 +149,7 @@
 
   test('weblinks are visible when other weblinks', () => {
     const router = document.createElement('gr-router');
-    sandbox.stub(GerritNav, '_generateWeblinks',
+    sinon.stub(GerritNav, '_generateWeblinks').callsFake(
         router._generateWeblinks.bind(router));
 
     element.commitInfo = {web_links: [{name: 'test', url: '#'}]};
@@ -170,7 +165,7 @@
 
   test('weblinks are visible when gitiles and other weblinks', () => {
     const router = document.createElement('gr-router');
-    sandbox.stub(GerritNav, '_generateWeblinks',
+    sinon.stub(GerritNav, '_generateWeblinks').callsFake(
         router._generateWeblinks.bind(router));
 
     element.commitInfo = {
@@ -612,7 +607,7 @@
 
   suite('remove reviewer votes', () => {
     setup(() => {
-      sandbox.stub(element, '_computeTopicReadOnly').returns(true);
+      sinon.stub(element, '_computeTopicReadOnly').returns(true);
       element.change = {
         _number: 42,
         change_id: 'the id',
@@ -647,8 +642,8 @@
       let setStub;
 
       setup(() => {
-        deleteStub = sandbox.stub(element.$.restAPI, 'deleteAssignee');
-        setStub = sandbox.stub(element.$.restAPI, 'setAssignee');
+        deleteStub = sinon.stub(element.$.restAPI, 'deleteAssignee');
+        setStub = sinon.stub(element.$.restAPI, 'setAssignee');
         element.serverConfig = {
           change: {
             enable_assignee: true,
@@ -692,10 +687,10 @@
 
     test('changing topic', () => {
       const newTopic = 'the new topic';
-      sandbox.stub(element.$.restAPI, 'setChangeTopic').returns(
+      sinon.stub(element.$.restAPI, 'setChangeTopic').returns(
           Promise.resolve(newTopic));
       element._handleTopicChanged({}, newTopic);
-      const topicChangedSpy = sandbox.spy();
+      const topicChangedSpy = sinon.spy();
       element.addEventListener('topic-changed', topicChangedSpy);
       assert.isTrue(element.$.restAPI.setChangeTopic.calledWith(
           42, newTopic));
@@ -707,12 +702,12 @@
     });
 
     test('topic removal', () => {
-      sandbox.stub(element.$.restAPI, 'setChangeTopic').returns(
+      sinon.stub(element.$.restAPI, 'setChangeTopic').returns(
           Promise.resolve());
       const chip = element.shadowRoot
           .querySelector('gr-linked-chip');
       const remove = chip.$.remove;
-      const topicChangedSpy = sandbox.spy();
+      const topicChangedSpy = sinon.spy();
       element.addEventListener('topic-changed', topicChangedSpy);
       MockInteractions.tap(remove);
       assert.isTrue(chip.disabled);
@@ -730,7 +725,7 @@
       flushAsynchronousOperations();
       element._newHashtag = 'new hashtag';
       const newHashtag = ['new hashtag'];
-      sandbox.stub(element.$.restAPI, 'setChangeHashtag').returns(
+      sinon.stub(element.$.restAPI, 'setChangeHashtag').returns(
           Promise.resolve(newHashtag));
       element._handleHashtagChanged({}, 'new hashtag');
       assert.isTrue(element.$.restAPI.setChangeHashtag.calledWith(
@@ -750,7 +745,7 @@
     const label = element.shadowRoot
         .querySelector('.topicEditableLabel');
     assert.ok(label);
-    sandbox.stub(label, 'open');
+    sinon.stub(label, 'open');
     element.editTopic();
     flushAsynchronousOperations();
 
@@ -780,4 +775,4 @@
       });
     });
   });
-});
\ No newline at end of file
+});
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.js
similarity index 81%
rename from polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
rename to polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.js
index 276e821..d8a90fc 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.js
@@ -1,45 +1,31 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2018 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-change-requirements</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-change-requirements></gr-change-requirements>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-change-requirements.js';
 import {isHidden} from '../../../test/test-utils.js';
+
+const basicFixture = fixtureFromElement('gr-change-requirements');
+
 suite('gr-change-metadata tests', () => {
   let element;
 
   setup(() => {
-    element = fixture('basic');
+    element = basicFixture.instantiate();
   });
 
   test('requirements computed fields', () => {
@@ -233,4 +219,4 @@
     assert.strictEqual(requirement.querySelector('.approved'), null);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
index 2f607a6..d2e8ea7 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
@@ -21,7 +21,6 @@
 import {PrimaryTab, SecondaryTab, ChangeStatus} from '../../../constants/constants.js';
 
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import {GrEditConstants} from '../../edit/gr-edit-constants.js';
 import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
 import {getComputedStyleValue} from '../../../utils/dom-util.js';
@@ -30,27 +29,35 @@
 import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
 
 import 'lodash/lodash.js';
+import {TestKeyboardShortcutBinder} from '../../../test/test-utils.js';
 
 const pluginApi = _testOnly_initGerritPluginApi();
 const fixture = fixtureFromElement('gr-change-view');
 
 suite('gr-change-view tests', () => {
-  const kb = KeyboardShortcutBinder;
-  kb.bindShortcut(kb.Shortcut.SEND_REPLY, 'ctrl+enter');
-  kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE, 'shift+r');
-  kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a');
-  kb.bindShortcut(kb.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
-  kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm');
-  kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's');
-  kb.bindShortcut(kb.Shortcut.UP_TO_DASHBOARD, 'u');
-  kb.bindShortcut(kb.Shortcut.EXPAND_ALL_MESSAGES, 'x');
-  kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
-  kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ',');
-  kb.bindShortcut(kb.Shortcut.EDIT_TOPIC, 't');
-
   let element;
-  let sandbox;
+
   let navigateToChangeStub;
+
+  suiteSetup(() => {
+    const kb = TestKeyboardShortcutBinder.push();
+    kb.bindShortcut(kb.Shortcut.SEND_REPLY, 'ctrl+enter');
+    kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE, 'shift+r');
+    kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a');
+    kb.bindShortcut(kb.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
+    kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm');
+    kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's');
+    kb.bindShortcut(kb.Shortcut.UP_TO_DASHBOARD, 'u');
+    kb.bindShortcut(kb.Shortcut.EXPAND_ALL_MESSAGES, 'x');
+    kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
+    kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ',');
+    kb.bindShortcut(kb.Shortcut.EDIT_TOPIC, 't');
+  });
+
+  suiteTeardown(() => {
+    TestKeyboardShortcutBinder.pop();
+  });
+
   const TEST_SCROLL_TOP_PX = 100;
 
   const ROBOT_COMMENTS_LIMIT = 10;
@@ -278,10 +285,9 @@
   ];
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     // Since pluginEndpoints are global, must reset state.
     _testOnly_resetEndpoints();
-    navigateToChangeStub = sandbox.stub(GerritNav, 'navigateToChange');
+    navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
     stub('gr-rest-api-interface', {
       getConfig() { return Promise.resolve({test: 'config'}); },
       getAccount() { return Promise.resolve(null); },
@@ -291,7 +297,7 @@
       _fetchSharedCacheURL() { return Promise.resolve({}); },
     });
     element = fixture.instantiate();
-    sandbox.stub(element.$.actions, 'reload').returns(Promise.resolve());
+    sinon.stub(element.$.actions, 'reload').returns(Promise.resolve());
     pluginLoader.loadPlugins([]);
     pluginApi.install(
         plugin => {
@@ -311,7 +317,6 @@
 
   teardown(done => {
     flush(() => {
-      sandbox.restore();
       done();
     });
   });
@@ -325,8 +330,8 @@
       basePatchNum: 'PARENT',
       patchNum: 1,
     };
-    const getUrlStub = sandbox.stub(GerritNav, 'getUrlForChange');
-    const replaceStateStub = sandbox.stub(history, 'replaceState');
+    const getUrlStub = sinon.stub(GerritNav, 'getUrlForChange');
+    const replaceStateStub = sinon.stub(history, 'replaceState');
     element._handleMessageAnchorTap({detail: {id: 'a12345'}});
 
     assert.equal(getUrlStub.lastCall.args[4], '#message-a12345');
@@ -339,8 +344,8 @@
       patchNum: 3,
       basePatchNum: 1,
     };
-    sandbox.stub(element, 'computeLatestPatchNum').returns(10);
-    sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+    sinon.stub(element, 'computeLatestPatchNum').returns(10);
+    sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
     element._handleDiffAgainstBase(new CustomEvent(''));
     assert(navigateToChangeStub.called);
     const args = navigateToChangeStub.getCall(0).args;
@@ -354,8 +359,8 @@
       basePatchNum: 1,
       patchNum: 3,
     };
-    sandbox.stub(element, 'computeLatestPatchNum').returns(10);
-    sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+    sinon.stub(element, 'computeLatestPatchNum').returns(10);
+    sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
     element._handleDiffAgainstLatest(new CustomEvent(''));
     assert(navigateToChangeStub.called);
     const args = navigateToChangeStub.getCall(0).args;
@@ -369,8 +374,8 @@
       patchNum: 3,
       basePatchNum: 1,
     };
-    sandbox.stub(element, 'computeLatestPatchNum').returns(10);
-    sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+    sinon.stub(element, 'computeLatestPatchNum').returns(10);
+    sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
     element._handleDiffBaseAgainstLeft(new CustomEvent(''));
     assert(navigateToChangeStub.called);
     const args = navigateToChangeStub.getCall(0).args;
@@ -384,8 +389,8 @@
       basePatchNum: 1,
       patchNum: 3,
     };
-    sandbox.stub(element, 'computeLatestPatchNum').returns(10);
-    sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+    sinon.stub(element, 'computeLatestPatchNum').returns(10);
+    sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
     element._handleDiffRightAgainstLatest(new CustomEvent(''));
     assert(navigateToChangeStub.called);
     const args = navigateToChangeStub.getCall(0).args;
@@ -399,8 +404,8 @@
       basePatchNum: 1,
       patchNum: 3,
     };
-    sandbox.stub(element, 'computeLatestPatchNum').returns(10);
-    sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+    sinon.stub(element, 'computeLatestPatchNum').returns(10);
+    sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
     element._handleDiffBaseAgainstLatest(new CustomEvent(''));
     assert(navigateToChangeStub.called);
     const args = navigateToChangeStub.getCall(0).args;
@@ -494,19 +499,19 @@
 
   suite('keyboard shortcuts', () => {
     test('t to add topic', () => {
-      const editStub = sandbox.stub(element.$.metadata, 'editTopic');
+      const editStub = sinon.stub(element.$.metadata, 'editTopic');
       MockInteractions.pressAndReleaseKeyOn(element, 83, null, 't');
       assert(editStub.called);
     });
 
     test('S should toggle the CL star', () => {
-      const starStub = sandbox.stub(element.$.changeStar, 'toggleStar');
+      const starStub = sinon.stub(element.$.changeStar, 'toggleStar');
       MockInteractions.pressAndReleaseKeyOn(element, 83, null, 's');
       assert(starStub.called);
     });
 
     test('U should navigate to root if no backPage set', () => {
-      const relativeNavStub = sandbox.stub(GerritNav,
+      const relativeNavStub = sinon.stub(GerritNav,
           'navigateToRelativeUrl');
       MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
       assert.isTrue(relativeNavStub.called);
@@ -515,7 +520,7 @@
     });
 
     test('U should navigate to backPage if set', () => {
-      const relativeNavStub = sandbox.stub(GerritNav,
+      const relativeNavStub = sinon.stub(GerritNav,
           'navigateToRelativeUrl');
       element.backPage = '/dashboard/self';
       MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
@@ -525,8 +530,8 @@
     });
 
     test('A fires an error event when not logged in', done => {
-      sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(false));
-      const loggedInErrorSpy = sandbox.spy();
+      sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(false));
+      const loggedInErrorSpy = sinon.spy();
       element.addEventListener('show-auth-required', loggedInErrorSpy);
       MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
       flush(() => {
@@ -537,7 +542,7 @@
     });
 
     test('shift A does not open reply overlay', done => {
-      sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
+      sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
       MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
       flush(() => {
         assert.isFalse(element.$.replyOverlay.opened);
@@ -546,11 +551,11 @@
     });
 
     test('A toggles overlay when logged in', done => {
-      sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
-      sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates')
+      sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
+      sinon.stub(element.$.replyDialog, 'fetchChangeUpdates')
           .returns(Promise.resolve({isLatest: true}));
       element._change = {labels: {}};
-      const openSpy = sandbox.spy(element, '_openReplyDialog');
+      const openSpy = sinon.spy(element, '_openReplyDialog');
 
       MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
       flush(() => {
@@ -580,7 +585,7 @@
           },
         },
       };
-      sandbox.spy(element, '_handleHideBackgroundContent');
+      sinon.spy(element, '_handleHideBackgroundContent');
       element.$.replyDialog.dispatchEvent(
           new CustomEvent('fullscreen-overlay-opened', {
             composed: true, bubbles: true,
@@ -605,7 +610,7 @@
           },
         },
       };
-      sandbox.spy(element, '_handleShowBackgroundContent');
+      sinon.spy(element, '_handleShowBackgroundContent');
       element.$.replyDialog.dispatchEvent(
           new CustomEvent('fullscreen-overlay-closed', {
             composed: true, bubbles: true,
@@ -616,7 +621,7 @@
 
     test('expand all messages when expand-diffs fired', () => {
       const handleExpand =
-          sandbox.stub(element.$.fileList, 'expandAllDiffs');
+          sinon.stub(element.$.fileList, 'expandAllDiffs');
       element.$.fileListHeader.dispatchEvent(
           new CustomEvent('expand-diffs', {
             composed: true, bubbles: true,
@@ -626,7 +631,7 @@
 
     test('collapse all messages when collapse-diffs fired', () => {
       const handleCollapse =
-      sandbox.stub(element.$.fileList, 'collapseAllDiffs');
+      sinon.stub(element.$.fileList, 'collapseAllDiffs');
       element.$.fileListHeader.dispatchEvent(
           new CustomEvent('collapse-diffs', {
             composed: true, bubbles: true,
@@ -636,7 +641,7 @@
 
     test('X should expand all messages', done => {
       flush(() => {
-        const handleExpand = sandbox.stub(element.messagesList,
+        const handleExpand = sinon.stub(element.messagesList,
             'handleExpandCollapse');
         MockInteractions.pressAndReleaseKeyOn(element, 88, null, 'x');
         assert(handleExpand.calledWith(true));
@@ -646,7 +651,7 @@
 
     test('Z should collapse all messages', done => {
       flush(() => {
-        const handleExpand = sandbox.stub(element.messagesList,
+        const handleExpand = sinon.stub(element.messagesList,
             'handleExpandCollapse');
         MockInteractions.pressAndReleaseKeyOn(element, 90, null, 'z');
         assert(handleExpand.calledWith(false));
@@ -674,8 +679,8 @@
           };
 
           navigateToChangeStub.restore();
-          navigateToChangeStub = sandbox.stub(GerritNav, 'navigateToChange',
-              (change, patchNum, basePatchNum) => {
+          navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange')
+              .callsFake((change, patchNum, basePatchNum) => {
                 assert.equal(change, element._change);
                 assert.isUndefined(patchNum);
                 assert.isUndefined(basePatchNum);
@@ -686,7 +691,7 @@
         });
 
     test('d should open download overlay', () => {
-      const stub = sandbox.stub(element.$.downloadOverlay, 'open').returns(
+      const stub = sinon.stub(element.$.downloadOverlay, 'open').returns(
           new Promise(resolve => {})
       );
       MockInteractions.pressAndReleaseKeyOn(element, 68, null, 'd');
@@ -694,7 +699,7 @@
     });
 
     test(', should open diff preferences', () => {
-      const stub = sandbox.stub(
+      const stub = sinon.stub(
           element.$.fileList.$.diffPreferencesDialog, 'open');
       element._loggedIn = false;
       element.disableDiffPrefs = true;
@@ -711,8 +716,8 @@
     });
 
     test('m should toggle diff mode', () => {
-      sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
-      const setModeStub = sandbox.stub(element.$.fileListHeader,
+      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+      const setModeStub = sinon.stub(element.$.fileListHeader,
           'setDiffViewMode');
       const e = {preventDefault: () => {}};
       flushAsynchronousOperations();
@@ -744,7 +749,7 @@
     setup(() => {
       // Fake computeDraftCount as its required for ChangeComments,
       // see gr-comment-api#reloadDrafts.
-      reloadStub = sandbox.stub(element.$.commentAPI, 'reloadDrafts')
+      reloadStub = sinon.stub(element.$.commentAPI, 'reloadDrafts')
           .returns(Promise.resolve({
             drafts,
             getAllThreadsForChange: () => ([]),
@@ -779,7 +784,7 @@
     setup(() => {
       // Fake computeDraftCount as its required for ChangeComments,
       // see gr-comment-api#reloadDrafts.
-      sandbox.stub(element.$.commentAPI, 'reloadDrafts')
+      sinon.stub(element.$.commentAPI, 'reloadDrafts')
           .returns(Promise.resolve({
             drafts: {},
             getAllThreadsForChange: () => THREADS,
@@ -826,7 +831,7 @@
   });
 
   test('diff comments modified', () => {
-    sandbox.spy(element, '_handleReloadCommentThreads');
+    sinon.spy(element, '_handleReloadCommentThreads');
     return element._reloadComments().then(() => {
       element.dispatchEvent(
           new CustomEvent('diff-comments-modified', {
@@ -837,7 +842,7 @@
   });
 
   test('thread list modified', () => {
-    sandbox.spy(element, '_handleReloadDiffComments');
+    sinon.spy(element, '_handleReloadDiffComments');
     element._activeTabs = [PrimaryTab.COMMENT_THREADS, SecondaryTab.CHANGE_LOG];
     flushAsynchronousOperations();
 
@@ -896,9 +901,9 @@
           },
         },
       };
-      sandbox.stub(element.$.relatedChanges, 'reload');
-      sandbox.stub(element, '_reload').returns(Promise.resolve());
-      sandbox.spy(element, '_paramsChanged');
+      sinon.stub(element.$.relatedChanges, 'reload');
+      sinon.stub(element, '_reload').returns(Promise.resolve());
+      sinon.spy(element, '_paramsChanged');
       element.params = {view: 'change', changeNum: '1'};
     });
   });
@@ -996,7 +1001,7 @@
   });
 
   test('download tap calls _handleOpenDownloadDialog', () => {
-    sandbox.stub(element, '_handleOpenDownloadDialog');
+    sinon.stub(element, '_handleOpenDownloadDialog');
     element.$.actions.dispatchEvent(
         new CustomEvent('download-tap', {
           composed: true, bubbles: true,
@@ -1012,7 +1017,7 @@
   });
 
   test('_changeStatuses', () => {
-    sandbox.stub(element, 'changeStatuses').returns(
+    sinon.stub(element, 'changeStatuses').returns(
         ['Merged', 'WIP']);
     element._loading = false;
     element._change = {
@@ -1044,7 +1049,7 @@
   });
 
   test('diff preferences open when open-diff-prefs is fired', () => {
-    const overlayOpenStub = sandbox.stub(element.$.fileList,
+    const overlayOpenStub = sinon.stub(element.$.fileList,
         'openDiffPrefs');
     element.$.fileListHeader.dispatchEvent(
         new CustomEvent('open-diff-prefs', {
@@ -1102,7 +1107,7 @@
       },
     };
     flushAsynchronousOperations();
-    const reloadStub = sandbox.stub(element, '_reload');
+    const reloadStub = sinon.stub(element, '_reload');
     element.splice('_change.labels.test.all', 0, 1);
     assert.isFalse(reloadStub.called);
     element._change.labels.test.all.push(vote);
@@ -1207,7 +1212,7 @@
 
   test('_setDiffViewMode is called with reset when new change is loaded',
       () => {
-        sandbox.stub(element, '_setDiffViewMode');
+        sinon.stub(element, '_setDiffViewMode');
         element.viewState = {changeNum: 1};
         element._changeNum = 2;
         element._resetFileListViewState();
@@ -1222,7 +1227,7 @@
   });
 
   test('diffMode defaults to side by side without preferences', done => {
-    sandbox.stub(element.$.restAPI, 'getPreferences').returns(
+    sinon.stub(element.$.restAPI, 'getPreferences').returns(
         Promise.resolve({}));
     // No user prefs or diff view mode set.
 
@@ -1233,7 +1238,7 @@
   });
 
   test('diffMode defaults to preference when not already set', done => {
-    sandbox.stub(element.$.restAPI, 'getPreferences').returns(
+    sinon.stub(element.$.restAPI, 'getPreferences').returns(
         Promise.resolve({default_diff_view: 'UNIFIED'}));
 
     element._setDiffViewMode().then(() => {
@@ -1244,7 +1249,7 @@
 
   test('existing diffMode overrides preference', done => {
     element.viewState.diffMode = 'SIDE_BY_SIDE';
-    sandbox.stub(element.$.restAPI, 'getPreferences').returns(
+    sinon.stub(element.$.restAPI, 'getPreferences').returns(
         Promise.resolve({default_diff_view: 'UNIFIED'}));
     element._setDiffViewMode().then(() => {
       assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE');
@@ -1253,13 +1258,13 @@
   });
 
   test('don’t reload entire page when patchRange changes', () => {
-    const reloadStub = sandbox.stub(element, '_reload',
+    const reloadStub = sinon.stub(element, '_reload').callsFake(
         () => Promise.resolve());
-    const reloadPatchDependentStub = sandbox.stub(element,
-        '_reloadPatchNumDependentResources',
-        () => Promise.resolve());
-    const relatedClearSpy = sandbox.spy(element.$.relatedChanges, 'clear');
-    const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs');
+    const reloadPatchDependentStub = sinon.stub(element,
+        '_reloadPatchNumDependentResources')
+        .callsFake(() => Promise.resolve());
+    const relatedClearSpy = sinon.spy(element.$.relatedChanges, 'clear');
+    const collapseStub = sinon.stub(element.$.fileList, 'collapseAllDiffs');
 
     const value = {
       view: GerritNav.View.CHANGE,
@@ -1281,9 +1286,9 @@
   });
 
   test('reload entire page when patchRange doesnt change', () => {
-    const reloadStub = sandbox.stub(element, '_reload',
+    const reloadStub = sinon.stub(element, '_reload').callsFake(
         () => Promise.resolve());
-    const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs');
+    const collapseStub = sinon.stub(element.$.fileList, 'collapseAllDiffs');
     const value = {
       view: GerritNav.View.CHANGE,
     };
@@ -1296,8 +1301,8 @@
   });
 
   test('related changes are not updated after other action', done => {
-    sandbox.stub(element, '_reload', () => Promise.resolve());
-    sandbox.stub(element.$.relatedChanges, 'reload');
+    sinon.stub(element, '_reload').callsFake(() => Promise.resolve());
+    sinon.stub(element.$.relatedChanges, 'reload');
     const e = {detail: {action: 'abandon'}};
     element._handleReloadChange(e).then(() => {
       assert.isFalse(navigateToChangeStub.called);
@@ -1330,7 +1335,7 @@
       },
       current_revision: 'rev3',
     };
-    sandbox.stub(GerritNav, 'getUrlForChange')
+    sinon.stub(GerritNav, 'getUrlForChange')
         .returns('/change/123');
     assert.equal(
         element._computeCopyTextForTitle(change),
@@ -1372,7 +1377,7 @@
   });
 
   test('_handleCommitMessageSave trims trailing whitespace', () => {
-    const putStub = sandbox.stub(element.$.restAPI, 'putChangeCommitMessage')
+    const putStub = sinon.stub(element.$.restAPI, 'putChangeCommitMessage')
         .returns(Promise.resolve({}));
 
     const mockEvent = content => { return {detail: {content}}; };
@@ -1459,13 +1464,14 @@
   });
 
   test('topic is coalesced to null', done => {
-    sandbox.stub(element, '_changeChanged');
-    sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({
-      id: '123456789',
-      labels: {},
-      current_revision: 'foo',
-      revisions: {foo: {commit: {}}},
-    }));
+    sinon.stub(element, '_changeChanged');
+    sinon.stub(element.$.restAPI, 'getChangeDetail').callsFake(
+        () => Promise.resolve({
+          id: '123456789',
+          labels: {},
+          current_revision: 'foo',
+          revisions: {foo: {commit: {}}},
+        }));
 
     element._getChangeDetail().then(() => {
       assert.isNull(element._change.topic);
@@ -1474,13 +1480,14 @@
   });
 
   test('commit sha is populated from getChangeDetail', done => {
-    sandbox.stub(element, '_changeChanged');
-    sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({
-      id: '123456789',
-      labels: {},
-      current_revision: 'foo',
-      revisions: {foo: {commit: {}}},
-    }));
+    sinon.stub(element, '_changeChanged');
+    sinon.stub(element.$.restAPI, 'getChangeDetail').callsFake(
+        () => Promise.resolve({
+          id: '123456789',
+          labels: {},
+          current_revision: 'foo',
+          revisions: {foo: {commit: {}}},
+        }));
 
     element._getChangeDetail().then(() => {
       assert.equal('foo', element._commitInfo.commit);
@@ -1489,14 +1496,15 @@
   });
 
   test('edit is added to change', () => {
-    sandbox.stub(element, '_changeChanged');
-    sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({
-      id: '123456789',
-      labels: {},
-      current_revision: 'foo',
-      revisions: {foo: {commit: {}}},
-    }));
-    sandbox.stub(element, '_getEdit', () => Promise.resolve({
+    sinon.stub(element, '_changeChanged');
+    sinon.stub(element.$.restAPI, 'getChangeDetail').callsFake(
+        () => Promise.resolve({
+          id: '123456789',
+          labels: {},
+          current_revision: 'foo',
+          revisions: {foo: {commit: {}}},
+        }));
+    sinon.stub(element, '_getEdit').callsFake(() => Promise.resolve({
       base_patch_set_number: 1,
       commit: {commit: 'bar'},
     }));
@@ -1564,7 +1572,7 @@
 
   test('_openReplyDialog called with `ANY` when coming from tap event',
       () => {
-        const openStub = sandbox.stub(element, '_openReplyDialog');
+        const openStub = sinon.stub(element, '_openReplyDialog');
         element._serverConfig = {};
         MockInteractions.tap(element.$.replyBtn);
         assert(openStub.lastCall.calledWithExactly(
@@ -1576,7 +1584,7 @@
   test('_openReplyDialog called with `BODY` when coming from message reply' +
       'event', done => {
     flush(() => {
-      const openStub = sandbox.stub(element, '_openReplyDialog');
+      const openStub = sinon.stub(element, '_openReplyDialog');
       element.messagesList.dispatchEvent(
           new CustomEvent('reply', {
             detail:
@@ -1593,7 +1601,7 @@
 
   test('reply dialog focus can be controlled', () => {
     const FocusTarget = element.$.replyDialog.FocusTarget;
-    const openStub = sandbox.stub(element, '_openReplyDialog');
+    const openStub = sinon.stub(element, '_openReplyDialog');
 
     const e = {detail: {}};
     element._handleShowReplyDialog(e);
@@ -1609,7 +1617,7 @@
   });
 
   test('getUrlParameter functionality', () => {
-    const locationStub = sandbox.stub(element, '_getLocationSearch');
+    const locationStub = sinon.stub(element, '_getLocationSearch');
 
     locationStub.returns('?test');
     assert.equal(element._getUrlParameter('test'), 'test');
@@ -1624,8 +1632,10 @@
   });
 
   test('revert dialog opened with revert param', done => {
-    sandbox.stub(element.$.restAPI, 'getLoggedIn', () => Promise.resolve(true));
-    sandbox.stub(pluginLoader, 'awaitPluginsLoaded', () => Promise.resolve());
+    sinon.stub(element.$.restAPI, 'getLoggedIn')
+        .callsFake(() => Promise.resolve(true));
+    sinon.stub(pluginLoader, 'awaitPluginsLoaded')
+        .callsFake(() => Promise.resolve());
 
     element._patchRange = {
       basePatchNum: 'PARENT',
@@ -1643,13 +1653,13 @@
       actions: {},
     };
 
-    sandbox.stub(element, '_getUrlParameter',
+    sinon.stub(element, '_getUrlParameter').callsFake(
         param => {
           assert.equal(param, 'revert');
           return param;
         });
 
-    sandbox.stub(element.$.actions, 'showRevertDialog',
+    sinon.stub(element.$.actions, 'showRevertDialog').callsFake(
         done);
 
     element._maybeShowRevertDialog();
@@ -1659,7 +1669,7 @@
   suite('scroll related tests', () => {
     test('document scrolling calls function to set scroll height', done => {
       const originalHeight = document.body.scrollHeight;
-      const scrollStub = sandbox.stub(element, '_handleScroll',
+      const scrollStub = sinon.stub(element, '_handleScroll').callsFake(
           () => {
             assert.isTrue(scrollStub.called);
             document.body.style.height = originalHeight + 'px';
@@ -1673,7 +1683,7 @@
     test('scrollTop is set correctly', () => {
       element.viewState = {scrollTop: TEST_SCROLL_TOP_PX};
 
-      sandbox.stub(element, '_reload', () => {
+      sinon.stub(element, '_reload').callsFake(() => {
         // When element is reloaded, ensure that the history
         // state has the scrollTop set earlier. This will then
         // be reset.
@@ -1694,8 +1704,8 @@
 
   suite('reply dialog tests', () => {
     setup(() => {
-      sandbox.stub(element.$.replyDialog, '_draftChanged');
-      sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates',
+      sinon.stub(element.$.replyDialog, '_draftChanged');
+      sinon.stub(element.$.replyDialog, 'fetchChangeUpdates').callsFake(
           () => Promise.resolve({isLatest: true}));
       element._change = {labels: {}};
     });
@@ -1728,7 +1738,7 @@
       const div = document.createElement('div');
       element.$.replyDialog.draft = '> quote text\n\n some draft text';
       element.$.replyDialog.quote = '> quote text\n\n';
-      const e = {target: div, preventDefault: sandbox.spy()};
+      const e = {target: div, preventDefault: sinon.spy()};
       element._handleReplyTap(e);
       assert.equal(element.$.replyDialog.draft,
           '> quote text\n\n some draft text');
@@ -1744,7 +1754,7 @@
 
   suite('commit message expand/collapse', () => {
     setup(() => {
-      sandbox.stub(element, 'fetchChangeUpdates',
+      sinon.stub(element, 'fetchChangeUpdates').callsFake(
           () => Promise.resolve({isLatest: false}));
     });
 
@@ -1775,17 +1785,18 @@
   suite('related changes expand/collapse', () => {
     let updateHeightSpy;
     setup(() => {
-      updateHeightSpy = sandbox.spy(element, '_updateRelatedChangeMaxHeight');
+      updateHeightSpy = sinon.spy(element, '_updateRelatedChangeMaxHeight');
     });
 
     test('relatedChangesToggle shown height greater than changeInfo height',
         () => {
           assert.isFalse(element.$.relatedChangesToggle.classList
               .contains('showToggle'));
-          sandbox.stub(element, '_getOffsetHeight', () => 50);
-          sandbox.stub(element, '_getScrollHeight', () => 60);
-          sandbox.stub(element, '_getLineHeight', () => 5);
-          sandbox.stub(window, 'matchMedia', () => { return {matches: true}; });
+          sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
+          sinon.stub(element, '_getScrollHeight').callsFake(() => 60);
+          sinon.stub(element, '_getLineHeight').callsFake(() => 5);
+          sinon.stub(window, 'matchMedia')
+              .callsFake(() => { return {matches: true}; });
           element.$.relatedChanges.dispatchEvent(
               new CustomEvent('new-section-loaded'));
           assert.isTrue(element.$.relatedChangesToggle.classList
@@ -1797,10 +1808,11 @@
         () => {
           assert.isFalse(element.$.relatedChangesToggle.classList
               .contains('showToggle'));
-          sandbox.stub(element, '_getOffsetHeight', () => 50);
-          sandbox.stub(element, '_getScrollHeight', () => 40);
-          sandbox.stub(element, '_getLineHeight', () => 5);
-          sandbox.stub(window, 'matchMedia', () => { return {matches: true}; });
+          sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
+          sinon.stub(element, '_getScrollHeight').callsFake(() => 40);
+          sinon.stub(element, '_getLineHeight').callsFake(() => 5);
+          sinon.stub(window, 'matchMedia')
+              .callsFake(() => { return {matches: true}; });
           element.$.relatedChanges.dispatchEvent(
               new CustomEvent('new-section-loaded'));
           assert.isFalse(element.$.relatedChangesToggle.classList
@@ -1809,8 +1821,9 @@
         });
 
     test('relatedChangesToggle functions', () => {
-      sandbox.stub(element, '_getOffsetHeight', () => 50);
-      sandbox.stub(window, 'matchMedia', () => { return {matches: false}; });
+      sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
+      sinon.stub(window, 'matchMedia')
+          .callsFake(() => { return {matches: false}; });
       element._relatedChangesLoading = false;
       assert.isTrue(element._relatedChangesCollapsed);
       assert.isTrue(
@@ -1822,9 +1835,10 @@
     });
 
     test('_updateRelatedChangeMaxHeight without commit toggle', () => {
-      sandbox.stub(element, '_getOffsetHeight', () => 50);
-      sandbox.stub(element, '_getLineHeight', () => 12);
-      sandbox.stub(window, 'matchMedia', () => { return {matches: false}; });
+      sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
+      sinon.stub(element, '_getLineHeight').callsFake(() => 12);
+      sinon.stub(window, 'matchMedia')
+          .callsFake(() => { return {matches: false}; });
 
       // 50 (existing height) - 30 (extra height) = 20 (adjusted height).
       // 20 (max existing height)  % 12 (line height) = 6 (remainder).
@@ -1839,9 +1853,10 @@
 
     test('_updateRelatedChangeMaxHeight with commit toggle', () => {
       element._latestCommitMessage = _.times(31, String).join('\n');
-      sandbox.stub(element, '_getOffsetHeight', () => 50);
-      sandbox.stub(element, '_getLineHeight', () => 12);
-      sandbox.stub(window, 'matchMedia', () => { return {matches: false}; });
+      sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
+      sinon.stub(element, '_getLineHeight').callsFake(() => 12);
+      sinon.stub(window, 'matchMedia')
+          .callsFake(() => { return {matches: false}; });
 
       // 50 (existing height) % 12 (line height) = 2 (remainder).
       // 50 (existing height)  - 2 (remainder) = 48 (max height to set).
@@ -1855,9 +1870,10 @@
 
     test('_updateRelatedChangeMaxHeight in small screen mode', () => {
       element._latestCommitMessage = _.times(31, String).join('\n');
-      sandbox.stub(element, '_getOffsetHeight', () => 50);
-      sandbox.stub(element, '_getLineHeight', () => 12);
-      sandbox.stub(window, 'matchMedia', () => { return {matches: true}; });
+      sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
+      sinon.stub(element, '_getLineHeight').callsFake(() => 12);
+      sinon.stub(window, 'matchMedia')
+          .callsFake(() => { return {matches: true}; });
 
       element._updateRelatedChangeMaxHeight();
 
@@ -1870,9 +1886,9 @@
 
     test('_updateRelatedChangeMaxHeight in medium screen mode', () => {
       element._latestCommitMessage = _.times(31, String).join('\n');
-      sandbox.stub(element, '_getOffsetHeight', () => 50);
-      sandbox.stub(element, '_getLineHeight', () => 12);
-      sandbox.stub(window, 'matchMedia', () => {
+      sinon.stub(element, '_getOffsetHeight').callsFake(() => 50);
+      sinon.stub(element, '_getLineHeight').callsFake(() => 12);
+      sinon.stub(window, 'matchMedia').callsFake(() => {
         if (window.matchMedia.lastCall.args[0] === '(max-width: 75em)') {
           return {matches: true};
         } else {
@@ -1889,8 +1905,8 @@
 
     suite('update checks', () => {
       setup(() => {
-        sandbox.spy(element, '_startUpdateCheckTimer');
-        sandbox.stub(element, 'async', f => {
+        sinon.spy(element, '_startUpdateCheckTimer');
+        sinon.stub(element, 'async').callsFake( f => {
           // Only fire the async callback one time.
           if (element.async.callCount > 1) { return; }
           f.call(element);
@@ -1898,7 +1914,7 @@
       });
 
       test('_startUpdateCheckTimer negative delay', () => {
-        sandbox.stub(element, 'fetchChangeUpdates');
+        sinon.stub(element, 'fetchChangeUpdates');
 
         element._serverConfig = {change: {update_delay: -1}};
 
@@ -1907,7 +1923,7 @@
       });
 
       test('_startUpdateCheckTimer up-to-date', () => {
-        sandbox.stub(element, 'fetchChangeUpdates',
+        sinon.stub(element, 'fetchChangeUpdates').callsFake(
             () => Promise.resolve({isLatest: true}));
 
         element._serverConfig = {change: {update_delay: 12345}};
@@ -1918,7 +1934,7 @@
       });
 
       test('_startUpdateCheckTimer out-of-date shows an alert', done => {
-        sandbox.stub(element, 'fetchChangeUpdates',
+        sinon.stub(element, 'fetchChangeUpdates').callsFake(
             () => Promise.resolve({isLatest: false}));
         element.addEventListener('show-alert', e => {
           assert.equal(e.detail.message,
@@ -1929,7 +1945,7 @@
       });
 
       test('_startUpdateCheckTimer new status shows an alert', done => {
-        sandbox.stub(element, 'fetchChangeUpdates')
+        sinon.stub(element, 'fetchChangeUpdates')
             .returns(Promise.resolve({
               isLatest: true,
               newStatus: ChangeStatus.MERGED,
@@ -1942,7 +1958,7 @@
       });
 
       test('_startUpdateCheckTimer new messages shows an alert', done => {
-        sandbox.stub(element, 'fetchChangeUpdates')
+        sinon.stub(element, 'fetchChangeUpdates')
             .returns(Promise.resolve({
               isLatest: true,
               newMessages: true,
@@ -1985,7 +2001,7 @@
 
   test('_maybeScrollToMessage', done => {
     flush(() => {
-      const scrollStub = sandbox.stub(element.messagesList,
+      const scrollStub = sinon.stub(element.messagesList,
           'scrollToMessage');
 
       element._maybeScrollToMessage('');
@@ -2000,7 +2016,7 @@
   });
 
   test('topic update reloads related changes', () => {
-    sandbox.stub(element.$.relatedChanges, 'reload');
+    sinon.stub(element.$.relatedChanges, 'reload');
     element.dispatchEvent(new CustomEvent('topic-changed'));
     assert.isTrue(element.$.relatedChanges.reload.calledOnce);
   });
@@ -2067,11 +2083,11 @@
     flushAsynchronousOperations();
     const controls = element.$.fileListHeader
         .shadowRoot.querySelector('#editControls');
-    sandbox.stub(controls, 'openDeleteDialog');
-    sandbox.stub(controls, 'openRenameDialog');
-    sandbox.stub(controls, 'openRestoreDialog');
-    sandbox.stub(GerritNav, 'getEditUrlForDiff');
-    sandbox.stub(GerritNav, 'navigateToRelativeUrl');
+    sinon.stub(controls, 'openDeleteDialog');
+    sinon.stub(controls, 'openRenameDialog');
+    sinon.stub(controls, 'openRestoreDialog');
+    sinon.stub(GerritNav, 'getEditUrlForDiff');
+    sinon.stub(GerritNav, 'navigateToRelativeUrl');
 
     // Delete
     fileList.dispatchEvent(new CustomEvent('file-action-tap', {
@@ -2123,7 +2139,7 @@
   test('_selectedRevision updates when patchNum is changed', () => {
     const revision1 = {_number: 1, commit: {parents: []}};
     const revision2 = {_number: 2, commit: {parents: []}};
-    sandbox.stub(element.$.restAPI, 'getChangeDetail').returns(
+    sinon.stub(element.$.restAPI, 'getChangeDetail').returns(
         Promise.resolve({
           revisions: {
             aaa: revision1,
@@ -2134,8 +2150,8 @@
           current_revision: 'bbb',
           change_id: 'loremipsumdolorsitamet',
         }));
-    sandbox.stub(element, '_getEdit').returns(Promise.resolve());
-    sandbox.stub(element, '_getPreferences').returns(Promise.resolve({}));
+    sinon.stub(element, '_getEdit').returns(Promise.resolve());
+    sinon.stub(element, '_getPreferences').returns(Promise.resolve({}));
     element._patchRange = {patchNum: '2'};
     return element._getChangeDetail().then(() => {
       assert.strictEqual(element._selectedRevision, revision2);
@@ -2149,7 +2165,7 @@
     const revision1 = {_number: 1, commit: {parents: []}};
     const revision2 = {_number: 2, commit: {parents: []}};
     const revision3 = {_number: 'edit', commit: {parents: []}};
-    sandbox.stub(element.$.restAPI, 'getChangeDetail').returns(
+    sinon.stub(element.$.restAPI, 'getChangeDetail').returns(
         Promise.resolve({
           revisions: {
             aaa: revision1,
@@ -2161,8 +2177,8 @@
           current_revision: 'ccc',
           change_id: 'loremipsumdolorsitamet',
         }));
-    sandbox.stub(element, '_getEdit').returns(Promise.resolve());
-    sandbox.stub(element, '_getPreferences').returns(Promise.resolve({}));
+    sinon.stub(element, '_getEdit').returns(Promise.resolve());
+    sinon.stub(element, '_getPreferences').returns(Promise.resolve({}));
     element._patchRange = {patchNum: 'edit'};
     return element._getChangeDetail().then(() => {
       assert.strictEqual(element._selectedRevision, revision3);
@@ -2173,7 +2189,7 @@
     element._change = {labels: {}};
     element._patchRange = {patchNum: 4};
     element._mergeable = true;
-    const showStub = sandbox.stub(element.$.jsAPI, 'handleEvent');
+    const showStub = sinon.stub(element.$.jsAPI, 'handleEvent');
     element._sendShowChangeEvent();
     assert.isTrue(showStub.calledOnce);
     assert.equal(
@@ -2198,7 +2214,7 @@
     });
 
     test('edit exists in revisions', done => {
-      sandbox.stub(GerritNav, 'navigateToChange', (...args) => {
+      sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
         assert.equal(args.length, 2);
         assert.equal(args[1], element.EDIT_NAME); // patchNum
         done();
@@ -2211,7 +2227,7 @@
     });
 
     test('no edit exists in revisions, non-latest patchset', done => {
-      sandbox.stub(GerritNav, 'navigateToChange', (...args) => {
+      sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
         assert.equal(args.length, 4);
         assert.equal(args[1], 1); // patchNum
         assert.equal(args[3], true); // opt_isEdit
@@ -2226,7 +2242,7 @@
     });
 
     test('no edit exists in revisions, latest patchset', done => {
-      sandbox.stub(GerritNav, 'navigateToChange', (...args) => {
+      sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
         assert.equal(args.length, 4);
         // No patch should be specified when patchNum == latest.
         assert.isNotOk(args[1]); // patchNum
@@ -2243,10 +2259,10 @@
   });
 
   test('_handleStopEditTap', done => {
-    sandbox.stub(element.$.metadata, '_computeLabelNames');
+    sinon.stub(element.$.metadata, '_computeLabelNames');
     navigateToChangeStub.restore();
-    sandbox.stub(element, 'computeLatestPatchNum').returns(1);
-    sandbox.stub(GerritNav, 'navigateToChange', (...args) => {
+    sinon.stub(element, 'computeLatestPatchNum').returns(1);
+    sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
       assert.equal(args.length, 2);
       assert.equal(args[1], 1); // patchNum
       done();
@@ -2286,7 +2302,7 @@
 
     setup(() => {
       element._change = {labels: {}};
-      getMergeableStub = sandbox.stub(element.$.restAPI, 'getMergeable')
+      getMergeableStub = sinon.stub(element.$.restAPI, 'getMergeable')
           .returns(Promise.resolve({mergeable: true}));
     });
 
@@ -2318,9 +2334,9 @@
   });
 
   test('_paramsChanged sets in projectLookup', () => {
-    sandbox.stub(element.$.relatedChanges, 'reload');
-    sandbox.stub(element, '_reload').returns(Promise.resolve());
-    const setStub = sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+    sinon.stub(element.$.relatedChanges, 'reload');
+    sinon.stub(element, '_reload').returns(Promise.resolve());
+    const setStub = sinon.stub(element.$.restAPI, 'setInProjectLookup');
     element._paramsChanged({
       view: GerritNav.View.CHANGE,
       changeNum: 101,
@@ -2336,7 +2352,7 @@
       starred: false,
     };
     element._loggedIn = true;
-    const stub = sandbox.stub(element, '_handleToggleStar');
+    const stub = sinon.stub(element, '_handleToggleStar');
     flushAsynchronousOperations();
 
     MockInteractions.tap(element.$.changeStar.shadowRoot
@@ -2350,19 +2366,19 @@
         basePatchNum: 'PARENT',
         patchNum: 1,
       };
-      sandbox.stub(element, '_getChangeDetail').returns(Promise.resolve());
-      sandbox.stub(element, '_getProjectConfig').returns(Promise.resolve());
-      sandbox.stub(element, '_reloadComments').returns(Promise.resolve());
-      sandbox.stub(element, '_getMergeability').returns(Promise.resolve());
-      sandbox.stub(element, '_getLatestCommitMessage')
+      sinon.stub(element, '_getChangeDetail').returns(Promise.resolve());
+      sinon.stub(element, '_getProjectConfig').returns(Promise.resolve());
+      sinon.stub(element, '_reloadComments').returns(Promise.resolve());
+      sinon.stub(element, '_getMergeability').returns(Promise.resolve());
+      sinon.stub(element, '_getLatestCommitMessage')
           .returns(Promise.resolve());
     });
 
     test('don\'t report changedDisplayed on reply', done => {
       const changeDisplayStub =
-        sandbox.stub(element.reporting, 'changeDisplayed');
+        sinon.stub(element.reporting, 'changeDisplayed');
       const changeFullyLoadedStub =
-        sandbox.stub(element.reporting, 'changeFullyLoaded');
+        sinon.stub(element.reporting, 'changeFullyLoaded');
       element._handleReplySent();
       flush(() => {
         assert.isFalse(changeDisplayStub.called);
@@ -2373,9 +2389,9 @@
 
     test('report changedDisplayed on _paramsChanged', done => {
       const changeDisplayStub =
-        sandbox.stub(element.reporting, 'changeDisplayed');
+        sinon.stub(element.reporting, 'changeDisplayed');
       const changeFullyLoadedStub =
-        sandbox.stub(element.reporting, 'changeFullyLoaded');
+        sinon.stub(element.reporting, 'changeFullyLoaded');
       element._paramsChanged({
         view: GerritNav.View.CHANGE,
         changeNum: 101,
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.js
similarity index 64%
rename from polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
rename to polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.js
index 075b883..704d83f 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.js
@@ -1,53 +1,35 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-comment-list</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-comment-list></gr-comment-list>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-comment-list.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
+const basicFixture = fixtureFromElement('gr-comment-list');
+
 suite('gr-comment-list tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
-    sandbox.stub(GerritNav, 'mapCommentlinks', x => x);
-  });
+    element = basicFixture.instantiate();
 
-  teardown(() => { sandbox.restore(); });
+    sinon.stub(GerritNav, 'mapCommentlinks').callsFake( x => x);
+  });
 
   test('_computeFilesFromComments w/ special file path sorting', () => {
     const comments = {
@@ -106,7 +88,7 @@
   });
 
   test('_computeDiffLineURL', () => {
-    const getUrlStub = sandbox.stub(GerritNav, 'getUrlForDiffById');
+    const getUrlStub = sinon.stub(GerritNav, 'getUrlForDiffById');
     element.projectName = 'proj';
     element.changeNum = 123;
 
@@ -129,4 +111,4 @@
         [123, 'proj', 'foo.cc', 4, -12, 456, true]);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.js
similarity index 65%
rename from polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
rename to polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.js
index d9664ec..c120c33 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.js
@@ -1,57 +1,36 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2015 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-commit-info</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-commit-info></gr-commit-info>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import '../../core/gr-router/gr-router.js';
 import './gr-commit-info.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
+const basicFixture = fixtureFromElement('gr-commit-info');
+
 suite('gr-commit-info tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    element = basicFixture.instantiate();
   });
 
   test('weblinks use GerritNav interface', () => {
-    const weblinksStub = sandbox.stub(GerritNav, '_generateWeblinks')
+    const weblinksStub = sinon.stub(GerritNav, '_generateWeblinks')
         .returns([{name: 'stubb', url: '#s'}]);
     element.change = {};
     element.commitInfo = {};
@@ -70,7 +49,7 @@
 
   test('use web link when available', () => {
     const router = document.createElement('gr-router');
-    sandbox.stub(GerritNav, '_generateWeblinks',
+    sinon.stub(GerritNav, '_generateWeblinks').callsFake(
         router._generateWeblinks.bind(router));
 
     element.change = {labels: [], project: ''};
@@ -86,7 +65,7 @@
 
   test('does not relativize web links that begin with scheme', () => {
     const router = document.createElement('gr-router');
-    sandbox.stub(GerritNav, '_generateWeblinks',
+    sinon.stub(GerritNav, '_generateWeblinks').callsFake(
         router._generateWeblinks.bind(router));
 
     element.change = {labels: [], project: ''};
@@ -104,7 +83,7 @@
 
   test('ignore web links that are neither gitweb nor gitiles', () => {
     const router = document.createElement('gr-router');
-    sandbox.stub(GerritNav, '_generateWeblinks',
+    sinon.stub(GerritNav, '_generateWeblinks').callsFake(
         router._generateWeblinks.bind(router));
 
     element.change = {project: 'project-name'};
@@ -136,4 +115,4 @@
         element.serverConfig));
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
deleted file mode 100644
index c1e1165..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
+++ /dev/null
@@ -1,82 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-confirm-abandon-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-confirm-abandon-dialog></gr-confirm-abandon-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-confirm-abandon-dialog.js';
-suite('gr-confirm-abandon-dialog tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('_handleConfirmTap', () => {
-    const confirmHandler = sandbox.stub();
-    element.addEventListener('confirm', confirmHandler);
-    sandbox.spy(element, '_handleConfirmTap');
-    sandbox.spy(element, '_confirm');
-    element.shadowRoot
-        .querySelector('gr-dialog').dispatchEvent(
-            new CustomEvent('confirm', {
-              composed: true, bubbles: true,
-            }));
-    assert.isTrue(confirmHandler.called);
-    assert.isTrue(confirmHandler.calledOnce);
-    assert.isTrue(element._handleConfirmTap.called);
-    assert.isTrue(element._confirm.called);
-    assert.isTrue(element._confirm.calledOnce);
-  });
-
-  test('_handleCancelTap', () => {
-    const cancelHandler = sandbox.stub();
-    element.addEventListener('cancel', cancelHandler);
-    sandbox.spy(element, '_handleCancelTap');
-    element.shadowRoot
-        .querySelector('gr-dialog').dispatchEvent(
-            new CustomEvent('cancel', {
-              composed: true, bubbles: true,
-            }));
-    assert.isTrue(cancelHandler.called);
-    assert.isTrue(cancelHandler.calledOnce);
-    assert.isTrue(element._handleCancelTap.called);
-    assert.isTrue(element._handleCancelTap.calledOnce);
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.js b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.js
new file mode 100644
index 0000000..14d16f5
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.js
@@ -0,0 +1,62 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-confirm-abandon-dialog.js';
+
+const basicFixture = fixtureFromElement('gr-confirm-abandon-dialog');
+
+suite('gr-confirm-abandon-dialog tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('_handleConfirmTap', () => {
+    const confirmHandler = sinon.stub();
+    element.addEventListener('confirm', confirmHandler);
+    sinon.spy(element, '_handleConfirmTap');
+    sinon.spy(element, '_confirm');
+    element.shadowRoot
+        .querySelector('gr-dialog').dispatchEvent(
+            new CustomEvent('confirm', {
+              composed: true, bubbles: true,
+            }));
+    assert.isTrue(confirmHandler.called);
+    assert.isTrue(confirmHandler.calledOnce);
+    assert.isTrue(element._handleConfirmTap.called);
+    assert.isTrue(element._confirm.called);
+    assert.isTrue(element._confirm.calledOnce);
+  });
+
+  test('_handleCancelTap', () => {
+    const cancelHandler = sinon.stub();
+    element.addEventListener('cancel', cancelHandler);
+    sinon.spy(element, '_handleCancelTap');
+    element.shadowRoot
+        .querySelector('gr-dialog').dispatchEvent(
+            new CustomEvent('cancel', {
+              composed: true, bubbles: true,
+            }));
+    assert.isTrue(cancelHandler.called);
+    assert.isTrue(cancelHandler.calledOnce);
+    assert.isTrue(element._handleCancelTap.called);
+    assert.isTrue(element._handleCancelTap.calledOnce);
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html
deleted file mode 100644
index e0016f0..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html
+++ /dev/null
@@ -1,78 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-confirm-cherrypick-conflict-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-confirm-cherrypick-conflict-dialog></gr-confirm-cherrypick-conflict-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-confirm-cherrypick-conflict-dialog.js';
-suite('gr-confirm-cherrypick-conflict-dialog tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => { sandbox.restore(); });
-
-  test('_handleConfirmTap', () => {
-    const confirmHandler = sandbox.stub();
-    element.addEventListener('confirm', confirmHandler);
-    sandbox.spy(element, '_handleConfirmTap');
-    element.shadowRoot
-        .querySelector('gr-dialog').dispatchEvent(
-            new CustomEvent('confirm', {
-              composed: true, bubbles: true,
-            }));
-    assert.isTrue(confirmHandler.called);
-    assert.isTrue(confirmHandler.calledOnce);
-    assert.isTrue(element._handleConfirmTap.called);
-    assert.isTrue(element._handleConfirmTap.calledOnce);
-  });
-
-  test('_handleCancelTap', () => {
-    const cancelHandler = sandbox.stub();
-    element.addEventListener('cancel', cancelHandler);
-    sandbox.spy(element, '_handleCancelTap');
-    element.shadowRoot
-        .querySelector('gr-dialog').dispatchEvent(
-            new CustomEvent('cancel', {
-              composed: true, bubbles: true,
-            }));
-    assert.isTrue(cancelHandler.called);
-    assert.isTrue(cancelHandler.calledOnce);
-    assert.isTrue(element._handleCancelTap.called);
-    assert.isTrue(element._handleCancelTap.calledOnce);
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.js
new file mode 100644
index 0000000..c98353b
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.js
@@ -0,0 +1,61 @@
+/**
+ * @license
+ * Copyright (C) 2018 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-confirm-cherrypick-conflict-dialog.js';
+
+const basicFixture =
+    fixtureFromElement('gr-confirm-cherrypick-conflict-dialog');
+
+suite('gr-confirm-cherrypick-conflict-dialog tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('_handleConfirmTap', () => {
+    const confirmHandler = sinon.stub();
+    element.addEventListener('confirm', confirmHandler);
+    sinon.spy(element, '_handleConfirmTap');
+    element.shadowRoot
+        .querySelector('gr-dialog').dispatchEvent(
+            new CustomEvent('confirm', {
+              composed: true, bubbles: true,
+            }));
+    assert.isTrue(confirmHandler.called);
+    assert.isTrue(confirmHandler.calledOnce);
+    assert.isTrue(element._handleConfirmTap.called);
+    assert.isTrue(element._handleConfirmTap.calledOnce);
+  });
+
+  test('_handleCancelTap', () => {
+    const cancelHandler = sinon.stub();
+    element.addEventListener('cancel', cancelHandler);
+    sinon.spy(element, '_handleCancelTap');
+    element.shadowRoot
+        .querySelector('gr-dialog').dispatchEvent(
+            new CustomEvent('cancel', {
+              composed: true, bubbles: true,
+            }));
+    assert.isTrue(cancelHandler.called);
+    assert.isTrue(cancelHandler.calledOnce);
+    assert.isTrue(element._handleCancelTap.called);
+    assert.isTrue(element._handleCancelTap.calledOnce);
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
similarity index 72%
rename from polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html
rename to polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
index 000718b..900412a 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
@@ -1,50 +1,33 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-confirm-cherrypick-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-confirm-cherrypick-dialog></gr-confirm-cherrypick-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-confirm-cherrypick-dialog.js';
 
+const basicFixture = fixtureFromElement('gr-confirm-cherrypick-dialog');
+
 const CHERRY_PICK_TYPES = {
   SINGLE_CHANGE: 1,
   TOPIC: 2,
 };
 suite('gr-confirm-cherrypick-dialog tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     stub('gr-rest-api-interface', {
       getRepoBranches(input) {
         if (input.startsWith('test')) {
@@ -60,12 +43,10 @@
         }
       },
     });
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.project = 'test-project';
   });
 
-  teardown(() => { sandbox.restore(); });
-
   test('with merged change', () => {
     element.changeStatus = 'MERGED';
     element.commitMessage = 'message\n';
@@ -132,7 +113,7 @@
 
     test('cherry pick topic submit', done => {
       element.branch = 'master';
-      const executeChangeActionStub = sandbox.stub(element.$.restAPI,
+      const executeChangeActionStub = sinon.stub(element.$.restAPI,
           'executeChangeAction').returns(Promise.resolve([]));
       MockInteractions.tap(element.shadowRoot.
           querySelector('gr-dialog').$.confirm);
@@ -156,7 +137,6 @@
     });
 
     test('submit button is blocked while cherry picks is running', done => {
-      console.log(element);
       const confirmButton = element.shadowRoot.querySelector('gr-dialog').$
           .confirm;
       assert.isFalse(confirmButton.hasAttribute('disabled'));
@@ -169,7 +149,7 @@
   });
 
   test('resetFocus', () => {
-    const focusStub = sandbox.stub(element.$.branchInput, 'focus');
+    const focusStub = sinon.stub(element.$.branchInput, 'focus');
     element.resetFocus();
     assert.isTrue(focusStub.called);
   });
@@ -182,4 +162,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html
deleted file mode 100644
index a8392aa..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html
+++ /dev/null
@@ -1,83 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-confirm-move-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-confirm-move-dialog></gr-confirm-move-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-confirm-move-dialog.js';
-suite('gr-confirm-move-dialog tests', () => {
-  let element;
-
-  setup(() => {
-    stub('gr-rest-api-interface', {
-      getRepoBranches(input) {
-        if (input.startsWith('test')) {
-          return Promise.resolve([
-            {
-              ref: 'refs/heads/test-branch',
-              revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
-              can_delete: true,
-            },
-          ]);
-        } else {
-          return Promise.resolve({});
-        }
-      },
-    });
-    element = fixture('basic');
-    element.project = 'test-project';
-  });
-
-  test('with updated commit message', () => {
-    element.branch = 'master';
-    const myNewMessage = 'updated commit message';
-    element.message = myNewMessage;
-    flushAsynchronousOperations();
-    assert.equal(element.message, myNewMessage);
-  });
-
-  test('_getProjectBranchesSuggestions empty', done => {
-    element._getProjectBranchesSuggestions('nonexistent').then(branches => {
-      assert.equal(branches.length, 0);
-      done();
-    });
-  });
-
-  test('_getProjectBranchesSuggestions non-empty', done => {
-    element._getProjectBranchesSuggestions('test-branch').then(branches => {
-      assert.equal(branches.length, 1);
-      assert.equal(branches[0].name, 'test-branch');
-      done();
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.js b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.js
new file mode 100644
index 0000000..0241112
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.js
@@ -0,0 +1,69 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-confirm-move-dialog.js';
+
+const basicFixture = fixtureFromElement('gr-confirm-move-dialog');
+
+suite('gr-confirm-move-dialog tests', () => {
+  let element;
+
+  setup(() => {
+    stub('gr-rest-api-interface', {
+      getRepoBranches(input) {
+        if (input.startsWith('test')) {
+          return Promise.resolve([
+            {
+              ref: 'refs/heads/test-branch',
+              revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
+              can_delete: true,
+            },
+          ]);
+        } else {
+          return Promise.resolve({});
+        }
+      },
+    });
+    element = basicFixture.instantiate();
+    element.project = 'test-project';
+  });
+
+  test('with updated commit message', () => {
+    element.branch = 'master';
+    const myNewMessage = 'updated commit message';
+    element.message = myNewMessage;
+    flushAsynchronousOperations();
+    assert.equal(element.message, myNewMessage);
+  });
+
+  test('_getProjectBranchesSuggestions empty', done => {
+    element._getProjectBranchesSuggestions('nonexistent').then(branches => {
+      assert.equal(branches.length, 0);
+      done();
+    });
+  });
+
+  test('_getProjectBranchesSuggestions non-empty', done => {
+    element._getProjectBranchesSuggestions('test-branch').then(branches => {
+      assert.equal(branches.length, 1);
+      assert.equal(branches[0].name, 'test-branch');
+      done();
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.js
similarity index 77%
rename from polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
rename to polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.js
index 080a7e0..498d31c 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.js
@@ -1,50 +1,30 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-confirm-rebase-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-confirm-rebase-dialog></gr-confirm-rebase-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-confirm-rebase-dialog.js';
+
+const basicFixture = fixtureFromElement('gr-confirm-rebase-dialog');
+
 suite('gr-confirm-rebase-dialog tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    element = basicFixture.instantiate();
   });
 
   test('controls with parent and rebase on current available', () => {
@@ -138,7 +118,7 @@
         },
       ];
 
-      sandbox.stub(element.$.restAPI, 'getChanges').returns(Promise.resolve(
+      sinon.stub(element.$.restAPI, 'getChanges').returns(Promise.resolve(
           [
             {
               _number: 123,
@@ -157,7 +137,7 @@
     });
 
     test('_getRecentChanges', () => {
-      sandbox.spy(element, '_getRecentChanges');
+      sinon.spy(element, '_getRecentChanges');
       return element._getRecentChanges()
           .then(() => {
             assert.deepEqual(element._recentChanges, recentChanges);
@@ -187,7 +167,7 @@
     });
 
     test('input text change triggers function', () => {
-      sandbox.spy(element, '_getRecentChanges');
+      sinon.spy(element, '_getRecentChanges');
       element.$.parentInput.noDebounce = true;
       MockInteractions.pressAndReleaseKeyOn(
           element.$.parentInput.$.input,
@@ -201,4 +181,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
deleted file mode 100644
index 3a341c5..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
+++ /dev/null
@@ -1,101 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-confirm-revert-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-confirm-revert-dialog></gr-confirm-revert-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-confirm-revert-dialog.js';
-suite('gr-confirm-revert-dialog tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    element = fixture('basic');
-    sandbox =sinon.sandbox.create();
-  });
-
-  teardown(() => sandbox.restore());
-
-  test('no match', () => {
-    assert.isNotOk(element._message);
-    const alertStub = sandbox.stub();
-    element.addEventListener('show-alert', alertStub);
-    element._populateRevertSingleChangeMessage({},
-        'not a commitHash in sight', undefined);
-    assert.isTrue(alertStub.calledOnce);
-  });
-
-  test('single line', () => {
-    assert.isNotOk(element._message);
-    element._populateRevertSingleChangeMessage({},
-        'one line commit\n\nChange-Id: abcdefg\n',
-        'abcd123');
-    const expected = 'Revert "one line commit"\n\n' +
-        'This reverts commit abcd123.\n\n' +
-        'Reason for revert: <INSERT REASONING HERE>\n';
-    assert.equal(element._message, expected);
-  });
-
-  test('multi line', () => {
-    assert.isNotOk(element._message);
-    element._populateRevertSingleChangeMessage({},
-        'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n',
-        'abcd123');
-    const expected = 'Revert "many lines"\n\n' +
-        'This reverts commit abcd123.\n\n' +
-        'Reason for revert: <INSERT REASONING HERE>\n';
-    assert.equal(element._message, expected);
-  });
-
-  test('issue above change id', () => {
-    assert.isNotOk(element._message);
-    element._populateRevertSingleChangeMessage({},
-        'much lines\nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n',
-        'abcd123');
-    const expected = 'Revert "much lines"\n\n' +
-        'This reverts commit abcd123.\n\n' +
-        'Reason for revert: <INSERT REASONING HERE>\n';
-    assert.equal(element._message, expected);
-  });
-
-  test('revert a revert', () => {
-    assert.isNotOk(element._message);
-    element._populateRevertSingleChangeMessage({},
-        'Revert "one line commit"\n\nChange-Id: abcdefg\n',
-        'abcd123');
-    const expected = 'Revert "Revert "one line commit""\n\n' +
-        'This reverts commit abcd123.\n\n' +
-        'Reason for revert: <INSERT REASONING HERE>\n';
-    assert.equal(element._message, expected);
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.js
new file mode 100644
index 0000000..7c84043
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.js
@@ -0,0 +1,83 @@
+/**
+ * @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-confirm-revert-dialog.js';
+
+const basicFixture = fixtureFromElement('gr-confirm-revert-dialog');
+
+suite('gr-confirm-revert-dialog tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('no match', () => {
+    assert.isNotOk(element._message);
+    const alertStub = sinon.stub();
+    element.addEventListener('show-alert', alertStub);
+    element._populateRevertSingleChangeMessage({},
+        'not a commitHash in sight', undefined);
+    assert.isTrue(alertStub.calledOnce);
+  });
+
+  test('single line', () => {
+    assert.isNotOk(element._message);
+    element._populateRevertSingleChangeMessage({},
+        'one line commit\n\nChange-Id: abcdefg\n',
+        'abcd123');
+    const expected = 'Revert "one line commit"\n\n' +
+        'This reverts commit abcd123.\n\n' +
+        'Reason for revert: <INSERT REASONING HERE>\n';
+    assert.equal(element._message, expected);
+  });
+
+  test('multi line', () => {
+    assert.isNotOk(element._message);
+    element._populateRevertSingleChangeMessage({},
+        'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n',
+        'abcd123');
+    const expected = 'Revert "many lines"\n\n' +
+        'This reverts commit abcd123.\n\n' +
+        'Reason for revert: <INSERT REASONING HERE>\n';
+    assert.equal(element._message, expected);
+  });
+
+  test('issue above change id', () => {
+    assert.isNotOk(element._message);
+    element._populateRevertSingleChangeMessage({},
+        'much lines\nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n',
+        'abcd123');
+    const expected = 'Revert "much lines"\n\n' +
+        'This reverts commit abcd123.\n\n' +
+        'Reason for revert: <INSERT REASONING HERE>\n';
+    assert.equal(element._message, expected);
+  });
+
+  test('revert a revert', () => {
+    assert.isNotOk(element._message);
+    element._populateRevertSingleChangeMessage({},
+        'Revert "one line commit"\n\nChange-Id: abcdefg\n',
+        'abcd123');
+    const expected = 'Revert "Revert "one line commit""\n\n' +
+        'This reverts commit abcd123.\n\n' +
+        'Reason for revert: <INSERT REASONING HERE>\n';
+    assert.equal(element._message, expected);
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.html
deleted file mode 100644
index a11d996..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.html
+++ /dev/null
@@ -1,99 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2019 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-confirm-revert-submission-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-confirm-revert-submission-dialog>
-    </gr-confirm-revert-submission-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-confirm-revert-submission-dialog.js';
-suite('gr-confirm-revert-submission-dialog tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
-  });
-
-  teardown(() => sandbox.restore());
-
-  test('no match', () => {
-    assert.isNotOk(element.message);
-    const alertStub = sandbox.stub();
-    element.addEventListener('show-alert', alertStub);
-    element._populateRevertSubmissionMessage(
-        'not a commitHash in sight', {}
-    );
-    assert.isTrue(alertStub.calledOnce);
-  });
-
-  test('single line', () => {
-    assert.isNotOk(element.message);
-    element._populateRevertSubmissionMessage(
-        'one line commit\n\nChange-Id: abcdefg\n',
-        {current_revision: 'abcd123', submission_id: '111'});
-    const expected = 'Revert submission 111\n\n' +
-      'Reason for revert: <INSERT REASONING HERE>\n';
-    assert.equal(element.message, expected);
-  });
-
-  test('multi line', () => {
-    assert.isNotOk(element.message);
-    element._populateRevertSubmissionMessage(
-        'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n',
-        {current_revision: 'abcd123', submission_id: '111'});
-    const expected = 'Revert submission 111\n\n' +
-      'Reason for revert: <INSERT REASONING HERE>\n';
-    assert.equal(element.message, expected);
-  });
-
-  test('issue above change id', () => {
-    assert.isNotOk(element.message);
-    element._populateRevertSubmissionMessage(
-        'test \nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n',
-        {current_revision: 'abcd123', submission_id: '111'});
-    const expected = 'Revert submission 111\n\n' +
-        'Reason for revert: <INSERT REASONING HERE>\n';
-    assert.equal(element.message, expected);
-  });
-
-  test('revert a revert', () => {
-    assert.isNotOk(element.message);
-    element._populateRevertSubmissionMessage(
-        'Revert "one line commit"\n\nChange-Id: abcdefg\n',
-        {current_revision: 'abcd123', submission_id: '111'});
-    const expected = 'Revert submission 111\n\n' +
-      'Reason for revert: <INSERT REASONING HERE>\n';
-    assert.equal(element.message, expected);
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.js
new file mode 100644
index 0000000..e2f2e9e
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.js
@@ -0,0 +1,80 @@
+/**
+ * @license
+ * Copyright (C) 2019 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-confirm-revert-submission-dialog.js';
+
+const basicFixture = fixtureFromElement('gr-confirm-revert-submission-dialog');
+
+suite('gr-confirm-revert-submission-dialog tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('no match', () => {
+    assert.isNotOk(element.message);
+    const alertStub = sinon.stub();
+    element.addEventListener('show-alert', alertStub);
+    element._populateRevertSubmissionMessage(
+        'not a commitHash in sight', {}
+    );
+    assert.isTrue(alertStub.calledOnce);
+  });
+
+  test('single line', () => {
+    assert.isNotOk(element.message);
+    element._populateRevertSubmissionMessage(
+        'one line commit\n\nChange-Id: abcdefg\n',
+        {current_revision: 'abcd123', submission_id: '111'});
+    const expected = 'Revert submission 111\n\n' +
+      'Reason for revert: <INSERT REASONING HERE>\n';
+    assert.equal(element.message, expected);
+  });
+
+  test('multi line', () => {
+    assert.isNotOk(element.message);
+    element._populateRevertSubmissionMessage(
+        'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n',
+        {current_revision: 'abcd123', submission_id: '111'});
+    const expected = 'Revert submission 111\n\n' +
+      'Reason for revert: <INSERT REASONING HERE>\n';
+    assert.equal(element.message, expected);
+  });
+
+  test('issue above change id', () => {
+    assert.isNotOk(element.message);
+    element._populateRevertSubmissionMessage(
+        'test \nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n',
+        {current_revision: 'abcd123', submission_id: '111'});
+    const expected = 'Revert submission 111\n\n' +
+        'Reason for revert: <INSERT REASONING HERE>\n';
+    assert.equal(element.message, expected);
+  });
+
+  test('revert a revert', () => {
+    assert.isNotOk(element.message);
+    element._populateRevertSubmissionMessage(
+        'Revert "one line commit"\n\nChange-Id: abcdefg\n',
+        {current_revision: 'abcd123', submission_id: '111'});
+    const expected = 'Revert submission 111\n\n' +
+      'Reason for revert: <INSERT REASONING HERE>\n';
+    assert.equal(element.message, expected);
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html
deleted file mode 100644
index 30699d5..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html
+++ /dev/null
@@ -1,100 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-confirm-submit-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script src="/node_modules/page/page.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-confirm-submit-dialog></gr-confirm-submit-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-confirm-submit-dialog.js';
-suite('gr-file-list-header tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('display', () => {
-    element.action = {label: 'my-label'};
-    element.change = {
-      subject: 'my-subject',
-      revisions: {},
-    };
-    flushAsynchronousOperations();
-    const header = element.shadowRoot
-        .querySelector('.header');
-    assert.equal(header.textContent.trim(), 'my-label');
-
-    const message = element.shadowRoot
-        .querySelector('.main p');
-    assert.notEqual(message.textContent.length, 0);
-    assert.notEqual(message.textContent.indexOf('my-subject'), -1);
-  });
-
-  test('_computeUnresolvedCommentsWarning', () => {
-    const change = {unresolved_comment_count: 1};
-    assert.equal(element._computeUnresolvedCommentsWarning(change),
-        'Heads Up! 1 unresolved comment.');
-
-    const change2 = {unresolved_comment_count: 2};
-    assert.equal(element._computeUnresolvedCommentsWarning(change2),
-        'Heads Up! 2 unresolved comments.');
-  });
-
-  test('_computeHasChangeEdit', () => {
-    const change = {
-      revisions: {
-        d442ff05d6c4f2a3af0eeca1f67374b39f9dc3d8: {
-          _number: 'edit',
-        },
-      },
-      unresolved_comment_count: 0,
-    };
-
-    assert.equal(element._computeHasChangeEdit(change), true);
-
-    const change2 = {
-      revisions: {
-        d442ff05d6c4f2a3af0eeca1f67374b39f9dc3d8: {
-          _number: 2,
-        },
-      },
-    };
-    assert.equal(element._computeHasChangeEdit(change2), false);
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.js b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.js
new file mode 100644
index 0000000..77331f7
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.js
@@ -0,0 +1,79 @@
+/**
+ * @license
+ * Copyright (C) 2018 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-confirm-submit-dialog.js';
+
+const basicFixture = fixtureFromElement('gr-confirm-submit-dialog');
+
+suite('gr-file-list-header tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('display', () => {
+    element.action = {label: 'my-label'};
+    element.change = {
+      subject: 'my-subject',
+      revisions: {},
+    };
+    flushAsynchronousOperations();
+    const header = element.shadowRoot
+        .querySelector('.header');
+    assert.equal(header.textContent.trim(), 'my-label');
+
+    const message = element.shadowRoot
+        .querySelector('.main p');
+    assert.notEqual(message.textContent.length, 0);
+    assert.notEqual(message.textContent.indexOf('my-subject'), -1);
+  });
+
+  test('_computeUnresolvedCommentsWarning', () => {
+    const change = {unresolved_comment_count: 1};
+    assert.equal(element._computeUnresolvedCommentsWarning(change),
+        'Heads Up! 1 unresolved comment.');
+
+    const change2 = {unresolved_comment_count: 2};
+    assert.equal(element._computeUnresolvedCommentsWarning(change2),
+        'Heads Up! 2 unresolved comments.');
+  });
+
+  test('_computeHasChangeEdit', () => {
+    const change = {
+      revisions: {
+        d442ff05d6c4f2a3af0eeca1f67374b39f9dc3d8: {
+          _number: 'edit',
+        },
+      },
+      unresolved_comment_count: 0,
+    };
+
+    assert.equal(element._computeHasChangeEdit(change), true);
+
+    const change2 = {
+      revisions: {
+        d442ff05d6c4f2a3af0eeca1f67374b39f9dc3d8: {
+          _number: 2,
+        },
+      },
+    };
+    assert.equal(element._computeHasChangeEdit(change2), false);
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.js
similarity index 75%
rename from polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
rename to polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.js
index 46c57fe..5dd5de7 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.js
@@ -1,46 +1,26 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-download-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-download-dialog></gr-download-dialog>
-  </template>
-</test-fixture>
-
-<test-fixture id="loggedIn">
-  <template>
-    <gr-download-dialog logged-in></gr-download-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-download-dialog.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromElement('gr-download-dialog');
+
 function getChangeObject() {
   return {
     current_revision: '34685798fe548b6d17d1e8e5edc43a26d055cc72',
@@ -121,12 +101,9 @@
 
 suite('gr-download-dialog', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.patchNum = '1';
     element.config = {
       schemes: {
@@ -141,10 +118,6 @@
     flushAsynchronousOperations();
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('anchors use download attribute', () => {
     const anchors = Array.from(
         dom(element.root).querySelectorAll('a'));
@@ -158,7 +131,7 @@
     });
 
     test('focuses on first download link if no copy links', () => {
-      const focusStub = sandbox.stub(element.$.download, 'focus');
+      const focusStub = sinon.stub(element.$.download, 'focus');
       element.focus();
       assert.isTrue(focusStub.called);
       focusStub.restore();
@@ -219,4 +192,4 @@
     assert.isFalse(element._computeHidePatchFile(change2, patchNum));
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
similarity index 84%
rename from polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
rename to polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
index 2a5a302..d756bf3 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
@@ -1,67 +1,42 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-file-list-header</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script src="/node_modules/page/page.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-file-list-header></gr-file-list-header>
-  </template>
-</test-fixture>
-
-<test-fixture id="blank">
-  <template>
-    <div></div>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-file-list-header.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GrFileListConstants} from '../gr-file-list-constants.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
+const basicFixture = fixtureFromElement('gr-file-list-header');
+
 suite('gr-file-list-header tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     stub('gr-rest-api-interface', {
       getConfig() { return Promise.resolve({test: 'config'}); },
       getAccount() { return Promise.resolve(null); },
       _fetchSharedCacheURL() { return Promise.resolve({}); },
     });
-    element = fixture('basic');
+    element = basicFixture.instantiate();
   });
 
   teardown(done => {
     flush(() => {
-      sandbox.restore();
       done();
     });
   });
@@ -102,7 +77,7 @@
   });
 
   test('description editing', () => {
-    const putDescStub = sandbox.stub(element.$.restAPI, 'setDescription')
+    const putDescStub = sinon.stub(element.$.restAPI, 'setDescription')
         .returns(Promise.resolve({ok: true}));
 
     element.changeNum = '42';
@@ -171,7 +146,7 @@
   test('expandAllDiffs called when expand button clicked', () => {
     element.shownFileCount = 1;
     flushAsynchronousOperations();
-    sandbox.stub(element, '_expandAllDiffs');
+    sinon.stub(element, '_expandAllDiffs');
     MockInteractions.tap(dom(element.root).querySelector(
         '#expandBtn'));
     assert.isTrue(element._expandAllDiffs.called);
@@ -180,14 +155,14 @@
   test('collapseAllDiffs called when expand button clicked', () => {
     element.shownFileCount = 1;
     flushAsynchronousOperations();
-    sandbox.stub(element, '_collapseAllDiffs');
+    sinon.stub(element, '_collapseAllDiffs');
     MockInteractions.tap(dom(element.root).querySelector(
         '#collapseBtn'));
     assert.isTrue(element._collapseAllDiffs.called);
   });
 
   test('show/hide diffs disabled for large amounts of files', done => {
-    const computeSpy = sandbox.spy(element, '_fileListActionsVisible');
+    const computeSpy = sinon.spy(element, '_fileListActionsVisible');
     element._files = [];
     element.changeNum = '42';
     element.basePatchNum = 'PARENT';
@@ -240,7 +215,7 @@
   });
 
   test('navigateToChange called when range select changes', () => {
-    const navigateToChangeStub = sandbox.stub(GerritNav, 'navigateToChange');
+    const navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
     element.change = {
       change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
       revisions: {
@@ -283,7 +258,7 @@
 
     test('patch specific elements', () => {
       element.editMode = true;
-      sandbox.stub(element, 'computeLatestPatchNum').returns('2');
+      sinon.stub(element, 'computeLatestPatchNum').returns('2');
       flushAsynchronousOperations();
 
       assert.isFalse(isVisible(element.$.diffPrefsContainer));
@@ -329,4 +304,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
similarity index 88%
rename from polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
rename to polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
index ad0d1cf..051e6b3 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
@@ -1,86 +1,81 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2015 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.
+ */
 
-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
+import '../../../test/common-test-setup-karma.js';
+import '../../diff/gr-comment-api/gr-comment-api.js';
+import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
+import './gr-file-list.js';
+import {createCommentApiMockWithTemplateElement} from '../../../test/mocks/comment-api.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {GrFileListConstants} from '../gr-file-list-constants.js';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {runA11yAudit} from '../../../test/a11y-test-utils.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import {TestKeyboardShortcutBinder} from '../../../test/test-utils.js';
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-file-list</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script src="/components/web-component-tester/data/a11ySuite.js"></script>
-<script src="/node_modules/page/page.js"></script>
-
-<dom-module id="comment-api-mock">
-  <template>
+const commentApiMock = createCommentApiMockWithTemplateElement(
+    'gr-file-list-comment-api-mock', html`
     <gr-file-list id="fileList"
         change-comments="[[_changeComments]]"
         on-reload-drafts="_reloadDraftsWithCallback"></gr-file-list>
     <gr-comment-api id="commentAPI"></gr-comment-api>
-  </template>
-  </dom-module>
+`);
 
-<test-fixture id="basic">
-  <template>
-    <comment-api-mock></comment-api-mock>
-  </template>
-</test-fixture>
+const basicFixture = fixtureFromElement(commentApiMock.is);
 
-<script type="module">
-import '../../../test/common-test-setup.js';
-import '../../diff/gr-comment-api/gr-comment-api.js';
-import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
-import './gr-file-list.js';
-import '../../../test/mocks/comment-api.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
-import {GrFileListConstants} from '../gr-file-list-constants.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+suite('gr-diff a11y test', () => {
+  test('audit', async () => {
+    await runA11yAudit(basicFixture);
+  });
+});
 
 suite('gr-file-list tests', () => {
-  const kb = KeyboardShortcutBinder;
-  kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left');
-  kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right');
-  kb.bindShortcut(kb.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
-  kb.bindShortcut(kb.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
-  kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
-  kb.bindShortcut(kb.Shortcut.CURSOR_PREV_FILE, 'k', 'up');
-  kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down');
-  kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up');
-  kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c');
-  kb.bindShortcut(kb.Shortcut.OPEN_LAST_FILE, '[');
-  kb.bindShortcut(kb.Shortcut.OPEN_FIRST_FILE, ']');
-  kb.bindShortcut(kb.Shortcut.OPEN_FILE, 'o');
-  kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n');
-  kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p');
-  kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
-  kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
-
   let element;
   let commentApiWrapper;
-  let sandbox;
+
   let saveStub;
   let loadCommentSpy;
 
+  suiteSetup(() => {
+    const kb = TestKeyboardShortcutBinder.push();
+    kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left');
+    kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right');
+    kb.bindShortcut(kb.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
+    kb.bindShortcut(kb.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
+    kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
+    kb.bindShortcut(kb.Shortcut.CURSOR_PREV_FILE, 'k', 'up');
+    kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down');
+    kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up');
+    kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c');
+    kb.bindShortcut(kb.Shortcut.OPEN_LAST_FILE, '[');
+    kb.bindShortcut(kb.Shortcut.OPEN_FIRST_FILE, ']');
+    kb.bindShortcut(kb.Shortcut.OPEN_FILE, 'o');
+    kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n');
+    kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p');
+    kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
+    kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
+  });
+
+  suiteTeardown(() => {
+    TestKeyboardShortcutBinder.pop();
+  });
+
   suite('basic tests', () => {
     setup(done => {
-      sandbox = sinon.sandbox.create();
       stub('gr-rest-api-interface', {
         getLoggedIn() { return Promise.resolve(true); },
         getPreferences() { return Promise.resolve({}); },
@@ -100,15 +95,15 @@
 
       // Element must be wrapped in an element with direct access to the
       // comment API.
-      commentApiWrapper = fixture('basic');
+      commentApiWrapper = basicFixture.instantiate();
       element = commentApiWrapper.$.fileList;
-      loadCommentSpy = sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');
+      loadCommentSpy = sinon.spy(commentApiWrapper.$.commentAPI, 'loadAll');
 
       // Stub methods on the changeComments object after changeComments has
       // been initialized.
       commentApiWrapper.loadComments().then(() => {
-        sandbox.stub(element.changeComments, 'getPaths').returns({});
-        sandbox.stub(element.changeComments, 'getCommentsBySideForPath')
+        sinon.stub(element.changeComments, 'getPaths').returns({});
+        sinon.stub(element.changeComments, 'getCommentsBySideForPath')
             .returns({meta: {}, left: [], right: []});
         done();
       });
@@ -119,19 +114,15 @@
         basePatchNum: 'PARENT',
         patchNum: '2',
       };
-      saveStub = sandbox.stub(element, '_saveReviewedState',
+      saveStub = sinon.stub(element, '_saveReviewedState').callsFake(
           () => Promise.resolve());
     });
 
-    teardown(() => {
-      sandbox.restore();
-    });
-
     test('correct number of files are shown', () => {
       element.fileListIncrement = 300;
-      element._filesByPath = _.range(500)
-          .reduce((_filesByPath, i) => {
-            _filesByPath['/file' + i] = {lines_inserted: 9};
+      element._filesByPath = Array(500).fill(0)
+          .reduce((_filesByPath, _, idx) => {
+            _filesByPath['/file' + idx] = {lines_inserted: 9};
             return _filesByPath;
           }, {});
 
@@ -156,10 +147,10 @@
     });
 
     test('rendering each row calls the _reportRenderedRow method', () => {
-      const renderedStub = sandbox.stub(element, '_reportRenderedRow');
-      element._filesByPath = _.range(10)
-          .reduce((_filesByPath, i) => {
-            _filesByPath['/file' + i] = {lines_inserted: 9};
+      const renderedStub = sinon.stub(element, '_reportRenderedRow');
+      element._filesByPath = Array(10).fill(0)
+          .reduce((_filesByPath, _, idx) => {
+            _filesByPath['/file' + idx] = {lines_inserted: 9};
             return _filesByPath;
           }, {});
       flushAsynchronousOperations();
@@ -606,14 +597,11 @@
       });
 
       test('toggle left diff via shortcut', () => {
-        const toggleLeftDiffStub = sandbox.stub();
+        const toggleLeftDiffStub = sinon.stub();
         // Property getter cannot be stubbed w/ sandbox due to a bug in Sinon.
         // https://github.com/sinonjs/sinon/issues/781
-        const diffsStub = sinon.stub(element, 'diffs', {
-          get() {
-            return [{toggleLeftDiff: toggleLeftDiffStub}];
-          },
-        });
+        const diffsStub = sinon.stub(element, 'diffs')
+            .get(() => [{toggleLeftDiff: toggleLeftDiffStub}]);
         MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
         assert.isTrue(toggleLeftDiffStub.calledOnce);
         diffsStub.restore();
@@ -641,7 +629,7 @@
         assert.equal(element.selectedIndex, 1);
         MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
 
-        const navStub = sandbox.stub(GerritNav, 'navigateToDiff');
+        const navStub = sinon.stub(GerritNav, 'navigateToDiff');
         assert.equal(element.$.fileCursor.index, 2);
         assert.equal(element.selectedIndex, 2);
 
@@ -668,7 +656,7 @@
         assert.equal(element.$.fileCursor.index, 0);
         assert.equal(element.selectedIndex, 0);
 
-        const createCommentInPlaceStub = sandbox.stub(element.$.diffCursor,
+        const createCommentInPlaceStub = sinon.stub(element.$.diffCursor,
             'createCommentInPlace');
         MockInteractions.pressAndReleaseKeyOn(element, 67, null, 'c');
         assert.isTrue(createCommentInPlaceStub.called);
@@ -676,7 +664,7 @@
 
       test('i key shows/hides selected inline diff', () => {
         const paths = Object.keys(element._filesByPath);
-        sandbox.stub(element, '_expandedFilesChanged');
+        sinon.stub(element, '_expandedFilesChanged');
         flushAsynchronousOperations();
         const files = dom(element.root).querySelectorAll('.file-row');
         element.$.fileCursor.stops = files;
@@ -744,12 +732,12 @@
         let interact;
 
         setup(() => {
-          sandbox.stub(element, 'shouldSuppressKeyboardShortcut')
+          sinon.stub(element, 'shouldSuppressKeyboardShortcut')
               .returns(false);
-          sandbox.stub(element, 'modifierPressed').returns(false);
-          const openCursorStub = sandbox.stub(element, '_openCursorFile');
-          const openSelectedStub = sandbox.stub(element, '_openSelectedFile');
-          const expandStub = sandbox.stub(element, '_toggleFileExpanded');
+          sinon.stub(element, 'modifierPressed').returns(false);
+          const openCursorStub = sinon.stub(element, '_openCursorFile');
+          const openSelectedStub = sinon.stub(element, '_openSelectedFile');
+          const expandStub = sinon.stub(element, '_toggleFileExpanded');
 
           interact = function(opt_payload) {
             openCursorStub.reset();
@@ -793,11 +781,12 @@
       });
 
       test('shift+left/shift+right', () => {
-        const moveLeftStub = sandbox.stub(element.$.diffCursor, 'moveLeft');
-        const moveRightStub = sandbox.stub(element.$.diffCursor, 'moveRight');
+        const moveLeftStub = sinon.stub(element.$.diffCursor, 'moveLeft');
+        const moveRightStub = sinon.stub(element.$.diffCursor, 'moveRight');
 
         let noDiffsExpanded = true;
-        sandbox.stub(element, '_noDiffsExpanded', () => noDiffsExpanded);
+        sinon.stub(element, '_noDiffsExpanded')
+            .callsFake(() => noDiffsExpanded);
 
         MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'left');
         assert.isFalse(moveLeftStub.called);
@@ -837,8 +826,8 @@
         patchNum: '2',
       };
       element.$.fileCursor.setCursorAtIndex(0);
-      const reviewSpy = sandbox.spy(element, '_reviewFile');
-      const toggleExpandSpy = sandbox.spy(element, '_toggleFileExpanded');
+      const reviewSpy = sinon.spy(element, '_reviewFile');
+      const toggleExpandSpy = sinon.spy(element, '_toggleFileExpanded');
 
       flushAsynchronousOperations();
       const fileRows =
@@ -857,7 +846,7 @@
       assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
       assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');
 
-      const clickSpy = sandbox.spy(element, '_reviewedClick');
+      const clickSpy = sinon.spy(element, '_reviewedClick');
       MockInteractions.tap(markReviewLabel);
       // assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', false));
       // assert.isFalse(commitReviewLabel.classList.contains('isReviewed'));
@@ -892,9 +881,9 @@
         patchNum: '2',
       };
 
-      const clickSpy = sandbox.spy(element, '_handleFileListClick');
-      const reviewStub = sandbox.stub(element, '_reviewFile');
-      const toggleExpandSpy = sandbox.spy(element, '_toggleFileExpanded');
+      const clickSpy = sinon.spy(element, '_handleFileListClick');
+      const reviewStub = sinon.stub(element, '_reviewFile');
+      const toggleExpandSpy = sinon.spy(element, '_toggleFileExpanded');
 
       const row = dom(element.root)
           .querySelector(`.row[data-file='{"path":"f1.txt"}']`);
@@ -928,8 +917,8 @@
       };
       element.editMode = true;
       flushAsynchronousOperations();
-      const clickSpy = sandbox.spy(element, '_handleFileListClick');
-      const toggleExpandSpy = sandbox.spy(element, '_toggleFileExpanded');
+      const clickSpy = sinon.spy(element, '_handleFileListClick');
+      const toggleExpandSpy = sinon.spy(element, '_toggleFileExpanded');
 
       // Tap the edit controls. Should be ignored by _handleFileListClick.
       MockInteractions.tap(element.shadowRoot
@@ -969,7 +958,7 @@
         patchNum: '2',
       };
       element.$.fileCursor.setCursorAtIndex(0);
-      sandbox.stub(element, '_expandedFilesChanged');
+      sinon.stub(element, '_expandedFilesChanged');
       flushAsynchronousOperations();
       const fileRows =
           dom(element.root).querySelectorAll('.row:not(.header-row)');
@@ -995,7 +984,7 @@
         basePatchNum: 'PARENT',
         patchNum: '2',
       };
-      sandbox.spy(element, '_updateDiffPreferences');
+      sinon.spy(element, '_updateDiffPreferences');
       element.$.fileCursor.setCursorAtIndex(0);
       flushAsynchronousOperations();
 
@@ -1029,14 +1018,14 @@
         basePatchNum: 'PARENT',
         patchNum: '2',
       };
-      sandbox.stub(element, '_expandedFilesChanged');
+      sinon.stub(element, '_expandedFilesChanged');
       flushAsynchronousOperations();
       const commitMsgFile = dom(element.root)
           .querySelectorAll('.row:not(.header-row) a.pathLink')[0];
 
       // Remove href attribute so the app doesn't route to a diff view
       commitMsgFile.removeAttribute('href');
-      const togglePathSpy = sandbox.spy(element, '_toggleFileExpanded');
+      const togglePathSpy = sinon.spy(element, '_toggleFileExpanded');
 
       MockInteractions.tap(commitMsgFile);
       flushAsynchronousOperations();
@@ -1051,8 +1040,8 @@
     test('_toggleFileExpanded', () => {
       const path = 'path/to/my/file.txt';
       element._filesByPath = {[path]: {}};
-      const renderSpy = sandbox.spy(element, '_renderInOrder');
-      const collapseStub = sandbox.stub(element, '_clearCollapsedDiffs');
+      const renderSpy = sinon.spy(element, '_renderInOrder');
+      const collapseStub = sinon.stub(element, '_clearCollapsedDiffs');
 
       assert.equal(element.shadowRoot
           .querySelector('iron-icon').icon, 'gr-icons:expand-more');
@@ -1076,10 +1065,10 @@
     });
 
     test('expandAllDiffs and collapseAllDiffs', () => {
-      const collapseStub = sandbox.stub(element, '_clearCollapsedDiffs');
-      const cursorUpdateStub = sandbox.stub(element.$.diffCursor,
+      const collapseStub = sinon.stub(element, '_clearCollapsedDiffs');
+      const cursorUpdateStub = sinon.stub(element.$.diffCursor,
           'handleDiffUpdate');
-      const reInitStub = sandbox.stub(element.$.diffCursor,
+      const reInitStub = sinon.stub(element.$.diffCursor,
           'reInitAndUpdateStops');
 
       const path = 'path/to/my/file.txt';
@@ -1099,7 +1088,7 @@
     });
 
     test('_expandedFilesChanged', done => {
-      sandbox.stub(element, '_reviewFile');
+      sinon.stub(element, '_reviewFile');
       const path = 'path/to/my/file.txt';
       const diffs = [{
         path,
@@ -1117,9 +1106,7 @@
           }
         },
       }];
-      sinon.stub(element, 'diffs', {
-        get() { return diffs; },
-      });
+      sinon.stub(element, 'diffs').get(() => diffs);
       element.push('_expandedFiles', {path});
     });
 
@@ -1160,7 +1147,7 @@
     });
 
     test('_renderInOrder', done => {
-      const reviewStub = sandbox.stub(element, '_reviewFile');
+      const reviewStub = sinon.stub(element, '_reviewFile');
       let callCount = 0;
       const diffs = [{
         path: 'p0',
@@ -1199,7 +1186,7 @@
 
     test('_renderInOrder logged in', done => {
       element._loggedIn = true;
-      const reviewStub = sandbox.stub(element, '_reviewFile');
+      const reviewStub = sinon.stub(element, '_reviewFile');
       let callCount = 0;
       const diffs = [{
         path: 'p0',
@@ -1241,7 +1228,7 @@
     test('_renderInOrder respects diffPrefs.manual_review', () => {
       element._loggedIn = true;
       element.diffPrefs = {manual_review: true};
-      const reviewStub = sandbox.stub(element, '_reviewFile');
+      const reviewStub = sinon.stub(element, '_reviewFile');
       const diffs = [{
         path: 'p',
         style: {},
@@ -1260,7 +1247,7 @@
     });
 
     test('_loadingChanged fired from reload in debouncer', done => {
-      sandbox.stub(element, '_getReviewedFiles').returns(Promise.resolve([]));
+      sinon.stub(element, '_getReviewedFiles').returns(Promise.resolve([]));
       element.changeNum = 123;
       element.patchRange = {patchNum: 12};
       element._filesByPath = {'foo.bar': {}};
@@ -1278,7 +1265,7 @@
     });
 
     test('_loadingChanged does not set class when there are no files', () => {
-      sandbox.stub(element, '_getReviewedFiles').returns(Promise.resolve([]));
+      sinon.stub(element, '_getReviewedFiles').returns(Promise.resolve([]));
       element.changeNum = 123;
       element.patchRange = {patchNum: 12};
       element.reload();
@@ -1290,7 +1277,7 @@
 
   suite('diff url file list', () => {
     test('diff url', () => {
-      const diffStub = sandbox.stub(GerritNav, 'getUrlForDiff')
+      const diffStub = sinon.stub(GerritNav, 'getUrlForDiff')
           .returns('/c/gerrit/+/1/1/index.php');
       const change = {
         _number: 1,
@@ -1307,7 +1294,7 @@
     });
 
     test('diff url commit msg', () => {
-      const diffStub = sandbox.stub(GerritNav, 'getUrlForDiff')
+      const diffStub = sinon.stub(GerritNav, 'getUrlForDiff')
           .returns('/c/gerrit/+/1/1//COMMIT_MSG');
       const change = {
         _number: 1,
@@ -1324,7 +1311,7 @@
     });
 
     test('edit url', () => {
-      const editStub = sandbox.stub(GerritNav, 'getEditUrlForDiff')
+      const editStub = sinon.stub(GerritNav, 'getEditUrlForDiff')
           .returns('/c/gerrit/+/1/edit/index.php,edit');
       const change = {
         _number: 1,
@@ -1341,7 +1328,7 @@
     });
 
     test('edit url commit msg', () => {
-      const editStub = sandbox.stub(GerritNav, 'getEditUrlForDiff')
+      const editStub = sinon.stub(GerritNav, 'getEditUrlForDiff')
           .returns('/c/gerrit/+/1/edit//COMMIT_MSG,edit');
       const change = {
         _number: 1,
@@ -1486,7 +1473,6 @@
 
   suite('gr-file-list inline diff tests', () => {
     let element;
-    let sandbox;
 
     const commitMsgComments = [
       {
@@ -1562,7 +1548,6 @@
     };
 
     setup(done => {
-      sandbox = sinon.sandbox.create();
       stub('gr-rest-api-interface', {
         getLoggedIn() { return Promise.resolve(true); },
         getPreferences() { return Promise.resolve({}); },
@@ -1580,17 +1565,17 @@
 
       // Element must be wrapped in an element with direct access to the
       // comment API.
-      commentApiWrapper = fixture('basic');
+      commentApiWrapper = basicFixture.instantiate();
       element = commentApiWrapper.$.fileList;
-      loadCommentSpy = sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');
+      loadCommentSpy = sinon.spy(commentApiWrapper.$.commentAPI, 'loadAll');
       element.diffPrefs = {};
-      sandbox.stub(element, '_reviewFile');
+      sinon.stub(element, '_reviewFile');
 
       // Stub methods on the changeComments object after changeComments has
       // been initialized.
       commentApiWrapper.loadComments().then(() => {
-        sandbox.stub(element.changeComments, 'getPaths').returns({});
-        sandbox.stub(element.changeComments, 'getCommentsBySideForPath')
+        sinon.stub(element.changeComments, 'getPaths').returns({});
+        sinon.stub(element.changeComments, 'getCommentsBySideForPath')
             .returns({meta: {}, left: [], right: []});
         done();
       });
@@ -1619,14 +1604,10 @@
         basePatchNum: 'PARENT',
         patchNum: '2',
       };
-      sandbox.stub(window, 'fetch', () => Promise.resolve());
+      sinon.stub(window, 'fetch').callsFake(() => Promise.resolve());
       flushAsynchronousOperations();
     });
 
-    teardown(() => {
-      sandbox.restore();
-    });
-
     test('cursor with individually opened files', () => {
       MockInteractions.keyUpOn(element, 73, null, 'i');
       flushAsynchronousOperations();
@@ -1705,11 +1686,11 @@
       let fileRows;
 
       setup(() => {
-        sandbox.stub(element, '_renderInOrder').returns(Promise.resolve());
-        nKeySpy = sandbox.spy(element, '_handleNextChunk');
-        nextCommentStub = sandbox.stub(element.$.diffCursor,
+        sinon.stub(element, '_renderInOrder').returns(Promise.resolve());
+        nKeySpy = sinon.spy(element, '_handleNextChunk');
+        nextCommentStub = sinon.stub(element.$.diffCursor,
             'moveToNextCommentThread');
-        nextChunkStub = sandbox.stub(element.$.diffCursor,
+        nextChunkStub = sinon.stub(element.$.diffCursor,
             'moveToNextChunk');
         fileRows =
             dom(element.root).querySelectorAll('.row:not(.header-row)');
@@ -1773,7 +1754,7 @@
     test('_openSelectedFile behavior', () => {
       const _filesByPath = element._filesByPath;
       element.set('_filesByPath', {});
-      const navStub = sandbox.stub(GerritNav, 'navigateToDiff');
+      const navStub = sinon.stub(GerritNav, 'navigateToDiff');
       // Noop when there are no files.
       element._openSelectedFile();
       assert.isFalse(navStub.called);
@@ -1786,8 +1767,10 @@
     });
 
     test('_displayLine', () => {
-      sandbox.stub(element, 'shouldSuppressKeyboardShortcut', () => false);
-      sandbox.stub(element, 'modifierPressed', () => false);
+      sinon.stub(element, 'shouldSuppressKeyboardShortcut')
+          .callsFake(() => false);
+      sinon.stub(element, 'modifierPressed')
+          .callsFake(() => false);
       element._showInlineDiffs = true;
       const mockEvent = {preventDefault() {}};
 
@@ -1807,7 +1790,7 @@
     suite('editMode behavior', () => {
       test('reviewed checkbox', () => {
         element._reviewFile.restore();
-        const saveReviewStub = sandbox.stub(element, '_saveReviewedState');
+        const saveReviewStub = sinon.stub(element, '_saveReviewedState');
 
         element.editMode = false;
         MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
@@ -1821,7 +1804,7 @@
       });
 
       test('_getReviewedFiles does not call API', () => {
-        const apiSpy = sandbox.spy(element.$.restAPI, 'getReviewedFiles');
+        const apiSpy = sinon.spy(element.$.restAPI, 'getReviewedFiles');
         element.editMode = true;
         return element._getReviewedFiles().then(files => {
           assert.equal(files.length, 0);
@@ -1871,7 +1854,7 @@
       assert.equal(thread2.comments[0].line, 20);
 
       const commentStub =
-          sandbox.stub(element.changeComments, 'getCommentsForThread');
+          sinon.stub(element.changeComments, 'getCommentsForThread');
       const commentStubRes1 = [
         {
           patch_set: 2,
@@ -1930,7 +1913,7 @@
       assert.equal(thread2.comments.length, 3);
 
       const commentStubCount = commentStub.callCount;
-      const getThreadsSpy = sandbox.spy(diffs[0], 'getThreadEls');
+      const getThreadsSpy = sinon.spy(diffs[0], 'getThreadEls');
 
       // Should not be getting threads when the file is not expanded.
       element.reloadCommentsForThreadWithRootId('ecf0b9fa_fe1a5f62',
@@ -1947,6 +1930,5 @@
       assert.equal(commentStubCount, commentStub.callCount);
     });
   });
-  a11ySuite('basic');
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
deleted file mode 100644
index 5d5b1fc..0000000
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
+++ /dev/null
@@ -1,100 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-included-in-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-included-in-dialog></gr-included-in-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-included-in-dialog.js';
-suite('gr-included-in-dialog', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => { sandbox.restore(); });
-
-  test('_computeGroups', () => {
-    const includedIn = {branches: [], tags: []};
-    let filterText = '';
-    assert.deepEqual(element._computeGroups(includedIn, filterText), []);
-
-    includedIn.branches.push('master', 'development', 'stable-2.0');
-    includedIn.tags.push('v1.9', 'v2.0', 'v2.1');
-    assert.deepEqual(element._computeGroups(includedIn, filterText), [
-      {title: 'Branches', items: ['master', 'development', 'stable-2.0']},
-      {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']},
-    ]);
-
-    includedIn.external = {};
-    assert.deepEqual(element._computeGroups(includedIn, filterText), [
-      {title: 'Branches', items: ['master', 'development', 'stable-2.0']},
-      {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']},
-    ]);
-
-    includedIn.external.foo = ['abc', 'def', 'ghi'];
-    assert.deepEqual(element._computeGroups(includedIn, filterText), [
-      {title: 'Branches', items: ['master', 'development', 'stable-2.0']},
-      {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']},
-      {title: 'foo', items: ['abc', 'def', 'ghi']},
-    ]);
-
-    filterText = 'v2';
-    assert.deepEqual(element._computeGroups(includedIn, filterText), [
-      {title: 'Tags', items: ['v2.0', 'v2.1']},
-    ]);
-
-    // Filtering is case-insensitive.
-    filterText = 'V2';
-    assert.deepEqual(element._computeGroups(includedIn, filterText), [
-      {title: 'Tags', items: ['v2.0', 'v2.1']},
-    ]);
-  });
-
-  test('_computeGroups with .bindValue', done => {
-    element.$.filterInput.bindValue = 'stable-3.2';
-    const includedIn = {branches: [], tags: []};
-    includedIn.branches.push('master', 'stable-3.2');
-
-    setTimeout(() => {
-      const filterText = element._filterText;
-      assert.deepEqual(element._computeGroups(includedIn, filterText), [
-        {title: 'Branches', items: ['stable-3.2']},
-      ]);
-
-      done();
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.js b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.js
new file mode 100644
index 0000000..c109538
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.js
@@ -0,0 +1,82 @@
+/**
+ * @license
+ * Copyright (C) 2018 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-included-in-dialog.js';
+
+const basicFixture = fixtureFromElement('gr-included-in-dialog');
+
+suite('gr-included-in-dialog', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('_computeGroups', () => {
+    const includedIn = {branches: [], tags: []};
+    let filterText = '';
+    assert.deepEqual(element._computeGroups(includedIn, filterText), []);
+
+    includedIn.branches.push('master', 'development', 'stable-2.0');
+    includedIn.tags.push('v1.9', 'v2.0', 'v2.1');
+    assert.deepEqual(element._computeGroups(includedIn, filterText), [
+      {title: 'Branches', items: ['master', 'development', 'stable-2.0']},
+      {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']},
+    ]);
+
+    includedIn.external = {};
+    assert.deepEqual(element._computeGroups(includedIn, filterText), [
+      {title: 'Branches', items: ['master', 'development', 'stable-2.0']},
+      {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']},
+    ]);
+
+    includedIn.external.foo = ['abc', 'def', 'ghi'];
+    assert.deepEqual(element._computeGroups(includedIn, filterText), [
+      {title: 'Branches', items: ['master', 'development', 'stable-2.0']},
+      {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']},
+      {title: 'foo', items: ['abc', 'def', 'ghi']},
+    ]);
+
+    filterText = 'v2';
+    assert.deepEqual(element._computeGroups(includedIn, filterText), [
+      {title: 'Tags', items: ['v2.0', 'v2.1']},
+    ]);
+
+    // Filtering is case-insensitive.
+    filterText = 'V2';
+    assert.deepEqual(element._computeGroups(includedIn, filterText), [
+      {title: 'Tags', items: ['v2.0', 'v2.1']},
+    ]);
+  });
+
+  test('_computeGroups with .bindValue', done => {
+    element.$.filterInput.bindValue = 'stable-3.2';
+    const includedIn = {branches: [], tags: []};
+    includedIn.branches.push('master', 'stable-3.2');
+
+    setTimeout(() => {
+      const filterText = element._filterText;
+      assert.deepEqual(element._computeGroups(includedIn, filterText), [
+        {title: 'Branches', items: ['stable-3.2']},
+      ]);
+
+      done();
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.js
similarity index 85%
rename from polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
rename to polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.js
index ca4fac3..0739f73 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.js
@@ -1,47 +1,31 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-label-score-row</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-label-score-row></gr-label-score-row>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-label-score-row.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromElement('gr-label-score-row');
+
 suite('gr-label-row-score tests', () => {
   let element;
-  let sandbox;
 
   setup(done => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.labels = {
       'Code-Review': {
         values: {
@@ -100,10 +84,6 @@
     flush(done);
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   function checkAriaCheckedValid() {
     const items = element.$.labelSelector.items;
     const selectedItem = element.selectedItem;
@@ -119,7 +99,7 @@
   }
 
   test('label picker', () => {
-    const labelsChangedHandler = sandbox.stub();
+    const labelsChangedHandler = sinon.stub();
     element.addEventListener('labels-changed', labelsChangedHandler);
     assert.ok(element.$.labelSelector);
     MockInteractions.tap(element.shadowRoot
@@ -387,4 +367,4 @@
     assert.isNull(element.selectedValue);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.js
similarity index 73%
rename from polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html
rename to polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.js
index 7ee86d6..ffc17cd 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.js
@@ -1,49 +1,33 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-label-scores</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-label-scores></gr-label-scores>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-label-scores.js';
+
+const basicFixture = fixtureFromElement('gr-label-scores');
+
 suite('gr-label-scores tests', () => {
   let element;
-  let sandbox;
 
   setup(done => {
-    sandbox = sinon.sandbox.create();
     stub('gr-rest-api-interface', {
       getLoggedIn() { return Promise.resolve(false); },
     });
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.change = {
       _number: '123',
       labels: {
@@ -101,10 +85,6 @@
     flush(done);
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('get and set label scores', () => {
     for (const label in element.permittedLabels) {
       if (element.permittedLabels.hasOwnProperty(label)) {
@@ -193,4 +173,4 @@
     ]);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js
similarity index 91%
rename from polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
rename to polygerrit-ui/app/elements/change/gr-message/gr-message_test.js
index 332ca52..d425398 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js
@@ -1,40 +1,26 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-message</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-message></gr-message>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-message.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromElement('gr-message');
+
 suite('gr-message tests', () => {
   let element;
 
@@ -47,7 +33,7 @@
         getIsAdmin() { return Promise.resolve(true); },
         deleteChangeCommitMessage() { return Promise.resolve({}); },
       });
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       flush(done);
     });
 
@@ -369,7 +355,7 @@
         getIsAdmin() { return Promise.resolve(false); },
         deleteChangeCommitMessage() { return Promise.resolve({}); },
       });
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       flush(done);
     });
 
@@ -399,7 +385,7 @@
 
   suite('patchset comment summary', () => {
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.message = {id: '6a07f64a82f96e7337ca5f7f84cfc73abf8ac2a3'};
     });
 
@@ -467,7 +453,7 @@
         getIsAdmin() { return Promise.resolve(false); },
         deleteChangeCommitMessage() { return Promise.resolve({}); },
       });
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       flush(done);
     });
 
@@ -521,4 +507,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_test.js
similarity index 86%
rename from polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_test.html
rename to polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_test.js
index c520b05..1a0969f 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_test.js
@@ -1,55 +1,42 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2020 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2020 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-messages-list-experimental</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<dom-module id="comment-api-mock">
-  <template>
-    <gr-messages-list-experimental
-        id="messagesList"
-        change-comments="[[_changeComments]]"></gr-messages-list-experimental>
-    <gr-comment-api id="commentAPI"></gr-comment-api>
-  </template>
-  </dom-module>
-
-<test-fixture id="basic">
-  <template>
-    <comment-api-mock>
-      <gr-messages-list-experimental></gr-messages-list-experimental>
-    </comment-api-mock>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import '../../diff/gr-comment-api/gr-comment-api.js';
 import './gr-messages-list-experimental.js';
-import '../../../test/mocks/comment-api.js';
+import {createCommentApiMockWithTemplateElement} from '../../../test/mocks/comment-api.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {TEST_ONLY} from './gr-messages-list-experimental.js';
 import {MessageTag} from '../../../constants/constants.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+createCommentApiMockWithTemplateElement(
+    'gr-messages-list-experimental-comment-mock-api', html`
+     <gr-messages-list-experimental
+         id="messagesList"
+         change-comments="[[_changeComments]]"></gr-messages-list-experimental>
+     <gr-comment-api id="commentAPI"></gr-comment-api>
+`);
+
+const basicFixture = fixtureFromTemplate(html`
+<gr-messages-list-experimental-comment-mock-api>
+  <gr-messages-list-experimental></gr-messages-list-experimental>
+</gr-messages-list-experimental-comment-mock-api>
+`);
 
 const randomMessage = function(opt_params) {
   const params = opt_params || {};
@@ -68,10 +55,15 @@
   };
 };
 
+function generateRandomMessages(count) {
+  return new Array(count).fill()
+      .map(() => randomMessage());
+}
+
 suite('gr-messages-list-experimental tests', () => {
   let element;
   let messages;
-  let sandbox;
+
   let commentApiWrapper;
 
   const getMessages = function() {
@@ -148,11 +140,11 @@
         getDiffRobotComments() { return Promise.resolve({}); },
         getDiffDrafts() { return Promise.resolve({}); },
       });
-      sandbox = sinon.sandbox.create();
-      messages = _.times(3, randomMessage);
+
+      messages = generateRandomMessages(3);
       // Element must be wrapped in an element with direct access to the
       // comment API.
-      commentApiWrapper = fixture('basic');
+      commentApiWrapper = basicFixture.instantiate();
       element = commentApiWrapper.$.messagesList;
       element.messages = messages;
 
@@ -161,10 +153,6 @@
       return commentApiWrapper.loadComments();
     });
 
-    teardown(() => {
-      sandbox.restore();
-    });
-
     test('expand/collapse all', () => {
       let allMessageEls = getMessages();
       for (const message of allMessageEls) {
@@ -219,8 +207,8 @@
         message.set('message.expanded', false);
       }
 
-      const scrollToStub = sandbox.stub(window, 'scrollTo');
-      const highlightStub = sandbox.stub(element, '_highlightEl');
+      const scrollToStub = sinon.stub(window, 'scrollTo');
+      const highlightStub = sinon.stub(element, '_highlightEl');
 
       element.scrollToMessage('invalid');
 
@@ -241,9 +229,9 @@
     });
 
     test('scroll to message offscreen', () => {
-      const scrollToStub = sandbox.stub(window, 'scrollTo');
-      const highlightStub = sandbox.stub(element, '_highlightEl');
-      element.messages = _.times(25, randomMessage);
+      const scrollToStub = sinon.stub(window, 'scrollTo');
+      const highlightStub = sinon.stub(element, '_highlightEl');
+      element.messages = generateRandomMessages(25);
       flushAsynchronousOperations();
       assert.isFalse(scrollToStub.called);
       assert.isFalse(highlightStub.called);
@@ -450,7 +438,7 @@
   suite('gr-messages-list-experimental automate tests', () => {
     let element;
     let messages;
-    let sandbox;
+
     let commentApiWrapper;
 
     setup(() => {
@@ -462,7 +450,6 @@
         getDiffDrafts() { return Promise.resolve({}); },
       });
 
-      sandbox = sinon.sandbox.create();
       messages = [
         randomMessage(),
         randomMessage({tag: 'auto', _revision_number: 2}),
@@ -471,9 +458,9 @@
 
       // Element must be wrapped in an element with direct access to the
       // comment API.
-      commentApiWrapper = fixture('basic');
+      commentApiWrapper = basicFixture.instantiate();
       element = commentApiWrapper.$.messagesList;
-      sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');
+      sinon.spy(commentApiWrapper.$.commentAPI, 'loadAll');
       element.messages = messages;
 
       // Stub methods on the changeComments object after changeComments has
@@ -481,10 +468,6 @@
       return commentApiWrapper.loadComments();
     });
 
-    teardown(() => {
-      sandbox.restore();
-    });
-
     test('hide autogenerated button is not hidden', () => {
       const toggle = dom(element.root).querySelector('.showAllActivityToggle');
       assert.isOk(toggle);
@@ -516,7 +499,7 @@
     });
 
     test('_computeLabelExtremes', () => {
-      const computeSpy = sandbox.spy(element, '_computeLabelExtremes');
+      const computeSpy = sinon.spy(element, '_computeLabelExtremes');
 
       element.labels = null;
       assert.isTrue(computeSpy.calledOnce);
@@ -558,4 +541,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.js
similarity index 81%
rename from polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
rename to polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.js
index 4a0f3f1..4f05dfc 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.js
@@ -1,53 +1,42 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+import {createCommentApiMockWithTemplateElement} from '../../../test/mocks/comment-api';
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-messages-list</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<dom-module id="comment-api-mock">
-  <template>
-    <gr-messages-list
-        id="messagesList"
-        change-comments="[[_changeComments]]"></gr-messages-list>
-    <gr-comment-api id="commentAPI"></gr-comment-api>
-  </template>
-  </dom-module>
-
-<test-fixture id="basic">
-  <template>
-    <comment-api-mock>
-      <gr-messages-list></gr-messages-list>
-    </comment-api-mock>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import '../../diff/gr-comment-api/gr-comment-api.js';
 import './gr-messages-list.js';
 import '../../../test/mocks/comment-api.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+createCommentApiMockWithTemplateElement(
+    'gr-messages-list-comment-mock-api', html`
+     <gr-messages-list
+         id="messagesList"
+         change-comments="[[_changeComments]]"></gr-messages-list>
+     <gr-comment-api id="commentAPI"></gr-comment-api>
+`);
+
+const basicFixture = fixtureFromTemplate(html`
+<gr-messages-list-comment-mock-api>
+  <gr-messages-list></gr-messages-list>
+</gr-messages-list-comment-mock-api>
+`);
+
 const randomMessage = function(opt_params) {
   const params = opt_params || {};
   const author1 = {
@@ -69,10 +58,36 @@
       randomMessage(opt_params));
 };
 
+function generateRandomMessages(count) {
+  return new Array(count).fill()
+      .map(() => randomMessage());
+}
+
+function generateRandomAutomatedMessages(count) {
+  return new Array(count).fill()
+      .map(() => randomAutomated());
+}
+
+// Returns a shuffled copy of array
+export function shuffle(arr) {
+  const result = [];
+  for (const item of arr) {
+    // Random number in the interval [0..array.length]
+    const j = Math.floor(Math.random() * (arr.length + 1));
+    if (j < result.length) {
+      result.push(result[j]);
+      result[j] = item;
+    } else {
+      result.push(item);
+    }
+  }
+  return result;
+}
+
 suite('gr-messages-list tests', () => {
   let element;
   let messages;
-  let sandbox;
+
   let commentApiWrapper;
 
   const getMessages = function() {
@@ -140,11 +155,11 @@
         getDiffRobotComments() { return Promise.resolve({}); },
         getDiffDrafts() { return Promise.resolve({}); },
       });
-      sandbox = sinon.sandbox.create();
-      messages = _.times(3, randomMessage);
+
+      messages = generateRandomMessages(3);
       // Element must be wrapped in an element with direct access to the
       // comment API.
-      commentApiWrapper = fixture('basic');
+      commentApiWrapper = basicFixture.instantiate();
       element = commentApiWrapper.$.messagesList;
       element.messages = messages;
 
@@ -153,13 +168,9 @@
       return commentApiWrapper.loadComments();
     });
 
-    teardown(() => {
-      sandbox.restore();
-    });
-
     test('show some old messages', () => {
       assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
-      element.messages = _.times(26, randomMessage);
+      element.messages = generateRandomMessages(26);
       flushAsynchronousOperations();
 
       assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
@@ -181,7 +192,7 @@
 
     test('show all old messages', () => {
       assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
-      element.messages = _.times(26, randomMessage);
+      element.messages = generateRandomMessages(26);
       flushAsynchronousOperations();
 
       assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
@@ -196,8 +207,8 @@
     });
 
     test('message count respects automated', () => {
-      element.messages = _.times(10, randomAutomated)
-          .concat(_.times(11, randomMessage));
+      element.messages = generateRandomAutomatedMessages(10)
+          .concat(generateRandomMessages(11));
       flushAsynchronousOperations();
 
       assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
@@ -210,8 +221,8 @@
     });
 
     test('message count still respects non-automated on toggle', () => {
-      element.messages = _.times(10, randomMessage)
-          .concat(_.times(11, randomAutomated));
+      element.messages = generateRandomMessages(10)
+          .concat(generateRandomAutomatedMessages(11));
       flushAsynchronousOperations();
 
       assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
@@ -226,8 +237,8 @@
     });
 
     test('show all messages respects expand', () => {
-      element.messages = _.times(10, randomAutomated)
-          .concat(_.times(11, randomMessage));
+      element.messages = generateRandomAutomatedMessages(10)
+          .concat(generateRandomMessages(11));
       flushAsynchronousOperations();
 
       MockInteractions.tap(element.shadowRoot
@@ -251,8 +262,8 @@
     });
 
     test('show all messages respects collapse', () => {
-      element.messages = _.times(10, randomAutomated)
-          .concat(_.times(11, randomMessage));
+      element.messages = generateRandomAutomatedMessages(10)
+          .concat(generateRandomMessages(11));
       flushAsynchronousOperations();
 
       MockInteractions.tap(element.shadowRoot
@@ -329,8 +340,8 @@
         message.set('message.expanded', false);
       }
 
-      const scrollToStub = sandbox.stub(window, 'scrollTo');
-      const highlightStub = sandbox.stub(element, '_highlightEl');
+      const scrollToStub = sinon.stub(window, 'scrollTo');
+      const highlightStub = sinon.stub(element, '_highlightEl');
 
       element.scrollToMessage('invalid');
 
@@ -351,9 +362,9 @@
     });
 
     test('scroll to message offscreen', () => {
-      const scrollToStub = sandbox.stub(window, 'scrollTo');
-      const highlightStub = sandbox.stub(element, '_highlightEl');
-      element.messages = _.times(25, randomMessage);
+      const scrollToStub = sinon.stub(window, 'scrollTo');
+      const highlightStub = sinon.stub(element, '_highlightEl');
+      element.messages = generateRandomMessages(25);
       flushAsynchronousOperations();
       assert.isFalse(scrollToStub.called);
       assert.isFalse(highlightStub.called);
@@ -430,12 +441,12 @@
     test('hide increment text if increment >= total remaining', () => {
       // Test with stubbed return values, as _numRemaining and _getDelta have
       // their own tests.
-      sandbox.stub(element, '_getDelta').returns(5);
-      const remainingStub = sandbox.stub(element, '_numRemaining').returns(6);
+      sinon.stub(element, '_getDelta').returns(5);
+      const remainingStub = sinon.stub(element, '_numRemaining').returns(6);
       assert.isFalse(element._computeIncrementHidden(null, null, null));
       remainingStub.restore();
 
-      sandbox.stub(element, '_numRemaining').returns(4);
+      sinon.stub(element, '_numRemaining').returns(4);
       assert.isTrue(element._computeIncrementHidden(null, null, null));
     });
   });
@@ -443,7 +454,7 @@
   suite('gr-messages-list automate tests', () => {
     let element;
     let messages;
-    let sandbox;
+
     let commentApiWrapper;
 
     const getMessages = function() {
@@ -467,15 +478,14 @@
         getDiffDrafts() { return Promise.resolve({}); },
       });
 
-      sandbox = sinon.sandbox.create();
-      messages = _.times(2, randomAutomated);
+      messages = generateRandomAutomatedMessages(2);
       messages.push(randomMessageReviewer);
 
       // Element must be wrapped in an element with direct access to the
       // comment API.
-      commentApiWrapper = fixture('basic');
+      commentApiWrapper = basicFixture.instantiate();
       element = commentApiWrapper.$.messagesList;
-      sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');
+      sinon.spy(commentApiWrapper.$.commentAPI, 'loadAll');
       element.messages = messages;
 
       // Stub methods on the changeComments object after changeComments has
@@ -483,10 +493,6 @@
       return commentApiWrapper.loadComments();
     });
 
-    teardown(() => {
-      sandbox.restore();
-    });
-
     test('hide autogenerated button is not hidden', () => {
       assert.isNotOk(element.shadowRoot
           .querySelector('#automatedMessageToggle[hidden]'));
@@ -529,34 +535,35 @@
       assert.equal(element._getDelta([], messages, false), 1);
       assert.equal(element._getDelta([], messages, true), 1);
 
-      messages = _.times(7, randomMessage);
+      messages = generateRandomMessages(7);
       assert.equal(element._getDelta([], messages, false), 5);
       assert.equal(element._getDelta([], messages, true), 5);
 
-      messages = _.times(4, randomMessage)
-          .concat(_.times(2, randomAutomated))
-          .concat(_.times(3, randomMessage));
+      messages = generateRandomMessages(4)
+          .concat(generateRandomAutomatedMessages(2))
+          .concat(generateRandomMessages(3));
 
-      const dummyArr = _.times(2, randomMessage);
+      const dummyArr = generateRandomMessages(2);
       assert.equal(element._getDelta(dummyArr, messages, false), 5);
       assert.equal(element._getDelta(dummyArr, messages, true), 7);
     });
 
     test('_getHumanMessages', () => {
       assert.equal(
-          element._getHumanMessages(_.times(5, randomAutomated)).length, 0);
+          element._getHumanMessages(
+              generateRandomAutomatedMessages(5)).length, 0);
       assert.equal(
-          element._getHumanMessages(_.times(5, randomMessage)).length, 5);
+          element._getHumanMessages(generateRandomMessages(5)).length, 5);
 
-      let messages = _.shuffle(_.times(5, randomMessage)
-          .concat(_.times(5, randomAutomated)));
+      let messages = shuffle(generateRandomMessages(5)
+          .concat(generateRandomAutomatedMessages(5)));
       messages = element._getHumanMessages(messages);
       assert.equal(messages.length, 5);
       assert.isFalse(element._hasAutomatedMessages(messages));
     });
 
     test('initially show only 20 messages', () => {
-      sandbox.stub(element.reporting, 'reportInteraction',
+      sinon.stub(element.reporting, 'reportInteraction').callsFake(
           (eventName, details) => {
             assert.equal(typeof(eventName), 'string');
             if (details) {
@@ -573,7 +580,7 @@
     });
 
     test('_computeLabelExtremes', () => {
-      const computeSpy = sandbox.spy(element, '_computeLabelExtremes');
+      const computeSpy = sinon.spy(element, '_computeLabelExtremes');
 
       element.labels = null;
       assert.isTrue(computeSpy.calledOnce);
@@ -615,4 +622,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.js
similarity index 86%
rename from polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
rename to polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.js
index e639217..4c164ba 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.js
@@ -1,38 +1,21 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-related-changes-list</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-related-changes-list></gr-related-changes-list>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-related-changes-list.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
@@ -40,17 +23,13 @@
 
 const pluginApi = _testOnly_initGerritPluginApi();
 
+const basicFixture = fixtureFromElement('gr-related-changes-list');
+
 suite('gr-related-changes-list tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    element = basicFixture.instantiate();
   });
 
   test('connected revisions', () => {
@@ -262,7 +241,7 @@
   });
 
   test('event for section loaded fires for each section ', () => {
-    const loadedStub = sandbox.stub();
+    const loadedStub = sinon.stub();
     element.patchNum = 7;
     element.change = {
       change_id: 123,
@@ -270,13 +249,13 @@
     };
     element.mergeable = true;
     element.addEventListener('new-section-loaded', loadedStub);
-    sandbox.stub(element, '_getRelatedChanges')
+    sinon.stub(element, '_getRelatedChanges')
         .returns(Promise.resolve({changes: []}));
-    sandbox.stub(element, '_getSubmittedTogether')
+    sinon.stub(element, '_getSubmittedTogether')
         .returns(Promise.resolve());
-    sandbox.stub(element, '_getCherryPicks')
+    sinon.stub(element, '_getCherryPicks')
         .returns(Promise.resolve());
-    sandbox.stub(element, '_getConflicts')
+    sinon.stub(element, '_getConflicts')
         .returns(Promise.resolve());
 
     return element.reload().then(() => {
@@ -288,15 +267,15 @@
     let element;
 
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
 
-      sandbox.stub(element, '_getRelatedChanges')
+      sinon.stub(element, '_getRelatedChanges')
           .returns(Promise.resolve({changes: []}));
-      sandbox.stub(element, '_getSubmittedTogether')
+      sinon.stub(element, '_getSubmittedTogether')
           .returns(Promise.resolve());
-      sandbox.stub(element, '_getCherryPicks')
+      sinon.stub(element, '_getCherryPicks')
           .returns(Promise.resolve());
-      sandbox.stub(element, '_getConflicts')
+      sinon.stub(element, '_getConflicts')
           .returns(Promise.resolve());
     });
 
@@ -317,15 +296,15 @@
     let conflictsStub;
 
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
 
-      sandbox.stub(element, '_getRelatedChanges')
+      sinon.stub(element, '_getRelatedChanges')
           .returns(Promise.resolve({changes: []}));
-      sandbox.stub(element, '_getSubmittedTogether')
+      sinon.stub(element, '_getSubmittedTogether')
           .returns(Promise.resolve());
-      sandbox.stub(element, '_getCherryPicks')
+      sinon.stub(element, '_getCherryPicks')
           .returns(Promise.resolve());
-      conflictsStub = sandbox.stub(element, '_getConflicts')
+      conflictsStub = sinon.stub(element, '_getConflicts')
           .returns(Promise.resolve());
     });
 
@@ -423,7 +402,7 @@
     });
 
     test('update fires', () => {
-      const updateHandler = sandbox.stub();
+      const updateHandler = sinon.stub();
       element.addEventListener('update', updateHandler);
 
       element._resultsChanged({}, {}, [], [], []);
@@ -486,7 +465,7 @@
   });
 
   test('_computeChangeURL uses GerritNav', () => {
-    const getUrlStub = sandbox.stub(GerritNav, 'getUrlForChangeById');
+    const getUrlStub = sinon.stub(GerritNav, 'getUrlForChangeById');
     element._computeChangeURL(123, 'abc/def', 12);
     assert.isTrue(getUrlStub.called);
   });
@@ -559,7 +538,8 @@
           .querySelector('.note'));
       assert.strictEqual(
           element.shadowRoot
-              .querySelector('.note').innerText, '(+ 1 non-visible change)');
+              .querySelector('.note').innerText.trim(),
+          '(+ 1 non-visible change)');
     });
 
     test('visible and non-visible submitted together changes', () => {
@@ -570,7 +550,8 @@
           .querySelector('.note'));
       assert.strictEqual(
           element.shadowRoot
-              .querySelector('.note').innerText, '(+ 2 non-visible changes)');
+              .querySelector('.note').innerText.trim(),
+          '(+ 2 non-visible changes)');
     });
   });
 
@@ -596,4 +577,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
index a2f51cb..9a94d27 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
@@ -30,8 +30,6 @@
   let changeNum;
   let patchNum;
 
-  let sandbox;
-
   const setupElement = element => {
     element.change = {
       _number: changeNum,
@@ -70,13 +68,11 @@
         '+1',
       ],
     };
-    sandbox.stub(element, 'fetchChangeUpdates')
+    sinon.stub(element, 'fetchChangeUpdates')
         .returns(Promise.resolve({isLatest: true}));
   };
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-
     changeNum = 42;
     patchNum = 1;
 
@@ -93,15 +89,14 @@
   });
 
   teardown(() => {
-    sandbox.restore();
     resetPlugins();
   });
 
   test('_submit blocked when invalid email is supplied to ccs', () => {
-    const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve());
+    const sendStub = sinon.stub(element, 'send').returns(Promise.resolve());
     // Stub the below function to avoid side effects from the send promise
     // resolving.
-    sandbox.stub(element, '_purgeReviewersPendingRemove');
+    sinon.stub(element, '_purgeReviewersPendingRemove');
 
     element.$.ccs.$.entry.setText('test');
     MockInteractions.tap(element.shadowRoot
@@ -131,7 +126,8 @@
     }, null, 'http://test.com/plugins/lgtm.js');
     element = basicFixture.instantiate();
     setupElement(element);
-    sandbox.stub(pluginEndpoints, 'importUrl', url => Promise.resolve());
+    sinon.stub(pluginEndpoints, 'importUrl')
+        .callsFake( url => Promise.resolve());
     pluginLoader.loadPlugins([]);
     pluginLoader.awaitPluginsLoaded().then(() => {
       flush(() => {
@@ -148,4 +144,4 @@
       });
     });
   });
-});
\ No newline at end of file
+});
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
similarity index 92%
rename from polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
rename to polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
index 8f3e3f7..4c65298 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
@@ -1,44 +1,29 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2015 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-reply-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-reply-dialog></gr-reply-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
+import '../../../test/common-test-setup-karma.js';
 import {IronOverlayManager} from '@polymer/iron-overlay-behavior/iron-overlay-manager.js';
-import '../../../test/common-test-setup.js';
 import './gr-reply-dialog.js';
 import {mockPromise} from '../../../test/test-utils.js';
 import {SpecialFilePath} from '../../../constants/constants.js';
 import {appContext} from '../../../services/app-context.js';
 
+const basicFixture = fixtureFromElement('gr-reply-dialog');
+
 function cloneableResponse(status, text) {
   return {
     ok: false,
@@ -63,7 +48,6 @@
   let changeNum;
   let patchNum;
 
-  let sandbox;
   let getDraftCommentStub;
   let setDraftCommentStub;
   let eraseDraftCommentStub;
@@ -73,8 +57,6 @@
   const makeGroup = function() { return {id: lastId++}; };
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-
     changeNum = 42;
     patchNum = 1;
 
@@ -85,9 +67,9 @@
       getChangeSuggestedReviewers() { return Promise.resolve([]); },
     });
 
-    sandbox.stub(appContext.flagsService, 'isEnabled').returns(true);
+    sinon.stub(appContext.flagsService, 'isEnabled').returns(true);
 
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.change = {
       _number: changeNum,
       owner: {
@@ -128,27 +110,23 @@
       ],
     };
 
-    getDraftCommentStub = sandbox.stub(element.$.storage, 'getDraftComment');
-    setDraftCommentStub = sandbox.stub(element.$.storage, 'setDraftComment');
-    eraseDraftCommentStub = sandbox.stub(element.$.storage,
+    getDraftCommentStub = sinon.stub(element.$.storage, 'getDraftComment');
+    setDraftCommentStub = sinon.stub(element.$.storage, 'setDraftComment');
+    eraseDraftCommentStub = sinon.stub(element.$.storage,
         'eraseDraftComment');
 
-    sandbox.stub(element, 'fetchChangeUpdates')
+    sinon.stub(element, 'fetchChangeUpdates')
         .returns(Promise.resolve({isLatest: true}));
 
     // Allow the elements created by dom-repeat to be stamped.
     flushAsynchronousOperations();
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   function stubSaveReview(jsonResponseProducer) {
-    return sandbox.stub(
+    return sinon.stub(
         element,
-        '_saveReview',
-        review => new Promise((resolve, reject) => {
+        '_saveReview')
+        .callsFake(review => new Promise((resolve, reject) => {
           try {
             const result = jsonResponseProducer(review) || {};
             const resultStr =
@@ -342,7 +320,7 @@
       });
     });
 
-    sandbox.stub(element.$.labelScores, 'getLabelValues', () => {
+    sinon.stub(element.$.labelScores, 'getLabelValues').callsFake( () => {
       return {
         'Code-Review': -1,
         'Verified': -1,
@@ -650,7 +628,7 @@
   });
 
   test('400 converts to human-readable server-error', done => {
-    sandbox.stub(window, 'fetch', () => {
+    sinon.stub(window, 'fetch').callsFake(() => {
       const text = '....{"reviewers":{"id1":{"error":"first error"}},' +
         '"ccs":{"id2":{"error":"second error"}}}';
       return Promise.resolve(cloneableResponse(400, text));
@@ -672,7 +650,7 @@
   });
 
   test('non-json 400 is treated as a normal server-error', done => {
-    sandbox.stub(window, 'fetch', () => {
+    sinon.stub(window, 'fetch').callsFake(() => {
       const text = 'Comment validation error!';
       return Promise.resolve(cloneableResponse(400, text));
     });
@@ -722,12 +700,12 @@
   });
 
   test('_focusOn', () => {
-    sandbox.spy(element, '_chooseFocusTarget');
+    sinon.spy(element, '_chooseFocusTarget');
     flushAsynchronousOperations();
-    const textareaStub = sandbox.stub(element.$.textarea, 'async');
-    const reviewerEntryStub = sandbox.stub(element.$.reviewers.focusStart,
+    const textareaStub = sinon.stub(element.$.textarea, 'async');
+    const reviewerEntryStub = sinon.stub(element.$.reviewers.focusStart,
         'async');
-    const ccStub = sandbox.stub(element.$.ccs.focusStart, 'async');
+    const ccStub = sinon.stub(element.$.ccs.focusStart, 'async');
     element._focusOn();
     assert.equal(element._chooseFocusTarget.callCount, 1);
     assert.deepEqual(textareaStub.callCount, 1);
@@ -826,7 +804,7 @@
   });
 
   test('_purgeReviewersPendingRemove', () => {
-    const removeStub = sandbox.stub(element, '_removeAccount');
+    const removeStub = sinon.stub(element, '_removeAccount');
     const mock = function() {
       element._reviewersPendingRemove = {
         test: [makeAccount()],
@@ -851,7 +829,7 @@
   });
 
   test('_removeAccount', done => {
-    sandbox.stub(element.$.restAPI, 'removeChangeReviewer')
+    sinon.stub(element.$.restAPI, 'removeChangeReviewer')
         .returns(Promise.resolve({ok: true}));
     const arr = [makeAccount(), makeAccount()];
     element.change.reviewers = {
@@ -952,7 +930,7 @@
 
     stubSaveReview(review => mutations.push(...review.reviewers));
 
-    sandbox.stub(element, '_removeAccount', (account, type) => {
+    sinon.stub(element, '_removeAccount').callsFake((account, type) => {
       mutations.push({state: 'REMOVED', account});
       return Promise.resolve();
     });
@@ -1022,7 +1000,7 @@
   });
 
   test('emits cancel on esc key', () => {
-    const cancelHandler = sandbox.spy();
+    const cancelHandler = sinon.spy();
     element.addEventListener('cancel', cancelHandler);
     MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc');
     flushAsynchronousOperations();
@@ -1130,10 +1108,10 @@
     let startReviewStub;
 
     setup(() => {
-      startReviewStub = sandbox.stub(
+      startReviewStub = sinon.stub(
           element.$.restAPI,
-          'startReview',
-          () => Promise.resolve());
+          'startReview')
+          .callsFake(() => Promise.resolve());
     });
 
     test('ready property in review input on start review', () => {
@@ -1160,7 +1138,7 @@
     let sendStub;
 
     setup(() => {
-      sendStub = sandbox.stub(element, 'send', () => Promise.resolve());
+      sendStub = sinon.stub(element, 'send').callsFake(() => Promise.resolve());
       element.canBeStarted = true;
       // Flush to make both Start/Save buttons appear in DOM.
       flushAsynchronousOperations();
@@ -1216,10 +1194,10 @@
     suite('pending diff drafts?', () => {
       test('yes', () => {
         const promise = mockPromise();
-        const refreshHandler = sandbox.stub();
+        const refreshHandler = sinon.stub();
 
         element.addEventListener('comment-refresh', refreshHandler);
-        sandbox.stub(element.$.restAPI, 'hasPendingDiffDrafts').returns(true);
+        sinon.stub(element.$.restAPI, 'hasPendingDiffDrafts').returns(true);
         element.$.restAPI._pendingRequests.sendDiffDraft = [promise];
         element.open();
 
@@ -1235,7 +1213,7 @@
       });
 
       test('no', () => {
-        sandbox.stub(element.$.restAPI, 'hasPendingDiffDrafts').returns(false);
+        sinon.stub(element.$.restAPI, 'hasPendingDiffDrafts').returns(false);
         element.open();
         assert.notOk(element._savingComments);
       });
@@ -1373,10 +1351,10 @@
   });
 
   test('_submit blocked when no mutations exist', () => {
-    const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve());
+    const sendStub = sinon.stub(element, 'send').returns(Promise.resolve());
     // Stub the below function to avoid side effects from the send promise
     // resolving.
-    sandbox.stub(element, '_purgeReviewersPendingRemove');
+    sinon.stub(element, '_purgeReviewersPendingRemove');
     element.draftCommentThreads = [];
     flushAsynchronousOperations();
 
@@ -1406,4 +1384,4 @@
     assert.equal(element.$.pluginMessage.textContent, 'foo');
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.js
similarity index 83%
rename from polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
rename to polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.js
index 6949afc..809a768 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.js
@@ -1,48 +1,33 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2015 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-reviewer-list</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-reviewer-list></gr-reviewer-list>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-reviewer-list.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromElement('gr-reviewer-list');
+
 suite('gr-reviewer-list tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.serverConfig = {};
-    sandbox = sinon.sandbox.create();
+
     stub('gr-rest-api-interface', {
       getConfig() { return Promise.resolve({}); },
       removeChangeReviewer() {
@@ -51,10 +36,6 @@
     });
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('controls hidden on immutable element', () => {
     element.mutable = false;
     assert.isTrue(element.shadowRoot
@@ -176,7 +157,7 @@
   });
 
   test('_handleAddTap passes mode with event', () => {
-    const fireStub = sandbox.stub(element, 'dispatchEvent');
+    const fireStub = sinon.stub(element, 'dispatchEvent');
     const e = {preventDefault() {}};
 
     element.ccsOnly = false;
@@ -320,4 +301,4 @@
         element._computeVoteableText({_account_id: 1}, change), '');
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js
similarity index 93%
rename from polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
rename to polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js
index 3b29ea7..df4850c 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js
@@ -1,46 +1,31 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2018 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-thread-list</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-thread-list></gr-thread-list>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-thread-list.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {NO_THREADS_MSG} from '../../../constants/messages.js';
 import {SpecialFilePath} from '../../../constants/constants.js';
 
+const basicFixture = fixtureFromElement('gr-thread-list');
+
 suite('gr-thread-list tests', () => {
   let element;
-  let sandbox;
+
   let threadElements;
 
   function getVisibleThreads() {
@@ -50,8 +35,7 @@
   }
 
   setup(done => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.threads = [
       {
         comments: [
@@ -283,10 +267,6 @@
     });
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('draft toggle only appears when logged in', () => {
     assert.equal(getComputedStyle(element.shadowRoot
         .querySelector('.draftToggle')).display,
@@ -594,8 +574,8 @@
   });
 
   test('modification events are consumed and displatched', () => {
-    sandbox.spy(element, '_handleCommentsChanged');
-    const dispatchSpy = sandbox.stub();
+    sinon.spy(element, '_handleCommentsChanged');
+    const dispatchSpy = sinon.stub();
     element.addEventListener('thread-list-modified', dispatchSpy);
     threadElements[0].dispatchEvent(
         new CustomEvent('thread-changed', {
@@ -648,4 +628,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.js
similarity index 67%
rename from polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html
rename to polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.js
index 164c483..d1af425 100644
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.js
@@ -1,44 +1,30 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2018 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-upload-help-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-upload-help-dialog></gr-upload-help-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-upload-help-dialog.js';
+
+const basicFixture = fixtureFromElement('gr-upload-help-dialog');
+
 suite('gr-upload-help-dialog tests', () => {
   let element;
 
   setup(() => {
-    element = fixture('basic');
+    element = basicFixture.instantiate();
   });
 
   test('constructs push command from branch', () => {
@@ -121,4 +107,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.js
similarity index 65%
rename from polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
rename to polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.js
index a366d09..a8f206c 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.js
@@ -1,39 +1,25 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2015 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-account-dropdown</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-account-dropdown></gr-account-dropdown>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-account-dropdown.js';
+
+const basicFixture = fixtureFromElement('gr-account-dropdown');
+
 suite('gr-account-dropdown tests', () => {
   let element;
 
@@ -41,7 +27,7 @@
     stub('gr-rest-api-interface', {
       getConfig() { return Promise.resolve({}); },
     });
-    element = fixture('basic');
+    element = basicFixture.instantiate();
   });
 
   test('account information', () => {
@@ -121,4 +107,4 @@
         '${}, TEST, , bar');
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html
deleted file mode 100644
index bd4991f..0000000
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html
+++ /dev/null
@@ -1,49 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-error-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-error-dialog></gr-error-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-error-dialog.js';
-suite('gr-error-dialog tests', () => {
-  let element;
-
-  setup(() => {
-    element = fixture('basic');
-  });
-
-  test('dismiss tap fires event', done => {
-    element.addEventListener('dismiss', () => { done(); });
-    MockInteractions.tap(element.$.dialog.$.confirm);
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.js b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.js
new file mode 100644
index 0000000..ea8f7c5
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.js
@@ -0,0 +1,35 @@
+/**
+ * @license
+ * Copyright (C) 2018 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-error-dialog.js';
+
+const basicFixture = fixtureFromElement('gr-error-dialog');
+
+suite('gr-error-dialog tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('dismiss tap fires event', done => {
+    element.addEventListener('dismiss', () => { done(); });
+    MockInteractions.tap(element.$.dialog.$.confirm);
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index 6cd2491..a6aef95 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -118,9 +118,10 @@
     this._clearHideAlertHandle();
     this.unlisten(document, 'server-error', '_handleServerError');
     this.unlisten(document, 'network-error', '_handleNetworkError');
-    this.unlisten(document, 'show-auth-required', '_handleAuthRequired');
-    this.unlisten(document, 'visibilitychange', '_handleVisibilityChange');
+    this.unlisten(document, 'show-alert', '_handleShowAlert');
     this.unlisten(document, 'show-error', '_handleShowErrorDialog');
+    this.unlisten(document, 'visibilitychange', '_handleVisibilityChange');
+    this.unlisten(document, 'show-auth-required', '_handleAuthRequired');
 
     this._authErrorHandlerDeregistrationHook();
   }
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.js
similarity index 68%
rename from polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
rename to polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.js
index b52bb7d..934f244 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.js
@@ -1,66 +1,53 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-error-manager</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-error-manager></gr-error-manager>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-error-manager.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
 
+const basicFixture = fixtureFromElement('gr-error-manager');
+
 _testOnly_initGerritPluginApi();
 
 suite('gr-error-manager tests', () => {
   let element;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
 
   suite('when authed', () => {
+    let toastSpy;
+    let openOverlaySpy;
+
     setup(() => {
-      sandbox.stub(window, 'fetch')
+      sinon.stub(window, 'fetch')
           .returns(Promise.resolve({ok: true, status: 204}));
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element._authService.clearCache();
+      toastSpy = sinon.spy(element, '_createToastAlert');
+      openOverlaySpy = sinon.spy(element.$.noInteractionOverlay, 'open');
+    });
+
+    teardown(() => {
+      toastSpy.getCalls().forEach(call => {
+        call.returnValue.remove();
+      });
     });
 
     test('does not show auth error on 403 by default', done => {
-      const showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert');
+      const showAuthErrorStub = sinon.stub(element, '_showAuthErrorAlert');
       const responseText = Promise.resolve('server says no.');
       element.dispatchEvent(
           new CustomEvent('server-error', {
@@ -76,7 +63,7 @@
 
     test('show auth required for 403 with auth error and not authed before',
         done => {
-          const showAuthErrorStub = sandbox.stub(
+          const showAuthErrorStub = sinon.stub(
               element, '_showAuthErrorAlert'
           );
           const responseText = Promise.resolve('Authentication required\n');
@@ -113,7 +100,7 @@
     });
 
     test('show logged in error', () => {
-      sandbox.stub(element, '_showAuthErrorAlert');
+      sinon.stub(element, '_showAuthErrorAlert');
       element.dispatchEvent(
           new CustomEvent('show-auth-required', {
             composed: true, bubbles: true,
@@ -123,8 +110,8 @@
     });
 
     test('show normal Error', done => {
-      const showErrorStub = sandbox.stub(element, '_showErrorDialog');
-      const textSpy = sandbox.spy(() => Promise.resolve('ZOMG'));
+      const showErrorStub = sinon.stub(element, '_showErrorDialog');
+      const textSpy = sinon.spy(() => Promise.resolve('ZOMG'));
       element.dispatchEvent(
           new CustomEvent('server-error', {
             detail: {response: {status: 500, text: textSpy}},
@@ -171,7 +158,7 @@
     });
 
     test('extract trace id from headers if exists', done => {
-      const textSpy = sandbox.spy(
+      const textSpy = sinon.spy(
           () => Promise.resolve('500')
       );
       const headers = new Headers();
@@ -197,8 +184,8 @@
     });
 
     test('suppress TOO_MANY_FILES error', done => {
-      const showAlertStub = sandbox.stub(element, '_showAlert');
-      const textSpy = sandbox.spy(
+      const showAlertStub = sinon.stub(element, '_showAlert');
+      const textSpy = sinon.spy(
           () => Promise.resolve('too many files to find conflicts')
       );
       element.dispatchEvent(
@@ -215,8 +202,8 @@
     });
 
     test('show network error', done => {
-      const consoleErrorStub = sandbox.stub(console, 'error');
-      const showAlertStub = sandbox.stub(element, '_showAlert');
+      const consoleErrorStub = sinon.stub(console, 'error');
+      const showAlertStub = sinon.stub(element, '_showAlert');
       element.dispatchEvent(
           new CustomEvent('network-error', {
             detail: {error: new Error('ZOMG')},
@@ -232,13 +219,12 @@
       });
     });
 
-    test('show auth refresh toast', done => {
+    test('show auth refresh toast', async () => {
       // starts with authed state
       element.$.restAPI.getLoggedIn();
-      const refreshStub = sandbox.stub(element.$.restAPI, 'getAccount',
+      const refreshStub = sinon.stub(element.$.restAPI, 'getAccount').callsFake(
           () => Promise.resolve({}));
-      const toastSpy = sandbox.spy(element, '_createToastAlert');
-      const windowOpen = sandbox.stub(window, 'open');
+      const windowOpen = sinon.stub(window, 'open');
       const responseText = Promise.resolve('Authentication required\n');
       // fake failed auth
       window.fetch.returns(Promise.resolve({status: 403}));
@@ -249,67 +235,65 @@
             composed: true, bubbles: true,
           }));
       assert.equal(window.fetch.callCount, 1);
-      flush(() => {
-        // here needs two flush as there are two chanined
-        // promises on server-error handler and flush only flushes one
-        assert.equal(window.fetch.callCount, 2);
-        flush(() => {
-          // auth-error fired
-          assert.isTrue(toastSpy.called);
+      await flush();
 
-          // toast
-          let toast = toastSpy.lastCall.returnValue;
-          assert.isOk(toast);
-          assert.include(
-              dom(toast.root).textContent, 'Credentials expired.');
-          assert.include(
-              dom(toast.root).textContent, 'Refresh credentials');
+      // here needs two flush as there are two chanined
+      // promises on server-error handler and flush only flushes one
+      assert.equal(window.fetch.callCount, 2);
+      await flush();
+      // Sometime overlay opens with delay, waiting while open is complete
+      await openOverlaySpy.lastCall.returnValue;
+      // auth-error fired
+      assert.isTrue(toastSpy.called);
 
-          // noInteractionOverlay
-          const noInteractionOverlay = element.$.noInteractionOverlay;
-          assert.isOk(noInteractionOverlay);
-          sinon.spy(noInteractionOverlay, 'close');
-          assert.equal(
-              noInteractionOverlay.backdropElement.getAttribute('opened'),
-              '');
-          assert.isFalse(windowOpen.called);
-          MockInteractions.tap(toast.shadowRoot
-              .querySelector('gr-button.action'));
-          assert.isTrue(windowOpen.called);
+      // toast
+      let toast = toastSpy.lastCall.returnValue;
+      assert.isOk(toast);
+      assert.include(
+          dom(toast.root).textContent, 'Credentials expired.');
+      assert.include(
+          dom(toast.root).textContent, 'Refresh credentials');
 
-          // @see Issue 5822: noopener breaks closeAfterLogin
-          assert.equal(windowOpen.lastCall.args[2].indexOf('noopener=yes'),
-              -1);
+      // noInteractionOverlay
+      const noInteractionOverlay = element.$.noInteractionOverlay;
+      assert.isOk(noInteractionOverlay);
+      sinon.spy(noInteractionOverlay, 'close');
+      assert.equal(
+          noInteractionOverlay.backdropElement.getAttribute('opened'),
+          '');
+      assert.isFalse(windowOpen.called);
+      MockInteractions.tap(toast.shadowRoot
+          .querySelector('gr-button.action'));
+      assert.isTrue(windowOpen.called);
 
-          const hideToastSpy = sandbox.spy(toast, 'hide');
+      // @see Issue 5822: noopener breaks closeAfterLogin
+      assert.equal(windowOpen.lastCall.args[2].indexOf('noopener=yes'),
+          -1);
 
-          // now fake authed
-          window.fetch.returns(Promise.resolve({status: 204}));
-          element._handleWindowFocus();
-          element.flushDebouncer('checkLoggedIn');
-          flush(() => {
-            assert.isTrue(refreshStub.called);
-            assert.isTrue(hideToastSpy.called);
+      const hideToastSpy = sinon.spy(toast, 'hide');
 
-            // toast update
-            assert.notStrictEqual(toastSpy.lastCall.returnValue, toast);
-            toast = toastSpy.lastCall.returnValue;
-            assert.isOk(toast);
-            assert.include(
-                dom(toast.root).textContent, 'Credentials refreshed');
+      // now fake authed
+      window.fetch.returns(Promise.resolve({status: 204}));
+      element._handleWindowFocus();
+      element.flushDebouncer('checkLoggedIn');
+      await flush();
+      assert.isTrue(refreshStub.called);
+      assert.isTrue(hideToastSpy.called);
 
-            // close overlay
-            assert.isTrue(noInteractionOverlay.close.called);
-            done();
-          });
-        });
-      });
+      // toast update
+      assert.notStrictEqual(toastSpy.lastCall.returnValue, toast);
+      toast = toastSpy.lastCall.returnValue;
+      assert.isOk(toast);
+      assert.include(
+          dom(toast.root).textContent, 'Credentials refreshed');
+
+      // close overlay
+      assert.isTrue(noInteractionOverlay.close.called);
     });
 
-    test('auth toast should dismiss existing toast', done => {
+    test('auth toast should dismiss existing toast', async () => {
       // starts with authed state
       element.$.restAPI.getLoggedIn();
-      const toastSpy = sandbox.spy(element, '_createToastAlert');
       const responseText = Promise.resolve('Authentication required\n');
 
       // fake an alert
@@ -318,7 +302,7 @@
             detail: {message: 'test reload', action: 'reload'},
             composed: true, bubbles: true,
           }));
-      const toast = toastSpy.lastCall.returnValue;
+      let toast = toastSpy.lastCall.returnValue;
       assert.isOk(toast);
       assert.include(
           dom(toast.root).textContent, 'test reload');
@@ -332,26 +316,24 @@
             composed: true, bubbles: true,
           }));
       assert.equal(window.fetch.callCount, 1);
-      flush(() => {
-        // here needs two flush as there are two chained
-        // promises on server-error handler and flush only flushes one
-        assert.equal(window.fetch.callCount, 2);
-        flush(() => {
-          // toast
-          const toast = toastSpy.lastCall.returnValue;
-          assert.include(
-              dom(toast.root).textContent, 'Credentials expired.');
-          assert.include(
-              dom(toast.root).textContent, 'Refresh credentials');
-          done();
-        });
-      });
+      await flush();
+      // here needs two flush as there are two chained
+      // promises on server-error handler and flush only flushes one
+      assert.equal(window.fetch.callCount, 2);
+      await flush();
+      // Sometime overlay opens with delay, waiting while open is complete
+      await openOverlaySpy.lastCall.returnValue;
+      // toast
+      toast = toastSpy.lastCall.returnValue;
+      assert.include(
+          dom(toast.root).textContent, 'Credentials expired.');
+      assert.include(
+          dom(toast.root).textContent, 'Refresh credentials');
     });
 
     test('regular toast should dismiss regular toast', () => {
       // starts with authed state
       element.$.restAPI.getLoggedIn();
-      const toastSpy = sandbox.spy(element, '_createToastAlert');
 
       // fake an alert
       element.dispatchEvent(
@@ -378,7 +360,6 @@
     test('regular toast should not dismiss auth toast', done => {
       // starts with authed state
       element.$.restAPI.getLoggedIn();
-      const toastSpy = sandbox.spy(element, '_createToastAlert');
       const responseText = Promise.resolve('Authentication required\n');
 
       // fake auth
@@ -422,7 +403,7 @@
 
     test('show alert', () => {
       const alertObj = {message: 'foo'};
-      sandbox.stub(element, '_showAlert');
+      sinon.stub(element, '_showAlert');
       element.dispatchEvent(
           new CustomEvent('show-alert', {
             detail: alertObj,
@@ -435,9 +416,9 @@
     });
 
     test('checks stale credentials on visibility change', () => {
-      const refreshStub = sandbox.stub(element,
+      const refreshStub = sinon.stub(element,
           '_checkSignedIn');
-      sandbox.stub(Date, 'now').returns(999999);
+      sinon.stub(Date, 'now').returns(999999);
       element._lastCredentialCheck = 0;
       element._handleVisibilityChange();
 
@@ -455,12 +436,12 @@
 
     test('refreshes with same credentials', done => {
       const accountPromise = Promise.resolve({_account_id: 1234});
-      sandbox.stub(element.$.restAPI, 'getAccount')
+      sinon.stub(element.$.restAPI, 'getAccount')
           .returns(accountPromise);
-      const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
-      const handleRefreshStub = sandbox.stub(element,
+      const requestCheckStub = sinon.stub(element, '_requestCheckLoggedIn');
+      const handleRefreshStub = sinon.stub(element,
           '_handleCredentialRefreshed');
-      const reloadStub = sandbox.stub(element, '_reloadPage');
+      const reloadStub = sinon.stub(element, '_reloadPage');
 
       element.knownAccountId = 1234;
       element._refreshingCredentials = true;
@@ -476,15 +457,15 @@
 
     test('_showAlert hides existing alerts', () => {
       element._alertElement = element._createToastAlert();
-      const hideStub = sandbox.stub(element, '_hideAlert');
+      const hideStub = sinon.stub(element, '_hideAlert');
       element._showAlert();
       assert.isTrue(hideStub.calledOnce);
     });
 
     test('show-error', () => {
-      const openStub = sandbox.stub(element.$.errorOverlay, 'open');
-      const closeStub = sandbox.stub(element.$.errorOverlay, 'close');
-      const reportStub = sandbox.stub(
+      const openStub = sinon.stub(element.$.errorOverlay, 'open');
+      const closeStub = sinon.stub(element.$.errorOverlay, 'close');
+      const reportStub = sinon.stub(
           element.reporting,
           'reportErrorDialog'
       );
@@ -512,14 +493,14 @@
 
     test('reloads when refreshed credentials differ', done => {
       const accountPromise = Promise.resolve({_account_id: 1234});
-      sandbox.stub(element.$.restAPI, 'getAccount')
+      sinon.stub(element.$.restAPI, 'getAccount')
           .returns(accountPromise);
-      const requestCheckStub = sandbox.stub(
+      const requestCheckStub = sinon.stub(
           element,
           '_requestCheckLoggedIn');
-      const handleRefreshStub = sandbox.stub(element,
+      const handleRefreshStub = sinon.stub(element,
           '_handleCredentialRefreshed');
-      const reloadStub = sandbox.stub(element, '_reloadPage');
+      const reloadStub = sinon.stub(element, '_reloadPage');
 
       element.knownAccountId = 4321; // Different from 1234
       element._refreshingCredentials = true;
@@ -535,20 +516,28 @@
   });
 
   suite('when not authed', () => {
+    let toastSpy;
     setup(() => {
       stub('gr-rest-api-interface', {
         getLoggedIn() { return Promise.resolve(false); },
       });
-      element = fixture('basic');
+      element = basicFixture.instantiate();
+      toastSpy = sinon.spy(element, '_createToastAlert');
+    });
+
+    teardown(() => {
+      toastSpy.getCalls().forEach(call => {
+        call.returnValue.remove();
+      });
     });
 
     test('refresh loop continues on credential fail', done => {
-      const requestCheckStub = sandbox.stub(
+      const requestCheckStub = sinon.stub(
           element,
           '_requestCheckLoggedIn');
-      const handleRefreshStub = sandbox.stub(element,
+      const handleRefreshStub = sinon.stub(element,
           '_handleCredentialRefreshed');
-      const reloadStub = sandbox.stub(element, '_reloadPage');
+      const reloadStub = sinon.stub(element, '_reloadPage');
 
       element._refreshingCredentials = true;
       element._checkSignedIn();
@@ -562,4 +551,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html
deleted file mode 100644
index 8ae0f69..0000000
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html
+++ /dev/null
@@ -1,67 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 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.
--->
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-key-binding-display</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-key-binding-display></gr-key-binding-display>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-key-binding-display.js';
-suite('gr-key-binding-display tests', () => {
-  let element;
-
-  setup(() => {
-    element = fixture('basic');
-  });
-
-  suite('_computeKey', () => {
-    test('unmodified key', () => {
-      assert.strictEqual(element._computeKey(['x']), 'x');
-    });
-
-    test('key with modifiers', () => {
-      assert.strictEqual(element._computeKey(['Ctrl', 'x']), 'x');
-      assert.strictEqual(element._computeKey(['Shift', 'Meta', 'x']), 'x');
-    });
-  });
-
-  suite('_computeModifiers', () => {
-    test('single unmodified key', () => {
-      assert.deepEqual(element._computeModifiers(['x']), []);
-    });
-
-    test('key with modifiers', () => {
-      assert.deepEqual(element._computeModifiers(['Ctrl', 'x']), ['Ctrl']);
-      assert.deepEqual(
-          element._computeModifiers(['Shift', 'Meta', 'x']),
-          ['Shift', 'Meta']);
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.js b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.js
new file mode 100644
index 0000000..0c25e6e
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.js
@@ -0,0 +1,54 @@
+/**
+ * @license
+ * Copyright (C) 2018 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-key-binding-display.js';
+
+const basicFixture = fixtureFromElement('gr-key-binding-display');
+
+suite('gr-key-binding-display tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  suite('_computeKey', () => {
+    test('unmodified key', () => {
+      assert.strictEqual(element._computeKey(['x']), 'x');
+    });
+
+    test('key with modifiers', () => {
+      assert.strictEqual(element._computeKey(['Ctrl', 'x']), 'x');
+      assert.strictEqual(element._computeKey(['Shift', 'Meta', 'x']), 'x');
+    });
+  });
+
+  suite('_computeModifiers', () => {
+    test('single unmodified key', () => {
+      assert.deepEqual(element._computeModifiers(['x']), []);
+    });
+
+    test('key with modifiers', () => {
+      assert.deepEqual(element._computeModifiers(['Ctrl', 'x']), ['Ctrl']);
+      assert.deepEqual(
+          element._computeModifiers(['Shift', 'Meta', 'x']),
+          ['Shift', 'Meta']);
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
index 00e781b..eeeb86b 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
@@ -59,15 +59,17 @@
   /** @override */
   attached() {
     super.attached();
+    this.keyboardShortcutDirectoryListener =
+        this._onDirectoryUpdated.bind(this);
     this.addKeyboardShortcutDirectoryListener(
-        this._onDirectoryUpdated.bind(this));
+        this.keyboardShortcutDirectoryListener);
   }
 
   /** @override */
   detached() {
     super.detached();
     this.removeKeyboardShortcutDirectoryListener(
-        this._onDirectoryUpdated.bind(this));
+        this.keyboardShortcutDirectoryListener);
   }
 
   _handleCloseTap(e) {
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.js
similarity index 75%
rename from polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html
rename to polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.js
index 1b5cd0f..2e6f5d3 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.js
@@ -1,45 +1,32 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2018 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.
+ */
 
-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.
--->
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-key-binding-display</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-keyboard-shortcuts-dialog></gr-keyboard-shortcuts-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-keyboard-shortcuts-dialog.js';
 import {KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+
+const basicFixture = fixtureFromElement('gr-keyboard-shortcuts-dialog');
+
 suite('gr-keyboard-shortcuts-dialog tests', () => {
   const kb = KeyboardShortcutBinder;
   let element;
 
   setup(() => {
-    element = fixture('basic');
+    element = basicFixture.instantiate();
   });
 
   function update(directory) {
@@ -177,5 +164,4 @@
     });
   });
 });
-</script>
 
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.js
similarity index 87%
rename from polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
rename to polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.js
index 336d873..b3ac40f 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.js
@@ -1,45 +1,29 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-main-header</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-main-header></gr-main-header>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-main-header.js';
+
+const basicFixture = fixtureFromElement('gr-main-header');
+
 suite('gr-main-header tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     stub('gr-rest-api-interface', {
       getConfig() { return Promise.resolve({}); },
       probePath(path) { return Promise.resolve(false); },
@@ -47,11 +31,7 @@
     stub('gr-main-header', {
       _loadAccount() {},
     });
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    element = basicFixture.instantiate();
   });
 
   test('link visibility', () => {
@@ -407,4 +387,4 @@
     assert.equal(element._registerText, 'Sign up');
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.js
similarity index 62%
rename from polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html
rename to polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.js
index 8fc4c75..93a1e9e 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.js
@@ -1,31 +1,21 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-navigation</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import {GerritNav} from './gr-navigation.js';
 
 suite('gr-navigation tests', () => {
@@ -85,4 +75,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
similarity index 91%
rename from polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
rename to polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
index b5bfd4e..b53b269 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
@@ -1,53 +1,34 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-router</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-router></gr-router>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-router.js';
 import page from 'page/page.mjs';
 import {GerritNav} from '../gr-navigation/gr-navigation.js';
 
+const basicFixture = fixtureFromElement('gr-router');
+
 suite('gr-router tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+    element = basicFixture.instantiate();
   });
 
-  teardown(() => { sandbox.restore(); });
-
   test('_firstCodeBrowserWeblink', () => {
     assert.deepEqual(element._firstCodeBrowserWeblink([
       {name: 'gitweb'},
@@ -65,7 +46,7 @@
     const link = {name: 'test', url: 'test/url'};
     const weblinks = [browserLink, link];
     const config = {gerrit: {primary_weblink_name: browserLink.name}};
-    sandbox.stub(element, '_firstCodeBrowserWeblink').returns(link);
+    sinon.stub(element, '_firstCodeBrowserWeblink').returns(link);
 
     assert.deepEqual(element._getBrowseCommitWeblink(weblinks, config),
         browserLink);
@@ -77,7 +58,7 @@
     const link = {name: 'test', url: 'test/url'};
     const browserLink = {name: 'browser', url: 'browser/url'};
     const mapLinksToConfig = weblinks => { return {options: {weblinks}}; };
-    sandbox.stub(element, '_getBrowseCommitWeblink').returns(browserLink);
+    sinon.stub(element, '_getBrowseCommitWeblink').returns(browserLink);
 
     assert.deepEqual(
         element._getChangeWeblinks(mapLinksToConfig([link, browserLink]))[0],
@@ -152,16 +133,17 @@
 
     const requiresAuth = {};
     const doesNotRequireAuth = {};
-    sandbox.stub(GerritNav, 'setup');
-    sandbox.stub(page, 'start');
-    sandbox.stub(page, 'base');
-    sandbox.stub(element, '_mapRoute', (pattern, methodName, usesAuth) => {
-      if (usesAuth) {
-        requiresAuth[methodName] = true;
-      } else {
-        doesNotRequireAuth[methodName] = true;
-      }
-    });
+    sinon.stub(GerritNav, 'setup');
+    sinon.stub(page, 'start');
+    sinon.stub(page, 'base');
+    sinon.stub(element, '_mapRoute').callsFake(
+        (pattern, methodName, usesAuth) => {
+          if (usesAuth) {
+            requiresAuth[methodName] = true;
+          } else {
+            doesNotRequireAuth[methodName] = true;
+          }
+        });
     element._startRouter();
 
     const actualRequiresAuth = Object.keys(requiresAuth);
@@ -242,19 +224,19 @@
   });
 
   test('_redirectIfNotLoggedIn while logged in', () => {
-    sandbox.stub(element.$.restAPI, 'getLoggedIn')
+    sinon.stub(element.$.restAPI, 'getLoggedIn')
         .returns(Promise.resolve(true));
     const data = {canonicalPath: ''};
-    const redirectStub = sandbox.stub(element, '_redirectToLogin');
+    const redirectStub = sinon.stub(element, '_redirectToLogin');
     return element._redirectIfNotLoggedIn(data).then(() => {
       assert.isFalse(redirectStub.called);
     });
   });
 
   test('_redirectIfNotLoggedIn while logged out', () => {
-    sandbox.stub(element.$.restAPI, 'getLoggedIn')
+    sinon.stub(element.$.restAPI, 'getLoggedIn')
         .returns(Promise.resolve(false));
-    const redirectStub = sandbox.stub(element, '_redirectToLogin');
+    const redirectStub = sinon.stub(element, '_redirectToLogin');
     const data = {canonicalPath: ''};
     return new Promise(resolve => {
       element._redirectIfNotLoggedIn(data)
@@ -535,9 +517,9 @@
     let projectLookupStub;
 
     setup(() => {
-      projectLookupStub = sandbox
+      projectLookupStub = sinon
           .stub(element.$.restAPI, 'getFromProjectLookup');
-      sandbox.stub(element, '_generateUrl');
+      sinon.stub(element, '_generateUrl');
     });
 
     suite('_normalizeLegacyRouteParams', () => {
@@ -546,10 +528,10 @@
       let show404Stub;
 
       setup(() => {
-        rangeStub = sandbox.stub(element, '_normalizePatchRangeParams')
+        rangeStub = sinon.stub(element, '_normalizePatchRangeParams')
             .returns(Promise.resolve());
-        redirectStub = sandbox.stub(element, '_redirect');
-        show404Stub = sandbox.stub(element, '_show404');
+        redirectStub = sinon.stub(element, '_redirect');
+        show404Stub = sinon.stub(element, '_show404');
       });
 
       test('w/o changeNum', () => {
@@ -622,9 +604,9 @@
     }
 
     setup(() => {
-      redirectStub = sandbox.stub(element, '_redirect');
-      setParamsStub = sandbox.stub(element, '_setParams');
-      handlePassThroughRoute = sandbox.stub(element, '_handlePassThroughRoute');
+      redirectStub = sinon.stub(element, '_redirect');
+      setParamsStub = sinon.stub(element, '_setParams');
+      handlePassThroughRoute = sinon.stub(element, '_handlePassThroughRoute');
     });
 
     test('_handleAgreementsRoute', () => {
@@ -679,10 +661,10 @@
       const onRegisteringExit = (match, _onExit) => {
         onExit = _onExit;
       };
-      sandbox.stub(page, 'exit', onRegisteringExit);
-      sandbox.stub(GerritNav, 'setup');
-      sandbox.stub(page, 'start');
-      sandbox.stub(page, 'base');
+      sinon.stub(page, 'exit').callsFake( onRegisteringExit);
+      sinon.stub(GerritNav, 'setup');
+      sinon.stub(page, 'start');
+      sinon.stub(page, 'base');
       element._startRouter();
 
       const appElementStub = {dispatchEvent: sinon.stub()};
@@ -704,7 +686,7 @@
           redirectStub.lastCall.args[0],
           '/c/test/+/42');
 
-      sandbox.stub(element, '_getHashFromCanonicalPath').returns('foo');
+      sinon.stub(element, '_getHashFromCanonicalPath').returns('foo');
       element._handleImproperlyEncodedPlusRoute(
           {canonicalPath: '/c/test/%20/42', params: ['test', '42']});
       assert.equal(
@@ -779,7 +761,7 @@
     suite('_handleRootRoute', () => {
       test('closes for closeAfterLogin', () => {
         const data = {querystring: 'closeAfterLogin', canonicalPath: ''};
-        const closeStub = sandbox.stub(window, 'close');
+        const closeStub = sinon.stub(window, 'close');
         const result = element._handleRootRoute(data);
         assert.isNotOk(result);
         assert.isTrue(closeStub.called);
@@ -787,7 +769,7 @@
       });
 
       test('redirects to dashboard if logged in', () => {
-        sandbox.stub(element.$.restAPI, 'getLoggedIn')
+        sinon.stub(element.$.restAPI, 'getLoggedIn')
             .returns(Promise.resolve(true));
         const data = {
           canonicalPath: '/', path: '/', querystring: '', hash: '',
@@ -800,7 +782,7 @@
       });
 
       test('redirects to open changes if not logged in', () => {
-        sandbox.stub(element.$.restAPI, 'getLoggedIn')
+        sinon.stub(element.$.restAPI, 'getLoggedIn')
             .returns(Promise.resolve(false));
         const data = {
           canonicalPath: '/', path: '/', querystring: '', hash: '',
@@ -856,7 +838,7 @@
             querystring: '',
             hash: '/foo/bar',
           };
-          sandbox.stub(element, 'getBaseUrl').returns('/baz');
+          sinon.stub(element, 'getBaseUrl').returns('/baz');
           const result = element._handleRootRoute(data);
           assert.isNotOk(result);
           assert.isTrue(redirectStub.called);
@@ -894,11 +876,11 @@
       let redirectToLoginStub;
 
       setup(() => {
-        redirectToLoginStub = sandbox.stub(element, '_redirectToLogin');
+        redirectToLoginStub = sinon.stub(element, '_redirectToLogin');
       });
 
       test('own dashboard but signed out redirects to login', () => {
-        sandbox.stub(element.$.restAPI, 'getLoggedIn')
+        sinon.stub(element.$.restAPI, 'getLoggedIn')
             .returns(Promise.resolve(false));
         const data = {canonicalPath: '/dashboard/', params: {0: 'seLF'}};
         return element._handleDashboardRoute(data, '').then(() => {
@@ -909,7 +891,7 @@
       });
 
       test('non-self dashboard but signed out does not redirect', () => {
-        sandbox.stub(element.$.restAPI, 'getLoggedIn')
+        sinon.stub(element.$.restAPI, 'getLoggedIn')
             .returns(Promise.resolve(false));
         const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}};
         return element._handleDashboardRoute(data, '').then(() => {
@@ -921,7 +903,7 @@
       });
 
       test('dashboard while signed in sets params', () => {
-        sandbox.stub(element.$.restAPI, 'getLoggedIn')
+        sinon.stub(element.$.restAPI, 'getLoggedIn')
             .returns(Promise.resolve(true));
         const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}};
         return element._handleDashboardRoute(data, '').then(() => {
@@ -940,7 +922,7 @@
       let redirectToLoginStub;
 
       setup(() => {
-        redirectToLoginStub = sandbox.stub(element, '_redirectToLogin');
+        redirectToLoginStub = sinon.stub(element, '_redirectToLogin');
       });
 
       test('no user specified', () => {
@@ -1347,7 +1329,7 @@
       });
 
       test('_handleChangeLegacyRoute', () => {
-        const normalizeRouteStub = sandbox.stub(element,
+        const normalizeRouteStub = sinon.stub(element,
             '_normalizeLegacyRouteParams');
         const ctx = {
           params: [
@@ -1372,7 +1354,7 @@
       });
 
       test('_handleDiffLegacyRoute', () => {
-        const normalizeRouteStub = sandbox.stub(element,
+        const normalizeRouteStub = sinon.stub(element,
             '_normalizeLegacyRouteParams');
         const ctx = {
           params: [
@@ -1435,14 +1417,14 @@
         }
 
         setup(() => {
-          normalizeRangeStub = sandbox.stub(element,
+          normalizeRangeStub = sinon.stub(element,
               '_normalizePatchRangeParams');
-          sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+          sinon.stub(element.$.restAPI, 'setInProjectLookup');
         });
 
         test('needs redirect', () => {
           normalizeRangeStub.returns(true);
-          sandbox.stub(element, '_generateUrl').returns('foo');
+          sinon.stub(element, '_generateUrl').returns('foo');
           const ctx = makeParams(null, '');
           element._handleChangeRoute(ctx);
           assert.isTrue(normalizeRangeStub.called);
@@ -1453,7 +1435,7 @@
 
         test('change view', () => {
           normalizeRangeStub.returns(false);
-          sandbox.stub(element, '_generateUrl').returns('foo');
+          sinon.stub(element, '_generateUrl').returns('foo');
           const ctx = makeParams(null, '');
           assertDataToParams(ctx, '_handleChangeRoute', {
             view: GerritNav.View.CHANGE,
@@ -1489,14 +1471,14 @@
         }
 
         setup(() => {
-          normalizeRangeStub = sandbox.stub(element,
+          normalizeRangeStub = sinon.stub(element,
               '_normalizePatchRangeParams');
-          sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+          sinon.stub(element.$.restAPI, 'setInProjectLookup');
         });
 
         test('needs redirect', () => {
           normalizeRangeStub.returns(true);
-          sandbox.stub(element, '_generateUrl').returns('foo');
+          sinon.stub(element, '_generateUrl').returns('foo');
           const ctx = makeParams(null, '');
           element._handleDiffRoute(ctx);
           assert.isTrue(normalizeRangeStub.called);
@@ -1507,7 +1489,7 @@
 
         test('diff view', () => {
           normalizeRangeStub.returns(false);
-          sandbox.stub(element, '_generateUrl').returns('foo');
+          sinon.stub(element, '_generateUrl').returns('foo');
           const ctx = makeParams('foo/bar/baz', 'b44');
           assertDataToParams(ctx, '_handleDiffRoute', {
             view: GerritNav.View.DIFF,
@@ -1526,8 +1508,8 @@
 
       test('_handleDiffEditRoute', () => {
         const normalizeRangeSpy =
-            sandbox.spy(element, '_normalizePatchRangeParams');
-        sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+            sinon.spy(element, '_normalizePatchRangeParams');
+        sinon.stub(element.$.restAPI, 'setInProjectLookup');
         const ctx = {
           params: [
             'foo/bar', // 0 Project
@@ -1555,8 +1537,8 @@
 
       test('_handleDiffEditRoute with lineNum', () => {
         const normalizeRangeSpy =
-            sandbox.spy(element, '_normalizePatchRangeParams');
-        sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+            sinon.spy(element, '_normalizePatchRangeParams');
+        sinon.stub(element.$.restAPI, 'setInProjectLookup');
         const ctx = {
           params: [
             'foo/bar', // 0 Project
@@ -1585,8 +1567,8 @@
 
       test('_handleChangeEditRoute', () => {
         const normalizeRangeSpy =
-            sandbox.spy(element, '_normalizePatchRangeParams');
-        sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+            sinon.spy(element, '_normalizePatchRangeParams');
+        sinon.stub(element.$.restAPI, 'setInProjectLookup');
         const ctx = {
           params: [
             'foo/bar', // 0 Project
@@ -1649,4 +1631,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.js
similarity index 70%
rename from polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
rename to polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.js
index 9e5fdfe..9529f9b 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.js
@@ -1,57 +1,42 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2015 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-search-bar</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script src="/node_modules/page/page.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-search-bar></gr-search-bar>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-search-bar.js';
 import '../../../scripts/util.js';
-import {KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {TestKeyboardShortcutBinder} from '../../../test/test-utils.js';
+
+const basicFixture = fixtureFromElement('gr-search-bar');
+
 suite('gr-search-bar tests', () => {
-  const kb = KeyboardShortcutBinder;
-  kb.bindShortcut(kb.Shortcut.SEARCH, '/');
-
   let element;
-  let sandbox;
 
-  setup(done => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-    flush(done);
+  suiteSetup(() => {
+    const kb = TestKeyboardShortcutBinder.push();
+    kb.bindShortcut(kb.Shortcut.SEARCH, '/');
   });
 
-  teardown(() => {
-    sandbox.restore();
+  suiteTeardown(() => {
+    TestKeyboardShortcutBinder.pop();
+  });
+
+  setup(done => {
+    element = basicFixture.instantiate();
+    flush(done);
   });
 
   test('value is propagated to _inputVal', () => {
@@ -75,7 +60,7 @@
   });
 
   test('input blurred after commit', () => {
-    const blurSpy = sandbox.spy(element.$.searchInput.$.input, 'blur');
+    const blurSpy = sinon.spy(element.$.searchInput.$.input, 'blur');
     element.$.searchInput.text = 'fate/stay';
     MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
         null, 'enter');
@@ -83,7 +68,7 @@
   });
 
   test('empty search query does not trigger nav', () => {
-    const searchSpy = sandbox.spy();
+    const searchSpy = sinon.spy();
     element.addEventListener('handle-search', searchSpy);
     element.value = '';
     MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
@@ -92,7 +77,7 @@
   });
 
   test('Predefined query op with no predication doesnt trigger nav', () => {
-    const searchSpy = sandbox.spy();
+    const searchSpy = sinon.spy();
     element.addEventListener('handle-search', searchSpy);
     element.value = 'added:';
     MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
@@ -101,7 +86,7 @@
   });
 
   test('predefined predicate query triggers nav', () => {
-    const searchSpy = sandbox.spy();
+    const searchSpy = sinon.spy();
     element.addEventListener('handle-search', searchSpy);
     element.value = 'age:1week';
     MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
@@ -110,7 +95,7 @@
   });
 
   test('undefined predicate query triggers nav', () => {
-    const searchSpy = sandbox.spy();
+    const searchSpy = sinon.spy();
     element.addEventListener('handle-search', searchSpy);
     element.value = 'random:1week';
     MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
@@ -119,7 +104,7 @@
   });
 
   test('empty undefined predicate query triggers nav', () => {
-    const searchSpy = sandbox.spy();
+    const searchSpy = sinon.spy();
     element.addEventListener('handle-search', searchSpy);
     element.value = 'random:';
     MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
@@ -128,8 +113,8 @@
   });
 
   test('keyboard shortcuts', () => {
-    const focusSpy = sandbox.spy(element.$.searchInput, 'focus');
-    const selectAllSpy = sandbox.spy(element.$.searchInput, 'selectAll');
+    const focusSpy = sinon.spy(element.$.searchInput, 'focus');
+    const selectAllSpy = sinon.spy(element.$.searchInput, 'selectAll');
     MockInteractions.pressAndReleaseKeyOn(document.body, 191, null, '/');
     assert.isTrue(focusSpy.called);
     assert.isTrue(selectAllSpy.called);
@@ -137,7 +122,7 @@
 
   suite('_getSearchSuggestions', () => {
     test('Autocompletes accounts', () => {
-      sandbox.stub(element, 'accountSuggestions', () =>
+      sinon.stub(element, 'accountSuggestions').callsFake(() =>
         Promise.resolve([{text: 'owner:fred@goog.co'}])
       );
       return element._getSearchSuggestions('owner:fr').then(s => {
@@ -146,7 +131,7 @@
     });
 
     test('Autocompletes groups', done => {
-      sandbox.stub(element, 'groupSuggestions', () =>
+      sinon.stub(element, 'groupSuggestions').callsFake(() =>
         Promise.resolve([
           {text: 'ownerin:Polygerrit'},
           {text: 'ownerin:gerrit'},
@@ -159,7 +144,7 @@
     });
 
     test('Autocompletes projects', done => {
-      sandbox.stub(element, 'projectSuggestions', () =>
+      sinon.stub(element, 'projectSuggestions').callsFake(() =>
         Promise.resolve([
           {text: 'project:Polygerrit'},
           {text: 'project:gerrit'},
@@ -213,7 +198,7 @@
           },
         });
 
-        element = fixture('basic');
+        element = basicFixture.instantiate();
         flush(done);
       });
 
@@ -230,4 +215,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.js
similarity index 60%
rename from polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html
rename to polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.js
index 87dfaf4..dc7bf0e 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.js
@@ -1,54 +1,34 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-smart-search</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-smart-search></gr-smart-search>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-smart-search.js';
+
+const basicFixture = fixtureFromElement('gr-smart-search');
+
 suite('gr-smart-search tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    element = basicFixture.instantiate();
   });
 
   test('Autocompletes accounts', () => {
-    sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+    sinon.stub(element.$.restAPI, 'getSuggestedAccounts').callsFake(() =>
       Promise.resolve([
         {
           name: 'fred',
@@ -62,7 +42,7 @@
   });
 
   test('Inserts self as option when valid', () => {
-    sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+    sinon.stub(element.$.restAPI, 'getSuggestedAccounts').callsFake( () =>
       Promise.resolve([
         {
           name: 'fred',
@@ -82,7 +62,7 @@
   });
 
   test('Inserts me as option when valid', () => {
-    sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+    sinon.stub(element.$.restAPI, 'getSuggestedAccounts').callsFake( () =>
       Promise.resolve([
         {
           name: 'fred',
@@ -102,7 +82,7 @@
   });
 
   test('Autocompletes groups', () => {
-    sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () =>
+    sinon.stub(element.$.restAPI, 'getSuggestedGroups').callsFake( () =>
       Promise.resolve({
         Polygerrit: 0,
         gerrit: 0,
@@ -115,7 +95,7 @@
   });
 
   test('Autocompletes projects', () => {
-    sandbox.stub(element.$.restAPI, 'getSuggestedProjects', () =>
+    sinon.stub(element.$.restAPI, 'getSuggestedProjects').callsFake( () =>
       Promise.resolve({Polygerrit: 0}));
     return element._fetchProjects('project', 'pol').then(s => {
       assert.deepEqual(s[0], {text: 'project:Polygerrit'});
@@ -123,7 +103,7 @@
   });
 
   test('Autocomplete doesnt override exact matches to input', () => {
-    sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () =>
+    sinon.stub(element.$.restAPI, 'getSuggestedGroups').callsFake( () =>
       Promise.resolve({
         Polygerrit: 0,
         gerrit: 0,
@@ -138,7 +118,7 @@
   });
 
   test('Autocompletes accounts with no email', () => {
-    sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+    sinon.stub(element.$.restAPI, 'getSuggestedAccounts').callsFake( () =>
       Promise.resolve([{name: 'fred'}]));
     return element._fetchAccounts('owner', 'fr').then(s => {
       assert.deepEqual(s[0], {text: 'owner:"fred"', label: 'fred'});
@@ -146,11 +126,11 @@
   });
 
   test('Autocompletes accounts with email', () => {
-    sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+    sinon.stub(element.$.restAPI, '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: ''});
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/custom-dark-theme_test.js b/polygerrit-ui/app/elements/custom-dark-theme_test.js
index cc955ce..d4f790f 100644
--- a/polygerrit-ui/app/elements/custom-dark-theme_test.js
+++ b/polygerrit-ui/app/elements/custom-dark-theme_test.js
@@ -25,20 +25,23 @@
 const basicFixture = fixtureFromElement('gr-app');
 
 suite('gr-app custom dark theme tests', () => {
-  let sandbox;
   let element;
   setup(done => {
     window.localStorage.setItem('dark-theme', 'true');
-    sandbox = sinon.sandbox.create();
+
     element = basicFixture.instantiate();
     pluginLoader.loadPlugins([]);
     pluginLoader.awaitPluginsLoaded().then(() => flush(done));
   });
 
   teardown(() => {
-    sandbox.restore();
     window.localStorage.removeItem('dark-theme');
     removeTheme();
+    // The app sends requests to server. This can lead to
+    // unexpected gr-alert elements in document.body
+    document.body.querySelectorAll('gr-alert').forEach(grAlert => {
+      grAlert.remove();
+    });
   });
 
   test('should tried to load dark theme', () => {
@@ -57,4 +60,4 @@
             .toLowerCase(),
         '#3b3d3f');
   });
-});
\ No newline at end of file
+});
diff --git a/polygerrit-ui/app/elements/custom-light-theme_test.js b/polygerrit-ui/app/elements/custom-light-theme_test.js
index a866bb3..5c0cf28 100644
--- a/polygerrit-ui/app/elements/custom-light-theme_test.js
+++ b/polygerrit-ui/app/elements/custom-light-theme_test.js
@@ -24,10 +24,8 @@
 const basicFixture = fixtureFromElement('gr-app');
 
 suite('gr-app custom light theme tests', () => {
-  let sandbox;
   let element;
   setup(done => {
-    sandbox = sinon.sandbox.create();
     window.localStorage.removeItem('dark-theme');
     stub('gr-rest-api-interface', {
       getConfig() { return Promise.resolve({test: 'config'}); },
@@ -42,7 +40,11 @@
     pluginLoader.awaitPluginsLoaded().then(() => flush(done));
   });
   teardown(() => {
-    sandbox.restore();
+    // The app sends requests to server. This can lead to
+    // unexpected gr-alert elements in document.body
+    document.body.querySelectorAll('gr-alert').forEach(grAlert => {
+      grAlert.remove();
+    });
   });
 
   test('should not load dark theme', () => {
@@ -60,4 +62,4 @@
             .toLowerCase(),
         'transparent');
   });
-});
\ No newline at end of file
+});
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.js
similarity index 78%
rename from polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html
rename to polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.js
index 65958c8..b3ba637 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.js
@@ -1,41 +1,29 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2019 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.
+ */
 
-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.
--->
-<meta name='viewport' content='width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes'>
-<title>gr-apply-fix-dialog</title>
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id='basic'>
-  <template>
-    <gr-apply-fix-dialog></gr-apply-fix-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-apply-fix-dialog.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
+const basicFixture = fixtureFromElement('gr-apply-fix-dialog');
+
 suite('gr-apply-fix-dialog tests', () => {
   let element;
-  let sandbox;
+
   const ROBOT_COMMENT_WITH_TWO_FIXES = {
     robot_id: 'robot_1',
     fix_suggestions: [{fix_id: 'fix_1'}, {fix_id: 'fix_2'}],
@@ -47,8 +35,7 @@
   };
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.changeNum = '1';
     element._patchNum = 2;
     element.change = {
@@ -67,13 +54,9 @@
     };
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   suite('dialog open', () => {
     setup(() => {
-      sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview')
+      sinon.stub(element.$.restAPI, 'getRobotCommentFixPreview')
           .returns(Promise.resolve({
             f1: {
               meta_a: {},
@@ -106,7 +89,7 @@
               ],
             },
           }));
-      sandbox.stub(element.$.applyFixOverlay, 'open')
+      sinon.stub(element.$.applyFixOverlay, 'open')
           .returns(Promise.resolve());
     });
 
@@ -164,9 +147,9 @@
   });
 
   test('next button state updated when suggestions changed', done => {
-    sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview')
+    sinon.stub(element.$.restAPI, 'getRobotCommentFixPreview')
         .returns(Promise.resolve({}));
-    sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
+    sinon.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
 
     element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_ONE_FIX}})
         .then(() => assert.isTrue(element.$.nextFix.disabled))
@@ -180,7 +163,7 @@
   });
 
   test('preview endpoint throws error should reset dialog', done => {
-    sandbox.stub(window, 'fetch', (url => {
+    sinon.stub(window, 'fetch').callsFake((url => {
       if (url.endsWith('/preview')) {
         return Promise.reject(new Error('backend error'));
       }
@@ -203,9 +186,9 @@
 
   test('apply fix button should call apply ' +
   'and navigate to change view', done => {
-    sandbox.stub(element.$.restAPI, 'applyFixSuggestion')
+    sinon.stub(element.$.restAPI, 'applyFixSuggestion')
         .returns(Promise.resolve({ok: true}));
-    sandbox.stub(GerritNav, 'navigateToChange');
+    sinon.stub(GerritNav, 'navigateToChange');
     element._currentFix = {fix_id: '123'};
 
     element._handleApplyFix().then(() => {
@@ -229,9 +212,9 @@
   });
 
   test('should not navigate to change view if incorect reponse', done => {
-    sandbox.stub(element.$.restAPI, 'applyFixSuggestion')
+    sinon.stub(element.$.restAPI, 'applyFixSuggestion')
         .returns(Promise.resolve({}));
-    sandbox.stub(GerritNav, 'navigateToChange');
+    sinon.stub(GerritNav, 'navigateToChange');
     element._currentFix = {fix_id: '123'};
 
     element._handleApplyFix().then(() => {
@@ -245,7 +228,7 @@
   });
 
   test('select fix forward and back of multiple suggested fixes', done => {
-    sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview')
+    sinon.stub(element.$.restAPI, 'getRobotCommentFixPreview')
         .returns(Promise.resolve({
           f1: {
             meta_a: {},
@@ -278,7 +261,7 @@
             ],
           },
         }));
-    sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
+    sinon.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
 
     element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_TWO_FIXES}})
         .then(() => {
@@ -291,7 +274,7 @@
   });
 
   test('server-error should throw for failed apply call', done => {
-    sandbox.stub(window, 'fetch', (url => {
+    sinon.stub(window, 'fetch').callsFake((url => {
       if (url.endsWith('/apply')) {
         return Promise.reject(new Error('backend error'));
       }
@@ -303,7 +286,7 @@
     }));
     const errorStub = sinon.stub();
     document.addEventListener('network-error', errorStub);
-    sandbox.stub(GerritNav, 'navigateToChange');
+    sinon.stub(GerritNav, 'navigateToChange');
     element._currentFix = {fix_id: '123'};
     element._handleApplyFix();
     flush(() => {
@@ -313,4 +296,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js
similarity index 90%
rename from polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
rename to polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js
index 172a342..0a7c3b5 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js
@@ -1,64 +1,46 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-comment-api</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-comment-api></gr-comment-api>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-comment-api.js';
+
+const basicFixture = fixtureFromElement('gr-comment-api');
+
 suite('gr-comment-api tests', () => {
   const PARENT = 'PARENT';
 
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+    element = basicFixture.instantiate();
   });
 
-  teardown(() => { sandbox.restore(); });
-
   test('loads logged-out', () => {
     const changeNum = 1234;
 
-    sandbox.stub(element.$.restAPI, 'getLoggedIn')
+    sinon.stub(element.$.restAPI, 'getLoggedIn')
         .returns(Promise.resolve(false));
-    sandbox.stub(element.$.restAPI, 'getDiffComments')
+    sinon.stub(element.$.restAPI, 'getDiffComments')
         .returns(Promise.resolve({
           'foo.c': [{id: '123', message: 'foo bar', in_reply_to: '321'}],
         }));
-    sandbox.stub(element.$.restAPI, 'getDiffRobotComments')
+    sinon.stub(element.$.restAPI, 'getDiffRobotComments')
         .returns(Promise.resolve({'foo.c': [{id: '321', message: 'done'}]}));
-    sandbox.stub(element.$.restAPI, 'getDiffDrafts')
+    sinon.stub(element.$.restAPI, 'getDiffDrafts')
         .returns(Promise.resolve({}));
 
     return element.loadAll(changeNum).then(() => {
@@ -77,15 +59,15 @@
   test('loads logged-in', () => {
     const changeNum = 1234;
 
-    sandbox.stub(element.$.restAPI, 'getLoggedIn')
+    sinon.stub(element.$.restAPI, 'getLoggedIn')
         .returns(Promise.resolve(true));
-    sandbox.stub(element.$.restAPI, 'getDiffComments')
+    sinon.stub(element.$.restAPI, 'getDiffComments')
         .returns(Promise.resolve({
           'foo.c': [{id: '123', message: 'foo bar', in_reply_to: '321'}],
         }));
-    sandbox.stub(element.$.restAPI, 'getDiffRobotComments')
+    sinon.stub(element.$.restAPI, 'getDiffRobotComments')
         .returns(Promise.resolve({'foo.c': [{id: '321', message: 'done'}]}));
-    sandbox.stub(element.$.restAPI, 'getDiffDrafts')
+    sinon.stub(element.$.restAPI, 'getDiffDrafts')
         .returns(Promise.resolve({'foo.c': [{id: '555', message: 'ack'}]}));
 
     return element.loadAll(changeNum).then(() => {
@@ -106,17 +88,17 @@
     let robotCommentStub;
     let draftStub;
     setup(() => {
-      commentStub = sandbox.stub(element.$.restAPI, 'getDiffComments')
+      commentStub = sinon.stub(element.$.restAPI, 'getDiffComments')
           .returns(Promise.resolve({}));
-      robotCommentStub = sandbox.stub(element.$.restAPI,
+      robotCommentStub = sinon.stub(element.$.restAPI,
           'getDiffRobotComments').returns(Promise.resolve({}));
-      draftStub = sandbox.stub(element.$.restAPI, 'getDiffDrafts')
+      draftStub = sinon.stub(element.$.restAPI, 'getDiffDrafts')
           .returns(Promise.resolve({}));
     });
 
     test('without loadAll first', done => {
       assert.isNotOk(element._changeComments);
-      sandbox.spy(element, 'loadAll');
+      sinon.spy(element, 'loadAll');
       element.reloadDrafts().then(() => {
         assert.isTrue(element.loadAll.called);
         assert.isOk(element._changeComments);
@@ -209,9 +191,9 @@
       const patchRange2 = {basePatchNum: 123, patchNum: 125};
       const patchRange3 = {basePatchNum: 124, patchNum: 125};
 
-      const isInBasePatchStub = sandbox.stub(element._changeComments,
+      const isInBasePatchStub = sinon.stub(element._changeComments,
           '_isInBaseOfPatchRange');
-      const isInRevisionPatchStub = sandbox.stub(element._changeComments,
+      const isInRevisionPatchStub = sinon.stub(element._changeComments,
           '_isInRevisionOfPatchRange');
 
       isInBasePatchStub.withArgs({}, patchRange1).returns(true);
@@ -760,4 +742,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.js
similarity index 67%
rename from polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html
rename to polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.js
index b80c56f3..e886e61 100644
--- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.js
@@ -1,40 +1,26 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2019 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-coverage-layer</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-coverage-layer></gr-coverage-layer>
-  </template>
-</test-fixture>
-
-<script type="module">
+import '../../../test/common-test-setup-karma.js';
 import '../gr-diff/gr-diff-line.js';
-import '../../../test/common-test-setup.js';
 import './gr-coverage-layer.js';
+
+const basicFixture = fixtureFromElement('gr-coverage-layer');
+
 suite('gr-coverage-layer', () => {
   let element;
 
@@ -74,7 +60,7 @@
       },
     ];
 
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.coverageRanges = initialCoverageRanges;
     element.side = 'right';
   });
@@ -135,4 +121,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
similarity index 91%
rename from polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
rename to polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
index 057401b..a88ac27 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
@@ -1,54 +1,21 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-diff-builder</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template is="dom-template">
-    <gr-diff-builder>
-      <table id="diffTable"></table>
-    </gr-diff-builder>
-  </template>
-</test-fixture>
-
-<test-fixture id="div-with-text">
-  <template>
-    <div>Lorem ipsum dolor sit amet, suspendisse inceptos vehicula</div>
-  </template>
-</test-fixture>
-
-<test-fixture id="mock-diff">
-  <template>
-    <gr-diff-builder view-mode="SIDE_BY_SIDE">
-      <table id="diffTable"></table>
-    </gr-diff-builder>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import '../gr-diff/gr-diff-group.js';
 import './gr-diff-builder.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -59,6 +26,23 @@
 import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
 import {GrDiffGroup} from '../gr-diff/gr-diff-group.js';
 import {GrDiffBuilder} from './gr-diff-builder.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromTemplate(html`
+    <gr-diff-builder>
+      <table id="diffTable"></table>
+    </gr-diff-builder>
+`);
+
+const divWithTextFixture = fixtureFromTemplate(html`
+<div>Lorem ipsum dolor sit amet, suspendisse inceptos vehicula</div>
+`);
+
+const mockDiffFixture = fixtureFromTemplate(html`
+<gr-diff-builder view-mode="SIDE_BY_SIDE">
+      <table id="diffTable"></table>
+    </gr-diff-builder>
+`);
 
 const DiffViewMode = {
   SIDE_BY_SIDE: 'SIDE_BY_SIDE',
@@ -69,12 +53,11 @@
   let prefs;
   let element;
   let builder;
-  let sandbox;
+
   const LINE_FEED_HTML = '<span class="style-scope gr-diff br"></span>';
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     stub('gr-rest-api-interface', {
       getLoggedIn() { return Promise.resolve(false); },
       getProjectConfig() { return Promise.resolve({}); },
@@ -87,8 +70,6 @@
     builder = new GrDiffBuilder({content: []}, prefs);
   });
 
-  teardown(() => { sandbox.restore(); });
-
   test('_createElement classStr applies all classes', () => {
     const node = builder._createElement('div', 'test classes');
     assert.isTrue(node.classList.contains('gr-diff'));
@@ -319,7 +300,7 @@
   });
 
   test('_handlePreferenceError called with invalid preference', () => {
-    sandbox.stub(element, '_handlePreferenceError');
+    sinon.stub(element, '_handlePreferenceError');
     const prefs = {tab_size: 0};
     element._getDiffBuilder(element.diff, prefs);
     assert.isTrue(element._handlePreferenceError.lastCall
@@ -379,9 +360,9 @@
     }
 
     setup(() => {
-      el = fixture('div-with-text');
+      el = divWithTextFixture.instantiate();
       str = el.textContent;
-      annotateElementSpy = sandbox.spy(GrAnnotation, 'annotateElement');
+      annotateElementSpy = sinon.spy(GrAnnotation, 'annotateElement');
       layer = document.createElement('gr-diff-builder')
           ._createIntralineLayer();
     });
@@ -537,7 +518,7 @@
     const lineNumberEl = document.createElement('td');
 
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element._showTabs = true;
       layer = element._createTabIndicatorLayer();
     });
@@ -546,7 +527,7 @@
       const line = {text: ''};
       const el = document.createElement('div');
       const annotateElementStub =
-          sandbox.stub(GrAnnotation, 'annotateElement');
+          sinon.stub(GrAnnotation, 'annotateElement');
 
       layer.annotate(el, lineNumberEl, line);
 
@@ -559,7 +540,7 @@
       const el = document.createElement('div');
       el.textContent = str;
       const annotateElementStub =
-          sandbox.stub(GrAnnotation, 'annotateElement');
+          sinon.stub(GrAnnotation, 'annotateElement');
 
       layer.annotate(el, lineNumberEl, line);
 
@@ -572,7 +553,7 @@
       const el = document.createElement('div');
       el.textContent = str;
       const annotateElementStub =
-          sandbox.stub(GrAnnotation, 'annotateElement');
+          sinon.stub(GrAnnotation, 'annotateElement');
 
       layer.annotate(el, lineNumberEl, line);
 
@@ -592,7 +573,7 @@
       const el = document.createElement('div');
       el.textContent = str;
       const annotateElementStub =
-          sandbox.stub(GrAnnotation, 'annotateElement');
+          sinon.stub(GrAnnotation, 'annotateElement');
 
       layer.annotate(el, lineNumberEl, line);
 
@@ -605,7 +586,7 @@
       const el = document.createElement('div');
       el.textContent = str;
       const annotateElementStub =
-          sandbox.stub(GrAnnotation, 'annotateElement');
+          sinon.stub(GrAnnotation, 'annotateElement');
 
       layer.annotate(el, lineNumberEl, line);
 
@@ -630,7 +611,7 @@
       const el = document.createElement('div');
       el.textContent = str;
       const annotateElementStub =
-          sandbox.stub(GrAnnotation, 'annotateElement');
+          sinon.stub(GrAnnotation, 'annotateElement');
 
       layer.annotate(el, lineNumberEl, line);
 
@@ -649,7 +630,7 @@
     let withLayerCount;
     setup(() => {
       const layers = [];
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.layers = layers;
       element._showTrailingWhitespace = true;
       element._setupAnnotationLayers();
@@ -664,7 +645,7 @@
     suite('with layers', () => {
       const layers = [{}, {}];
       setup(() => {
-        element = fixture('basic');
+        element = basicFixture.instantiate();
         element.layers = layers;
         element._showTrailingWhitespace = true;
         element._setupAnnotationLayers();
@@ -685,7 +666,7 @@
     const lineNumberEl = document.createElement('td');
 
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element._showTrailingWhitespace = true;
       layer = element._createTrailingWhitespaceLayer();
     });
@@ -694,7 +675,7 @@
       const line = {text: ''};
       const el = document.createElement('div');
       const annotateElementStub =
-          sandbox.stub(GrAnnotation, 'annotateElement');
+          sinon.stub(GrAnnotation, 'annotateElement');
       layer.annotate(el, lineNumberEl, line);
       assert.isFalse(annotateElementStub.called);
     });
@@ -705,7 +686,7 @@
       const el = document.createElement('div');
       el.textContent = str;
       const annotateElementStub =
-          sandbox.stub(GrAnnotation, 'annotateElement');
+          sinon.stub(GrAnnotation, 'annotateElement');
       layer.annotate(el, lineNumberEl, line);
       assert.isFalse(annotateElementStub.called);
     });
@@ -716,7 +697,7 @@
       const el = document.createElement('div');
       el.textContent = str;
       const annotateElementStub =
-          sandbox.stub(GrAnnotation, 'annotateElement');
+          sinon.stub(GrAnnotation, 'annotateElement');
       layer.annotate(el, lineNumberEl, line);
       assert.isTrue(annotateElementStub.called);
       assert.equal(annotateElementStub.lastCall.args[1], 11);
@@ -729,7 +710,7 @@
       const el = document.createElement('div');
       el.textContent = str;
       const annotateElementStub =
-          sandbox.stub(GrAnnotation, 'annotateElement');
+          sinon.stub(GrAnnotation, 'annotateElement');
       layer.annotate(el, lineNumberEl, line);
       assert.isTrue(annotateElementStub.called);
       assert.equal(annotateElementStub.lastCall.args[1], 11);
@@ -742,7 +723,7 @@
       const el = document.createElement('div');
       el.textContent = str;
       const annotateElementStub =
-          sandbox.stub(GrAnnotation, 'annotateElement');
+          sinon.stub(GrAnnotation, 'annotateElement');
       layer.annotate(el, lineNumberEl, line);
       assert.isTrue(annotateElementStub.called);
       assert.equal(annotateElementStub.lastCall.args[1], 11);
@@ -755,7 +736,7 @@
       const el = document.createElement('div');
       el.textContent = str;
       const annotateElementStub =
-          sandbox.stub(GrAnnotation, 'annotateElement');
+          sinon.stub(GrAnnotation, 'annotateElement');
       layer.annotate(el, lineNumberEl, line);
       assert.isTrue(annotateElementStub.called);
       assert.equal(annotateElementStub.lastCall.args[1], 1);
@@ -769,7 +750,7 @@
       const el = document.createElement('div');
       el.textContent = str;
       const annotateElementStub =
-          sandbox.stub(GrAnnotation, 'annotateElement');
+          sinon.stub(GrAnnotation, 'annotateElement');
       layer.annotate(el, lineNumberEl, line);
       assert.isFalse(annotateElementStub.called);
     });
@@ -782,9 +763,9 @@
     let content;
 
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.viewMode = 'SIDE_BY_SIDE';
-      processStub = sandbox.stub(element.$.processor, 'process')
+      processStub = sinon.stub(element.$.processor, 'process')
           .returns(Promise.resolve());
       keyLocations = {left: {}, right: {}};
       prefs = {
@@ -856,12 +837,12 @@
           ],
         },
       ];
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       outputEl = element.queryEffectiveChildren('#diffTable');
       keyLocations = {left: {}, right: {}};
-      sandbox.stub(element, '_getDiffBuilder', () => {
+      sinon.stub(element, '_getDiffBuilder').callsFake(() => {
         const builder = new GrDiffBuilder({content}, prefs, outputEl);
-        sandbox.stub(builder, 'addColumns');
+        sinon.stub(builder, 'addColumns');
         builder.buildSectionElement = function(group) {
           const section = document.createElement('stub');
           section.textContent = group.lines
@@ -898,7 +879,7 @@
     });
 
     test('render-start and render-content are fired', done => {
-      const dispatchEventStub = sandbox.stub(element, 'dispatchEvent');
+      const dispatchEventStub = sinon.stub(element, 'dispatchEvent');
       element.render(keyLocations, {}).then(() => {
         const firedEventTypes = dispatchEventStub.getCalls()
             .map(c => c.args[0].type);
@@ -909,7 +890,7 @@
     });
 
     test('cancel', () => {
-      const processorCancelStub = sandbox.stub(element.$.processor, 'cancel');
+      const processorCancelStub = sinon.stub(element.$.processor, 'cancel');
       element.cancel();
       assert.isTrue(processorCancelStub.called);
     });
@@ -923,7 +904,7 @@
     let keyLocations;
 
     setup(done => {
-      element = fixture('mock-diff');
+      element = mockDiffFixture.instantiate();
       diff = getMockDiffResponse();
       element.diff = diff;
 
@@ -1014,7 +995,7 @@
     });
 
     test('_renderContentByRange', () => {
-      const spy = sandbox.spy(builder, '_createTextEl');
+      const spy = sinon.spy(builder, '_createTextEl');
       const start = 9;
       const end = 14;
       const count = end - start + 1;
@@ -1028,9 +1009,9 @@
     });
 
     test('_renderContentByRange notexistent elements', () => {
-      const spy = sandbox.spy(builder, '_createTextEl');
+      const spy = sinon.spy(builder, '_createTextEl');
 
-      sandbox.stub(builder, 'findLinesByRange',
+      sinon.stub(builder, 'findLinesByRange').callsFake(
           (s, e, d, lines, elements) => {
             // Add a line and a corresponding element.
             lines.push(new GrDiffLine(GrDiffLine.Type.BOTH));
@@ -1186,7 +1167,7 @@
     });
 
     test('setBlame attempts to render each blamed line', () => {
-      const getBlameStub = sandbox.stub(builder, '_getBlameByLineNum')
+      const getBlameStub = sinon.stub(builder, '_getBlameByLineNum')
           .returns(null);
       builder.setBlame(mockBlame);
       assert.equal(getBlameStub.callCount, 32);
@@ -1252,4 +1233,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.js
similarity index 84%
rename from polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html
rename to polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.js
index 2d26667..7346bf7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.js
@@ -1,32 +1,21 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>GrDiffBuilderUnified</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import '../gr-diff/gr-diff-group.js';
 import './gr-diff-builder.js';
 import './gr-diff-builder-unified.js';
@@ -202,4 +191,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js
similarity index 84%
rename from polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
rename to polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js
index 440b233..4ca75eb 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js
@@ -1,60 +1,42 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-diff-cursor</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-diff></gr-diff>
-    <gr-diff-cursor></gr-diff-cursor>
-    <gr-rest-api-interface></gr-rest-api-interface>
-  </template>
-</test-fixture>
-
-<test-fixture id="empty">
-  <template>
-    <div></div>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import '../gr-diff/gr-diff.js';
 import './gr-diff-cursor.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromTemplate(html`
+  <gr-diff></gr-diff>
+  <gr-diff-cursor></gr-diff-cursor>
+  <gr-rest-api-interface></gr-rest-api-interface>
+`);
+
+const emptyFixture = fixtureFromElement('div');
+
 suite('gr-diff-cursor tests', () => {
-  let sandbox;
   let cursorElement;
   let diffElement;
 
   setup(done => {
-    sandbox = sinon.sandbox.create();
-
-    const fixtureElems = fixture('basic');
+    const fixtureElems = basicFixture.instantiate();
     diffElement = fixtureElems[0];
     cursorElement = fixtureElems[1];
     const restAPI = fixtureElems[2];
@@ -83,8 +65,6 @@
     });
   });
 
-  teardown(() => sandbox.restore());
-
   test('diff cursor functionality (side-by-side)', () => {
     // The cursor has been initialized to the first delta.
     assert.isOk(cursorElement.diffRow);
@@ -134,7 +114,7 @@
   });
 
   test('moves to selected line', () => {
-    const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber');
+    const moveToNumStub = sinon.stub(cursorElement, 'moveToLineNumber');
 
     cursorElement._handleDiffLineSelected(
         new CustomEvent('line-selected', {
@@ -247,7 +227,7 @@
   test('navigate to next unreviewed file via moveToNextChunk', () => {
     const cursor = cursorElement.shadowRoot.querySelector('#cursorManager');
     cursor.index = cursor.stops.length - 1;
-    const dispatchEventStub = sandbox.stub(cursor, 'dispatchEvent');
+    const dispatchEventStub = sinon.stub(cursor, 'dispatchEvent');
     cursorElement.moveToNextChunk(/* opt_clipToTop = */false,
         /* opt_navigateToNextFile = */true);
     assert.isTrue(dispatchEventStub.called);
@@ -261,9 +241,10 @@
 
   test('initialLineNumber not provided', done => {
     let scrollBehaviorDuringMove;
-    const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber');
-    const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk',
-        () => { scrollBehaviorDuringMove = cursorElement._scrollMode; });
+    const moveToNumStub = sinon.stub(cursorElement, 'moveToLineNumber');
+    const moveToChunkStub = sinon.stub(cursorElement, 'moveToFirstChunk')
+        .callsFake(
+            () => { scrollBehaviorDuringMove = cursorElement._scrollMode; });
 
     function renderHandler() {
       diffElement.removeEventListener('render', renderHandler);
@@ -280,9 +261,10 @@
 
   test('initialLineNumber provided', done => {
     let scrollBehaviorDuringMove;
-    const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber',
-        () => { scrollBehaviorDuringMove = cursorElement._scrollMode; });
-    const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk');
+    const moveToNumStub = sinon.stub(cursorElement, 'moveToLineNumber')
+        .callsFake(
+            () => { scrollBehaviorDuringMove = cursorElement._scrollMode; });
+    const moveToChunkStub = sinon.stub(cursorElement, 'moveToFirstChunk');
     function renderHandler() {
       diffElement.removeEventListener('render', renderHandler);
       cursorElement.reInitCursor();
@@ -364,9 +346,9 @@
     });
 
     test('createCommentInPlace ignores call if nothing is selected', () => {
-      const createRangeCommentStub = sandbox.stub(diffElement,
+      const createRangeCommentStub = sinon.stub(diffElement,
           'createRangeComment');
-      const addDraftAtLineStub = sandbox.stub(diffElement, 'addDraftAtLine');
+      const addDraftAtLineStub = sinon.stub(diffElement, 'addDraftAtLine');
       cursorElement.diffRow = undefined;
       cursorElement.createCommentInPlace();
       assert.isFalse(createRangeCommentStub.called);
@@ -413,7 +395,7 @@
   });
 
   test('expand context updates stops', done => {
-    sandbox.spy(cursorElement, '_updateStops');
+    sinon.spy(cursorElement, '_updateStops');
     MockInteractions.tap(diffElement.shadowRoot
         .querySelector('.showContext'));
     flush(() => {
@@ -423,15 +405,13 @@
   });
 
   suite('gr-diff-cursor event tests', () => {
-    let sandbox;
     let someEmptyDiv;
 
     setup(() => {
-      sandbox = sinon.sandbox.create();
-      someEmptyDiv = fixture('empty');
+      someEmptyDiv = emptyFixture.instantiate();
     });
 
-    teardown(() => sandbox.restore());
+    teardown(() => sinon.restore());
 
     test('ready is fired after component is rendered', done => {
       const cursorElement = document.createElement('gr-diff-cursor');
@@ -442,4 +422,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.js
similarity index 85%
rename from polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
rename to polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.js
index 2bda950..65f5e07 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.js
@@ -1,57 +1,40 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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
+const basicFixture = fixtureFromTemplate(html`
+<div>Lorem ipsum dolor sit amet, suspendisse inceptos vehicula</div>
+`);
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-annotation</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <div>Lorem ipsum dolor sit amet, suspendisse inceptos vehicula</div>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import {GrAnnotation} from './gr-annotation.js';
 import {sanitizeDOMValue, setSanitizeDOMValue} from '@polymer/polymer/lib/utils/settings.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
 suite('annotation', () => {
   let str;
   let parent;
   let textNode;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    parent = fixture('basic');
+    parent = basicFixture.instantiate();
     textNode = parent.childNodes[0];
     str = textNode.textContent;
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('_annotateText Case 1', () => {
     GrAnnotation._annotateText(textNode, 0, str.length, 'foobar');
 
@@ -205,7 +188,7 @@
     setup(() => {
       originalSanitizeDOMValue = sanitizeDOMValue;
       assert.isDefined(originalSanitizeDOMValue);
-      mockSanitize = sandbox.spy(originalSanitizeDOMValue);
+      mockSanitize = sinon.spy(originalSanitizeDOMValue);
       setSanitizeDOMValue(mockSanitize);
     });
 
@@ -295,4 +278,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.js
similarity index 85%
rename from polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
rename to polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.js
index ec7e6d2..957d039 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.js
@@ -1,37 +1,34 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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
+import '../../../test/common-test-setup-karma.js';
+import './gr-diff-highlight.js';
+import {GrRangeNormalizer} from './gr-range-normalizer.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-diff-highlight</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <style>
+// Splitting long lines in html into shorter rows breaks tests:
+// zero-length text nodes and new lines are not expected in some places
+/* eslint-disable max-len */
+const basicFixture = fixtureFromTemplate(html`
+<style>
       .tab-indicator:before {
         color: #C62828;
         /* >> character */
-        content: '\00BB';
+        content: '\\00BB';
       }
     </style>
     <gr-diff-highlight>
@@ -50,10 +47,10 @@
           <tr class="diff-row side-by-side" left-type="remove" right-type="add">
             <td class="left lineNum" data-value="2"></td>
             <!-- Next tag is formatted to eliminate zero-length text nodes. -->
-            <td class="content remove"><div class="contentText">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab-indicator" style="tab-size:8;">	</span></hl>udiam, <hl>quid</hl> sit, <span class="tab-indicator" style="tab-size:8;">	</span>quod <hl>Epicurum</hl></div></td>
+            <td class="content remove"><div class="contentText">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl>udiam, <hl>quid</hl> sit, <span class="tab-indicator" style="tab-size:8;">\u0009</span>quod <hl>Epicurum</hl></div></td>
             <td class="right lineNum" data-value="2"></td>
             <!-- Next tag is formatted to eliminate zero-length text nodes. -->
-            <td class="content add"><div class="contentText">nacti , <hl>,</hl> sumus <hl><span class="tab-indicator" style="tab-size:8;">	</span></hl> otiosum,  <span class="tab-indicator" style="tab-size:8;">	</span> audiam,  sit, quod</div></td>
+            <td class="content add"><div class="contentText">nacti , <hl>,</hl> sumus <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl> otiosum,  <span class="tab-indicator" style="tab-size:8;">\u0009</span> audiam,  sit, quod</div></td>
           </tr>
         </tbody>
 
@@ -70,19 +67,19 @@
           <tr class="diff-row side-by-side" left-type="remove" right-type="add">
             <td class="left lineNum" data-value="140"></td>
             <!-- Next tag is formatted to eliminate zero-length text nodes. -->
-            <td class="content remove"><div class="contentText">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab-indicator" style="tab-size:8;">	</span></hl>udiam, <hl>quid</hl> sit, <span class="tab-indicator" style="tab-size:8;">	</span>quod <hl>Epicurum</hl></div><div class="comment-thread">
+            <td class="content remove"><div class="contentText">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl>udiam, <hl>quid</hl> sit, <span class="tab-indicator" style="tab-size:8;">\u0009</span>quod <hl>Epicurum</hl></div><div class="comment-thread">
                 [Yet another random diff thread content here]
             </div></td>
             <td class="right lineNum" data-value="120"></td>
             <!-- Next tag is formatted to eliminate zero-length text nodes. -->
-            <td class="content add"><div class="contentText">nacti , <hl>,</hl> sumus <hl><span class="tab-indicator" style="tab-size:8;">	</span></hl> otiosum,  <span class="tab-indicator" style="tab-size:8;">	</span> audiam,  sit, quod</div></td>
+            <td class="content add"><div class="contentText">nacti , <hl>,</hl> sumus <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl> otiosum,  <span class="tab-indicator" style="tab-size:8;">\u0009</span> audiam,  sit, quod</div></td>
           </tr>
         </tbody>
 
         <tbody class="section both">
           <tr class="diff-row side-by-side" left-type="both" right-type="both">
             <td class="left lineNum" data-value="141"></td>
-            <td class="content both"><div class="contentText">nam et<hl><span class="tab-indicator" style="tab-size:8;">	</span></hl>complectitur<span class="tab-indicator" style="tab-size:8;">	</span>verbis, quod vult, et dicit plane, quod intellegam;</div></td>
+            <td class="content both"><div class="contentText">nam et<hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl>complectitur<span class="tab-indicator" style="tab-size:8;">\u0009</span>verbis, quod vult, et dicit plane, quod intellegam;</div></td>
             <td class="right lineNum" data-value="130"></td>
             <td class="content both"><div class="contentText">nam et complectitur verbis, quod vult, et dicit plane, quod intellegam;</div></td>
           </tr>
@@ -123,41 +120,20 @@
             <td class="left lineNum" data-value="165"></td>
             <td class="content both"><div class="contentText"></div></td>
             <td class="right lineNum" data-value="147"></td>
-            <td class="content both"><div class="contentText">in physicis, <hl><span class="tab-indicator" style="tab-size:8;">	</span></hl> quibus maxime gloriatur, primum totus est alienus. Democritea dicit</div></td>
+            <td class="content both"><div class="contentText">in physicis, <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl> quibus maxime gloriatur, primum totus est alienus. Democritea dicit</div></td>
           </tr>
         </tbody>
 
       </table>
     </gr-diff-highlight>
-  </template>
-</test-fixture>
-
-<test-fixture id="highlighted">
-  <template>
-    <div>
-      <hl class="rangeHighlight">foo</hl>
-      bar
-      <hl class="rangeHighlight">baz</hl>
-    </div>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-diff-highlight.js';
-import {GrRangeNormalizer} from './gr-range-normalizer.js';
+`);
+/* eslint-enable max-len */
 
 suite('gr-diff-highlight', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic')[1];
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    element = basicFixture.instantiate()[1];
   });
 
   suite('comment events', () => {
@@ -165,9 +141,9 @@
 
     setup(() => {
       builder = {
-        getContentsByLineRange: sandbox.stub().returns([]),
-        getLineElByChild: sandbox.stub().returns({}),
-        getSideByLineEl: sandbox.stub().returns('other-side'),
+        getContentsByLineRange: sinon.stub().returns([]),
+        getLineElByChild: sinon.stub().returns({}),
+        getSideByLineEl: sinon.stub().returns('other-side'),
       };
       element._cachedDiffBuilder = builder;
     });
@@ -180,7 +156,7 @@
       element.appendChild(threadEl);
       element.commentRanges = [{side: 'right'}];
 
-      sandbox.stub(element, 'set');
+      sinon.stub(element, 'set');
       threadEl.dispatchEvent(new CustomEvent(
           'comment-thread-mouseenter', {bubbles: true, composed: true}));
       assert.isFalse(element.set.called);
@@ -205,7 +181,7 @@
         end_character: 6,
       }}];
 
-      sandbox.stub(element, 'set');
+      sinon.stub(element, 'set');
       threadEl.dispatchEvent(new CustomEvent(
           'comment-thread-mouseenter', {bubbles: true, composed: true}));
       assert.isTrue(element.set.called);
@@ -222,7 +198,7 @@
       element.appendChild(threadEl);
       element.commentRanges = [{side: 'right'}];
 
-      sandbox.stub(element, 'set');
+      sinon.stub(element, 'set');
       threadEl.dispatchEvent(new CustomEvent(
           'comment-thread-mouseleave', {bubbles: true, composed: true}));
       assert.isFalse(element.set.called);
@@ -230,7 +206,7 @@
 
     test(`create-range-comment for range when create-comment-requested
           is fired`, () => {
-      sandbox.stub(element, '_removeActionBox');
+      sinon.stub(element, '_removeActionBox');
       element.selectedRange = {
         side: 'left',
         range: {
@@ -291,16 +267,16 @@
     setup(() => {
       contentStubs = [];
       stub('gr-selection-action-box', {
-        placeAbove: sandbox.stub(),
-        placeBelow: sandbox.stub(),
+        placeAbove: sinon.stub(),
+        placeBelow: sinon.stub(),
       });
       diff = element.querySelector('#diffTable');
       builder = {
-        getContentTdByLine: sandbox.stub(),
-        getContentTdByLineEl: sandbox.stub(),
+        getContentTdByLine: sinon.stub(),
+        getContentTdByLineEl: sinon.stub(),
         getLineElByChild,
-        getLineNumberByChild: sandbox.stub(),
-        getSideByLineEl: sandbox.stub(),
+        getLineNumberByChild: sinon.stub(),
+        getSideByLineEl: sinon.stub(),
       };
       element._cachedDiffBuilder = builder;
     });
@@ -312,7 +288,7 @@
 
     test('single first line', () => {
       const content = stubContent(1, 'right');
-      sandbox.spy(element, '_positionActionBox');
+      sinon.spy(element, '_positionActionBox');
       emulateSelection(content.firstChild, 5, content.firstChild, 12);
       const actionBox = element.shadowRoot
           .querySelector('gr-selection-action-box');
@@ -322,7 +298,7 @@
     test('multiline starting on first line', () => {
       const startContent = stubContent(1, 'right');
       const endContent = stubContent(2, 'right');
-      sandbox.spy(element, '_positionActionBox');
+      sinon.spy(element, '_positionActionBox');
       emulateSelection(
           startContent.firstChild, 10, endContent.lastChild, 7);
       const actionBox = element.shadowRoot
@@ -332,7 +308,7 @@
 
     test('single line', () => {
       const content = stubContent(138, 'left');
-      sandbox.spy(element, '_positionActionBox');
+      sinon.spy(element, '_positionActionBox');
       emulateSelection(content.firstChild, 5, content.firstChild, 12);
       const actionBox = element.shadowRoot
           .querySelector('gr-selection-action-box');
@@ -350,7 +326,7 @@
     test('multiline', () => {
       const startContent = stubContent(119, 'right');
       const endContent = stubContent(120, 'right');
-      sandbox.spy(element, '_positionActionBox');
+      sinon.spy(element, '_positionActionBox');
       emulateSelection(
           startContent.firstChild, 10, endContent.lastChild, 7);
       const actionBox = element.shadowRoot
@@ -378,7 +354,7 @@
       endRange.setStart(endContent.lastChild, 6);
       endRange.setEnd(endContent.lastChild, 7);
 
-      const getRangeAtStub = sandbox.stub();
+      const getRangeAtStub = sinon.stub();
       getRangeAtStub
           .onFirstCall().returns(startRange)
           .onSecondCall()
@@ -386,7 +362,7 @@
       const selection = {
         rangeCount: 2,
         getRangeAt: getRangeAtStub,
-        removeAllRanges: sandbox.stub(),
+        removeAllRanges: sinon.stub(),
       };
       element._handleSelection(selection);
       const {range} = element.selectedRange;
@@ -627,4 +603,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
similarity index 91%
rename from polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
rename to polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
index 342c7ec..d0e6b62 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
@@ -1,62 +1,42 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2018 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-diff</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-diff-host></gr-diff-host>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-diff-host.js';
 import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {DiffSide} from '../gr-diff/gr-diff-utils.js';
 
+const basicFixture = fixtureFromElement('gr-diff-host');
+
 suite('gr-diff-host tests', () => {
   let element;
-  let sandbox;
+
   let getLoggedIn;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     getLoggedIn = false;
     stub('gr-rest-api-interface', {
       async getLoggedIn() { return getLoggedIn; },
     });
-    element = fixture('basic');
-    sandbox.stub(element.reporting, 'time');
-    sandbox.stub(element.reporting, 'timeEnd');
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    element = basicFixture.instantiate();
+    sinon.stub(element.reporting, 'time');
+    sinon.stub(element.reporting, 'timeEnd');
   });
 
   suite('plugin layers', () => {
@@ -65,7 +45,7 @@
       stub('gr-js-api-interface', {
         getDiffLayers() { return pluginLayers; },
       });
-      element = fixture('basic');
+      element = basicFixture.instantiate();
     });
     test('plugin layers requested', () => {
       element.patchRange = {};
@@ -76,7 +56,7 @@
 
   suite('handle comment-update', () => {
     setup(() => {
-      sandbox.stub(element, '_commentsChanged');
+      sinon.stub(element, '_commentsChanged');
       element.comments = {
         meta: {
           changeNum: '42',
@@ -122,7 +102,7 @@
         side: 'PARENT',
         __commentSide: 'left',
       };
-      const diffCommentsModifiedStub = sandbox.stub();
+      const diffCommentsModifiedStub = sinon.stub();
       element.addEventListener('diff-comments-modified',
           diffCommentsModifiedStub);
       element.comments.left.push(comment);
@@ -147,7 +127,7 @@
         side: 'PARENT',
         __commentSide: 'left',
       };
-      const diffCommentsModifiedStub = sandbox.stub();
+      const diffCommentsModifiedStub = sinon.stub();
       element.addEventListener('diff-comments-modified',
           diffCommentsModifiedStub);
       element.comments.left.push(comment);
@@ -166,7 +146,7 @@
   });
 
   test('remove comment', () => {
-    sandbox.stub(element, '_commentsChanged');
+    sinon.stub(element, '_commentsChanged');
     element.comments = {
       meta: {
         changeNum: '42',
@@ -320,13 +300,13 @@
     });
 
     test('ends total and syntax timer after syntax layer processing', done => {
-      sandbox.stub(element.reporting, 'diffViewContentDisplayed');
+      sinon.stub(element.reporting, 'diffViewContentDisplayed');
       let notifySyntaxProcessed;
-      sandbox.stub(element.$.syntaxLayer, 'process').returns(new Promise(
+      sinon.stub(element.$.syntaxLayer, 'process').returns(new Promise(
           resolve => {
             notifySyntaxProcessed = resolve;
           }));
-      sandbox.stub(element.$.restAPI, 'getDiff').returns(
+      sinon.stub(element.$.restAPI, 'getDiff').returns(
           Promise.resolve({content: []}));
       element.patchRange = {};
       element.$.restAPI.getDiffPreferences().then(prefs => {
@@ -349,26 +329,30 @@
     });
 
     test('ends total timer w/ no syntax layer processing', done => {
-      sandbox.stub(element.$.restAPI, 'getDiff').returns(
+      sinon.stub(element.$.restAPI, 'getDiff').returns(
           Promise.resolve({content: []}));
       element.patchRange = {};
       element.reload();
       // Multiple cascading microtasks are scheduled.
       setTimeout(() => {
-        assert.isTrue(element.reporting.timeEnd.calledOnce);
-        assert.isTrue(element.reporting.timeEnd.calledWithExactly(
-            'Diff Total Render'));
+        // Reporting can be called with other parameters (ex. PluginsLoaded),
+        // but only 'Diff Total Render' is important in this test.
+        assert.equal(
+            element.reporting.timeEnd.getCalls()
+                .filter(call => call.calledWithExactly('Diff Total Render'))
+                .length,
+            1);
         done();
       });
     });
 
     test('completes reload promise after syntax layer processing', done => {
       let notifySyntaxProcessed;
-      sandbox.stub(element.$.syntaxLayer, 'process').returns(new Promise(
+      sinon.stub(element.$.syntaxLayer, 'process').returns(new Promise(
           resolve => {
             notifySyntaxProcessed = resolve;
           }));
-      sandbox.stub(element.$.restAPI, 'getDiff').returns(
+      sinon.stub(element.$.restAPI, 'getDiff').returns(
           Promise.resolve({content: []}));
       element.patchRange = {};
       let reloadComplete = false;
@@ -394,10 +378,10 @@
   });
 
   test('reload() cancels before network resolves', () => {
-    const cancelStub = sandbox.stub(element.$.diff, 'cancel');
+    const cancelStub = sinon.stub(element.$.diff, 'cancel');
 
     // Stub the network calls into requests that never resolve.
-    sandbox.stub(element, '_getDiff', () => new Promise(() => {}));
+    sinon.stub(element, '_getDiff').callsFake(() => new Promise(() => {}));
     element.patchRange = {};
 
     element.reload();
@@ -407,13 +391,13 @@
   suite('not logged in', () => {
     setup(() => {
       getLoggedIn = false;
-      element = fixture('basic');
+      element = basicFixture.instantiate();
     });
 
     test('reload() loads files weblinks', () => {
-      const weblinksStub = sandbox.stub(GerritNav, '_generateWeblinks')
+      const weblinksStub = sinon.stub(GerritNav, '_generateWeblinks')
           .returns({name: 'stubb', url: '#s'});
-      sandbox.stub(element.$.restAPI, 'getDiff').returns(Promise.resolve({
+      sinon.stub(element.$.restAPI, 'getDiff').returns(Promise.resolve({
         content: [],
       }));
       element.projectName = 'test-project';
@@ -446,7 +430,7 @@
     });
 
     test('prefetch getDiff', done => {
-      const diffRestApiStub = sandbox.stub(element.$.restAPI, 'getDiff')
+      const diffRestApiStub = sinon.stub(element.$.restAPI, 'getDiff')
           .returns(Promise.resolve({content: []}));
       element.changeNum = 123;
       element.patchRange = {basePatchNum: 1, patchNum: 2};
@@ -469,9 +453,9 @@
     });
 
     test('reload resolves on error', () => {
-      const onErrStub = sandbox.stub(element, '_handleGetDiffError');
+      const onErrStub = sinon.stub(element, '_handleGetDiffError');
       const error = {ok: false, status: 500};
-      sandbox.stub(element.$.restAPI, 'getDiff',
+      sinon.stub(element.$.restAPI, 'getDiff').callsFake(
           (changeNum, basePatchNum, patchNum, path, onErr) => {
             onErr(error);
           });
@@ -530,12 +514,12 @@
           'wsAAAAAAAAAAAAA/////w==',
           type: 'image/bmp',
         };
-        sandbox.stub(element.$.restAPI,
-            'getB64FileContents',
-            (changeId, patchNum, path, opt_parentIndex) => Promise.resolve(
-                opt_parentIndex === 1 ? mockFile1 :
-                  mockFile2)
-        );
+        sinon.stub(element.$.restAPI,
+            'getB64FileContents')
+            .callsFake(
+                (changeId, patchNum, path, opt_parentIndex) => Promise.resolve(
+                    opt_parentIndex === 1 ? mockFile1 : mockFile2)
+            );
 
         element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
         element.comments = {
@@ -562,7 +546,7 @@
           content: [{skip: 66}],
           binary: true,
         };
-        sandbox.stub(element.$.restAPI, 'getDiff')
+        sinon.stub(element.$.restAPI, 'getDiff')
             .returns(Promise.resolve(mockDiff));
 
         const rendered = () => {
@@ -643,7 +627,7 @@
           content: [{skip: 66}],
           binary: true,
         };
-        sandbox.stub(element.$.restAPI, 'getDiff')
+        sinon.stub(element.$.restAPI, 'getDiff')
             .returns(Promise.resolve(mockDiff));
 
         const rendered = () => {
@@ -725,7 +709,7 @@
           content: [{skip: 66}],
           binary: true,
         };
-        sandbox.stub(element.$.restAPI, 'getDiff')
+        sinon.stub(element.$.restAPI, 'getDiff')
             .returns(Promise.resolve(mockDiff));
 
         element.addEventListener('render', () => {
@@ -766,7 +750,7 @@
           content: [{skip: 66}],
           binary: true,
         };
-        sandbox.stub(element.$.restAPI, 'getDiff')
+        sinon.stub(element.$.restAPI, 'getDiff')
             .returns(Promise.resolve(mockDiff));
 
         element.addEventListener('render', () => {
@@ -809,7 +793,7 @@
         };
         mockFile1.type = 'image/jpeg-evil';
 
-        sandbox.stub(element.$.restAPI, 'getDiff')
+        sinon.stub(element.$.restAPI, 'getDiff')
             .returns(Promise.resolve(mockDiff));
 
         element.addEventListener('render', () => {
@@ -832,7 +816,7 @@
   });
 
   test('delegates cancel()', () => {
-    const stub = sandbox.stub(element.$.diff, 'cancel');
+    const stub = sinon.stub(element.$.diff, 'cancel');
     element.patchRange = {};
     element.reload();
     assert.isTrue(stub.calledOnce);
@@ -841,7 +825,7 @@
 
   test('delegates getCursorStops()', () => {
     const returnValue = [document.createElement('b')];
-    const stub = sandbox.stub(element.$.diff, 'getCursorStops')
+    const stub = sinon.stub(element.$.diff, 'getCursorStops')
         .returns(returnValue);
     assert.equal(element.getCursorStops(), returnValue);
     assert.isTrue(stub.calledOnce);
@@ -850,7 +834,7 @@
 
   test('delegates isRangeSelected()', () => {
     const returnValue = true;
-    const stub = sandbox.stub(element.$.diff, 'isRangeSelected')
+    const stub = sinon.stub(element.$.diff, 'isRangeSelected')
         .returns(returnValue);
     assert.equal(element.isRangeSelected(), returnValue);
     assert.isTrue(stub.calledOnce);
@@ -858,7 +842,7 @@
   });
 
   test('delegates toggleLeftDiff()', () => {
-    const stub = sandbox.stub(element.$.diff, 'toggleLeftDiff');
+    const stub = sinon.stub(element.$.diff, 'toggleLeftDiff');
     element.toggleLeftDiff();
     assert.isTrue(stub.calledOnce);
     assert.equal(stub.lastCall.args.length, 0);
@@ -866,12 +850,12 @@
 
   suite('blame', () => {
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
     });
 
     test('clearBlame', () => {
       element._blame = [];
-      const setBlameSpy = sandbox.spy(element.$.diff.$.diffBuilder, 'setBlame');
+      const setBlameSpy = sinon.spy(element.$.diff.$.diffBuilder, 'setBlame');
       element.clearBlame();
       assert.isNull(element._blame);
       assert.isTrue(setBlameSpy.calledWithExactly(null));
@@ -882,7 +866,7 @@
       const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
       const showAlertStub = sinon.stub();
       element.addEventListener('show-alert', showAlertStub);
-      const getBlameStub = sandbox.stub(element.$.restAPI, 'getBlame')
+      const getBlameStub = sinon.stub(element.$.restAPI, 'getBlame')
           .returns(Promise.resolve(mockBlame));
       element.changeNum = 42;
       element.patchRange = {patchNum: 5, basePatchNum: 4};
@@ -900,7 +884,7 @@
       const mockBlame = [];
       const showAlertStub = sinon.stub();
       element.addEventListener('show-alert', showAlertStub);
-      sandbox.stub(element.$.restAPI, 'getBlame')
+      sinon.stub(element.$.restAPI, 'getBlame')
           .returns(Promise.resolve(mockBlame));
       element.changeNum = 42;
       element.patchRange = {patchNum: 5, basePatchNum: 4};
@@ -926,7 +910,7 @@
 
   test('delegates addDraftAtLine(el)', () => {
     const param0 = document.createElement('b');
-    const stub = sandbox.stub(element.$.diff, 'addDraftAtLine');
+    const stub = sinon.stub(element.$.diff, 'addDraftAtLine');
     element.addDraftAtLine(param0);
     assert.isTrue(stub.calledOnce);
     assert.equal(stub.lastCall.args.length, 1);
@@ -934,14 +918,14 @@
   });
 
   test('delegates clearDiffContent()', () => {
-    const stub = sandbox.stub(element.$.diff, 'clearDiffContent');
+    const stub = sinon.stub(element.$.diff, 'clearDiffContent');
     element.clearDiffContent();
     assert.isTrue(stub.calledOnce);
     assert.equal(stub.lastCall.args.length, 0);
   });
 
   test('delegates expandAllContext()', () => {
-    const stub = sandbox.stub(element.$.diff, 'expandAllContext');
+    const stub = sinon.stub(element.$.diff, 'expandAllContext');
     element.expandAllContext();
     assert.isTrue(stub.calledOnce);
     assert.equal(stub.lastCall.args.length, 0);
@@ -1036,10 +1020,10 @@
     let reportStub;
 
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.path = 'file.txt';
       element.patchRange = {basePatchNum: 1};
-      reportStub = sandbox.stub(element.reporting, 'reportInteraction');
+      reportStub = sinon.stub(element.reporting, 'reportInteraction');
     });
 
     test('null and content-less', () => {
@@ -1489,9 +1473,9 @@
     });
 
     test('starts syntax layer processing on render event', done => {
-      sandbox.stub(element.$.syntaxLayer, 'process')
+      sinon.stub(element.$.syntaxLayer, 'process')
           .returns(Promise.resolve());
-      sandbox.stub(element.$.restAPI, 'getDiff').returns(
+      sinon.stub(element.$.restAPI, 'getDiff').returns(
           Promise.resolve({content: []}));
       element.reload();
       setTimeout(() => {
@@ -1571,7 +1555,7 @@
           });
         },
       });
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       const prefs = {
         line_length: 10,
         show_tabs: true,
@@ -1665,7 +1649,7 @@
     suite('_hasTrailingNewlines', () => {
       test('shared no trailing', () => {
         const diff = undefined;
-        sandbox.stub(element, '_lastChunkForSide')
+        sinon.stub(element, '_lastChunkForSide')
             .returns({ab: ['foo', 'bar']});
         assert.isFalse(element._hasTrailingNewlines(diff, false));
         assert.isFalse(element._hasTrailingNewlines(diff, true));
@@ -1673,7 +1657,7 @@
 
       test('delta trailing in right', () => {
         const diff = undefined;
-        sandbox.stub(element, '_lastChunkForSide')
+        sinon.stub(element, '_lastChunkForSide')
             .returns({a: ['foo', 'bar'], b: ['baz', '']});
         assert.isTrue(element._hasTrailingNewlines(diff, false));
         assert.isFalse(element._hasTrailingNewlines(diff, true));
@@ -1681,7 +1665,7 @@
 
       test('addition', () => {
         const diff = undefined;
-        sandbox.stub(element, '_lastChunkForSide', (diff, leftSide) => {
+        sinon.stub(element, '_lastChunkForSide').callsFake((diff, leftSide) => {
           if (leftSide) { return null; }
           return {b: ['foo', '']};
         });
@@ -1691,7 +1675,7 @@
 
       test('deletion', () => {
         const diff = undefined;
-        sandbox.stub(element, '_lastChunkForSide', (diff, leftSide) => {
+        sinon.stub(element, '_lastChunkForSide').callsFake((diff, leftSide) => {
           if (!leftSide) { return null; }
           return {a: ['foo']};
         });
@@ -1701,4 +1685,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html
deleted file mode 100644
index 309f4ac..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html
+++ /dev/null
@@ -1,84 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-diff-mode-selector</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script src="/node_modules/page/page.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-diff-mode-selector></gr-diff-mode-selector>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-diff-mode-selector.js';
-suite('gr-diff-mode-selector tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('_computeSelectedClass', () => {
-    assert.equal(
-        element._computeSelectedClass('SIDE_BY_SIDE', 'SIDE_BY_SIDE'),
-        'selected');
-    assert.equal(
-        element._computeSelectedClass('SIDE_BY_SIDE', 'UNIFIED_DIFF'), '');
-  });
-
-  test('setMode', () => {
-    const saveStub = sandbox.stub(element.$.restAPI, 'savePreferences');
-
-    // Setting the mode initially does not save prefs.
-    element.saveOnChange = true;
-    element.setMode('SIDE_BY_SIDE');
-    assert.isFalse(saveStub.called);
-
-    // Setting the mode to itself does not save prefs.
-    element.setMode('SIDE_BY_SIDE');
-    assert.isFalse(saveStub.called);
-
-    // Setting the mode to something else does not save prefs if saveOnChange
-    // is false.
-    element.saveOnChange = false;
-    element.setMode('UNIFIED_DIFF');
-    assert.isFalse(saveStub.called);
-
-    // Setting the mode to something else does not save prefs if saveOnChange
-    // is false.
-    element.saveOnChange = true;
-    element.setMode('SIDE_BY_SIDE');
-    assert.isTrue(saveStub.calledOnce);
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.js b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.js
new file mode 100644
index 0000000..e84ef2b
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.js
@@ -0,0 +1,63 @@
+/**
+ * @license
+ * Copyright (C) 2018 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-diff-mode-selector.js';
+
+const basicFixture = fixtureFromElement('gr-diff-mode-selector');
+
+suite('gr-diff-mode-selector tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('_computeSelectedClass', () => {
+    assert.equal(
+        element._computeSelectedClass('SIDE_BY_SIDE', 'SIDE_BY_SIDE'),
+        'selected');
+    assert.equal(
+        element._computeSelectedClass('SIDE_BY_SIDE', 'UNIFIED_DIFF'), '');
+  });
+
+  test('setMode', () => {
+    const saveStub = sinon.stub(element.$.restAPI, 'savePreferences');
+
+    // Setting the mode initially does not save prefs.
+    element.saveOnChange = true;
+    element.setMode('SIDE_BY_SIDE');
+    assert.isFalse(saveStub.called);
+
+    // Setting the mode to itself does not save prefs.
+    element.setMode('SIDE_BY_SIDE');
+    assert.isFalse(saveStub.called);
+
+    // Setting the mode to something else does not save prefs if saveOnChange
+    // is false.
+    element.saveOnChange = false;
+    element.setMode('UNIFIED_DIFF');
+    assert.isFalse(saveStub.called);
+
+    // Setting the mode to something else does not save prefs if saveOnChange
+    // is false.
+    element.saveOnChange = true;
+    element.setMode('SIDE_BY_SIDE');
+    assert.isTrue(saveStub.calledOnce);
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.html
deleted file mode 100644
index d3050af..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.html
+++ /dev/null
@@ -1,69 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2020 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-diff-preferences-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script src="/components/web-component-tester/data/a11ySuite.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-diff-preferences-dialog></gr-diff-preferences-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-diff-preferences-dialog.js';
-import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-
-suite('gr-diff-preferences-dialog', () => {
-  let element;
-  setup(() => {
-    element = fixture('basic');
-  });
-  test('changes applies only on save', async () => {
-    const originalDiffPrefs = {
-      line_wrapping: true,
-    };
-    element.diffPrefs = originalDiffPrefs;
-
-    element.open();
-    await flush();
-    assert.isTrue(element.$.diffPreferences.$.lineWrappingInput.checked);
-
-    MockInteractions.tap(element.$.diffPreferences.$.lineWrappingInput);
-    await flush();
-    assert.isFalse(element.$.diffPreferences.$.lineWrappingInput.checked);
-    assert.isTrue(element._diffPrefsChanged);
-    assert.isTrue(element.diffPrefs.line_wrapping);
-    assert.isTrue(originalDiffPrefs.line_wrapping);
-
-    MockInteractions.tap(element.$.saveButton);
-    await flush();
-    // Original prefs must remains unchanged, dialog must expose a new object
-    assert.isTrue(originalDiffPrefs.line_wrapping);
-    assert.isFalse(element.diffPrefs.line_wrapping);
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.js
similarity index 94%
rename from polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
rename to polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.js
index aafd374..a8fba5d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.js
@@ -1,42 +1,27 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-diff-processor test</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-diff-processor></gr-diff-processor>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-diff-processor.js';
 import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
 import {GrDiffGroup} from '../gr-diff/gr-diff-group.js';
 
+const basicFixture = fixtureFromElement('gr-diff-processor');
+
 suite('gr-diff-processor tests', () => {
   const WHOLE_FILE = -1;
   const loremIpsum =
@@ -47,19 +32,14 @@
       'fugit assum per.';
 
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-  });
 
-  teardown(() => {
-    sandbox.restore();
   });
 
   suite('not logged in', () => {
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
 
       element.context = 4;
     });
@@ -609,12 +589,12 @@
     test('scrolling pauses rendering', () => {
       const contentRow = {
         ab: [
-          '<!DOCTYPE html>',
-          '<meta charset="utf-8">',
+          '',
+          '',
         ],
       };
       const content = _.times(200, _.constant(contentRow));
-      sandbox.stub(element, 'async');
+      sinon.stub(element, 'async');
       element._isScrolling = true;
       element.process(content);
       // Just the files group - no more processing during scrolling.
@@ -629,12 +609,12 @@
     test('image diffs', () => {
       const contentRow = {
         ab: [
-          '<!DOCTYPE html>',
-          '<meta charset="utf-8">',
+          '',
+          '',
         ],
       };
       const content = _.times(200, _.constant(contentRow));
-      sandbox.stub(element, 'async');
+      sinon.stub(element, 'async');
       element.process(content, true);
       assert.equal(element.groups.length, 1);
 
@@ -870,7 +850,7 @@
 
     suite('_breakdown*', () => {
       test('_breakdownChunk breaks down additions', () => {
-        sandbox.spy(element, '_breakdown');
+        sinon.spy(element, '_breakdown');
         const chunk = {b: ['blah', 'blah', 'blah']};
         const result = element._breakdownChunk(chunk);
         assert.deepEqual(result, [chunk]);
@@ -879,7 +859,7 @@
 
       test('_breakdownChunk keeps due_to_rebase for broken down additions',
           () => {
-            sandbox.spy(element, '_breakdown');
+            sinon.spy(element, '_breakdown');
             const chunk = {b: ['blah', 'blah', 'blah'], due_to_rebase: true};
             const result = element._breakdownChunk(chunk);
             for (const subResult of result) {
@@ -926,10 +906,10 @@
   });
 
   test('detaching cancels', () => {
-    element = fixture('basic');
-    sandbox.stub(element, 'cancel');
+    element = basicFixture.instantiate();
+    sinon.stub(element, 'cancel');
     element.detached();
     assert(element.cancel.called);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.js
similarity index 85%
rename from polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
rename to polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.js
index 1221b58..c37ac93 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.js
@@ -1,33 +1,28 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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-diff-selection.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-diff-selection</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-diff-selection>
+// Splitting long lines in html into shorter rows breaks tests:
+// zero-length text nodes and new lines are not expected in some places
+/* eslint-disable max-len */
+const basicFixture = fixtureFromTemplate(html`
+<gr-diff-selection>
       <table id="diffTable" class="side-by-side">
         <tr class="diff-row">
           <td class="blame" data-line-number="1"></td>
@@ -100,22 +95,18 @@
         </tr>
       </table>
     </gr-diff-selection>
-  </template>
-</test-fixture>
+`);
+/* eslint-enable max-len */
 
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-diff-selection.js';
 suite('gr-diff-selection', () => {
   let element;
-  let sandbox;
 
   const emulateCopyOn = function(target) {
     const fakeEvent = {
       target,
-      preventDefault: sandbox.stub(),
+      preventDefault: sinon.stub(),
       clipboardData: {
-        setData: sandbox.stub(),
+        setData: sinon.stub(),
       },
     };
     element._getCopyEventTarget.returns(target);
@@ -124,12 +115,12 @@
   };
 
   setup(() => {
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
-    sandbox.stub(element, '_getCopyEventTarget');
+    element = basicFixture.instantiate();
+
+    sinon.stub(element, '_getCopyEventTarget');
     element._cachedDiffBuilder = {
-      getLineElByChild: sandbox.stub().returns({}),
-      getSideByLineEl: sandbox.stub(),
+      getLineElByChild: sinon.stub().returns({}),
+      getSideByLineEl: sinon.stub(),
       diffElement: element.querySelector('#diffTable'),
     };
     element.diff = {
@@ -150,10 +141,6 @@
     };
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('applies selected-left on left side click', () => {
     element.classList.add('selected-right');
     element._cachedDiffBuilder.getSideByLineEl.returns('left');
@@ -178,7 +165,7 @@
   test('applies selected-blame on blame click', () => {
     element.classList.add('selected-left');
     element.diffBuilder.getLineElByChild.returns(null);
-    sandbox.stub(element, '_elementDescendedFromClass',
+    sinon.stub(element, '_elementDescendedFromClass').callsFake(
         (el, className) => className === 'blame');
     MockInteractions.down(element);
     assert.isTrue(
@@ -188,26 +175,26 @@
   });
 
   test('ignores copy for non-content Element', () => {
-    sandbox.stub(element, '_getSelectedText');
+    sinon.stub(element, '_getSelectedText');
     emulateCopyOn(element.querySelector('.not-diff-row'));
     assert.isFalse(element._getSelectedText.called);
   });
 
   test('asks for text for left side Elements', () => {
     element._cachedDiffBuilder.getSideByLineEl.returns('left');
-    sandbox.stub(element, '_getSelectedText');
+    sinon.stub(element, '_getSelectedText');
     emulateCopyOn(element.querySelector('div.contentText'));
     assert.deepEqual(['left', false], element._getSelectedText.lastCall.args);
   });
 
   test('reacts to copy for content Elements', () => {
-    sandbox.stub(element, '_getSelectedText');
+    sinon.stub(element, '_getSelectedText');
     emulateCopyOn(element.querySelector('div.contentText'));
     assert.isTrue(element._getSelectedText.called);
   });
 
   test('copy event is prevented for content Elements', () => {
-    sandbox.stub(element, '_getSelectedText');
+    sinon.stub(element, '_getSelectedText');
     element._cachedDiffBuilder.getSideByLineEl.returns('left');
     element._getSelectedText.returns('test');
     const event = emulateCopyOn(element.querySelector('div.contentText'));
@@ -215,7 +202,7 @@
   });
 
   test('inserts text into clipboard on copy', () => {
-    sandbox.stub(element, '_getSelectedText').returns('the text');
+    sinon.stub(element, '_getSelectedText').returns('the text');
     const event = emulateCopyOn(element.querySelector('div.contentText'));
     assert.deepEqual(
         ['Text', 'the text'], event.clipboardData.setData.lastCall.args);
@@ -238,10 +225,11 @@
 
   test('_setClasses removes before it ads', () => {
     element.classList.add('selected-right');
-    const addStub = sandbox.stub(element.classList, 'add');
-    const removeStub = sandbox.stub(element.classList, 'remove', () => {
-      assert.isFalse(addStub.called);
-    });
+    const addStub = sinon.stub(element.classList, 'add');
+    const removeStub = sinon.stub(element.classList, 'remove').callsFake(
+        () => {
+          assert.isFalse(addStub.called);
+        });
     element._setClasses(['selected-comment', 'selected-left']);
     assert.isTrue(addStub.called);
     assert.isTrue(removeStub.called);
@@ -303,7 +291,7 @@
   test('defers to default behavior for textarea', () => {
     element.classList.add('selected-left');
     element.classList.remove('selected-right');
-    const selectedTextSpy = sandbox.spy(element, '_getSelectedText');
+    const selectedTextSpy = sinon.spy(element, '_getSelectedText');
     emulateCopyOn(element.querySelector('textarea'));
     assert.isFalse(selectedTextSpy.called);
   });
@@ -398,4 +386,4 @@
     assert.deepEqual(element._linesCache, {left: null, right: null});
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
similarity index 83%
rename from polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
rename to polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
index 8a64cdb..73e6f0c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
@@ -1,83 +1,68 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2015 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-diff-view</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script src="/node_modules/page/page.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-diff-view></gr-diff-view>
-  </template>
-</test-fixture>
-
-<test-fixture id="blank">
-  <template>
-    <div></div>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-diff-view.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {ChangeStatus} from '../../../constants/constants.js';
+import {TestKeyboardShortcutBinder} from '../../../test/test-utils';
+
+const basicFixture = fixtureFromElement('gr-diff-view');
+
+const blankFixture = fixtureFromElement('div');
 
 suite('gr-diff-view tests', () => {
   suite('basic tests', () => {
-    const kb = KeyboardShortcutBinder;
-    kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left');
-    kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right');
-    kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down');
-    kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up');
-    kb.bindShortcut(kb.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
-    kb.bindShortcut(kb.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
-    kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c');
-    kb.bindShortcut(kb.Shortcut.SAVE_COMMENT, 'ctrl+s');
-    kb.bindShortcut(kb.Shortcut.NEXT_FILE, ']');
-    kb.bindShortcut(kb.Shortcut.PREV_FILE, '[');
-    kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n');
-    kb.bindShortcut(kb.Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
-    kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p');
-    kb.bindShortcut(kb.Shortcut.PREV_COMMENT_THREAD, 'shift+p');
-    kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a');
-    kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
-    kb.bindShortcut(kb.Shortcut.UP_TO_CHANGE, 'u');
-    kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ',');
-    kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm');
-    kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
-    kb.bindShortcut(kb.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
-    kb.bindShortcut(kb.Shortcut.EXPAND_ALL_COMMENT_THREADS, 'e');
-    kb.bindShortcut(kb.Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, 'h');
-    kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_COMMENT_THREADS, 'shift+e');
-    kb.bindShortcut(kb.Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
-    kb.bindShortcut(kb.Shortcut.TOGGLE_BLAME, 'b');
-
     let element;
-    let sandbox;
+
+    suiteSetup(() => {
+      const kb = TestKeyboardShortcutBinder.push();
+      kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left');
+      kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right');
+      kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down');
+      kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up');
+      kb.bindShortcut(kb.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
+      kb.bindShortcut(kb.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
+      kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c');
+      kb.bindShortcut(kb.Shortcut.SAVE_COMMENT, 'ctrl+s');
+      kb.bindShortcut(kb.Shortcut.NEXT_FILE, ']');
+      kb.bindShortcut(kb.Shortcut.PREV_FILE, '[');
+      kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n');
+      kb.bindShortcut(kb.Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
+      kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p');
+      kb.bindShortcut(kb.Shortcut.PREV_COMMENT_THREAD, 'shift+p');
+      kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a');
+      kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
+      kb.bindShortcut(kb.Shortcut.UP_TO_CHANGE, 'u');
+      kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ',');
+      kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm');
+      kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
+      kb.bindShortcut(kb.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
+      kb.bindShortcut(kb.Shortcut.EXPAND_ALL_COMMENT_THREADS, 'e');
+      kb.bindShortcut(kb.Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, 'h');
+      kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_COMMENT_THREADS, 'shift+e');
+      kb.bindShortcut(kb.Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
+      kb.bindShortcut(kb.Shortcut.TOGGLE_BLAME, 'b');
+    });
+
+    suiteTeardown(() => {
+      TestKeyboardShortcutBinder.pop();
+    });
 
     const PARENT = 'PARENT';
 
@@ -93,8 +78,6 @@
     }
 
     setup(() => {
-      sandbox = sinon.sandbox.create();
-
       stub('gr-rest-api-interface', {
         getConfig() {
           return Promise.resolve({change: {}});
@@ -127,18 +110,14 @@
           return Promise.resolve([]);
         },
       });
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       return element._loadComments();
     });
 
-    teardown(() => {
-      sandbox.restore();
-    });
-
     test('params change triggers diffViewDisplayed()', () => {
-      sandbox.stub(element.reporting, 'diffViewDisplayed');
-      sandbox.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
-      sandbox.spy(element, '_paramsChanged');
+      sinon.stub(element.reporting, 'diffViewDisplayed');
+      sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
+      sinon.spy(element, '_paramsChanged');
       element.params = {
         view: GerritNav.View.DIFF,
         changeNum: '42',
@@ -155,10 +134,10 @@
     test('params change cases blame to load if it was set to true', () => {
       // Blame loads for subsequent files if it was loaded for one file
       element._isBlameLoaded = true;
-      sandbox.stub(element.reporting, 'diffViewDisplayed');
-      sandbox.stub(element, '_loadBlame');
-      sandbox.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
-      sandbox.spy(element, '_paramsChanged');
+      sinon.stub(element.reporting, 'diffViewDisplayed');
+      sinon.stub(element, '_loadBlame');
+      sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
+      sinon.spy(element, '_paramsChanged');
       element.params = {
         view: GerritNav.View.DIFF,
         changeNum: '42',
@@ -174,7 +153,7 @@
     });
 
     test('toggle left diff with a hotkey', () => {
-      const toggleLeftDiffStub = sandbox.stub(
+      const toggleLeftDiffStub = sinon.stub(
           element.$.diffHost, 'toggleLeftDiff');
       MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
       assert.isTrue(toggleLeftDiffStub.calledOnce);
@@ -198,8 +177,8 @@
       element.changeViewState.selectedFileIndex = 1;
       element._loggedIn = true;
 
-      const diffNavStub = sandbox.stub(GerritNav, 'navigateToDiff');
-      const changeNavStub = sandbox.stub(GerritNav, 'navigateToChange');
+      const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
+      const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
 
       MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
       assert(changeNavStub.lastCall.calledWith(element._change),
@@ -233,7 +212,7 @@
       assert.isTrue(element._loading);
 
       const showPrefsStub =
-          sandbox.stub(element.$.diffPreferencesDialog, 'open',
+          sinon.stub(element.$.diffPreferencesDialog, 'open').callsFake(
               () => Promise.resolve());
 
       MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
@@ -243,24 +222,24 @@
       MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
       assert(showPrefsStub.calledOnce);
 
-      let scrollStub = sandbox.stub(element.$.cursor, 'moveToNextChunk');
+      let scrollStub = sinon.stub(element.$.cursor, 'moveToNextChunk');
       MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
       assert(scrollStub.calledOnce);
 
-      scrollStub = sandbox.stub(element.$.cursor, 'moveToPreviousChunk');
+      scrollStub = sinon.stub(element.$.cursor, 'moveToPreviousChunk');
       MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
       assert(scrollStub.calledOnce);
 
-      scrollStub = sandbox.stub(element.$.cursor, 'moveToNextCommentThread');
+      scrollStub = sinon.stub(element.$.cursor, 'moveToNextCommentThread');
       MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
       assert(scrollStub.calledOnce);
 
-      scrollStub = sandbox.stub(element.$.cursor,
+      scrollStub = sinon.stub(element.$.cursor,
           'moveToPreviousCommentThread');
       MockInteractions.pressAndReleaseKeyOn(element, 80, 'shift', 'p');
       assert(scrollStub.calledOnce);
 
-      const computeContainerClassStub = sandbox.stub(element.$.diffHost.$.diff,
+      const computeContainerClassStub = sinon.stub(element.$.diffHost.$.diff,
           '_computeContainerClass');
       MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
       assert(computeContainerClassStub.lastCall.calledWithExactly(
@@ -270,18 +249,21 @@
       assert(computeContainerClassStub.lastCall.calledWithExactly(
           false, 'SIDE_BY_SIDE', false));
 
-      sandbox.stub(element, '_setReviewed');
+      sinon.stub(element, '_setReviewed');
+      sinon.spy(element, '_handleToggleFileReviewed');
       element.$.reviewed.checked = false;
       MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
       assert.isFalse(element._setReviewed.called);
+      assert.isTrue(element._handleToggleFileReviewed.calledOnce);
 
       MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+      assert.isTrue(element._handleToggleFileReviewed.calledTwice);
       assert.isTrue(element._setReviewed.called);
       assert.equal(element._setReviewed.lastCall.args[0], true);
     });
 
     test('shift+x shortcut expands all diff context', () => {
-      const expandStub = sandbox.stub(element.$.diffHost, 'expandAllContext');
+      const expandStub = sinon.stub(element.$.diffHost, 'expandAllContext');
       MockInteractions.pressAndReleaseKeyOn(element, 88, 'shift', 'x');
       flushAsynchronousOperations();
       assert.isTrue(expandStub.called);
@@ -292,8 +274,8 @@
         basePatchNum: '5',
         patchNum: '10',
       };
-      sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
-      const diffNavStub = sandbox.stub(GerritNav, 'navigateToDiff');
+      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+      const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
       element._handleDiffAgainstBase(new CustomEvent(''));
       const args = diffNavStub.getCall(0).args;
       assert.equal(args[2], 10);
@@ -305,9 +287,9 @@
         basePatchNum: '5',
         patchNum: '10',
       };
-      sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
-      sandbox.stub(element, 'computeLatestPatchNum').returns(12);
-      const diffNavStub = sandbox.stub(GerritNav, 'navigateToDiff');
+      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+      sinon.stub(element, 'computeLatestPatchNum').returns(12);
+      const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
       element._handleDiffAgainstLatest(new CustomEvent(''));
       const args = diffNavStub.getCall(0).args;
       assert.equal(args[2], 12);
@@ -320,9 +302,9 @@
         patchNum: 3,
         basePatchNum: 1,
       };
-      sandbox.stub(element, 'computeLatestPatchNum').returns(10);
-      sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
-      const diffNavStub = sandbox.stub(GerritNav, 'navigateToDiff');
+      sinon.stub(element, 'computeLatestPatchNum').returns(10);
+      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+      const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
       element._handleDiffBaseAgainstLeft(new CustomEvent(''));
       assert(diffNavStub.called);
       const args = diffNavStub.getCall(0).args;
@@ -336,9 +318,9 @@
         basePatchNum: 1,
         patchNum: 3,
       };
-      sandbox.stub(element, 'computeLatestPatchNum').returns(10);
-      sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
-      const diffNavStub = sandbox.stub(GerritNav, 'navigateToDiff');
+      sinon.stub(element, 'computeLatestPatchNum').returns(10);
+      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+      const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
       element._handleDiffRightAgainstLatest(new CustomEvent(''));
       assert(diffNavStub.called);
       const args = diffNavStub.getCall(0).args;
@@ -352,9 +334,9 @@
         basePatchNum: 1,
         patchNum: 3,
       };
-      sandbox.stub(element, 'computeLatestPatchNum').returns(10);
-      sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
-      const diffNavStub = sandbox.stub(GerritNav, 'navigateToDiff');
+      sinon.stub(element, 'computeLatestPatchNum').returns(10);
+      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+      const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
       element._handleDiffBaseAgainstLatest(new CustomEvent(''));
       assert(diffNavStub.called);
       const args = diffNavStub.getCall(0).args;
@@ -379,8 +361,8 @@
           ['chell.go', 'glados.txt', 'wheatley.md']);
       element._path = 'glados.txt';
 
-      const diffNavStub = sandbox.stub(GerritNav, 'navigateToDiff');
-      const changeNavStub = sandbox.stub(GerritNav, 'navigateToChange');
+      const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
+      const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
 
       MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
       assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' +
@@ -447,8 +429,8 @@
           ['chell.go', 'glados.txt', 'wheatley.md']);
       element._path = 'glados.txt';
 
-      const diffNavStub = sandbox.stub(GerritNav, 'navigateToDiff');
-      const changeNavStub = sandbox.stub(GerritNav, 'navigateToChange');
+      const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
+      const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
 
       MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
       assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' +
@@ -508,7 +490,7 @@
           b: {_number: 2, commit: {parents: []}},
         },
       };
-      const redirectStub = sandbox.stub(GerritNav, 'navigateToRelativeUrl');
+      const redirectStub = sinon.stub(GerritNav, 'navigateToRelativeUrl');
       flush(() => {
         const editBtn = element.shadowRoot
             .querySelector('.editButton gr-button');
@@ -618,8 +600,8 @@
     });
 
     test('prefsButton opens gr-diff-preferences', () => {
-      const handlePrefsTapSpy = sandbox.spy(element, '_handlePrefsTap');
-      const overlayOpenStub = sandbox.stub(element.$.diffPreferencesDialog,
+      const handlePrefsTapSpy = sinon.spy(element, '_handlePrefsTap');
+      const overlayOpenStub = sinon.stub(element.$.diffPreferencesDialog,
           'open');
       const prefsButton =
           dom(element.root).querySelector('.prefsButton');
@@ -634,9 +616,9 @@
       const path = '/test';
       element.$.commentAPI.loadAll().then(comments => {
         const commentCountStub =
-            sandbox.stub(comments, 'computeCommentCount');
+            sinon.stub(comments, 'computeCommentCount');
         const unresolvedCountStub =
-            sandbox.stub(comments, 'computeUnresolvedNum');
+            sinon.stub(comments, 'computeUnresolvedNum');
         commentCountStub.withArgs({patchNum: 1, path}).returns(0);
         commentCountStub.withArgs({patchNum: 2, path}).returns(1);
         commentCountStub.withArgs({patchNum: 3, path}).returns(2);
@@ -670,14 +652,14 @@
 
     suite('url params', () => {
       setup(() => {
-        sandbox.stub(
+        sinon.stub(
             GerritNav,
-            'getUrlForDiff',
-            (c, p, pn, bpn) => `${c._number}-${p}-${pn}-${bpn}`);
-        sandbox.stub(
+            'getUrlForDiff')
+            .callsFake((c, p, pn, bpn) => `${c._number}-${p}-${pn}-${bpn}`);
+        sinon.stub(
             GerritNav
-            , 'getUrlForChange',
-            (c, pn, bpn) => `${c._number}-${pn}-${bpn}`);
+            , 'getUrlForChange')
+            .callsFake((c, pn, bpn) => `${c._number}-${pn}-${bpn}`);
       });
 
       test('_formattedFiles', () => {
@@ -804,7 +786,7 @@
     });
 
     test('_handlePatchChange calls navigateToDiff correctly', () => {
-      const navigateStub = sandbox.stub(GerritNav, 'navigateToDiff');
+      const navigateStub = sinon.stub(GerritNav, 'navigateToDiff');
       element._change = {_number: 321, project: 'foo/bar'};
       element._path = 'path/to/file.txt';
 
@@ -826,12 +808,12 @@
     });
 
     test('_prefs.manual_review is respected', () => {
-      const saveReviewedStub = sandbox.stub(element, '_saveReviewedState',
-          () => Promise.resolve());
-      const getReviewedStub = sandbox.stub(element, '_getReviewedStatus',
-          () => Promise.resolve());
+      const saveReviewedStub = sinon.stub(element, '_saveReviewedState')
+          .callsFake(() => Promise.resolve());
+      const getReviewedStub = sinon.stub(element, '_getReviewedStatus')
+          .callsFake(() => Promise.resolve());
 
-      sandbox.stub(element.$.diffHost, 'reload');
+      sinon.stub(element.$.diffHost, 'reload');
       element._loggedIn = true;
       element.params = {
         view: GerritNav.View.DIFF,
@@ -854,9 +836,9 @@
     });
 
     test('file review status', () => {
-      const saveReviewedStub = sandbox.stub(element, '_saveReviewedState',
-          () => Promise.resolve());
-      sandbox.stub(element.$.diffHost, 'reload');
+      const saveReviewedStub = sinon.stub(element, '_saveReviewedState')
+          .callsFake(() => Promise.resolve());
+      sinon.stub(element.$.diffHost, 'reload');
 
       element._loggedIn = true;
       element.params = {
@@ -891,7 +873,7 @@
     });
 
     test('file review status with edit loaded', () => {
-      const saveReviewedStub = sandbox.stub(element, '_saveReviewedState');
+      const saveReviewedStub = sinon.stub(element, '_saveReviewedState');
 
       element._patchRange = {patchNum: element.EDIT_NAME};
       flushAsynchronousOperations();
@@ -902,8 +884,8 @@
     });
 
     test('hash is determined from params', done => {
-      sandbox.stub(element.$.diffHost, 'reload');
-      sandbox.stub(element, '_initCursor');
+      sinon.stub(element.$.diffHost, 'reload');
+      sinon.stub(element, '_initCursor');
 
       element._loggedIn = true;
       element.params = {
@@ -950,11 +932,12 @@
       const prefsPromise = new Promise(resolve => {
         resolvePrefs = resolve;
       });
-      sandbox.stub(element.$.restAPI, 'getPreferences', () => prefsPromise);
+      sinon.stub(element.$.restAPI, 'getPreferences')
+          .callsFake(() => prefsPromise);
 
       // Attach a new gr-diff-view so we can intercept the preferences fetch.
       const view = document.createElement('gr-diff-view');
-      fixture('blank').appendChild(view);
+      blankFixture.instantiate().appendChild(view);
       flushAsynchronousOperations();
 
       // At this point the diff mode doesn't yet have the user's preference.
@@ -979,9 +962,9 @@
 
     suite('_commitRange', () => {
       setup(() => {
-        sandbox.stub(element.$.diffHost, 'reload');
-        sandbox.stub(element, '_initCursor');
-        sandbox.stub(element, '_getChangeDetail').returns(Promise.resolve({
+        sinon.stub(element.$.diffHost, 'reload');
+        sinon.stub(element, '_initCursor');
+        sinon.stub(element, '_getChangeDetail').returns(Promise.resolve({
           _number: 42,
           revisions: {
             'commit-sha-1': {
@@ -1077,9 +1060,9 @@
     });
 
     test('_onLineSelected', () => {
-      const getUrlStub = sandbox.stub(GerritNav, 'getUrlForDiffById');
-      const replaceStateStub = sandbox.stub(history, 'replaceState');
-      sandbox.stub(element.$.cursor, 'getAddress')
+      const getUrlStub = sinon.stub(GerritNav, 'getUrlForDiffById');
+      const replaceStateStub = sinon.stub(history, 'replaceState');
+      sinon.stub(element.$.cursor, 'getAddress')
           .returns({number: 123, isLeftSide: false});
 
       element._changeNum = 321;
@@ -1098,10 +1081,10 @@
     });
 
     test('_onLineSelected w/o line address', () => {
-      const getUrlStub = sandbox.stub(GerritNav, 'getUrlForDiffById');
-      sandbox.stub(history, 'replaceState');
-      sandbox.stub(element.$.cursor, 'moveToLineNumber');
-      sandbox.stub(element.$.cursor, 'getAddress').returns(null);
+      const getUrlStub = sinon.stub(GerritNav, 'getUrlForDiffById');
+      sinon.stub(history, 'replaceState');
+      sinon.stub(element.$.cursor, 'moveToLineNumber');
+      sinon.stub(element.$.cursor, 'getAddress').returns(null);
       element._changeNum = 321;
       element._change = {_number: 321, project: 'foo/bar'};
       element._patchRange = {basePatchNum: '3', patchNum: '5'};
@@ -1125,7 +1108,7 @@
     });
 
     test('_handleToggleDiffMode', () => {
-      sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
       const e = {preventDefault: () => {}};
       // Initial state.
       assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
@@ -1146,11 +1129,11 @@
       });
 
       test('has paths', done => {
-        sandbox.stub(element, '_getPaths').returns({
+        sinon.stub(element, '_getPaths').returns({
           'path/to/file/one.cpp': [{patch_set: 3, message: 'lorem'}],
           'path-to/file/two.py': [{patch_set: 5, message: 'ipsum'}],
         });
-        sandbox.stub(element, '_getCommentsForPath').returns({meta: {}});
+        sinon.stub(element, '_getCommentsForPath').returns({meta: {}});
         element._changeNum = '42';
         element._patchRange = {
           basePatchNum: '3',
@@ -1213,8 +1196,8 @@
         let navToDiffStub;
 
         setup(() => {
-          navToChangeStub = sandbox.stub(element, '_navToChangeView');
-          navToDiffStub = sandbox.stub(GerritNav, 'navigateToDiff');
+          navToChangeStub = sinon.stub(element, '_navToChangeView');
+          navToDiffStub = sinon.stub(GerritNav, 'navigateToDiff');
           element._files = getFilesFromFileList([
             'path/one.jpg', 'path/two.m4v', 'path/three.wav',
           ]);
@@ -1316,7 +1299,7 @@
       const promises = [];
       element.$.restAPI.getReviewedFiles.restore();
 
-      sandbox.stub(element.$.restAPI, 'getReviewedFiles')
+      sinon.stub(element.$.restAPI, 'getReviewedFiles')
           .returns(Promise.resolve(['path']));
 
       promises.push(element._getReviewedStatus(true, null, null, 'path')
@@ -1333,14 +1316,15 @@
 
     suite('blame', () => {
       test('toggle blame with button', () => {
-        const toggleBlame = sandbox.stub(
-            element.$.diffHost, 'loadBlame', () => Promise.resolve());
+        const toggleBlame = sinon.stub(
+            element.$.diffHost, 'loadBlame')
+            .callsFake(() => Promise.resolve());
         MockInteractions.tap(element.$.toggleBlame);
         assert.isTrue(toggleBlame.calledOnce);
       });
       test('toggle blame with shortcut', () => {
-        const toggleBlame = sandbox.stub(
-            element.$.diffHost, 'loadBlame', () => Promise.resolve());
+        const toggleBlame = sinon.stub(
+            element.$.diffHost, 'loadBlame').callsFake(() => Promise.resolve());
         MockInteractions.pressAndReleaseKeyOn(element, 66, null, 'b');
         assert.isTrue(toggleBlame.calledOnce);
       });
@@ -1357,7 +1341,7 @@
       };
 
       test('reviewed checkbox', () => {
-        sandbox.stub(element, '_handlePatchChange');
+        sinon.stub(element, '_handlePatchChange');
         element._patchRange = {patchNum: '1'};
         // Reviewed checkbox should be shown.
         assert.isTrue(isVisible(element.$.reviewed));
@@ -1369,9 +1353,9 @@
     });
 
     test('_paramsChanged sets in projectLookup', () => {
-      sandbox.stub(element, '_getLineOfInterest');
-      sandbox.stub(element, '_initCursor');
-      const setStub = sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+      sinon.stub(element, '_getLineOfInterest');
+      sinon.stub(element, '_initCursor');
+      const setStub = sinon.stub(element.$.restAPI, 'setInProjectLookup');
       element._paramsChanged({
         view: GerritNav.View.DIFF,
         changeNum: 101,
@@ -1386,8 +1370,8 @@
       element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
       element._reviewedFiles = new Set(['file1', 'file2']);
       element._path = 'file1';
-      const reviewedStub = sandbox.stub(element, '_setReviewed');
-      const navStub = sandbox.stub(element, '_navToFile');
+      const reviewedStub = sinon.stub(element, '_setReviewed');
+      const navStub = sinon.stub(element, '_navToFile');
       MockInteractions.pressAndReleaseKeyOn(element, 77, 'shift', 'm');
       flushAsynchronousOperations();
 
@@ -1401,9 +1385,9 @@
 
     test('File change should trigger navigateToDiff once', () => {
       element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
-      sandbox.stub(element, '_getLineOfInterest');
-      sandbox.stub(element, '_initCursor');
-      sandbox.stub(GerritNav, 'navigateToDiff');
+      sinon.stub(element, '_getLineOfInterest');
+      sinon.stub(element, '_initCursor');
+      sinon.stub(GerritNav, 'navigateToDiff');
 
       // Load file1
       element._paramsChanged({
@@ -1529,10 +1513,8 @@
   });
 
   suite('gr-diff-view tests unmodified files with comments', () => {
-    let sandbox;
     let element;
     setup(() => {
-      sandbox = sinon.sandbox.create();
       const changedFiles = {
         'file1.txt': {},
         'a/b/test.c': {},
@@ -1549,14 +1531,10 @@
         getDiffDrafts() { return Promise.resolve({}); },
         getReviewedFiles() { return Promise.resolve([]); },
       });
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       return element._loadComments();
     });
 
-    teardown(() => {
-      sandbox.restore();
-    });
-
     test('_getFiles add files with comments without changes', () => {
       const patchChangeRecord = {
         base: {
@@ -1565,7 +1543,7 @@
         },
       };
       const changeComments = {
-        getPaths: sandbox.stub().returns({
+        getPaths: sinon.stub().returns({
           'file2.txt': {},
           'file1.txt': {},
         }),
@@ -1584,4 +1562,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.js
similarity index 86%
rename from polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
rename to polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.js
index d50a7f4..d72f981 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.js
@@ -1,30 +1,21 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-diff-group</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import {GrDiffLine} from './gr-diff-line.js';
 import {GrDiffGroup} from './gr-diff-group.js';
 
@@ -206,4 +197,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
index a10db97..d60d174 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
@@ -36,16 +36,11 @@
 
 suite('gr-diff tests', () => {
   let element;
-  let sandbox;
 
   const MINIMAL_PREFS = {tab_size: 2, line_length: 80};
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-  });
 
-  teardown(() => {
-    sandbox.restore();
   });
 
   suite('selectionchange event handling', () => {
@@ -55,7 +50,7 @@
 
     setup(() => {
       element = basicFixture.instantiate();
-      sandbox.stub(element.$.highlights, 'handleSelectionChange');
+      sinon.stub(element.$.highlights, 'handleSelectionChange');
     });
 
     test('enabled if logged in', () => {
@@ -73,7 +68,7 @@
 
   test('cancel', () => {
     element = basicFixture.instantiate();
-    const cancelStub = sandbox.stub(element.$.diffBuilder, 'cancel');
+    const cancelStub = sinon.stub(element.$.diffBuilder, 'cancel');
     element.cancel();
     assert.isTrue(cancelStub.calledOnce);
   });
@@ -195,8 +190,8 @@
     });
 
     test('addDraftAtLine', () => {
-      sandbox.stub(element, '_selectLine');
-      const loggedInErrorSpy = sandbox.spy();
+      sinon.stub(element, '_selectLine');
+      const loggedInErrorSpy = sinon.spy();
       element.addEventListener('show-auth-required', loggedInErrorSpy);
       element.addDraftAtLine();
       assert.isTrue(loggedInErrorSpy.called);
@@ -211,7 +206,7 @@
     });
 
     test('displayLine class added called when displayLine is true', () => {
-      const spy = sandbox.spy(element, '_computeContainerClass');
+      const spy = sinon.spy(element, '_computeContainerClass');
       element.displayLine = true;
       assert.isTrue(spy.called);
       assert.isTrue(
@@ -545,7 +540,7 @@
     });
 
     test('_handleTap lineNum', done => {
-      const addDraftStub = sandbox.stub(element, 'addDraftAtLine');
+      const addDraftStub = sinon.stub(element, 'addDraftAtLine');
       const el = document.createElement('div');
       el.className = 'lineNum';
       el.addEventListener('click', e => {
@@ -559,7 +554,7 @@
 
     test('_handleTap context', done => {
       const showContextStub =
-          sandbox.stub(element.$.diffBuilder, 'showContext');
+          sinon.stub(element.$.diffBuilder, 'showContext');
       const el = document.createElement('div');
       el.className = 'showContext';
       el.addEventListener('click', e => {
@@ -574,8 +569,9 @@
       const content = document.createElement('div');
       const lineEl = document.createElement('div');
 
-      const selectStub = sandbox.stub(element, '_selectLine');
-      sandbox.stub(element.$.diffBuilder, 'getLineElByChild', () => lineEl);
+      const selectStub = sinon.stub(element, '_selectLine');
+      sinon.stub(element.$.diffBuilder, 'getLineElByChild')
+          .callsFake(() => lineEl);
 
       content.className = 'content';
       content.addEventListener('click', e => {
@@ -642,16 +638,16 @@
       element.patchRange = {};
 
       fakeLineEl = {
-        getAttribute: sandbox.stub().returns(42),
+        getAttribute: sinon.stub().returns(42),
         classList: {
-          contains: sandbox.stub().returns(true),
+          contains: sinon.stub().returns(true),
         },
       };
     });
 
     test('addDraftAtLine', () => {
-      sandbox.stub(element, '_selectLine');
-      sandbox.stub(element, '_createComment');
+      sinon.stub(element, '_selectLine');
+      sinon.stub(element, '_createComment');
       element.addDraftAtLine(fakeLineEl);
       assert.isTrue(element._createComment
           .calledWithExactly(fakeLineEl, 42));
@@ -659,9 +655,9 @@
 
     test('addDraftAtLine on an edit', () => {
       element.patchRange.basePatchNum = element.EDIT_NAME;
-      sandbox.stub(element, '_selectLine');
-      sandbox.stub(element, '_createComment');
-      const alertSpy = sandbox.spy();
+      sinon.stub(element, '_selectLine');
+      sinon.stub(element, '_createComment');
+      const alertSpy = sinon.spy();
       element.addEventListener('show-alert', alertSpy);
       element.addDraftAtLine(fakeLineEl);
       assert.isTrue(alertSpy.called);
@@ -671,9 +667,9 @@
     test('addDraftAtLine on an edit base', () => {
       element.patchRange.patchNum = element.EDIT_NAME;
       element.patchRange.basePatchNum = element.PARENT_NAME;
-      sandbox.stub(element, '_selectLine');
-      sandbox.stub(element, '_createComment');
-      const alertSpy = sandbox.spy();
+      sinon.stub(element, '_selectLine');
+      sinon.stub(element, '_createComment');
+      const alertSpy = sinon.spy();
       element.addEventListener('show-alert', alertSpy);
       element.addDraftAtLine(fakeLineEl);
       assert.isTrue(alertSpy.called);
@@ -695,7 +691,7 @@
       });
 
       test('change in preferences re-renders diff', () => {
-        sandbox.stub(element, '_renderDiffTable');
+        sinon.stub(element, '_renderDiffTable');
         element.prefs = Object.assign(
             {}, MINIMAL_PREFS, {time_format: 'HHMM_12'});
         element.flushDebouncer('renderDiffTable');
@@ -703,7 +699,7 @@
       });
 
       test('adding/removing property in preferences re-renders diff', () => {
-        const stub = sandbox.stub(element, '_renderDiffTable');
+        const stub = sinon.stub(element, '_renderDiffTable');
         const newPrefs1 = Object.assign({}, MINIMAL_PREFS,
             {line_wrapping: true});
         element.prefs = newPrefs1;
@@ -720,7 +716,7 @@
 
       test('change in preferences does not re-renders diff with ' +
           'noRenderOnPrefsChange', () => {
-        sandbox.stub(element, '_renderDiffTable');
+        sinon.stub(element, '_renderDiffTable');
         element.noRenderOnPrefsChange = true;
         element.prefs = Object.assign(
             {}, MINIMAL_PREFS, {time_format: 'HHMM_12'});
@@ -778,13 +774,13 @@
 
     setup(() => {
       element = basicFixture.instantiate();
-      renderStub = sandbox.stub(element.$.diffBuilder, 'render',
+      renderStub = sinon.stub(element.$.diffBuilder, 'render').callsFake(
           () => {
             element.$.diffBuilder.dispatchEvent(
                 new CustomEvent('render', {bubbles: true, composed: true}));
             return Promise.resolve({});
           });
-      sandbox.stub(element, 'getDiffLength').returns(10000);
+      sinon.stub(element, 'getDiffLength').returns(10000);
       element.diff = getMockDiffResponse();
       element.noRenderOnPrefsChange = true;
     });
@@ -834,7 +830,7 @@
 
     test('unsetting', () => {
       element.blame = [];
-      const setBlameSpy = sandbox.spy(element.$.diffBuilder, 'setBlame');
+      const setBlameSpy = sinon.spy(element.$.diffBuilder, 'setBlame');
       element.classList.add('showBlame');
       element.blame = null;
       assert.isTrue(setBlameSpy.calledWithExactly(null));
@@ -935,7 +931,7 @@
     setup(() => {
       element = basicFixture.instantiate();
       element.prefs = {};
-      renderStub = sandbox.stub(element.$.diffBuilder, 'render')
+      renderStub = sinon.stub(element.$.diffBuilder, 'render')
           .returns(new Promise(() => {}));
     });
 
@@ -1143,7 +1139,7 @@
   test('`render` event has contentRendered field in detail', done => {
     element = basicFixture.instantiate();
     element.prefs = {};
-    sandbox.stub(element.$.diffBuilder, 'render')
+    sinon.stub(element.$.diffBuilder, 'render')
         .returns(Promise.resolve());
     element.addEventListener('render', event => {
       assert.isTrue(event.detail.contentRendered);
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
index 804fa38..806e147 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -289,14 +289,18 @@
   _handlePatchChange(e) {
     const detail = {patchNum: this.patchNum, basePatchNum: this.basePatchNum};
     const target = dom(e).localTarget;
-
+    const latestPatchNum = this.computeLatestPatchNum(this.availablePatches);
     if (target === this.$.patchNumDropdown) {
       if (detail.patchNum === e.detail.value) return;
       this.reporting.reportInteraction('right-patchset-changed',
-          {previous: detail.patchNum, current: e.detail.value});
+          {
+            previous: detail.patchNum,
+            current: e.detail.value,
+            latest: latestPatchNum,
+          });
       detail.patchNum = e.detail.value;
     } else {
-      if (detail.basePatchNum === e.detail.value) return;
+      if (this.patchNumEquals(detail.basePatchNum, e.detail.value)) return;
       this.reporting.reportInteraction('left-patchset-changed',
           {previous: detail.basePatchNum, current: e.detail.value});
       detail.basePatchNum = e.detail.value;
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
similarity index 86%
rename from polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
rename to polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
index 218ca7e..9145b19 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
@@ -1,56 +1,42 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2015 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-patch-range-select</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script src="/node_modules/page/page.js"></script>
-
-<dom-module id="comment-api-mock">
-  <template>
-    <gr-patch-range-select id="patchRange" auto
-        change-comments="[[_changeComments]]"></gr-patch-range-select>
-    <gr-comment-api id="commentAPI"></gr-comment-api>
-  </template>
-  </dom-module>
-
-<test-fixture id="basic">
-  <template>
-    <comment-api-mock></comment-api-mock>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import '../gr-comment-api/gr-comment-api.js';
 import '../../shared/revision-info/revision-info.js';
 import './gr-patch-range-select.js';
 import '../../../test/mocks/comment-api.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {RevisionInfo} from '../../shared/revision-info/revision-info.js';
+import {createCommentApiMockWithTemplateElement} from '../../../test/mocks/comment-api';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const commentApiMockElement = createCommentApiMockWithTemplateElement(
+    'gr-patch-range-select-comment-api-mock', html`
+    <gr-patch-range-select id="patchRange" auto
+        change-comments="[[_changeComments]]"></gr-patch-range-select>
+    <gr-comment-api id="commentAPI"></gr-comment-api>
+`);
+
+const basicFixture = fixtureFromElement(commentApiMockElement.is);
+
 suite('gr-patch-range-select tests', () => {
   let element;
-  let sandbox;
+
   let commentApiWrapper;
 
   function getInfo(revisions) {
@@ -62,8 +48,6 @@
   }
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-
     stub('gr-rest-api-interface', {
       getDiffComments() { return Promise.resolve({}); },
       getDiffRobotComments() { return Promise.resolve({}); },
@@ -72,7 +56,7 @@
 
     // Element must be wrapped in an element with direct access to the
     // comment API.
-    commentApiWrapper = fixture('basic');
+    commentApiWrapper = basicFixture.instantiate();
     element = commentApiWrapper.$.patchRange;
 
     // Stub methods on the changeComments object after changeComments has
@@ -80,8 +64,6 @@
     return commentApiWrapper.loadComments();
   });
 
-  teardown(() => sandbox.restore());
-
   test('enabled/disabled options', () => {
     const patchRange = {
       basePatchNum: 'PARENT',
@@ -204,7 +186,7 @@
     element.basePatchNum = 'PARENT';
     flushAsynchronousOperations();
 
-    sandbox.stub(element, '_computeBaseDropdownContent');
+    sinon.stub(element, '_computeBaseDropdownContent');
 
     // Should be recomputed for each available patch
     element.set('patchNum', 1);
@@ -231,7 +213,7 @@
         flushAsynchronousOperations();
 
         // Should be recomputed for each available patch
-        sandbox.stub(element, '_computeBaseDropdownContent');
+        sinon.stub(element, '_computeBaseDropdownContent');
         assert.equal(element._computeBaseDropdownContent.callCount, 0);
         commentApiWrapper.loadComments().then()
             .then(() => {
@@ -259,7 +241,7 @@
     flushAsynchronousOperations();
 
     // Should be recomputed for each available patch
-    sandbox.stub(element, '_computePatchDropdownContent');
+    sinon.stub(element, '_computePatchDropdownContent');
     element.set('basePatchNum', 1);
     assert.equal(element._computePatchDropdownContent.callCount, 1);
   });
@@ -283,7 +265,7 @@
     flushAsynchronousOperations();
 
     // Should be recomputed for each available patch
-    sandbox.stub(element, '_computePatchDropdownContent');
+    sinon.stub(element, '_computePatchDropdownContent');
     assert.equal(element._computePatchDropdownContent.callCount, 0);
     commentApiWrapper.loadComments().then()
         .then(() => {
@@ -410,7 +392,7 @@
   });
 
   test('patch-range-change fires', () => {
-    const handler = sandbox.stub();
+    const handler = sinon.stub();
     element.basePatchNum = 1;
     element.patchNum = 3;
     element.addEventListener('patch-range-change', handler);
@@ -426,4 +408,4 @@
         {basePatchNum: 1, patchNum: 'edit'});
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.js
similarity index 83%
rename from polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
rename to polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.js
index 37d1707..2ce0afa 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.js
@@ -1,46 +1,30 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-ranged-comment-layer</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-ranged-comment-layer></gr-ranged-comment-layer>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import '../gr-diff/gr-diff-line.js';
 import './gr-ranged-comment-layer.js';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
 import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
 
+const basicFixture = fixtureFromElement('gr-ranged-comment-layer');
+
 suite('gr-ranged-comment-layer', () => {
   let element;
-  let sandbox;
 
   setup(() => {
     const initialCommentRanges = [
@@ -82,35 +66,24 @@
       },
     ];
 
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.commentRanges = initialCommentRanges;
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   suite('annotate', () => {
-    let sandbox;
     let el;
     let line;
     let annotateElementStub;
     const lineNumberEl = document.createElement('td');
 
     setup(() => {
-      sandbox = sinon.sandbox.create();
-      annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
+      annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
       el = document.createElement('div');
       el.setAttribute('data-side', 'left');
       line = new GrDiffLine(GrDiffLine.Type.BOTH);
       line.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit,';
     });
 
-    teardown(() => {
-      sandbox.restore();
-    });
-
     test('type=Remove no-comment', () => {
       line.type = GrDiffLine.Type.REMOVE;
       line.beforeNumber = 40;
@@ -210,7 +183,7 @@
   test('_handleCommentRangesChange hovering', () => {
     const notifyStub = sinon.stub();
     element.addListener(notifyStub);
-    const updateRangesMapSpy = sandbox.spy(element, '_updateRangesMap');
+    const updateRangesMapSpy = sinon.spy(element, '_updateRangesMap');
 
     element.set(['commentRanges', 1, 'hovering'], true);
 
@@ -257,7 +230,7 @@
   test('_handleCommentRangesChange mixed actions', () => {
     const notifyStub = sinon.stub();
     element.addListener(notifyStub);
-    const updateRangesMapSpy = sandbox.spy(element, '_updateRangesMap');
+    const updateRangesMapSpy = sinon.spy(element, '_updateRangesMap');
 
     element.set(['commentRanges', 1, 'hovering'], true);
     assert.isTrue(updateRangesMapSpy.callCount === 1);
@@ -339,4 +312,4 @@
     assert.equal(range.end, line.text.length);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
deleted file mode 100644
index ff6fba7..0000000
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
+++ /dev/null
@@ -1,135 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-selection-action-box</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <div>
-      <gr-selection-action-box></gr-selection-action-box>
-      <div class="target">some text</div>
-    </div>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-selection-action-box.js';
-suite('gr-selection-action-box', () => {
-  let container;
-  let element;
-  let sandbox;
-
-  setup(() => {
-    container = fixture('basic');
-    element = container.querySelector('gr-selection-action-box');
-    sandbox = sinon.sandbox.create();
-    sandbox.stub(element, 'dispatchEvent');
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('ignores regular keys', () => {
-    MockInteractions.pressAndReleaseKeyOn(document.body, 27, null, 'esc');
-    assert.isFalse(element.dispatchEvent.called);
-  });
-
-  suite('mousedown reacts only to main button', () => {
-    let e;
-
-    setup(() => {
-      e = {
-        button: 0,
-        preventDefault: sandbox.stub(),
-        stopPropagation: sandbox.stub(),
-      };
-    });
-
-    test('event handled if main button', () => {
-      element._handleMouseDown(e);
-      assert.isTrue(e.preventDefault.called);
-      assert.equal(
-          element.dispatchEvent.lastCall.args[0].type,
-          'create-comment-requested'
-      );
-    });
-
-    test('event ignored if not main button', () => {
-      e.button = 1;
-      element._handleMouseDown(e);
-      assert.isFalse(e.preventDefault.called);
-      assert.isFalse(element.dispatchEvent.called);
-    });
-  });
-
-  suite('placeAbove', () => {
-    let target;
-
-    setup(() => {
-      target = container.querySelector('.target');
-      sandbox.stub(container, 'getBoundingClientRect').returns(
-          {top: 1, bottom: 2, left: 3, right: 4, width: 50, height: 6});
-      sandbox.stub(element, '_getTargetBoundingRect').returns(
-          {top: 42, bottom: 20, left: 30, right: 40, width: 100, height: 60});
-      sandbox.stub(element.$.tooltip, 'getBoundingClientRect').returns(
-          {width: 10, height: 10});
-    });
-
-    test('placeAbove for Element argument', () => {
-      element.placeAbove(target);
-      assert.equal(element.style.top, '25px');
-      assert.equal(element.style.left, '72px');
-    });
-
-    test('placeAbove for Text Node argument', () => {
-      element.placeAbove(target.firstChild);
-      assert.equal(element.style.top, '25px');
-      assert.equal(element.style.left, '72px');
-    });
-
-    test('placeBelow for Element argument', () => {
-      element.placeBelow(target);
-      assert.equal(element.style.top, '45px');
-      assert.equal(element.style.left, '72px');
-    });
-
-    test('placeBelow for Text Node argument', () => {
-      element.placeBelow(target.firstChild);
-      assert.equal(element.style.top, '45px');
-      assert.equal(element.style.left, '72px');
-    });
-
-    test('uses document.createRange', () => {
-      sandbox.spy(document, 'createRange');
-      element._getTargetBoundingRect.restore();
-      sandbox.spy(element, '_getTargetBoundingRect');
-      element.placeAbove(target.firstChild);
-      assert.isTrue(document.createRange.called);
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.js
new file mode 100644
index 0000000..81cf0d6
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.js
@@ -0,0 +1,119 @@
+/**
+ * @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-selection-action-box.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromTemplate(html`
+  <div>
+    <gr-selection-action-box></gr-selection-action-box>
+    <div class="target">some text</div>
+  </div>
+`);
+
+suite('gr-selection-action-box', () => {
+  let container;
+  let element;
+
+  setup(() => {
+    container = basicFixture.instantiate();
+    element = container.querySelector('gr-selection-action-box');
+
+    sinon.stub(element, 'dispatchEvent');
+  });
+
+  test('ignores regular keys', () => {
+    MockInteractions.pressAndReleaseKeyOn(document.body, 27, null, 'esc');
+    assert.isFalse(element.dispatchEvent.called);
+  });
+
+  suite('mousedown reacts only to main button', () => {
+    let e;
+
+    setup(() => {
+      e = {
+        button: 0,
+        preventDefault: sinon.stub(),
+        stopPropagation: sinon.stub(),
+      };
+    });
+
+    test('event handled if main button', () => {
+      element._handleMouseDown(e);
+      assert.isTrue(e.preventDefault.called);
+      assert.equal(
+          element.dispatchEvent.lastCall.args[0].type,
+          'create-comment-requested'
+      );
+    });
+
+    test('event ignored if not main button', () => {
+      e.button = 1;
+      element._handleMouseDown(e);
+      assert.isFalse(e.preventDefault.called);
+      assert.isFalse(element.dispatchEvent.called);
+    });
+  });
+
+  suite('placeAbove', () => {
+    let target;
+
+    setup(() => {
+      target = container.querySelector('.target');
+      sinon.stub(container, 'getBoundingClientRect').returns(
+          {top: 1, bottom: 2, left: 3, right: 4, width: 50, height: 6});
+      sinon.stub(element, '_getTargetBoundingRect').returns(
+          {top: 42, bottom: 20, left: 30, right: 40, width: 100, height: 60});
+      sinon.stub(element.$.tooltip, 'getBoundingClientRect').returns(
+          {width: 10, height: 10});
+    });
+
+    test('placeAbove for Element argument', () => {
+      element.placeAbove(target);
+      assert.equal(element.style.top, '25px');
+      assert.equal(element.style.left, '72px');
+    });
+
+    test('placeAbove for Text Node argument', () => {
+      element.placeAbove(target.firstChild);
+      assert.equal(element.style.top, '25px');
+      assert.equal(element.style.left, '72px');
+    });
+
+    test('placeBelow for Element argument', () => {
+      element.placeBelow(target);
+      assert.equal(element.style.top, '45px');
+      assert.equal(element.style.left, '72px');
+    });
+
+    test('placeBelow for Text Node argument', () => {
+      element.placeBelow(target.firstChild);
+      assert.equal(element.style.top, '45px');
+      assert.equal(element.style.left, '72px');
+    });
+
+    test('uses document.createRange', () => {
+      sinon.spy(document, 'createRange');
+      element._getTargetBoundingRect.restore();
+      sinon.spy(element, '_getTargetBoundingRect');
+      element.placeAbove(target.firstChild);
+      assert.isTrue(document.createRange.called);
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.js
similarity index 87%
rename from polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
rename to polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.js
index 638b188..e83d4b64 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.js
@@ -1,45 +1,29 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-syntax-layer</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-syntax-layer></gr-syntax-layer>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 import './gr-syntax-layer.js';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
 import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
 
+const basicFixture = fixtureFromElement('gr-syntax-layer');
+
 suite('gr-syntax-layer tests', () => {
-  let sandbox;
   let diff;
   let element;
   const lineNumberEl = document.createElement('td');
@@ -64,18 +48,13 @@
   }
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     diff = getMockDiffResponse();
     element.diff = diff;
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('annotate without range does nothing', () => {
-    const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
+    const annotationSpy = sinon.spy(GrAnnotation, 'annotateElement');
     const el = document.createElement('div');
     el.textContent = 'Etiam dui, blandit wisi.';
     const line = new GrDiffLine(GrDiffLine.Type.REMOVE);
@@ -92,7 +71,7 @@
     const length = 3;
     const className = 'foobar';
 
-    const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
+    const annotationSpy = sinon.spy(GrAnnotation, 'annotateElement');
     const el = document.createElement('div');
     el.textContent = str;
     const line = new GrDiffLine(GrDiffLine.Type.REMOVE);
@@ -119,7 +98,7 @@
     const length = 3;
     const className = 'foobar';
 
-    const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
+    const annotationSpy = sinon.spy(GrAnnotation, 'annotateElement');
     const el = document.createElement('div');
     el.textContent = str;
     const line = new GrDiffLine(GrDiffLine.Type.REMOVE);
@@ -142,7 +121,7 @@
       meta_b: {content_type: 'application/json'},
       content: [],
     };
-    const processNextSpy = sandbox.spy(element, '_processNextLine');
+    const processNextSpy = sinon.spy(element, '_processNextLine');
 
     const processPromise = element.process();
 
@@ -160,7 +139,7 @@
       meta_b: {content_type: 'application/not-a-real-language'},
       content: [],
     };
-    const processNextSpy = sandbox.spy(element, '_processNextLine');
+    const processNextSpy = sinon.spy(element, '_processNextLine');
 
     const processPromise = element.process();
 
@@ -173,9 +152,9 @@
   });
 
   test('process while disabled does nothing', done => {
-    const processNextSpy = sandbox.spy(element, '_processNextLine');
+    const processNextSpy = sinon.spy(element, '_processNextLine');
     element.enabled = false;
-    const loadHLJSSpy = sandbox.spy(element, '_loadHLJS');
+    const loadHLJSSpy = sinon.spy(element, '_loadHLJS');
 
     const processPromise = element.process();
 
@@ -194,9 +173,9 @@
 
     const mockHLJS = getMockHLJS();
     const highlightSpy = sinon.spy(mockHLJS, 'highlight');
-    sandbox.stub(element.$.libLoader, 'getHLJS',
+    sinon.stub(element.$.libLoader, 'getHLJS').callsFake(
         () => Promise.resolve(mockHLJS));
-    const processNextSpy = sandbox.spy(element, '_processNextLine');
+    const processNextSpy = sinon.spy(element, '_processNextLine');
     const processPromise = element.process();
 
     processPromise.then(() => {
@@ -259,7 +238,7 @@
   });
 
   test('_diffChanged calls cancel', () => {
-    const cancelSpy = sandbox.spy(element, '_diffChanged');
+    const cancelSpy = sinon.spy(element, '_diffChanged');
     element.diff = {content: []};
     assert.isTrue(cancelSpy.called);
   });
@@ -394,7 +373,7 @@
   });
 
   test('_rangesFromString cache same syntax markers', () => {
-    sandbox.spy(element, '_rangesFromElement');
+    sinon.spy(element, '_rangesFromElement');
     const str =
       '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>';
     const cacheMap = new Map();
@@ -500,4 +479,4 @@
     assert.equal(element._workaround('go', line), expected);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html
deleted file mode 100644
index d0581ef..0000000
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html
+++ /dev/null
@@ -1,124 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-documentation-search</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/page/page.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-documentation-search></gr-documentation-search>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-documentation-search.js';
-import page from 'page/page.mjs';
-
-let counter;
-const documentationGenerator = () => {
-  return {
-    title: `Gerrit Code Review - REST API Developers Notes${++counter}`,
-    url: 'Documentation/dev-rest-api.html',
-  };
-};
-
-suite('gr-documentation-search tests', () => {
-  let element;
-  let documentationSearches;
-  let sandbox;
-  let value;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    sandbox.stub(page, 'show');
-    element = fixture('basic');
-    counter = 0;
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  suite('list with searches for documentation', () => {
-    setup(done => {
-      documentationSearches = _.times(26, documentationGenerator);
-      stub('gr-rest-api-interface', {
-        getDocumentationSearches() {
-          return Promise.resolve(documentationSearches);
-        },
-      });
-      element._paramsChanged(value).then(() => { flush(done); });
-    });
-
-    test('test for test repo in the list', done => {
-      flush(() => {
-        assert.equal(element._documentationSearches[0].title,
-            'Gerrit Code Review - REST API Developers Notes1');
-        assert.equal(element._documentationSearches[0].url,
-            'Documentation/dev-rest-api.html');
-        done();
-      });
-    });
-  });
-
-  suite('filter', () => {
-    setup(() => {
-      documentationSearches = _.times(25, documentationGenerator);
-      _.times(1, documentationSearches);
-    });
-
-    test('_paramsChanged', done => {
-      sandbox.stub(
-          element.$.restAPI,
-          'getDocumentationSearches',
-          () => Promise.resolve(documentationSearches));
-      const value = {
-        filter: 'test',
-      };
-      element._paramsChanged(value).then(() => {
-        assert.isTrue(element.$.restAPI.getDocumentationSearches.lastCall
-            .calledWithExactly('test'));
-        done();
-      });
-    });
-  });
-
-  suite('loading', () => {
-    test('correct contents are displayed', () => {
-      assert.isTrue(element._loading);
-      assert.equal(element.computeLoadingClass(element._loading), 'loading');
-      assert.equal(getComputedStyle(element.$.loading).display, 'block');
-
-      element._loading = false;
-      element._repos = _.times(25, documentationGenerator);
-
-      flushAsynchronousOperations();
-      assert.equal(element.computeLoadingClass(element._loading), '');
-      assert.equal(getComputedStyle(element.$.loading).display, 'none');
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.js b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.js
new file mode 100644
index 0000000..c2a3f3d
--- /dev/null
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.js
@@ -0,0 +1,103 @@
+/**
+ * @license
+ * Copyright (C) 2018 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-documentation-search.js';
+import page from 'page/page.mjs';
+
+const basicFixture = fixtureFromElement('gr-documentation-search');
+
+let counter;
+const documentationGenerator = () => {
+  return {
+    title: `Gerrit Code Review - REST API Developers Notes${++counter}`,
+    url: 'Documentation/dev-rest-api.html',
+  };
+};
+
+suite('gr-documentation-search tests', () => {
+  let element;
+  let documentationSearches;
+
+  let value;
+
+  setup(() => {
+    sinon.stub(page, 'show');
+    element = basicFixture.instantiate();
+    counter = 0;
+  });
+
+  suite('list with searches for documentation', () => {
+    setup(done => {
+      documentationSearches = _.times(26, documentationGenerator);
+      stub('gr-rest-api-interface', {
+        getDocumentationSearches() {
+          return Promise.resolve(documentationSearches);
+        },
+      });
+      element._paramsChanged(value).then(() => { flush(done); });
+    });
+
+    test('test for test repo in the list', done => {
+      flush(() => {
+        assert.equal(element._documentationSearches[0].title,
+            'Gerrit Code Review - REST API Developers Notes1');
+        assert.equal(element._documentationSearches[0].url,
+            'Documentation/dev-rest-api.html');
+        done();
+      });
+    });
+  });
+
+  suite('filter', () => {
+    setup(() => {
+      documentationSearches = _.times(25, documentationGenerator);
+      _.times(1, documentationSearches);
+    });
+
+    test('_paramsChanged', done => {
+      sinon.stub(
+          element.$.restAPI,
+          'getDocumentationSearches')
+          .callsFake(() => Promise.resolve(documentationSearches));
+      const value = {
+        filter: 'test',
+      };
+      element._paramsChanged(value).then(() => {
+        assert.isTrue(element.$.restAPI.getDocumentationSearches.lastCall
+            .calledWithExactly('test'));
+        done();
+      });
+    });
+  });
+
+  suite('loading', () => {
+    test('correct contents are displayed', () => {
+      assert.isTrue(element._loading);
+      assert.equal(element.computeLoadingClass(element._loading), 'loading');
+      assert.equal(getComputedStyle(element.$.loading).display, 'block');
+
+      element._loading = false;
+      element._repos = _.times(25, documentationGenerator);
+
+      flushAsynchronousOperations();
+      assert.equal(element.computeLoadingClass(element._loading), '');
+      assert.equal(getComputedStyle(element.$.loading).display, 'none');
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
deleted file mode 100644
index 229c6c3..0000000
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
+++ /dev/null
@@ -1,56 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-default-editor</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-default-editor></gr-default-editor>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-default-editor.js';
-suite('gr-default-editor tests', () => {
-  let element;
-
-  setup(() => {
-    element = fixture('basic');
-    element.fileContent = '';
-  });
-
-  test('fires content-change event', done => {
-    const contentChangedHandler = e => {
-      assert.equal(e.detail.value, 'test');
-      done();
-    };
-    const textarea = element.$.textarea;
-    element.addEventListener('content-change', contentChangedHandler);
-    textarea.value = 'test';
-    textarea.dispatchEvent(new CustomEvent('input',
-        {target: textarea, bubbles: true, composed: true}));
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.js b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.js
new file mode 100644
index 0000000..d40e83d
--- /dev/null
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.js
@@ -0,0 +1,43 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-default-editor.js';
+
+const basicFixture = fixtureFromElement('gr-default-editor');
+
+suite('gr-default-editor tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+    element.fileContent = '';
+  });
+
+  test('fires content-change event', done => {
+    const contentChangedHandler = e => {
+      assert.equal(e.detail.value, 'test');
+      done();
+    };
+    const textarea = element.$.textarea;
+    element.addEventListener('content-change', contentChangedHandler);
+    textarea.value = 'test';
+    textarea.dispatchEvent(new CustomEvent('input',
+        {target: textarea, bubbles: true, composed: true}));
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.js
similarity index 85%
rename from polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
rename to polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.js
index 1267525..8269b81 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.js
@@ -1,63 +1,46 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-edit-controls</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-edit-controls></gr-edit-controls>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-edit-controls.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
+const basicFixture = fixtureFromElement('gr-edit-controls');
+
 suite('gr-edit-controls tests', () => {
   let element;
-  let sandbox;
+
   let showDialogSpy;
   let closeDialogSpy;
   let queryStub;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.change = {_number: '42'};
-    showDialogSpy = sandbox.spy(element, '_showDialog');
-    closeDialogSpy = sandbox.spy(element, '_closeDialog');
-    sandbox.stub(element, '_hideAllDialogs');
-    queryStub = sandbox.stub(element.$.restAPI, 'queryChangeFiles')
+    showDialogSpy = sinon.spy(element, '_showDialog');
+    closeDialogSpy = sinon.spy(element, '_closeDialog');
+    sinon.stub(element, '_hideAllDialogs');
+    queryStub = sinon.stub(element.$.restAPI, 'queryChangeFiles')
         .returns(Promise.resolve([]));
     flushAsynchronousOperations();
   });
 
-  teardown(() => { sandbox.restore(); });
-
   test('all actions exist', () => {
     // We take 1 away from the total found, due to an extra button being
     // added for the file uploads (browse).
@@ -72,8 +55,8 @@
 
     setup(() => {
       navStubs = [
-        sandbox.stub(GerritNav, 'getEditUrlForDiff'),
-        sandbox.stub(GerritNav, 'navigateToRelativeUrl'),
+        sinon.stub(GerritNav, 'getEditUrlForDiff'),
+        sinon.stub(GerritNav, 'navigateToRelativeUrl'),
       ];
       openAutoCcmplete = element.$.openDialog.querySelector('gr-autocomplete');
     });
@@ -131,8 +114,8 @@
     let deleteAutocomplete;
 
     setup(() => {
-      navStub = sandbox.stub(GerritNav, 'navigateToChange');
-      deleteStub = sandbox.stub(element.$.restAPI, 'deleteFileInChangeEdit');
+      navStub = sinon.stub(GerritNav, 'navigateToChange');
+      deleteStub = sinon.stub(element.$.restAPI, 'deleteFileInChangeEdit');
       deleteAutocomplete =
           element.$.deleteDialog.querySelector('gr-autocomplete');
     });
@@ -215,8 +198,8 @@
       '.newPathInput';
 
     setup(() => {
-      navStub = sandbox.stub(GerritNav, 'navigateToChange');
-      renameStub = sandbox.stub(element.$.restAPI, 'renameFileInChangeEdit');
+      navStub = sinon.stub(GerritNav, 'navigateToChange');
+      renameStub = sinon.stub(element.$.restAPI, 'renameFileInChangeEdit');
       renameAutocomplete =
           element.$.renameDialog.querySelector('gr-autocomplete');
     });
@@ -308,8 +291,8 @@
     let restoreStub;
 
     setup(() => {
-      navStub = sandbox.stub(GerritNav, 'navigateToChange');
-      restoreStub = sandbox.stub(element.$.restAPI, 'restoreFileInChangeEdit');
+      navStub = sinon.stub(GerritNav, 'navigateToChange');
+      restoreStub = sinon.stub(element.$.restAPI, 'restoreFileInChangeEdit');
     });
 
     test('restore hidden by default', () => {
@@ -372,8 +355,8 @@
     let fileStub;
 
     setup(() => {
-      navStub = sandbox.stub(GerritNav, 'navigateToChange');
-      fileStub = sandbox.stub(element.$.restAPI, 'saveFileUploadChangeEdit');
+      navStub = sinon.stub(GerritNav, 'navigateToChange');
+      fileStub = sinon.stub(element.$.restAPI, 'saveFileUploadChangeEdit');
     });
 
     test('_handleUploadConfirm', () => {
@@ -409,7 +392,7 @@
   });
 
   test('_getDialogFromEvent', () => {
-    const spy = sandbox.spy(element, '_getDialogFromEvent');
+    const spy = sinon.spy(element, '_getDialogFromEvent');
     element.addEventListener('tap', element._getDialogFromEvent);
 
     MockInteractions.tap(element.$.openDialog);
@@ -430,4 +413,4 @@
     assert.notOk(spy.lastCall.returnValue);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.js
similarity index 61%
rename from polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
rename to polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.js
index e11a2bd..8a1c186 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.js
@@ -1,55 +1,38 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-edit-file-controls</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-edit-file-controls></gr-edit-file-controls>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import '../gr-edit-constants.js';
 import './gr-edit-file-controls.js';
 import {GrEditConstants} from '../gr-edit-constants.js';
 
+const basicFixture = fixtureFromElement('gr-edit-file-controls');
+
 suite('gr-edit-file-controls tests', () => {
   let element;
-  let sandbox;
+
   let fileActionHandler;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-    fileActionHandler = sandbox.stub();
+    element = basicFixture.instantiate();
+    fileActionHandler = sinon.stub();
     element.addEventListener('file-action-tap', fileActionHandler);
   });
 
-  teardown(() => { sandbox.restore(); });
-
   test('open tap emits event', () => {
     const actions = element.$.actions;
     element.filePath = 'foo';
@@ -106,4 +89,4 @@
     assert.equal(element._allFileActions.length, 4);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js
similarity index 79%
rename from polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
rename to polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js
index e385854..618f595 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js
@@ -1,43 +1,29 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-editor-view</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-editor-view></gr-editor-view>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-editor-view.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
+const basicFixture = fixtureFromElement('gr-editor-view');
+
 suite('gr-editor-view tests', () => {
   let element;
-  let sandbox;
+
   let savePathStub;
   let saveFileStub;
   let changeDetailStub;
@@ -53,15 +39,13 @@
       getLoggedIn() { return Promise.resolve(true); },
       getEditPreferences() { return Promise.resolve({}); },
     });
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-    savePathStub = sandbox.stub(element.$.restAPI, 'renameFileInChangeEdit');
-    saveFileStub = sandbox.stub(element.$.restAPI, 'saveChangeEdit');
-    changeDetailStub = sandbox.stub(element.$.restAPI, 'getDiffChangeDetail');
-    navigateStub = sandbox.stub(element, '_viewEditInChangeView');
-  });
 
-  teardown(() => { sandbox.restore(); });
+    element = basicFixture.instantiate();
+    savePathStub = sinon.stub(element.$.restAPI, 'renameFileInChangeEdit');
+    saveFileStub = sinon.stub(element.$.restAPI, 'saveChangeEdit');
+    changeDetailStub = sinon.stub(element.$.restAPI, 'getDiffChangeDetail');
+    navigateStub = sinon.stub(element, '_viewEditInChangeView');
+  });
 
   suite('_paramsChanged', () => {
     test('incorrect view returns immediately', () => {
@@ -72,7 +56,7 @@
 
     test('good params proceed', () => {
       changeDetailStub.returns(Promise.resolve({}));
-      const fileStub = sandbox.stub(element, '_getFileData', () => {
+      const fileStub = sinon.stub(element, '_getFileData').callsFake(() => {
         element._content = 'text';
         element._newContent = 'text';
         element._type = 'application/octet-stream';
@@ -120,7 +104,7 @@
   });
 
   test('reacts to content-change event', () => {
-    const storeStub = sandbox.spy(element.$.storage, 'setEditableContentItem');
+    const storeStub = sinon.spy(element.$.storage, 'setEditableContentItem');
     element._newContent = 'test';
     element.$.editorEndpoint.dispatchEvent(new CustomEvent('content-change', {
       bubbles: true, composed: true,
@@ -152,10 +136,10 @@
     });
 
     test('file modification and save, !ok response', () => {
-      const saveSpy = sandbox.spy(element, '_saveEdit');
-      const eraseStub = sandbox.stub(element.$.storage,
+      const saveSpy = sinon.spy(element, '_saveEdit');
+      const eraseStub = sinon.stub(element.$.storage,
           'eraseEditableContentItem');
-      const alertStub = sandbox.stub(element, '_showAlert');
+      const alertStub = sinon.stub(element, '_showAlert');
       saveFileStub.returns(Promise.resolve({ok: false}));
       element._newContent = newText;
       flushAsynchronousOperations();
@@ -183,8 +167,8 @@
     });
 
     test('file modification and save', () => {
-      const saveSpy = sandbox.spy(element, '_saveEdit');
-      const alertStub = sandbox.stub(element, '_showAlert');
+      const saveSpy = sinon.spy(element, '_saveEdit');
+      const alertStub = sinon.stub(element, '_showAlert');
       saveFileStub.returns(Promise.resolve({ok: true}));
       element._newContent = newText;
       flushAsynchronousOperations();
@@ -210,7 +194,7 @@
     });
 
     test('file modification and close', () => {
-      const closeSpy = sandbox.spy(element, '_handleCloseTap');
+      const closeSpy = sinon.spy(element, '_handleCloseTap');
       element._newContent = newText;
       flushAsynchronousOperations();
 
@@ -228,11 +212,11 @@
       element._newContent = 'initial';
       element._content = 'initial';
       element._type = 'initial';
-      sandbox.stub(element.$.storage, 'getEditableContentItem').returns(null);
+      sinon.stub(element.$.storage, 'getEditableContentItem').returns(null);
     });
 
     test('res.ok', () => {
-      sandbox.stub(element.$.restAPI, 'getFileContent')
+      sinon.stub(element.$.restAPI, 'getFileContent')
           .returns(Promise.resolve({
             ok: true,
             type: 'text/javascript',
@@ -248,7 +232,7 @@
     });
 
     test('!res.ok', () => {
-      sandbox.stub(element.$.restAPI, 'getFileContent')
+      sinon.stub(element.$.restAPI, 'getFileContent')
           .returns(Promise.resolve({}));
 
       // Ensure no data is set with a bad response.
@@ -260,7 +244,7 @@
     });
 
     test('content is undefined', () => {
-      sandbox.stub(element.$.restAPI, 'getFileContent')
+      sinon.stub(element.$.restAPI, 'getFileContent')
           .returns(Promise.resolve({
             ok: true,
             type: 'text/javascript',
@@ -274,7 +258,7 @@
     });
 
     test('content and type is undefined', () => {
-      sandbox.stub(element.$.restAPI, 'getFileContent')
+      sinon.stub(element.$.restAPI, 'getFileContent')
           .returns(Promise.resolve({
             ok: true,
           }));
@@ -299,7 +283,7 @@
 
   test('_viewEditInChangeView respects _patchNum', () => {
     navigateStub.restore();
-    const navStub = sandbox.stub(GerritNav, 'navigateToChange');
+    const navStub = sinon.stub(GerritNav, 'navigateToChange');
     element._patchNum = element.EDIT_NAME;
     element._viewEditInChangeView();
     assert.equal(navStub.lastCall.args[1], element.EDIT_NAME);
@@ -318,8 +302,8 @@
     suite('_handleSaveShortcut', () => {
       let saveStub;
       setup(() => {
-        handleSpy = sandbox.spy(element, '_handleSaveShortcut');
-        saveStub = sandbox.stub(element, '_saveEdit');
+        handleSpy = sinon.spy(element, '_handleSaveShortcut');
+        saveStub = sinon.stub(element, '_saveEdit');
       });
 
       test('save enabled', () => {
@@ -356,16 +340,16 @@
 
   suite('gr-storage caching', () => {
     test('local edit exists', () => {
-      sandbox.stub(element.$.storage, 'getEditableContentItem')
+      sinon.stub(element.$.storage, 'getEditableContentItem')
           .returns({message: 'pending edit'});
-      sandbox.stub(element.$.restAPI, 'getFileContent')
+      sinon.stub(element.$.restAPI, 'getFileContent')
           .returns(Promise.resolve({
             ok: true,
             type: 'text/javascript',
             content: 'old content',
           }));
 
-      const alertStub = sandbox.stub();
+      const alertStub = sinon.stub();
       element.addEventListener('show-alert', alertStub);
 
       return element._getFileData(1, 'test', 1).then(() => {
@@ -379,16 +363,16 @@
     });
 
     test('local edit exists, is same as remote edit', () => {
-      sandbox.stub(element.$.storage, 'getEditableContentItem')
+      sinon.stub(element.$.storage, 'getEditableContentItem')
           .returns({message: 'pending edit'});
-      sandbox.stub(element.$.restAPI, 'getFileContent')
+      sinon.stub(element.$.restAPI, 'getFileContent')
           .returns(Promise.resolve({
             ok: true,
             type: 'text/javascript',
             content: 'pending edit',
           }));
 
-      const alertStub = sandbox.stub();
+      const alertStub = sinon.stub();
       element.addEventListener('show-alert', alertStub);
 
       return element._getFileData(1, 'test', 1).then(() => {
@@ -409,4 +393,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html
deleted file mode 100644
index 6322518..0000000
--- a/polygerrit-ui/app/elements/gr-app_test.html
+++ /dev/null
@@ -1,106 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-app</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-app id="app"></gr-app>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../test/common-test-setup.js';
-import './gr-app.js';
-import {appContext} from '../services/app-context.js';
-import {GerritNav} from './core/gr-navigation/gr-navigation.js';
-
-suite('gr-app tests', () => {
-  let sandbox;
-  let element;
-
-  setup(done => {
-    sandbox = sinon.sandbox.create();
-    sandbox.stub(appContext.reportingService, 'appStarted');
-    stub('gr-account-dropdown', {
-      _getTopContent: sinon.stub(),
-    });
-    stub('gr-router', {
-      start: sandbox.stub(),
-    });
-    stub('gr-rest-api-interface', {
-      getAccount() { return Promise.resolve({}); },
-      getAccountCapabilities() { return Promise.resolve({}); },
-      getConfig() {
-        return Promise.resolve({
-          plugin: {},
-          auth: {
-            auth_type: undefined,
-          },
-        });
-      },
-      getPreferences() { return Promise.resolve({my: []}); },
-      getDiffPreferences() { return Promise.resolve({}); },
-      getEditPreferences() { return Promise.resolve({}); },
-      getVersion() { return Promise.resolve(42); },
-      probePath() { return Promise.resolve(42); },
-    });
-
-    element = fixture('basic');
-    flush(done);
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  const appElement = () => element.$['app-element'];
-
-  test('reporting', () => {
-    assert.isTrue(appElement().reporting.appStarted.calledOnce);
-  });
-
-  test('reporting called before router start', () => {
-    const element = appElement();
-    const appStartedStub = element.reporting.appStarted;
-    const routerStartStub = element.$.router.start;
-    sinon.assert.callOrder(appStartedStub, routerStartStub);
-  });
-
-  test('passes config to gr-plugin-host', () => {
-    const config = appElement().$.restAPI.getConfig;
-    return config.lastCall.returnValue.then(config => {
-      assert.deepEqual(appElement().$.plugins.config, config);
-    });
-  });
-
-  test('_paramsChanged sets search page', () => {
-    appElement()._paramsChanged({base: {view: GerritNav.View.CHANGE}});
-    assert.notOk(appElement()._lastSearchPage);
-    appElement()._paramsChanged({base: {view: GerritNav.View.SEARCH}});
-    assert.ok(appElement()._lastSearchPage);
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/gr-app_test.js b/polygerrit-ui/app/elements/gr-app_test.js
new file mode 100644
index 0000000..4c5e965
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-app_test.js
@@ -0,0 +1,86 @@
+/**
+ * @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-app.js';
+import {appContext} from '../services/app-context.js';
+import {GerritNav} from './core/gr-navigation/gr-navigation.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromTemplate(html`<gr-app id="app"></gr-app>`);
+
+suite('gr-app tests', () => {
+  let element;
+
+  setup(done => {
+    sinon.stub(appContext.reportingService, 'appStarted');
+    stub('gr-account-dropdown', {
+      _getTopContent: sinon.stub(),
+    });
+    stub('gr-router', {
+      start: sinon.stub(),
+    });
+    stub('gr-rest-api-interface', {
+      getAccount() { return Promise.resolve({}); },
+      getAccountCapabilities() { return Promise.resolve({}); },
+      getConfig() {
+        return Promise.resolve({
+          plugin: {},
+          auth: {
+            auth_type: undefined,
+          },
+        });
+      },
+      getPreferences() { return Promise.resolve({my: []}); },
+      getDiffPreferences() { return Promise.resolve({}); },
+      getEditPreferences() { return Promise.resolve({}); },
+      getVersion() { return Promise.resolve(42); },
+      probePath() { return Promise.resolve(42); },
+    });
+
+    element = basicFixture.instantiate();
+    flush(done);
+  });
+
+  const appElement = () => element.$['app-element'];
+
+  test('reporting', () => {
+    assert.isTrue(appElement().reporting.appStarted.calledOnce);
+  });
+
+  test('reporting called before router start', () => {
+    const element = appElement();
+    const appStartedStub = element.reporting.appStarted;
+    const routerStartStub = element.$.router.start;
+    sinon.assert.callOrder(appStartedStub, routerStartStub);
+  });
+
+  test('passes config to gr-plugin-host', () => {
+    const config = appElement().$.restAPI.getConfig;
+    return config.lastCall.returnValue.then(config => {
+      assert.deepEqual(appElement().$.plugins.config, config);
+    });
+  });
+
+  test('_paramsChanged sets search page', () => {
+    appElement()._paramsChanged({base: {view: GerritNav.View.CHANGE}});
+    assert.notOk(appElement()._lastSearchPage);
+    appElement()._paramsChanged({base: {view: GerritNav.View.SEARCH}});
+    assert.ok(appElement()._lastSearchPage);
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
deleted file mode 100644
index a865233..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
+++ /dev/null
@@ -1,72 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-admin-api</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
-
-const pluginApi = _testOnly_initGerritPluginApi();
-
-suite('gr-admin-api tests', () => {
-  let sandbox;
-  let adminApi;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    let plugin;
-    pluginApi.install(p => { plugin = p; }, '0.1',
-        'http://test.com/plugins/testplugin/static/test.js');
-    pluginLoader.loadPlugins([]);
-    adminApi = plugin.admin();
-  });
-
-  teardown(() => {
-    adminApi = null;
-    sandbox.restore();
-  });
-
-  test('exists', () => {
-    assert.isOk(adminApi);
-  });
-
-  test('addMenuLink', () => {
-    adminApi.addMenuLink('text', 'url');
-    const links = adminApi.getMenuLinks();
-    assert.equal(links.length, 1);
-    assert.deepEqual(links[0], {text: 'text', url: 'url', capability: null});
-  });
-
-  test('addMenuLinkWithCapability', () => {
-    adminApi.addMenuLink('text', 'url', 'capability');
-    const links = adminApi.getMenuLinks();
-    assert.equal(links.length, 1);
-    assert.deepEqual(links[0],
-        {text: 'text', url: 'url', capability: 'capability'});
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.js b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.js
new file mode 100644
index 0000000..c3552cb
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.js
@@ -0,0 +1,59 @@
+/**
+ * @license
+ * Copyright (C) 2018 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 '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
+
+const pluginApi = _testOnly_initGerritPluginApi();
+
+suite('gr-admin-api tests', () => {
+  let adminApi;
+
+  setup(() => {
+    let plugin;
+    pluginApi.install(p => { plugin = p; }, '0.1',
+        'http://test.com/plugins/testplugin/static/test.js');
+    pluginLoader.loadPlugins([]);
+    adminApi = plugin.admin();
+  });
+
+  teardown(() => {
+    adminApi = null;
+  });
+
+  test('exists', () => {
+    assert.isOk(adminApi);
+  });
+
+  test('addMenuLink', () => {
+    adminApi.addMenuLink('text', 'url');
+    const links = adminApi.getMenuLinks();
+    assert.equal(links.length, 1);
+    assert.deepEqual(links[0], {text: 'text', url: 'url', capability: null});
+  });
+
+  test('addMenuLinkWithCapability', () => {
+    adminApi.addMenuLink('text', 'url', 'capability');
+    const links = adminApi.getMenuLinks();
+    assert.equal(links.length, 1);
+    assert.deepEqual(links[0],
+        {text: 'text', url: 'url', capability: 'capability'});
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
deleted file mode 100644
index 50f9002..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
+++ /dev/null
@@ -1,101 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-attribute-helper</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<dom-element id="some-element">
-  <script type="module">
-import '../../../test/common-test-setup.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-Polymer({
-  is: 'some-element',
-  properties: {
-    fooBar: {
-      type: Object,
-      notify: true,
-    },
-  },
-});
-</script>
-
-</dom-element>
-
-<test-fixture id="basic">
-  <template>
-    <some-element></some-element>
-  </template>
-</test-fixture>
-
-<script type="module">
-import {GrAttributeHelper} from './gr-attribute-helper.js';
-
-suite('gr-attribute-helper tests', () => {
-  let element;
-  let instance;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-    instance = new GrAttributeHelper(element);
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('resolved on value change from undefined', () => {
-    const promise = instance.get('fooBar').then(value => {
-      assert.equal(value, 'foo! bar!');
-    });
-    element.fooBar = 'foo! bar!';
-    return promise;
-  });
-
-  test('resolves to current attribute value', () => {
-    element.fooBar = 'foo-foo-bar';
-    const promise = instance.get('fooBar').then(value => {
-      assert.equal(value, 'foo-foo-bar');
-    });
-    element.fooBar = 'no bar';
-    return promise;
-  });
-
-  test('bind', () => {
-    const stub = sandbox.stub();
-    element.fooBar = 'bar foo';
-    const unbind = instance.bind('fooBar', stub);
-    element.fooBar = 'partridge in a foo tree';
-    element.fooBar = 'five gold bars';
-    assert.equal(stub.callCount, 3);
-    assert.deepEqual(stub.args[0], ['bar foo']);
-    assert.deepEqual(stub.args[1], ['partridge in a foo tree']);
-    assert.deepEqual(stub.args[2], ['five gold bars']);
-    stub.reset();
-    unbind();
-    instance.fooBar = 'ladies dancing';
-    assert.isFalse(stub.called);
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.js b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.js
new file mode 100644
index 0000000..ea7bdc3
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.js
@@ -0,0 +1,76 @@
+/**
+ * @license
+ * Copyright (C) 2017 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 {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+import {GrAttributeHelper} from './gr-attribute-helper.js';
+
+Polymer({
+  is: 'gr-attrubute-helper-some-element',
+  properties: {
+    fooBar: {
+      type: Object,
+      notify: true,
+    },
+  },
+});
+
+const basicFixture = fixtureFromElement('gr-attrubute-helper-some-element');
+
+suite('gr-attribute-helper tests', () => {
+  let element;
+  let instance;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+    instance = new GrAttributeHelper(element);
+  });
+
+  test('resolved on value change from undefined', () => {
+    const promise = instance.get('fooBar').then(value => {
+      assert.equal(value, 'foo! bar!');
+    });
+    element.fooBar = 'foo! bar!';
+    return promise;
+  });
+
+  test('resolves to current attribute value', () => {
+    element.fooBar = 'foo-foo-bar';
+    const promise = instance.get('fooBar').then(value => {
+      assert.equal(value, 'foo-foo-bar');
+    });
+    element.fooBar = 'no bar';
+    return promise;
+  });
+
+  test('bind', () => {
+    const stub = sinon.stub();
+    element.fooBar = 'bar foo';
+    const unbind = instance.bind('fooBar', stub);
+    element.fooBar = 'partridge in a foo tree';
+    element.fooBar = 'five gold bars';
+    assert.equal(stub.callCount, 3);
+    assert.deepEqual(stub.args[0], ['bar foo']);
+    assert.deepEqual(stub.args[1], ['partridge in a foo tree']);
+    assert.deepEqual(stub.args[2], ['five gold bars']);
+    stub.reset();
+    unbind();
+    instance.fooBar = 'ladies dancing';
+    assert.isFalse(stub.called);
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.js
similarity index 73%
rename from polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
rename to polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.js
index 17a22e9..a2b92ec 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.js
@@ -1,37 +1,21 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-dom-hooks</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <div></div>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
 import {GrDomHook, GrDomHooksManager} from './gr-dom-hooks.js';
 import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
@@ -48,25 +32,20 @@
   ];
 
   let instance;
-  let sandbox;
+
   let hook;
   let hookInternal;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     let plugin;
     pluginApi.install(p => { plugin = p; }, '0.1',
         'http://test.com/plugins/testplugin/static/test.js');
     instance = new GrDomHooksManager(plugin);
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   suite('placeholder', () => {
     setup(()=>{
-      sandbox.stub(GrDomHook.prototype, '_createPlaceholder');
+      sinon.stub(GrDomHook.prototype, '_createPlaceholder');
       hookInternal = instance.getDomHook('foo-bar');
       hook = hookInternal.getPublicAPI();
     });
@@ -104,7 +83,7 @@
     });
 
     test('onAttached', () => {
-      const onAttachedSpy = sandbox.spy();
+      const onAttachedSpy = sinon.spy();
       hook.onAttached(onAttachedSpy);
       const [el1, el2] = [
         document.createElement(hook.getModuleName()),
@@ -117,7 +96,7 @@
     });
 
     test('onDetached', () => {
-      const onDetachedSpy = sandbox.spy();
+      const onDetachedSpy = sinon.spy();
       hook.onDetached(onDetachedSpy);
       const [el1, el2] = [
         document.createElement(hook.getModuleName()),
@@ -163,4 +142,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.js
index 17ada2c..72c73b7 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.js
@@ -48,17 +48,17 @@
 
 suite('gr-endpoint-decorator', () => {
   let container;
-  let sandbox;
+
   let plugin;
   let decorationHook;
   let decorationHookWithSlot;
   let replacementHook;
 
   setup(done => {
-    sandbox = sinon.sandbox.create();
     resetPlugins();
     container = basicFixture.instantiate();
-    sandbox.stub(pluginEndpoints, 'importUrl', url => Promise.resolve());
+    sinon.stub(pluginEndpoints, 'importUrl')
+        .callsFake( url => Promise.resolve());
     pluginApi.install(p => plugin = p, '0.1',
         'http://some/plugin/url.html');
     // Decoration
@@ -77,7 +77,6 @@
   });
 
   teardown(() => {
-    sandbox.restore();
     resetPlugins();
   });
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
deleted file mode 100644
index a27c817..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
+++ /dev/null
@@ -1,138 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-event-helper</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<dom-element id="some-element">
-  <script type="module">
-import '../../../test/common-test-setup.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-Polymer({
-  is: 'some-element',
-
-  properties: {
-    fooBar: {
-      type: Object,
-      notify: true,
-    },
-  },
-});
-</script>
-
-</dom-element>
-
-<test-fixture id="basic">
-  <template>
-    <some-element></some-element>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import {addListener} from '@polymer/polymer/lib/utils/gestures.js';
-import {GrEventHelper} from './gr-event-helper.js';
-
-suite('gr-event-helper tests', () => {
-  let element;
-  let instance;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-    instance = new GrEventHelper(element);
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('onTap()', done => {
-    instance.onTap(() => {
-      done();
-    });
-    MockInteractions.tap(element);
-  });
-
-  test('onTap() cancel', () => {
-    const tapStub = sandbox.stub();
-    addListener(element.parentElement, 'tap', tapStub);
-    instance.onTap(() => false);
-    MockInteractions.tap(element);
-    flushAsynchronousOperations();
-    assert.isFalse(tapStub.called);
-  });
-
-  test('onClick() cancel', () => {
-    const tapStub = sandbox.stub();
-    element.parentElement.addEventListener('click', tapStub);
-    instance.onTap(() => false);
-    MockInteractions.tap(element);
-    flushAsynchronousOperations();
-    assert.isFalse(tapStub.called);
-  });
-
-  test('captureTap()', done => {
-    instance.captureTap(() => {
-      done();
-    });
-    MockInteractions.tap(element);
-  });
-
-  test('captureClick()', done => {
-    instance.captureClick(() => {
-      done();
-    });
-    MockInteractions.tap(element);
-  });
-
-  test('captureTap() cancels tap()', () => {
-    const tapStub = sandbox.stub();
-    addListener(element.parentElement, 'tap', tapStub);
-    instance.captureTap(() => false);
-    MockInteractions.tap(element);
-    flushAsynchronousOperations();
-    assert.isFalse(tapStub.called);
-  });
-
-  test('captureClick() cancels click()', () => {
-    const tapStub = sandbox.stub();
-    element.addEventListener('click', tapStub);
-    instance.captureTap(() => false);
-    MockInteractions.tap(element);
-    flushAsynchronousOperations();
-    assert.isFalse(tapStub.called);
-  });
-
-  test('on()', done => {
-    instance.on('foo', () => {
-      done();
-    });
-    element.dispatchEvent(
-        new CustomEvent('foo', {
-          composed: true, bubbles: true,
-        }));
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.js b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.js
new file mode 100644
index 0000000..e56278f
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.js
@@ -0,0 +1,112 @@
+/**
+ * @license
+ * Copyright (C) 2017 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 {addListener} from '@polymer/polymer/lib/utils/gestures.js';
+import {GrEventHelper} from './gr-event-helper.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+
+Polymer({
+  is: 'gr-event-helper-some-element',
+
+  properties: {
+    fooBar: {
+      type: Object,
+      notify: true,
+    },
+  },
+});
+
+const basicFixture = fixtureFromElement('gr-event-helper-some-element');
+
+suite('gr-event-helper tests', () => {
+  let element;
+  let instance;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+    instance = new GrEventHelper(element);
+  });
+
+  test('onTap()', done => {
+    instance.onTap(() => {
+      done();
+    });
+    MockInteractions.tap(element);
+  });
+
+  test('onTap() cancel', () => {
+    const tapStub = sinon.stub();
+    addListener(element.parentElement, 'tap', tapStub);
+    instance.onTap(() => false);
+    MockInteractions.tap(element);
+    flushAsynchronousOperations();
+    assert.isFalse(tapStub.called);
+  });
+
+  test('onClick() cancel', () => {
+    const tapStub = sinon.stub();
+    element.parentElement.addEventListener('click', tapStub);
+    instance.onTap(() => false);
+    MockInteractions.tap(element);
+    flushAsynchronousOperations();
+    assert.isFalse(tapStub.called);
+  });
+
+  test('captureTap()', done => {
+    instance.captureTap(() => {
+      done();
+    });
+    MockInteractions.tap(element);
+  });
+
+  test('captureClick()', done => {
+    instance.captureClick(() => {
+      done();
+    });
+    MockInteractions.tap(element);
+  });
+
+  test('captureTap() cancels tap()', () => {
+    const tapStub = sinon.stub();
+    addListener(element.parentElement, 'tap', tapStub);
+    instance.captureTap(() => false);
+    MockInteractions.tap(element);
+    flushAsynchronousOperations();
+    assert.isFalse(tapStub.called);
+  });
+
+  test('captureClick() cancels click()', () => {
+    const tapStub = sinon.stub();
+    element.addEventListener('click', tapStub);
+    instance.captureTap(() => false);
+    MockInteractions.tap(element);
+    flushAsynchronousOperations();
+    assert.isFalse(tapStub.called);
+  });
+
+  test('on()', done => {
+    instance.on('foo', () => {
+      done();
+    });
+    element.dispatchEvent(
+        new CustomEvent('foo', {
+          composed: true, bubbles: true,
+        }));
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.js
index 12444f5..6659207 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.js
@@ -18,10 +18,11 @@
 import '../../../test/common-test-setup-karma.js';
 import {resetPlugins} from '../../../test/test-utils.js';
 import './gr-external-style.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 import {pluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
 import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
 const pluginApi = _testOnly_initGerritPluginApi();
 
 const basicFixture = fixtureFromTemplate(
@@ -31,7 +32,6 @@
 suite('gr-external-style integration tests', () => {
   const TEST_URL = 'http://some.com/plugins/url.html';
 
-  let sandbox;
   let element;
   let plugin;
 
@@ -44,7 +44,7 @@
 
   const createElement = () => {
     element = basicFixture.instantiate();
-    sandbox.spy(element, '_applyStyle');
+    sinon.spy(element, '_applyStyle');
   };
 
   /**
@@ -66,15 +66,16 @@
   };
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    sandbox.stub(pluginEndpoints, 'importUrl', url => Promise.resolve());
-    sandbox.stub(pluginLoader, 'awaitPluginsLoaded')
+    sinon.stub(pluginEndpoints, 'importUrl')
+        .callsFake( url => Promise.resolve());
+    sinon.stub(pluginLoader, 'awaitPluginsLoaded')
         .returns(Promise.resolve());
   });
 
   teardown(() => {
-    sandbox.restore();
     resetPlugins();
+    document.body.querySelectorAll('custom-style')
+        .forEach(style => style.remove());
   });
 
   test('imports plugin-provided module', async () => {
@@ -113,4 +114,4 @@
     await new Promise(flush);
     assert.isTrue(element._applyStyle.calledWith('some-module'));
   });
-});
\ No newline at end of file
+});
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
deleted file mode 100644
index 2c8a8c0..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
+++ /dev/null
@@ -1,94 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-plugin-host</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-plugin-host></gr-plugin-host>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-plugin-host.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
-
-suite('gr-plugin-host tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
-    sandbox.stub(document.body, 'appendChild');
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('load plugins should be called', () => {
-    sandbox.stub(pluginLoader, 'loadPlugins');
-    element.config = {
-      plugin: {
-        html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
-        js_resource_paths: ['plugins/42'],
-      },
-    };
-    assert.isTrue(pluginLoader.loadPlugins.calledOnce);
-    assert.isTrue(pluginLoader.loadPlugins.calledWith([
-      'plugins/42', 'plugins/foo/bar', 'plugins/baz',
-    ], {}));
-  });
-
-  test('theme plugins should be loaded if enabled', () => {
-    sandbox.stub(pluginLoader, 'loadPlugins');
-    element.config = {
-      default_theme: 'gerrit-theme.html',
-      plugin: {
-        html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
-        js_resource_paths: ['plugins/42'],
-      },
-    };
-    assert.isTrue(pluginLoader.loadPlugins.calledOnce);
-    assert.isTrue(pluginLoader.loadPlugins.calledWith([
-      'gerrit-theme.html', 'plugins/42', 'plugins/foo/bar', 'plugins/baz',
-    ], {'gerrit-theme.html': {sync: true}}));
-  });
-
-  test('skip theme if preloaded', () => {
-    sandbox.stub(pluginLoader, 'isPluginPreloaded')
-        .withArgs('preloaded:gerrit-theme')
-        .returns(true);
-    sandbox.stub(pluginLoader, 'loadPlugins');
-    element.config = {
-      default_theme: '/oof',
-      plugin: {},
-    };
-    assert.isTrue(pluginLoader.loadPlugins.calledOnce);
-    assert.isTrue(pluginLoader.loadPlugins.calledWith([], {}));
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.js
new file mode 100644
index 0000000..83e0a84
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.js
@@ -0,0 +1,75 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-plugin-host.js';
+import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+
+const basicFixture = fixtureFromElement('gr-plugin-host');
+
+suite('gr-plugin-host tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+
+    sinon.stub(document.body, 'appendChild');
+  });
+
+  test('load plugins should be called', () => {
+    sinon.stub(pluginLoader, 'loadPlugins');
+    element.config = {
+      plugin: {
+        html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
+        js_resource_paths: ['plugins/42'],
+      },
+    };
+    assert.isTrue(pluginLoader.loadPlugins.calledOnce);
+    assert.isTrue(pluginLoader.loadPlugins.calledWith([
+      'plugins/42', 'plugins/foo/bar', 'plugins/baz',
+    ], {}));
+  });
+
+  test('theme plugins should be loaded if enabled', () => {
+    sinon.stub(pluginLoader, 'loadPlugins');
+    element.config = {
+      default_theme: 'gerrit-theme.html',
+      plugin: {
+        html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
+        js_resource_paths: ['plugins/42'],
+      },
+    };
+    assert.isTrue(pluginLoader.loadPlugins.calledOnce);
+    assert.isTrue(pluginLoader.loadPlugins.calledWith([
+      'gerrit-theme.html', 'plugins/42', 'plugins/foo/bar', 'plugins/baz',
+    ], {'gerrit-theme.html': {sync: true}}));
+  });
+
+  test('skip theme if preloaded', () => {
+    sinon.stub(pluginLoader, 'isPluginPreloaded')
+        .withArgs('preloaded:gerrit-theme')
+        .returns(true);
+    sinon.stub(pluginLoader, 'loadPlugins');
+    element.config = {
+      default_theme: '/oof',
+      plugin: {},
+    };
+    assert.isTrue(pluginLoader.loadPlugins.calledOnce);
+    assert.isTrue(pluginLoader.loadPlugins.calledWith([], {}));
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
deleted file mode 100644
index 2e65365..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
+++ /dev/null
@@ -1,69 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-plugin-popup</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-plugin-popup></gr-plugin-popup>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-plugin-popup.js';
-suite('gr-plugin-popup tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-    stub('gr-overlay', {
-      open: sandbox.stub().returns(Promise.resolve()),
-      close: sandbox.stub(),
-    });
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('exists', () => {
-    assert.isOk(element);
-  });
-
-  test('open uses open() from gr-overlay', done => {
-    element.open().then(() => {
-      assert.isTrue(element.$.overlay.open.called);
-      done();
-    });
-  });
-
-  test('close uses close() from gr-overlay', () => {
-    element.close();
-    assert.isTrue(element.$.overlay.close.called);
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.js
new file mode 100644
index 0000000..f2d83e8
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.js
@@ -0,0 +1,50 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-plugin-popup.js';
+
+const basicFixture = fixtureFromElement('gr-plugin-popup');
+
+suite('gr-plugin-popup tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+    stub('gr-overlay', {
+      open: sinon.stub().returns(Promise.resolve()),
+      close: sinon.stub(),
+    });
+  });
+
+  test('exists', () => {
+    assert.isOk(element);
+  });
+
+  test('open uses open() from gr-overlay', done => {
+    element.open().then(() => {
+      assert.isTrue(element.$.overlay.open.called);
+      done();
+    });
+  });
+
+  test('close uses close() from gr-overlay', () => {
+    element.close();
+    assert.isTrue(element.$.overlay.close.called);
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
deleted file mode 100644
index 62ab0e7..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
+++ /dev/null
@@ -1,127 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-popup-interface</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="container">
-  <template>
-    <div></div>
-  </template>
-</test-fixture>
-
-<dom-module id="gr-user-test-popup">
-  <template>
-    <div id="barfoo">some test module</div>
-  </template>
-  <script type="module">
-import '../../../test/common-test-setup.js';
-import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-Polymer({is: 'gr-user-test-popup'});
-</script>
-</dom-module>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {GrPopupInterface} from './gr-popup-interface.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
-
-const pluginApi = _testOnly_initGerritPluginApi();
-suite('gr-popup-interface tests', () => {
-  let container;
-  let instance;
-  let plugin;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    pluginApi.install(p => { plugin = p; }, '0.1',
-        'http://test.com/plugins/testplugin/static/test.js');
-    container = fixture('container');
-    sandbox.stub(plugin, 'hook').returns({
-      getLastAttached() {
-        return Promise.resolve(container);
-      },
-    });
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  suite('manual', () => {
-    setup(() => {
-      instance = new GrPopupInterface(plugin);
-    });
-
-    test('open', done => {
-      instance.open().then(api => {
-        assert.strictEqual(api, instance);
-        const manual = document.createElement('div');
-        manual.id = 'foobar';
-        manual.innerHTML = 'manual content';
-        api._getElement().appendChild(manual);
-        flushAsynchronousOperations();
-        assert.equal(
-            container.querySelector('#foobar').textContent, 'manual content');
-        done();
-      });
-    });
-
-    test('close', done => {
-      instance.open().then(api => {
-        assert.isTrue(api._getElement().node.opened);
-        api.close();
-        assert.isFalse(api._getElement().node.opened);
-        done();
-      });
-    });
-  });
-
-  suite('components', () => {
-    setup(() => {
-      instance = new GrPopupInterface(plugin, 'gr-user-test-popup');
-    });
-
-    test('open', done => {
-      instance.open().then(api => {
-        assert.isNotNull(
-            dom(container).querySelector('gr-user-test-popup'));
-        done();
-      });
-    });
-
-    test('close', done => {
-      instance.open().then(api => {
-        assert.isTrue(api._getElement().node.opened);
-        api.close();
-        assert.isFalse(api._getElement().node.opened);
-        done();
-      });
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.js
new file mode 100644
index 0000000..9312332
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.js
@@ -0,0 +1,107 @@
+/**
+ * @license
+ * Copyright (C) 2017 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 '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {GrPopupInterface} from './gr-popup-interface.js';
+import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+
+class GrUserTestPopupElement extends PolymerElement {
+  static get is() { return 'gr-user-test-popup'; }
+
+  static get template() {
+    return html`<div id="barfoo">some test module</div>`;
+  }
+}
+
+customElements.define(GrUserTestPopupElement.is, GrUserTestPopupElement);
+
+const containerFixture = fixtureFromElement('div');
+
+const pluginApi = _testOnly_initGerritPluginApi();
+suite('gr-popup-interface tests', () => {
+  let container;
+  let instance;
+  let plugin;
+
+  setup(() => {
+    pluginApi.install(p => { plugin = p; }, '0.1',
+        'http://test.com/plugins/testplugin/static/test.js');
+    container = containerFixture.instantiate();
+    sinon.stub(plugin, 'hook').returns({
+      getLastAttached() {
+        return Promise.resolve(container);
+      },
+    });
+  });
+
+  suite('manual', () => {
+    setup(() => {
+      instance = new GrPopupInterface(plugin);
+    });
+
+    test('open', done => {
+      instance.open().then(api => {
+        assert.strictEqual(api, instance);
+        const manual = document.createElement('div');
+        manual.id = 'foobar';
+        manual.innerHTML = 'manual content';
+        api._getElement().appendChild(manual);
+        flushAsynchronousOperations();
+        assert.equal(
+            container.querySelector('#foobar').textContent, 'manual content');
+        done();
+      });
+    });
+
+    test('close', done => {
+      instance.open().then(api => {
+        assert.isTrue(api._getElement().node.opened);
+        api.close();
+        assert.isFalse(api._getElement().node.opened);
+        done();
+      });
+    });
+  });
+
+  suite('components', () => {
+    setup(() => {
+      instance = new GrPopupInterface(plugin, 'gr-user-test-popup');
+    });
+
+    test('open', done => {
+      instance.open().then(api => {
+        assert.isNotNull(
+            dom(container).querySelector('gr-user-test-popup'));
+        done();
+      });
+    });
+
+    test('close', done => {
+      instance.open().then(api => {
+        assert.isTrue(api._getElement().node.opened);
+        api.close();
+        assert.isFalse(api._getElement().node.opened);
+        done();
+      });
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
deleted file mode 100644
index 1af91fd..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
+++ /dev/null
@@ -1,87 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-repo-api</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-endpoint-decorator name="repo-command">
-    </gr-endpoint-decorator>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import '../gr-endpoint-decorator/gr-endpoint-decorator.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
-
-const pluginApi = _testOnly_initGerritPluginApi();
-
-suite('gr-repo-api tests', () => {
-  let sandbox;
-  let repoApi;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    let plugin;
-    pluginApi.install(p => { plugin = p; }, '0.1',
-        'http://test.com/plugins/testplugin/static/test.js');
-    pluginLoader.loadPlugins([]);
-    repoApi = plugin.project();
-  });
-
-  teardown(() => {
-    repoApi = null;
-    sandbox.restore();
-  });
-
-  test('exists', () => {
-    assert.isOk(repoApi);
-  });
-
-  test('works', done => {
-    const attachedStub = sandbox.stub();
-    const tapStub = sandbox.stub();
-    repoApi
-        .createCommand('foo', attachedStub)
-        .onTap(tapStub);
-    const element = fixture('basic');
-    flush(() => {
-      assert.isTrue(attachedStub.called);
-      const pluginCommand = element.shadowRoot
-          .querySelector('gr-plugin-repo-command');
-      assert.isOk(pluginCommand);
-      const btn = pluginCommand.shadowRoot
-          .querySelector('gr-button');
-      assert.isOk(btn);
-      assert.equal(btn.textContent, 'foo');
-      assert.isFalse(tapStub.called);
-      MockInteractions.tap(btn);
-      assert.isTrue(tapStub.called);
-      done();
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.js b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.js
new file mode 100644
index 0000000..c7f1b06
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.js
@@ -0,0 +1,73 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-endpoint-decorator/gr-endpoint-decorator.js';
+import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromTemplate(html`
+<gr-endpoint-decorator name="repo-command">
+    </gr-endpoint-decorator>
+`);
+
+const pluginApi = _testOnly_initGerritPluginApi();
+
+suite('gr-repo-api tests', () => {
+  let repoApi;
+
+  setup(() => {
+    let plugin;
+    pluginApi.install(p => { plugin = p; }, '0.1',
+        'http://test.com/plugins/testplugin/static/test.js');
+    pluginLoader.loadPlugins([]);
+    repoApi = plugin.project();
+  });
+
+  teardown(() => {
+    repoApi = null;
+  });
+
+  test('exists', () => {
+    assert.isOk(repoApi);
+  });
+
+  test('works', done => {
+    const attachedStub = sinon.stub();
+    const tapStub = sinon.stub();
+    repoApi
+        .createCommand('foo', attachedStub)
+        .onTap(tapStub);
+    const element = basicFixture.instantiate();
+    flush(() => {
+      assert.isTrue(attachedStub.called);
+      const pluginCommand = element.shadowRoot
+          .querySelector('gr-plugin-repo-command');
+      assert.isOk(pluginCommand);
+      const btn = pluginCommand.shadowRoot
+          .querySelector('gr-button');
+      assert.isOk(btn);
+      assert.equal(btn.textContent, 'foo');
+      assert.isFalse(tapStub.called);
+      MockInteractions.tap(btn);
+      assert.isTrue(tapStub.called);
+      done();
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html
deleted file mode 100644
index 5057992..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html
+++ /dev/null
@@ -1,89 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Settings
-
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-settings-api</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-endpoint-decorator name="settings-menu-item">
-    </gr-endpoint-decorator>
-    <gr-endpoint-decorator name="settings-screen">
-    </gr-endpoint-decorator>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import '../gr-endpoint-decorator/gr-endpoint-decorator.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
-
-const pluginApi = _testOnly_initGerritPluginApi();
-
-suite('gr-settings-api tests', () => {
-  let sandbox;
-  let settingsApi;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    let plugin;
-    pluginApi.install(p => { plugin = p; }, '0.1',
-        'http://test.com/plugins/testplugin/static/test.js');
-    pluginLoader.loadPlugins([]);
-    settingsApi = plugin.settings();
-  });
-
-  teardown(() => {
-    settingsApi = null;
-    sandbox.restore();
-  });
-
-  test('exists', () => {
-    assert.isOk(settingsApi);
-  });
-
-  test('works', done => {
-    settingsApi
-        .title('foo')
-        .token('bar')
-        .module('some-settings-screen')
-        .build();
-    const element = fixture('basic');
-    flush(() => {
-      const [menuItemEl, itemEl] = element;
-      const menuItem = menuItemEl.shadowRoot
-          .querySelector('gr-settings-menu-item');
-      assert.isOk(menuItem);
-      assert.equal(menuItem.title, 'foo');
-      assert.equal(menuItem.href, '#x/testplugin/bar');
-      const item = itemEl.shadowRoot
-          .querySelector('gr-settings-item');
-      assert.isOk(item);
-      assert.equal(item.title, 'foo');
-      assert.equal(item.anchor, 'x/testplugin/bar');
-      done();
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.js b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.js
new file mode 100644
index 0000000..82d58fe
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.js
@@ -0,0 +1,75 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-endpoint-decorator/gr-endpoint-decorator.js';
+import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromTemplate(html`
+<gr-endpoint-decorator name="settings-menu-item">
+    </gr-endpoint-decorator>
+    <gr-endpoint-decorator name="settings-screen">
+    </gr-endpoint-decorator>
+`);
+
+const pluginApi = _testOnly_initGerritPluginApi();
+
+suite('gr-settings-api tests', () => {
+  let settingsApi;
+
+  setup(() => {
+    let plugin;
+    pluginApi.install(p => { plugin = p; }, '0.1',
+        'http://test.com/plugins/testplugin/static/test.js');
+    pluginLoader.loadPlugins([]);
+    settingsApi = plugin.settings();
+  });
+
+  teardown(() => {
+    settingsApi = null;
+  });
+
+  test('exists', () => {
+    assert.isOk(settingsApi);
+  });
+
+  test('works', done => {
+    settingsApi
+        .title('foo')
+        .token('bar')
+        .module('some-settings-screen')
+        .build();
+    const element = basicFixture.instantiate();
+    flush(() => {
+      const [menuItemEl, itemEl] = element;
+      const menuItem = menuItemEl.shadowRoot
+          .querySelector('gr-settings-menu-item');
+      assert.isOk(menuItem);
+      assert.equal(menuItem.title, 'foo');
+      assert.equal(menuItem.href, '#x/testplugin/bar');
+      const item = itemEl.shadowRoot
+          .querySelector('gr-settings-item');
+      assert.isOk(item);
+      assert.equal(item.title, 'foo');
+      assert.equal(item.anchor, 'x/testplugin/bar');
+      done();
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.js
similarity index 73%
rename from polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html
rename to polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.js
index d6bae9b..5ccda28 100644
--- a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.js
@@ -1,56 +1,44 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2019 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-admin-api</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<dom-module id="gr-style-test-element">
-  <template>
-    <div id="wrapper"></div>
-  </template>
-  <script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-Polymer({is: 'gr-style-test-element'});
-</script>
-</dom-module>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+class GrStyleTestElement extends PolymerElement {
+  static get is() { return 'gr-style-test-element'; }
+
+  static get template() {
+    return html`<div id="wrapper"></div>`;
+  }
+}
+
+customElements.define(GrStyleTestElement.is, GrStyleTestElement);
 
 const pluginApi = _testOnly_initGerritPluginApi();
 
 suite('gr-styles-api tests', () => {
-  let sandbox;
   let stylesApi;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     let plugin;
     pluginApi.install(p => { plugin = p; }, '0.1',
         'http://test.com/plugins/testplugin/static/test.js');
@@ -60,7 +48,6 @@
 
   teardown(() => {
     stylesApi = null;
-    sandbox.restore();
   });
 
   test('exists', () => {
@@ -73,13 +60,12 @@
   });
 
   suite('GrStyleObject tests', () => {
-    let sandbox;
     let stylesApi;
     let displayInlineStyle;
     let displayNoneStyle;
+    let elementsToRemove;
 
     setup(() => {
-      sandbox = sinon.sandbox.create();
       let plugin;
       pluginApi.install(p => { plugin = p; }, '0.1',
           'http://test.com/plugins/testplugin/static/test.js');
@@ -87,13 +73,18 @@
       stylesApi = plugin.styles();
       displayInlineStyle = stylesApi.css('display: inline');
       displayNoneStyle = stylesApi.css('display: none');
+      elementsToRemove = [];
     });
 
     teardown(() => {
       displayInlineStyle = null;
       displayNoneStyle = null;
       stylesApi = null;
-      sandbox.restore();
+      elementsToRemove.forEach(element => {
+        element.remove();
+      });
+      elementsToRemove = null;
+      sinon.restore();
     });
 
     function createNestedElements(parentElement) {
@@ -109,6 +100,11 @@
       dom(parentElement).appendChild(element2);
       dom(element2).appendChild(element3);
 
+      if (parentElement === document.body) {
+        elementsToRemove.push(element1);
+        elementsToRemove.push(element2);
+      }
+
       return [element1, element2, element3];
     }
 
@@ -121,6 +117,7 @@
     test('getClassName  - elements inside polymer element', () => {
       const polymerElement = document.createElement('gr-style-test-element');
       dom(document.body).appendChild(polymerElement);
+      elementsToRemove.push(polymerElement);
       const contentElements = createNestedElements(polymerElement.$.wrapper);
 
       testGetClassName(contentElements);
@@ -154,6 +151,7 @@
     test('apply - elements inside polymer element', () => {
       const polymerElement = document.createElement('gr-style-test-element');
       dom(document.body).appendChild(polymerElement);
+      elementsToRemove.push(polymerElement);
       const contentElements = createNestedElements(polymerElement.$.wrapper);
 
       testApply(contentElements);
@@ -185,4 +183,4 @@
     }
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
deleted file mode 100644
index 9e2e190..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
+++ /dev/null
@@ -1,87 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-theme-api</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="header-title">
-  <template>
-    <gr-endpoint-decorator name="header-title">
-      <span class="titleText"></span>
-    </gr-endpoint-decorator>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import '../gr-endpoint-decorator/gr-endpoint-decorator.js';
-import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
-
-const pluginApi = _testOnly_initGerritPluginApi();
-
-suite('gr-theme-api tests', () => {
-  let sandbox;
-  let theme;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    let plugin;
-    pluginApi.install(p => { plugin = p; }, '0.1',
-        'http://test.com/plugins/testplugin/static/test.js');
-    theme = plugin.theme();
-  });
-
-  teardown(() => {
-    theme = null;
-    sandbox.restore();
-  });
-
-  test('exists', () => {
-    assert.isOk(theme);
-  });
-
-  suite('header-title', () => {
-    let customHeader;
-
-    setup(() => {
-      fixture('header-title');
-      stub('gr-custom-plugin-header', {
-        /** @override */
-        ready() { customHeader = this; },
-      });
-      pluginLoader.loadPlugins([]);
-    });
-
-    test('sets logo and title', done => {
-      theme.setHeaderLogoAndTitle('foo.jpg', 'bar');
-      flush(() => {
-        assert.isNotNull(customHeader);
-        assert.equal(customHeader.logoUrl, 'foo.jpg');
-        assert.equal(customHeader.title, 'bar');
-        done();
-      });
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.js
new file mode 100644
index 0000000..8a70303
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.js
@@ -0,0 +1,73 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-endpoint-decorator/gr-endpoint-decorator.js';
+import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const headerTitleFixture = fixtureFromTemplate(html`
+<gr-endpoint-decorator name="header-title">
+      <span class="titleText"></span>
+    </gr-endpoint-decorator>
+`);
+
+const pluginApi = _testOnly_initGerritPluginApi();
+
+suite('gr-theme-api tests', () => {
+  let theme;
+
+  setup(() => {
+    let plugin;
+    pluginApi.install(p => { plugin = p; }, '0.1',
+        'http://test.com/plugins/testplugin/static/test.js');
+    theme = plugin.theme();
+  });
+
+  teardown(() => {
+    theme = null;
+  });
+
+  test('exists', () => {
+    assert.isOk(theme);
+  });
+
+  suite('header-title', () => {
+    let customHeader;
+
+    setup(() => {
+      headerTitleFixture.instantiate();
+      stub('gr-custom-plugin-header', {
+        /** @override */
+        ready() { customHeader = this; },
+      });
+      pluginLoader.loadPlugins([]);
+    });
+
+    test('sets logo and title', done => {
+      theme.setHeaderLogoAndTitle('foo.jpg', 'bar');
+      flush(() => {
+        assert.isNotNull(customHeader);
+        assert.equal(customHeader.logoUrl, 'foo.jpg');
+        assert.equal(customHeader.title, 'bar');
+        done();
+      });
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.js
similarity index 79%
rename from polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
rename to polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.js
index 53641d9..4a62bab 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.js
@@ -1,45 +1,30 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-account-info</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-account-info></gr-account-info>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-account-info.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromElement('gr-account-info');
+
 suite('gr-account-info tests', () => {
   let element;
   let account;
   let config;
-  let sandbox;
 
   function valueOf(title) {
     const sections = dom(element.root).querySelectorAll('section');
@@ -53,7 +38,6 @@
   }
 
   setup(done => {
-    sandbox = sinon.sandbox.create();
     account = {
       _account_id: 123,
       name: 'user name',
@@ -70,15 +54,11 @@
         return Promise.resolve({time_format: 'HHMM_12'});
       },
     });
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     // Allow the element to render.
     element.loadData().then(() => { flush(done); });
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('basic account info render', () => {
     assert.isFalse(element._loading);
 
@@ -148,17 +128,17 @@
     let statusStub;
 
     setup(() => {
-      nameChangedSpy = sandbox.spy(element, '_nameChanged');
-      usernameChangedSpy = sandbox.spy(element, '_usernameChanged');
-      statusChangedSpy = sandbox.spy(element, '_statusChanged');
+      nameChangedSpy = sinon.spy(element, '_nameChanged');
+      usernameChangedSpy = sinon.spy(element, '_usernameChanged');
+      statusChangedSpy = sinon.spy(element, '_statusChanged');
       element.set('_serverConfig',
           {auth: {editable_account_fields: ['FULL_NAME', 'USER_NAME']}});
 
-      nameStub = sandbox.stub(element.$.restAPI, 'setAccountName',
+      nameStub = sinon.stub(element.$.restAPI, 'setAccountName').callsFake(
           name => Promise.resolve());
-      usernameStub = sandbox.stub(element.$.restAPI, 'setAccountUsername',
-          username => Promise.resolve());
-      statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
+      usernameStub = sinon.stub(element.$.restAPI, 'setAccountUsername')
+          .callsFake(username => Promise.resolve());
+      statusStub = sinon.stub(element.$.restAPI, 'setAccountStatus').callsFake(
           status => Promise.resolve());
     });
 
@@ -233,16 +213,16 @@
     let statusStub;
 
     setup(() => {
-      nameChangedSpy = sandbox.spy(element, '_nameChanged');
-      statusChangedSpy = sandbox.spy(element, '_statusChanged');
+      nameChangedSpy = sinon.spy(element, '_nameChanged');
+      statusChangedSpy = sinon.spy(element, '_statusChanged');
       element.set('_serverConfig',
           {auth: {editable_account_fields: ['FULL_NAME']}});
 
-      nameStub = sandbox.stub(element.$.restAPI, 'setAccountName',
+      nameStub = sinon.stub(element.$.restAPI, 'setAccountName').callsFake(
           name => Promise.resolve());
-      statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
+      statusStub = sinon.stub(element.$.restAPI, 'setAccountStatus').callsFake(
           status => Promise.resolve());
-      sandbox.stub(element.$.restAPI, 'setAccountUsername',
+      sinon.stub(element.$.restAPI, 'setAccountUsername').callsFake(
           username => Promise.resolve());
     });
 
@@ -278,11 +258,11 @@
     let statusStub;
 
     setup(() => {
-      statusChangedSpy = sandbox.spy(element, '_statusChanged');
+      statusChangedSpy = sinon.spy(element, '_statusChanged');
       element.set('_serverConfig',
           {auth: {editable_account_fields: []}});
 
-      statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
+      statusStub = sinon.stub(element.$.restAPI, 'setAccountStatus').callsFake(
           status => Promise.resolve());
     });
 
@@ -339,4 +319,4 @@
     assert.equal(element._hideAvatarChangeUrl('https://example.com'), '');
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html
deleted file mode 100644
index 3a2b86d..0000000
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html
+++ /dev/null
@@ -1,70 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-settings-view</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-agreements-list></gr-agreements-list>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-agreements-list.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-suite('gr-agreements-list tests', () => {
-  let element;
-  let agreements;
-
-  setup(done => {
-    agreements = [{
-      url: 'some url',
-      description: 'Agreements 1 description',
-      name: 'Agreements 1',
-    }];
-
-    stub('gr-rest-api-interface', {
-      getAccountAgreements() { return Promise.resolve(agreements); },
-    });
-
-    element = fixture('basic');
-
-    element.loadData().then(() => { flush(done); });
-  });
-
-  test('renders', () => {
-    const rows = dom(element.root).querySelectorAll('tbody tr');
-
-    assert.equal(rows.length, 1);
-
-    const nameCells = Array.from(rows).map(row =>
-      row.querySelectorAll('td')[0].textContent.trim()
-    );
-
-    assert.equal(nameCells[0], 'Agreements 1');
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.js b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.js
new file mode 100644
index 0000000..ed0bdb3
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.js
@@ -0,0 +1,56 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-agreements-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromElement('gr-agreements-list');
+
+suite('gr-agreements-list tests', () => {
+  let element;
+  let agreements;
+
+  setup(done => {
+    agreements = [{
+      url: 'some url',
+      description: 'Agreements 1 description',
+      name: 'Agreements 1',
+    }];
+
+    stub('gr-rest-api-interface', {
+      getAccountAgreements() { return Promise.resolve(agreements); },
+    });
+
+    element = basicFixture.instantiate();
+
+    element.loadData().then(() => { flush(done); });
+  });
+
+  test('renders', () => {
+    const rows = dom(element.root).querySelectorAll('tbody tr');
+
+    assert.equal(rows.length, 1);
+
+    const nameCells = Array.from(rows).map(row =>
+      row.querySelectorAll('td')[0].textContent.trim()
+    );
+
+    assert.equal(nameCells[0], 'Agreements 1');
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.js
similarity index 70%
rename from polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
rename to polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.js
index 27532b4..3fca9d2 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.js
@@ -1,47 +1,31 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-settings-view</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-change-table-editor></gr-change-table-editor>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-change-table-editor.js';
+
+const basicFixture = fixtureFromElement('gr-change-table-editor');
+
 suite('gr-change-table-editor tests', () => {
   let element;
   let columns;
-  let sandbox;
 
   setup(() => {
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
+    element = basicFixture.instantiate();
 
     columns = [
       'Subject',
@@ -60,10 +44,6 @@
     flushAsynchronousOperations();
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('renders', () => {
     const rows = element.shadowRoot
         .querySelector('tbody').querySelectorAll('tr');
@@ -126,8 +106,8 @@
   });
 
   test('_handleCheckboxContainerClick relays taps to checkboxes', () => {
-    sandbox.stub(element, '_handleNumberCheckboxClick');
-    sandbox.stub(element, '_handleTargetClick');
+    sinon.stub(element, '_handleNumberCheckboxClick');
+    sinon.stub(element, '_handleTargetClick');
 
     MockInteractions.tap(
         element.shadowRoot
@@ -143,7 +123,7 @@
   });
 
   test('_handleNumberCheckboxClick', () => {
-    sandbox.spy(element, '_handleNumberCheckboxClick');
+    sinon.spy(element, '_handleNumberCheckboxClick');
 
     MockInteractions
         .tap(element.shadowRoot
@@ -159,7 +139,7 @@
   });
 
   test('_handleTargetClick', () => {
-    sandbox.spy(element, '_handleTargetClick');
+    sinon.spy(element, '_handleTargetClick');
     assert.include(element.displayedColumns, 'Assignee');
     MockInteractions
         .tap(element.shadowRoot
@@ -168,4 +148,4 @@
     assert.notInclude(element.displayedColumns, 'Assignee');
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.js
similarity index 80%
rename from polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html
rename to polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.js
index bc3c10c..6f89c49 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.js
@@ -1,40 +1,26 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2018 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-cla-view</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-cla-view></gr-cla-view>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-cla-view.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromElement('gr-cla-view');
+
 suite('gr-cla-view tests', () => {
   let element;
   const signedAgreements = [{
@@ -124,7 +110,7 @@
       getAccountGroups() { return Promise.resolve(groups); },
       getAccountAgreements() { return Promise.resolve(signedAgreements); },
     });
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.loadData().then(() => { flush(done); });
   });
 
@@ -191,4 +177,4 @@
         'test_cla.html'), '/test_cla.html');
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.js
similarity index 66%
rename from polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
rename to polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.js
index 3cc7bfe..b1b8b61 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.js
@@ -1,42 +1,28 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2018 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-edit-preferences</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-edit-preferences></gr-edit-preferences>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-edit-preferences.js';
+
+const basicFixture = fixtureFromElement('gr-edit-preferences');
+
 suite('gr-edit-preferences tests', () => {
   let element;
-  let sandbox;
+
   let editPreferences;
 
   function valueOf(title, fieldsetid) {
@@ -76,13 +62,11 @@
       },
     });
 
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
+    element = basicFixture.instantiate();
+
     return element.loadData();
   });
 
-  teardown(() => { sandbox.restore(); });
-
   test('renders', () => {
     // Rendered with the expected preferences selected.
     assert.equal(valueOf('Tab width', 'editPreferences')
@@ -108,7 +92,7 @@
   });
 
   test('save changes', () => {
-    sandbox.stub(element.$.restAPI, 'saveEditPreferences')
+    sinon.stub(element.$.restAPI, 'saveEditPreferences')
         .returns(Promise.resolve());
     const showTabsCheckbox = valueOf('Show tabs', 'editPreferences')
         .firstElementChild;
@@ -123,4 +107,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.js
similarity index 75%
rename from polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
rename to polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.js
index ad2553d..805b8c8 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.js
@@ -1,39 +1,25 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-email-editor</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-email-editor></gr-email-editor>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-email-editor.js';
+
+const basicFixture = fixtureFromElement('gr-email-editor');
+
 suite('gr-email-editor tests', () => {
   let element;
 
@@ -48,7 +34,7 @@
       getAccountEmails() { return Promise.resolve(emails); },
     });
 
-    element = fixture('basic');
+    element = basicFixture.instantiate();
 
     element.loadData().then(flush(done));
   });
@@ -149,4 +135,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.js
similarity index 77%
rename from polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
rename to polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.js
index 4a0af5b..5281e17 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.js
@@ -1,40 +1,26 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-gpg-editor</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-gpg-editor></gr-gpg-editor>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-gpg-editor.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromElement('gr-gpg-editor');
+
 suite('gr-gpg-editor tests', () => {
   let element;
   let keys;
@@ -69,7 +55,7 @@
       getAccountGPGKeys() { return Promise.resolve(keys); },
     });
 
-    element = fixture('basic');
+    element = basicFixture.instantiate();
 
     element.loadData().then(() => { flush(done); });
   });
@@ -89,8 +75,8 @@
   test('remove key', done => {
     const lastKey = keys[Object.keys(keys)[1]];
 
-    const saveStub = sinon.stub(element.$.restAPI, 'deleteAccountGPGKey',
-        () => Promise.resolve());
+    const saveStub = sinon.stub(element.$.restAPI, 'deleteAccountGPGKey')
+        .callsFake(() => Promise.resolve());
 
     assert.equal(element._keysToRemove.length, 0);
     assert.isFalse(element.hasUnsavedChanges);
@@ -145,7 +131,7 @@
       },
     };
 
-    const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey',
+    const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey').callsFake(
         () => Promise.resolve(newKeyObject));
 
     element._newKey = newKeyString;
@@ -170,7 +156,7 @@
   test('add invalid key', done => {
     const newKeyString = 'not even close to valid';
 
-    const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey',
+    const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey').callsFake(
         () => Promise.reject(new Error('error')));
 
     element._newKey = newKeyString;
@@ -192,4 +178,4 @@
     assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]});
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
deleted file mode 100644
index 2fdc7b3..0000000
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
+++ /dev/null
@@ -1,124 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-settings-view</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-group-list></gr-group-list>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-group-list.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-
-suite('gr-group-list tests', () => {
-  let sandbox;
-  let element;
-  let groups;
-
-  setup(done => {
-    sandbox = sinon.sandbox.create();
-    groups = [{
-      url: 'some url',
-      options: {},
-      description: 'Group 1 description',
-      group_id: 1,
-      owner: 'Administrators',
-      owner_id: '123',
-      id: 'abc',
-      name: 'Group 1',
-    }, {
-      options: {visible_to_all: true},
-      id: '456',
-      name: 'Group 2',
-    }, {
-      options: {},
-      id: '789',
-      name: 'Group 3',
-    }];
-
-    stub('gr-rest-api-interface', {
-      getAccountGroups() { return Promise.resolve(groups); },
-    });
-
-    element = fixture('basic');
-
-    element.loadData().then(() => { flush(done); });
-  });
-
-  teardown(() => { sandbox.restore(); });
-
-  test('renders', () => {
-    const rows = Array.from(
-        dom(element.root).querySelectorAll('tbody tr'));
-
-    assert.equal(rows.length, 3);
-
-    const nameCells = rows.map(row =>
-      row.querySelectorAll('td a')[0].textContent.trim()
-    );
-
-    assert.equal(nameCells[0], 'Group 1');
-    assert.equal(nameCells[1], 'Group 2');
-    assert.equal(nameCells[2], 'Group 3');
-  });
-
-  test('_computeVisibleToAll', () => {
-    assert.equal(element._computeVisibleToAll(groups[0]), 'No');
-    assert.equal(element._computeVisibleToAll(groups[1]), 'Yes');
-  });
-
-  test('_computeGroupPath', () => {
-    let urlStub = sandbox.stub(GerritNav, 'getUrlForGroup',
-        () => '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5');
-
-    let group = {
-      id: 'e2cd66f88a2db4d391ac068a92d987effbe872f5',
-    };
-    assert.equal(element._computeGroupPath(group),
-        '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5');
-
-    group = {
-      name: 'admin',
-    };
-    assert.isUndefined(element._computeGroupPath(group));
-
-    urlStub.restore();
-
-    urlStub = sandbox.stub(GerritNav, 'getUrlForGroup',
-        () => '/admin/groups/user/test');
-
-    group = {
-      id: 'user%2Ftest',
-    };
-    assert.equal(element._computeGroupPath(group),
-        '/admin/groups/user/test');
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.js b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.js
new file mode 100644
index 0000000..bfd42ac
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.js
@@ -0,0 +1,105 @@
+/**
+ * @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-group-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+
+const basicFixture = fixtureFromElement('gr-group-list');
+
+suite('gr-group-list tests', () => {
+  let element;
+  let groups;
+
+  setup(done => {
+    groups = [{
+      url: 'some url',
+      options: {},
+      description: 'Group 1 description',
+      group_id: 1,
+      owner: 'Administrators',
+      owner_id: '123',
+      id: 'abc',
+      name: 'Group 1',
+    }, {
+      options: {visible_to_all: true},
+      id: '456',
+      name: 'Group 2',
+    }, {
+      options: {},
+      id: '789',
+      name: 'Group 3',
+    }];
+
+    stub('gr-rest-api-interface', {
+      getAccountGroups() { return Promise.resolve(groups); },
+    });
+
+    element = basicFixture.instantiate();
+
+    element.loadData().then(() => { flush(done); });
+  });
+
+  test('renders', () => {
+    const rows = Array.from(
+        dom(element.root).querySelectorAll('tbody tr'));
+
+    assert.equal(rows.length, 3);
+
+    const nameCells = rows.map(row =>
+      row.querySelectorAll('td a')[0].textContent.trim()
+    );
+
+    assert.equal(nameCells[0], 'Group 1');
+    assert.equal(nameCells[1], 'Group 2');
+    assert.equal(nameCells[2], 'Group 3');
+  });
+
+  test('_computeVisibleToAll', () => {
+    assert.equal(element._computeVisibleToAll(groups[0]), 'No');
+    assert.equal(element._computeVisibleToAll(groups[1]), 'Yes');
+  });
+
+  test('_computeGroupPath', () => {
+    let urlStub = sinon.stub(GerritNav, 'getUrlForGroup').callsFake(
+        () => '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5');
+
+    let group = {
+      id: 'e2cd66f88a2db4d391ac068a92d987effbe872f5',
+    };
+    assert.equal(element._computeGroupPath(group),
+        '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5');
+
+    group = {
+      name: 'admin',
+    };
+    assert.isUndefined(element._computeGroupPath(group));
+
+    urlStub.restore();
+
+    urlStub = sinon.stub(GerritNav, 'getUrlForGroup').callsFake(
+        () => '/admin/groups/user/test');
+
+    group = {
+      id: 'user%2Ftest',
+    };
+    assert.equal(element._computeGroupPath(group),
+        '/admin/groups/user/test');
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
deleted file mode 100644
index 26fa84d..0000000
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
+++ /dev/null
@@ -1,91 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-settings-view</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-http-password></gr-http-password>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-http-password.js';
-suite('gr-http-password tests', () => {
-  let element;
-  let account;
-  let config;
-
-  setup(done => {
-    account = {username: 'user name'};
-    config = {auth: {}};
-
-    stub('gr-rest-api-interface', {
-      getAccount() { return Promise.resolve(account); },
-      getConfig() { return Promise.resolve(config); },
-    });
-
-    element = fixture('basic');
-    element.loadData().then(() => { flush(done); });
-  });
-
-  test('generate password', () => {
-    const button = element.$.generateButton;
-    const nextPassword = 'the new password';
-    let generateResolve;
-    const generateStub = sinon.stub(element.$.restAPI,
-        'generateAccountHttpPassword', () => new Promise(resolve => {
-          generateResolve = resolve;
-        }));
-
-    assert.isNotOk(element._generatedPassword);
-
-    MockInteractions.tap(button);
-
-    assert.isTrue(generateStub.called);
-    assert.equal(element._generatedPassword, 'Generating...');
-
-    generateResolve(nextPassword);
-
-    generateStub.lastCall.returnValue.then(() => {
-      assert.equal(element._generatedPassword, nextPassword);
-    });
-  });
-
-  test('without http_password_url', () => {
-    assert.isNull(element._passwordUrl);
-  });
-
-  test('with http_password_url', done => {
-    config.auth.http_password_url = 'http://example.com/';
-    element.loadData().then(() => {
-      assert.isNotNull(element._passwordUrl);
-      assert.equal(element._passwordUrl, config.auth.http_password_url);
-      done();
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.js b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.js
new file mode 100644
index 0000000..920ad48
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.js
@@ -0,0 +1,78 @@
+/**
+ * @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-http-password.js';
+
+const basicFixture = fixtureFromElement('gr-http-password');
+
+suite('gr-http-password tests', () => {
+  let element;
+  let account;
+  let config;
+
+  setup(done => {
+    account = {username: 'user name'};
+    config = {auth: {}};
+
+    stub('gr-rest-api-interface', {
+      getAccount() { return Promise.resolve(account); },
+      getConfig() { return Promise.resolve(config); },
+    });
+
+    element = basicFixture.instantiate();
+    element.loadData().then(() => { flush(done); });
+  });
+
+  test('generate password', () => {
+    const button = element.$.generateButton;
+    const nextPassword = 'the new password';
+    let generateResolve;
+    const generateStub = sinon.stub(element.$.restAPI,
+        'generateAccountHttpPassword')
+        .callsFake(() => new Promise(resolve => {
+          generateResolve = resolve;
+        }));
+
+    assert.isNotOk(element._generatedPassword);
+
+    MockInteractions.tap(button);
+
+    assert.isTrue(generateStub.called);
+    assert.equal(element._generatedPassword, 'Generating...');
+
+    generateResolve(nextPassword);
+
+    generateStub.lastCall.returnValue.then(() => {
+      assert.equal(element._generatedPassword, nextPassword);
+    });
+  });
+
+  test('without http_password_url', () => {
+    assert.isNull(element._passwordUrl);
+  });
+
+  test('with http_password_url', done => {
+    config.auth.http_password_url = 'http://example.com/';
+    element.loadData().then(() => {
+      assert.isNotNull(element._passwordUrl);
+      assert.equal(element._passwordUrl, config.auth.http_password_url);
+      done();
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.js
similarity index 70%
rename from polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html
rename to polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.js
index 0965826..e01c58b 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.js
@@ -1,43 +1,29 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-identities</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-identities></gr-identities>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-identities.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromElement('gr-identities');
+
 suite('gr-identities tests', () => {
   let element;
-  let sandbox;
+
   const ids = [
     {
       identity: 'username:john',
@@ -55,21 +41,15 @@
   ];
 
   setup(done => {
-    sandbox = sinon.sandbox.create();
-
     stub('gr-rest-api-interface', {
       getExternalIds() { return Promise.resolve(ids); },
     });
 
-    element = fixture('basic');
+    element = basicFixture.instantiate();
 
     element.loadData().then(() => { flush(done); });
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('renders', () => {
     const rows = Array.from(
         dom(element.root).querySelectorAll('tbody tr'));
@@ -112,7 +92,7 @@
 
   test('delete id', done => {
     element._idName = 'mailto:gerrit2@example.com';
-    const loadDataStub = sandbox.stub(element, 'loadData');
+    const loadDataStub = sinon.stub(element, 'loadData');
     element._handleDeleteItemConfirm().then(() => {
       assert.isTrue(loadDataStub.called);
       done();
@@ -122,7 +102,7 @@
   test('_handleDeleteItem opens modal', () => {
     const deleteBtn =
         dom(element.root).querySelector('.deleteButton');
-    const deleteItem = sandbox.stub(element, '_handleDeleteItem');
+    const deleteItem = sinon.stub(element, '_handleDeleteItem');
     MockInteractions.tap(deleteBtn);
     assert.isTrue(deleteItem.called);
   });
@@ -187,4 +167,4 @@
     assert.isFalse(element._showLinkAnotherIdentity);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.js
similarity index 77%
rename from polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
rename to polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.js
index 9c8db6d..19852d9 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.js
@@ -1,40 +1,26 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-settings-view</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-menu-editor></gr-menu-editor>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-menu-editor.js';
 import {flush as flush$0} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromElement('gr-menu-editor');
+
 suite('gr-menu-editor tests', () => {
   let element;
   let menu;
@@ -61,7 +47,7 @@
   }
 
   setup(done => {
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     menu = [
       {url: '/first/url', name: 'first name', target: '_blank'},
       {url: '/second/url', name: 'second name', target: '_blank'},
@@ -175,4 +161,4 @@
     assertMenuNamesEqual(element, ['new name']);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.js
similarity index 72%
rename from polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
rename to polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.js
index a3f8548..468ef57 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.js
@@ -1,53 +1,31 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-registration-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-registration-dialog></gr-registration-dialog>
-  </template>
-</test-fixture>
-
-<test-fixture id="blank">
-  <template>
-    <div></div>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+/**
+ * @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-registration-dialog.js';
+
+const basicFixture = fixtureFromElement('gr-registration-dialog');
+
 suite('gr-registration-dialog tests', () => {
   let element;
   let account;
-  let sandbox;
+
   let _listeners;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     _listeners = {};
 
     account = {
@@ -82,13 +60,12 @@
       },
     });
 
-    element = fixture('basic');
+    element = basicFixture.instantiate();
 
     return element.loadData();
   });
 
   teardown(() => {
-    sandbox.restore();
     for (const eventType in _listeners) {
       if (_listeners.hasOwnProperty(eventType)) {
         element.removeEventListener(eventType, _listeners[eventType]);
@@ -183,4 +160,4 @@
         {auth: {editable_account_fields: []}}, 'abc'));
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.js
index 09ad0e3..c0ec344 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.js
@@ -28,7 +28,6 @@
   let account;
   let preferences;
   let config;
-  let sandbox;
 
   function valueOf(title, fieldsetid) {
     const sections = element.$[fieldsetid].querySelectorAll('section');
@@ -51,12 +50,11 @@
   }
 
   function stubAddAccountEmail(statusCode) {
-    return sandbox.stub(element.$.restAPI, 'addAccountEmail',
+    return sinon.stub(element.$.restAPI, 'addAccountEmail').callsFake(
         () => Promise.resolve({status: statusCode}));
   }
 
   setup(done => {
-    sandbox = sinon.sandbox.create();
     account = {
       _account_id: 123,
       name: 'user name',
@@ -100,10 +98,6 @@
     element._loadingPromise.then(done);
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('theme changing', () => {
     window.localStorage.removeItem('dark-theme');
     assert.isFalse(window.localStorage.getItem('dark-theme') === 'true');
@@ -119,7 +113,7 @@
   });
 
   test('calls the title-change event', () => {
-    const titleChangedStub = sandbox.stub();
+    const titleChangedStub = sinon.stub();
 
     // Create a new view.
     const newElement = document.createElement('gr-settings-view');
@@ -346,7 +340,7 @@
   });
 
   test('emails are loaded without emailToken', () => {
-    sandbox.stub(element.$.emailEditor, 'loadData');
+    sinon.stub(element.$.emailEditor, 'loadData');
     element.params = {};
     element.attached();
     assert.isTrue(element.$.emailEditor.loadData.calledOnce);
@@ -356,7 +350,7 @@
     let newColumns = ['Owner', 'Project', 'Branch'];
     element._localChangeTableColumns = newColumns.slice(0);
     element._showNumber = false;
-    const cloneStub = sandbox.stub(element, '_cloneChangeTableColumns');
+    const cloneStub = sinon.stub(element, '_cloneChangeTableColumns');
     element._handleSaveChangeTable();
     assert.isTrue(cloneStub.calledOnce);
     assert.deepEqual(element.prefs.change_table, newColumns);
@@ -400,7 +394,7 @@
   });
 
   test('test that reset button is called', () => {
-    const overlayOpen = sandbox.stub(element, '_handleResetMenuButton');
+    const overlayOpen = sinon.stub(element, '_handleResetMenuButton');
 
     MockInteractions.tap(element.$.resetMenu);
 
@@ -484,11 +478,13 @@
     let resolveConfirm;
 
     setup(() => {
-      sandbox.stub(element.$.emailEditor, 'loadData');
-      sandbox.stub(
+      sinon.stub(element.$.emailEditor, 'loadData');
+      sinon.stub(
           element.$.restAPI,
-          'confirmEmail',
-          () => new Promise(resolve => { resolveConfirm = resolve; }));
+          'confirmEmail')
+          .callsFake(
+              () => new Promise(
+                  resolve => { resolveConfirm = resolve; }));
       element.params = {emailToken: 'foo'};
       element.attached();
     });
@@ -511,7 +507,7 @@
     });
 
     test('show-alert is fired when email is confirmed', done => {
-      sandbox.spy(element, 'dispatchEvent');
+      sinon.spy(element, 'dispatchEvent');
       element._loadingPromise.then(() => {
         assert.equal(
             element.dispatchEvent.lastCall.args[0].type, 'show-alert');
@@ -523,4 +519,4 @@
       resolveConfirm('bar');
     });
   });
-});
\ No newline at end of file
+});
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.js
similarity index 75%
rename from polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
rename to polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.js
index 56625ae..d4a0372 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.js
@@ -1,40 +1,26 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-ssh-editor</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-ssh-editor></gr-ssh-editor>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-ssh-editor.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromElement('gr-ssh-editor');
+
 suite('gr-ssh-editor tests', () => {
   let element;
   let keys;
@@ -60,7 +46,7 @@
       getAccountSSHKeys() { return Promise.resolve(keys); },
     });
 
-    element = fixture('basic');
+    element = basicFixture.instantiate();
 
     element.loadData().then(() => { flush(done); });
   });
@@ -80,8 +66,8 @@
   test('remove key', done => {
     const lastKey = keys[1];
 
-    const saveStub = sinon.stub(element.$.restAPI, 'deleteAccountSSHKey',
-        () => Promise.resolve());
+    const saveStub = sinon.stub(element.$.restAPI, 'deleteAccountSSHKey')
+        .callsFake(() => Promise.resolve());
 
     assert.equal(element._keysToRemove.length, 0);
     assert.isFalse(element.hasUnsavedChanges);
@@ -131,7 +117,7 @@
       valid: true,
     };
 
-    const addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey',
+    const addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey').callsFake(
         () => Promise.resolve(newKeyObject));
 
     element._newKey = newKeyString;
@@ -156,7 +142,7 @@
   test('add invalid key', done => {
     const newKeyString = 'not even close to valid';
 
-    const addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey',
+    const addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey').callsFake(
         () => Promise.reject(new Error('error')));
 
     element._newKey = newKeyString;
@@ -178,4 +164,4 @@
     assert.equal(addStub.lastCall.args[0], newKeyString);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.js
similarity index 81%
rename from polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
rename to polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.js
index 2a08e4f..d42f579 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.js
@@ -1,39 +1,25 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-settings-view</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-watched-projects-editor></gr-watched-projects-editor>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-watched-projects-editor.js';
+
+const basicFixture = fixtureFromElement('gr-watched-projects-editor');
+
 suite('gr-watched-projects-editor tests', () => {
   let element;
 
@@ -75,7 +61,7 @@
       },
     });
 
-    element = fixture('basic');
+    element = basicFixture.instantiate();
 
     element.loadData().then(() => { flush(done); });
   });
@@ -212,4 +198,4 @@
     assert.equal(element._projectsToRemove[0].project, 'project b');
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html
deleted file mode 100644
index 5899ad4..0000000
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html
+++ /dev/null
@@ -1,109 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-account-entry</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-account-entry></gr-account-entry>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-account-entry.js';
-suite('gr-account-entry tests', () => {
-  let sandbox;
-  let element;
-
-  const suggestion1 = {
-    email: 'email1@example.com',
-    _account_id: 1,
-    some_property: 'value',
-  };
-  const suggestion2 = {
-    email: 'email2@example.com',
-    _account_id: 2,
-  };
-  const suggestion3 = {
-    email: 'email25@example.com',
-    _account_id: 25,
-    some_other_property: 'other value',
-  };
-
-  setup(done => {
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
-    return flush(done);
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  suite('stubbed values for querySuggestions', () => {
-    setup(() => {
-      element.querySuggestions = input => Promise.resolve([
-        suggestion1,
-        suggestion2,
-        suggestion3,
-      ]);
-    });
-  });
-
-  test('account-text-changed fired when input text changed and allowAnyInput',
-      () => {
-        // Spy on query, as that is called when _updateSuggestions proceeds.
-        const changeStub = sandbox.stub();
-        element.allowAnyInput = true;
-        element.querySuggestions = input => Promise.resolve([]);
-        element.addEventListener('account-text-changed', changeStub);
-        element.$.input.text = 'a';
-        assert.isTrue(changeStub.calledOnce);
-        element.$.input.text = 'ab';
-        assert.isTrue(changeStub.calledTwice);
-      });
-
-  test('account-text-changed not fired when input text changed without ' +
-      'allowAnyInput', () => {
-    // Spy on query, as that is called when _updateSuggestions proceeds.
-    const changeStub = sandbox.stub();
-    element.querySuggestions = input => Promise.resolve([]);
-    element.addEventListener('account-text-changed', changeStub);
-    element.$.input.text = 'a';
-    assert.isFalse(changeStub.called);
-  });
-
-  test('setText', () => {
-    // Spy on query, as that is called when _updateSuggestions proceeds.
-    const suggestSpy = sandbox.spy(element.$.input, 'query');
-    element.setText('test text');
-    flushAsynchronousOperations();
-
-    assert.equal(element.$.input.$.input.value, 'test text');
-    assert.isFalse(suggestSpy.called);
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.js b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.js
new file mode 100644
index 0000000..396145b
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.js
@@ -0,0 +1,90 @@
+/**
+ * @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-account-entry.js';
+
+const basicFixture = fixtureFromElement('gr-account-entry');
+
+suite('gr-account-entry tests', () => {
+  let element;
+
+  const suggestion1 = {
+    email: 'email1@example.com',
+    _account_id: 1,
+    some_property: 'value',
+  };
+  const suggestion2 = {
+    email: 'email2@example.com',
+    _account_id: 2,
+  };
+  const suggestion3 = {
+    email: 'email25@example.com',
+    _account_id: 25,
+    some_other_property: 'other value',
+  };
+
+  setup(done => {
+    element = basicFixture.instantiate();
+
+    return flush(done);
+  });
+
+  suite('stubbed values for querySuggestions', () => {
+    setup(() => {
+      element.querySuggestions = input => Promise.resolve([
+        suggestion1,
+        suggestion2,
+        suggestion3,
+      ]);
+    });
+  });
+
+  test('account-text-changed fired when input text changed and allowAnyInput',
+      () => {
+        // Spy on query, as that is called when _updateSuggestions proceeds.
+        const changeStub = sinon.stub();
+        element.allowAnyInput = true;
+        element.querySuggestions = input => Promise.resolve([]);
+        element.addEventListener('account-text-changed', changeStub);
+        element.$.input.text = 'a';
+        assert.isTrue(changeStub.calledOnce);
+        element.$.input.text = 'ab';
+        assert.isTrue(changeStub.calledTwice);
+      });
+
+  test('account-text-changed not fired when input text changed without ' +
+      'allowAnyInput', () => {
+    // Spy on query, as that is called when _updateSuggestions proceeds.
+    const changeStub = sinon.stub();
+    element.querySuggestions = input => Promise.resolve([]);
+    element.addEventListener('account-text-changed', changeStub);
+    element.$.input.text = 'a';
+    assert.isFalse(changeStub.called);
+  });
+
+  test('setText', () => {
+    // Spy on query, as that is called when _updateSuggestions proceeds.
+    const suggestSpy = sinon.spy(element.$.input, 'query');
+    element.setText('test text');
+    flushAsynchronousOperations();
+
+    assert.equal(element.$.input.$.input.value, 'test text');
+    assert.isFalse(suggestSpy.called);
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
deleted file mode 100644
index 4cc66c4..0000000
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
+++ /dev/null
@@ -1,94 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-account-label</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-account-label></gr-account-label>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-account-label.js';
-suite('gr-account-label tests', () => {
-  let element;
-
-  setup(() => {
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({}); },
-      getLoggedIn() { return Promise.resolve(false); },
-    });
-    element = fixture('basic');
-    element._config = {
-      user: {
-        anonymous_coward_name: 'Anonymous Coward',
-      },
-    };
-  });
-
-  test('null guard', () => {
-    assert.doesNotThrow(() => {
-      element.account = null;
-    });
-  });
-
-  suite('_computeName', () => {
-    test('not showing anonymous', () => {
-      const account = {name: 'Wyatt'};
-      assert.deepEqual(element._computeName(account, null), 'Wyatt');
-    });
-
-    test('showing anonymous but no config', () => {
-      const account = {};
-      assert.deepEqual(element._computeName(account, null),
-          'Anonymous');
-    });
-
-    test('test for Anonymous Coward user and replace with Anonymous', () => {
-      const config = {
-        user: {
-          anonymous_coward_name: 'Anonymous Coward',
-        },
-      };
-      const account = {};
-      assert.deepEqual(element._computeName(account, config),
-          'Anonymous');
-    });
-
-    test('test for anonymous_coward_name', () => {
-      const config = {
-        user: {
-          anonymous_coward_name: 'TestAnon',
-        },
-      };
-      const account = {};
-      assert.deepEqual(element._computeName(account, config),
-          'TestAnon');
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.js
new file mode 100644
index 0000000..94274a7
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.js
@@ -0,0 +1,80 @@
+/**
+ * @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-account-label.js';
+
+const basicFixture = fixtureFromElement('gr-account-label');
+
+suite('gr-account-label tests', () => {
+  let element;
+
+  setup(() => {
+    stub('gr-rest-api-interface', {
+      getConfig() { return Promise.resolve({}); },
+      getLoggedIn() { return Promise.resolve(false); },
+    });
+    element = basicFixture.instantiate();
+    element._config = {
+      user: {
+        anonymous_coward_name: 'Anonymous Coward',
+      },
+    };
+  });
+
+  test('null guard', () => {
+    assert.doesNotThrow(() => {
+      element.account = null;
+    });
+  });
+
+  suite('_computeName', () => {
+    test('not showing anonymous', () => {
+      const account = {name: 'Wyatt'};
+      assert.deepEqual(element._computeName(account, null), 'Wyatt');
+    });
+
+    test('showing anonymous but no config', () => {
+      const account = {};
+      assert.deepEqual(element._computeName(account, null),
+          'Anonymous');
+    });
+
+    test('test for Anonymous Coward user and replace with Anonymous', () => {
+      const config = {
+        user: {
+          anonymous_coward_name: 'Anonymous Coward',
+        },
+      };
+      const account = {};
+      assert.deepEqual(element._computeName(account, config),
+          'Anonymous');
+    });
+
+    test('test for anonymous_coward_name', () => {
+      const config = {
+        user: {
+          anonymous_coward_name: 'TestAnon',
+        },
+      };
+      const account = {};
+      assert.deepEqual(element._computeName(account, config),
+          'TestAnon');
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
deleted file mode 100644
index f3bff6e..0000000
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
+++ /dev/null
@@ -1,81 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-account-link</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-account-link></gr-account-link>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-account-link.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-
-suite('gr-account-link tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({}); },
-    });
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('computed fields', () => {
-    const url = 'test/url';
-    const urlStub = sandbox.stub(GerritNav, 'getUrlForOwner').returns(url);
-    const account = {
-      email: 'email',
-      username: 'username',
-      name: 'name',
-      _account_id: '_account_id',
-    };
-    assert.isNotOk(element._computeOwnerLink());
-    assert.equal(element._computeOwnerLink(account), url);
-    assert.isTrue(urlStub.lastCall.calledWithExactly('email'));
-
-    delete account.email;
-    assert.equal(element._computeOwnerLink(account), url);
-    assert.isTrue(urlStub.lastCall.calledWithExactly('username'));
-
-    delete account.username;
-    assert.equal(element._computeOwnerLink(account), url);
-    assert.isTrue(urlStub.lastCall.calledWithExactly('name'));
-
-    delete account.name;
-    assert.equal(element._computeOwnerLink(account), url);
-    assert.isTrue(urlStub.lastCall.calledWithExactly('_account_id'));
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.js
new file mode 100644
index 0000000..554953e
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.js
@@ -0,0 +1,60 @@
+/**
+ * @license
+ * Copyright (C) 2015 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-account-link.js';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+
+const basicFixture = fixtureFromElement('gr-account-link');
+
+suite('gr-account-link tests', () => {
+  let element;
+
+  setup(() => {
+    stub('gr-rest-api-interface', {
+      getConfig() { return Promise.resolve({}); },
+    });
+    element = basicFixture.instantiate();
+  });
+
+  test('computed fields', () => {
+    const url = 'test/url';
+    const urlStub = sinon.stub(GerritNav, 'getUrlForOwner').returns(url);
+    const account = {
+      email: 'email',
+      username: 'username',
+      name: 'name',
+      _account_id: '_account_id',
+    };
+    assert.isNotOk(element._computeOwnerLink());
+    assert.equal(element._computeOwnerLink(account), url);
+    assert.isTrue(urlStub.lastCall.calledWithExactly('email'));
+
+    delete account.email;
+    assert.equal(element._computeOwnerLink(account), url);
+    assert.isTrue(urlStub.lastCall.calledWithExactly('username'));
+
+    delete account.username;
+    assert.equal(element._computeOwnerLink(account), url);
+    assert.isTrue(urlStub.lastCall.calledWithExactly('name'));
+
+    delete account.name;
+    assert.equal(element._computeOwnerLink(account), url);
+    assert.isTrue(urlStub.lastCall.calledWithExactly('_account_id'));
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.js
similarity index 84%
rename from polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
rename to polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.js
index 41158a8..b26f468 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.js
@@ -1,41 +1,26 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-account-list</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-account-list></gr-account-list>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-account-list.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 
+const basicFixture = fixtureFromElement('gr-account-list');
+
 class MockSuggestionsProvider {
   getSuggestions(input) {
     return Promise.resolve([]);
@@ -64,7 +49,7 @@
 
   let existingAccount1;
   let existingAccount2;
-  let sandbox;
+
   let element;
   let suggestionsProvider;
 
@@ -73,23 +58,18 @@
   }
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     existingAccount1 = makeAccount();
     existingAccount2 = makeAccount();
 
     stub('gr-rest-api-interface', {
       getConfig() { return Promise.resolve({}); },
     });
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.accounts = [existingAccount1, existingAccount2];
     suggestionsProvider = new MockSuggestionsProvider();
     element.suggestionsProvider = suggestionsProvider;
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('account entry only appears when editable', () => {
     element.readonly = false;
     assert.isFalse(element.$.entry.hasAttribute('hidden'));
@@ -99,7 +79,7 @@
 
   test('addition and removal of account/group chips', () => {
     flushAsynchronousOperations();
-    sandbox.stub(element, '_computeRemovable').returns(true);
+    sinon.stub(element, '_computeRemovable').returns(true);
     // Existing accounts are listed.
     let chips = getChips();
     assert.equal(chips.length, 2);
@@ -195,14 +175,15 @@
         _account_id: 25,
       },
     ];
-    sandbox.stub(suggestionsProvider, 'getSuggestions')
+    sinon.stub(suggestionsProvider, 'getSuggestions')
         .returns(Promise.resolve(originalSuggestions));
-    sandbox.stub(suggestionsProvider, 'makeSuggestionItem', suggestion => {
-      return {
-        name: suggestion.email,
-        value: suggestion._account_id,
-      };
-    });
+    sinon.stub(suggestionsProvider, 'makeSuggestionItem')
+        .callsFake( suggestion => {
+          return {
+            name: suggestion.email,
+            value: suggestion._account_id,
+          };
+        });
 
     element._getSuggestions().then(suggestions => {
       // Default is no filtering.
@@ -257,13 +238,13 @@
     element.allowAnyInput = true;
     flushAsynchronousOperations();
 
-    const getTextStub = sandbox.stub(element.$.entry, 'getText');
+    const getTextStub = sinon.stub(element.$.entry, 'getText');
     getTextStub.onFirstCall().returns('');
     getTextStub.onSecondCall().returns('test');
     getTextStub.onThirdCall().returns('test@test');
 
     // When entry is empty, return true.
-    const clearStub = sandbox.stub(element.$.entry, 'clear');
+    const clearStub = sinon.stub(element.$.entry, 'clear');
     assert.isTrue(element.submitEntryText());
     assert.isFalse(clearStub.called);
 
@@ -381,11 +362,12 @@
       },
     ];
     const getSuggestionsStub =
-        sandbox.stub(suggestionsProvider, 'getSuggestions')
+        sinon.stub(suggestionsProvider, 'getSuggestions')
             .returns(Promise.resolve(suggestions));
 
     const makeSuggestionItemStub =
-        sandbox.stub(suggestionsProvider, 'makeSuggestionItem', item => item);
+        sinon.stub(suggestionsProvider, 'makeSuggestionItem')
+            .callsFake( item => item);
 
     const input = element.$.entry.$.input;
 
@@ -414,11 +396,12 @@
       },
     ];
     const getSuggestionsStub =
-        sandbox.stub(suggestionsProvider, 'getSuggestions')
+        sinon.stub(suggestionsProvider, 'getSuggestions')
             .returns(Promise.resolve(suggestions));
 
     const makeSuggestionItemStub =
-        sandbox.stub(suggestionsProvider, 'makeSuggestionItem', item => item);
+        sinon.stub(suggestionsProvider, 'makeSuggestionItem')
+            .callsFake( item => item);
 
     const input = element.$.entry.$.input;
 
@@ -437,7 +420,7 @@
   test('skip suggestion on empty', done => {
     element.skipSuggestOnEmpty = true;
     const getSuggestionsStub =
-        sandbox.stub(suggestionsProvider, 'getSuggestions')
+        sinon.stub(suggestionsProvider, 'getSuggestions')
             .returns(Promise.resolve([]));
 
     const input = element.$.entry.$.input;
@@ -465,7 +448,7 @@
     });
 
     test('toasts on invalid email', () => {
-      const toastHandler = sandbox.stub();
+      const toastHandler = sinon.stub();
       element.addEventListener('show-alert', toastHandler);
       element._handleAdd({detail: {value: 'test'}});
       assert.isTrue(toastHandler.called);
@@ -488,8 +471,8 @@
   suite('keyboard interactions', () => {
     test('backspace at text input start removes last account', done => {
       const input = element.$.entry.$.input;
-      sandbox.stub(input, '_updateSuggestions');
-      sandbox.stub(element, '_computeRemovable').returns(true);
+      sinon.stub(input, '_updateSuggestions');
+      sinon.stub(element, '_computeRemovable').returns(true);
       flush(() => {
         // Next line is a workaround for Firefox not moving cursor
         // on input field update
@@ -519,10 +502,10 @@
         MockInteractions.focus(input.$.input);
         flushAsynchronousOperations();
         const chips = element.accountChips;
-        const chipsOneSpy = sandbox.spy(chips[1], 'focus');
+        const chipsOneSpy = sinon.spy(chips[1], 'focus');
         MockInteractions.pressAndReleaseKeyOn(input.$.input, 37); // Left
         assert.isTrue(chipsOneSpy.called);
-        const chipsZeroSpy = sandbox.spy(chips[0], 'focus');
+        const chipsZeroSpy = sinon.spy(chips[0], 'focus');
         MockInteractions.pressAndReleaseKeyOn(chips[1], 37); // Left
         assert.isTrue(chipsZeroSpy.called);
         MockInteractions.pressAndReleaseKeyOn(chips[0], 37); // Left
@@ -536,8 +519,8 @@
     test('delete', done => {
       element.accounts = [makeAccount(), makeAccount()];
       flush(() => {
-        const focusSpy = sandbox.spy(element.accountChips[1], 'focus');
-        const removeSpy = sandbox.spy(element, 'removeAccount');
+        const focusSpy = sinon.spy(element.accountChips[1], 'focus');
+        const removeSpy = sinon.spy(element, 'removeAccount');
         MockInteractions.pressAndReleaseKeyOn(
             element.accountChips[0], 8); // Backspace
         assert.isTrue(focusSpy.called);
@@ -551,4 +534,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
deleted file mode 100644
index 557ec28..0000000
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
+++ /dev/null
@@ -1,59 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-alert</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-alert.js';
-suite('gr-alert tests', () => {
-  let element;
-
-  setup(() => {
-    element = document.createElement('gr-alert');
-  });
-
-  teardown(() => {
-    if (element.parentNode) {
-      element.parentNode.removeChild(element);
-    }
-  });
-
-  test('show/hide', () => {
-    assert.isNull(element.parentNode);
-    element.show();
-    assert.equal(element.parentNode, document.body);
-    element.updateStyles({'--gr-alert-transition-duration': '0ms'});
-    element.hide();
-    assert.isNull(element.parentNode);
-  });
-
-  test('action event', done => {
-    element.show();
-    element._actionCallback = done;
-    MockInteractions.tap(element.shadowRoot
-        .querySelector('.action'));
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.js
new file mode 100644
index 0000000..8105584
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.js
@@ -0,0 +1,49 @@
+/**
+ * @license
+ * Copyright (C) 2015 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-alert.js';
+suite('gr-alert tests', () => {
+  let element;
+
+  setup(() => {
+    element = document.createElement('gr-alert');
+  });
+
+  teardown(() => {
+    if (element.parentNode) {
+      element.parentNode.removeChild(element);
+    }
+  });
+
+  test('show/hide', () => {
+    assert.isNull(element.parentNode);
+    element.show();
+    assert.equal(element.parentNode, document.body);
+    element.updateStyles({'--gr-alert-transition-duration': '0ms'});
+    element.hide();
+    assert.isNull(element.parentNode);
+  });
+
+  test('action event', done => {
+    element.show();
+    element._actionCallback = done;
+    MockInteractions.tap(element.shadowRoot
+        .querySelector('.action'));
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.js
similarity index 63%
rename from polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
rename to polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.js
index d836155..f76d070 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.js
@@ -1,46 +1,30 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-autocomplete-dropdown</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-autocomplete-dropdown></gr-autocomplete-dropdown>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-autocomplete-dropdown.js';
+
+const basicFixture = fixtureFromElement('gr-autocomplete-dropdown');
+
 suite('gr-autocomplete-dropdown', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.open();
     element.suggestions = [
       {dataValue: 'test value 1', name: 'test name 1', text: 1, label: 'hi'},
@@ -49,7 +33,6 @@
   });
 
   teardown(() => {
-    sandbox.restore();
     if (element.isOpen) element.close();
   });
 
@@ -60,7 +43,7 @@
   });
 
   test('escape key', done => {
-    const closeSpy = sandbox.spy(element, 'close');
+    const closeSpy = sinon.spy(element, 'close');
     MockInteractions.pressAndReleaseKeyOn(element, 27);
     flushAsynchronousOperations();
     assert.isTrue(closeSpy.called);
@@ -68,8 +51,8 @@
   });
 
   test('tab key', () => {
-    const handleTabSpy = sandbox.spy(element, '_handleTab');
-    const itemSelectedStub = sandbox.stub();
+    const handleTabSpy = sinon.spy(element, '_handleTab');
+    const itemSelectedStub = sinon.stub();
     element.addEventListener('item-selected', itemSelectedStub);
     MockInteractions.pressAndReleaseKeyOn(element, 9);
     assert.isTrue(handleTabSpy.called);
@@ -82,8 +65,8 @@
   });
 
   test('enter key', () => {
-    const handleEnterSpy = sandbox.spy(element, '_handleEnter');
-    const itemSelectedStub = sandbox.stub();
+    const handleEnterSpy = sinon.spy(element, '_handleEnter');
+    const itemSelectedStub = sinon.stub();
     element.addEventListener('item-selected', itemSelectedStub);
     MockInteractions.pressAndReleaseKeyOn(element, 13);
     assert.isTrue(handleEnterSpy.called);
@@ -96,7 +79,7 @@
 
   test('down key', () => {
     element.isHidden = true;
-    const nextSpy = sandbox.spy(element.$.cursor, 'next');
+    const nextSpy = sinon.spy(element.$.cursor, 'next');
     MockInteractions.pressAndReleaseKeyOn(element, 40);
     assert.isFalse(nextSpy.called);
     assert.equal(element.$.cursor.index, 0);
@@ -108,7 +91,7 @@
 
   test('up key', () => {
     element.isHidden = true;
-    const prevSpy = sandbox.spy(element.$.cursor, 'previous');
+    const prevSpy = sinon.spy(element.$.cursor, 'previous');
     MockInteractions.pressAndReleaseKeyOn(element, 38);
     assert.isFalse(prevSpy.called);
     assert.equal(element.$.cursor.index, 0);
@@ -121,7 +104,7 @@
   });
 
   test('tapping selects item', () => {
-    const itemSelectedStub = sandbox.stub();
+    const itemSelectedStub = sinon.stub();
     element.addEventListener('item-selected', itemSelectedStub);
 
     MockInteractions.tap(element.$.suggestions.querySelectorAll('li')[1]);
@@ -133,7 +116,7 @@
   });
 
   test('tapping child still selects item', () => {
-    const itemSelectedStub = sandbox.stub();
+    const itemSelectedStub = sinon.stub();
     element.addEventListener('item-selected', itemSelectedStub);
 
     MockInteractions.tap(element.$.suggestions.querySelectorAll('li')[0]
@@ -146,9 +129,9 @@
   });
 
   test('updated suggestions resets cursor stops', () => {
-    const resetStopsSpy = sandbox.spy(element, '_resetCursorStops');
+    const resetStopsSpy = sinon.spy(element, '_resetCursorStops');
     element.suggestions = [];
     assert.isTrue(resetStopsSpy.called);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.js
index eb05b90..e9753c9 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.js
@@ -25,7 +25,7 @@
 
 suite('gr-autocomplete tests', () => {
   let element;
-  let sandbox;
+
   const focusOnInput = element => {
     MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
         'enter');
@@ -33,16 +33,11 @@
 
   setup(() => {
     element = basicFixture.instantiate();
-    sandbox = sinon.sandbox.create();
-  });
-
-  teardown(() => {
-    sandbox.restore();
   });
 
   test('renders', () => {
     let promise;
-    const queryStub = sandbox.spy(input => promise = Promise.resolve([
+    const queryStub = sinon.spy(input => promise = Promise.resolve([
       {name: input + ' 0', value: 0},
       {name: input + ' 1', value: 1},
       {name: input + ' 2', value: 2},
@@ -76,7 +71,7 @@
   test('selectAll', done => {
     flush(() => {
       const nativeInput = element._nativeInput;
-      const selectionStub = sandbox.stub(nativeInput, 'setSelectionRange');
+      const selectionStub = sinon.stub(nativeInput, 'setSelectionRange');
 
       element.selectAll();
       assert.isFalse(selectionStub.called);
@@ -90,7 +85,7 @@
 
   test('esc key behavior', done => {
     let promise;
-    const queryStub = sandbox.spy(() => promise = Promise.resolve([
+    const queryStub = sinon.spy(() => promise = Promise.resolve([
       {name: 'blah', value: 123},
     ]));
     element.query = queryStub;
@@ -103,7 +98,7 @@
     promise.then(() => {
       assert.isFalse(element.$.suggestions.isHidden);
 
-      const cancelHandler = sandbox.spy();
+      const cancelHandler = sinon.spy();
       element.addEventListener('cancel', cancelHandler);
 
       MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc');
@@ -119,7 +114,7 @@
 
   test('emits commit and handles cursor movement', done => {
     let promise;
-    const queryStub = sandbox.spy(input => promise = Promise.resolve([
+    const queryStub = sinon.spy(input => promise = Promise.resolve([
       {name: input + ' 0', value: 0},
       {name: input + ' 1', value: 1},
       {name: input + ' 2', value: 2},
@@ -136,7 +131,7 @@
     promise.then(() => {
       assert.isFalse(element.$.suggestions.isHidden);
 
-      const commitHandler = sandbox.spy();
+      const commitHandler = sinon.spy();
       element.addEventListener('commit', commitHandler);
 
       assert.equal(element.$.suggestions.$.cursor.index, 0);
@@ -169,7 +164,7 @@
 
   test('clear-on-commit behavior (off)', done => {
     let promise;
-    const queryStub = sandbox.spy(() => {
+    const queryStub = sinon.spy(() => {
       promise = Promise.resolve([{name: 'suggestion', value: 0}]);
       return promise;
     });
@@ -178,7 +173,7 @@
     element.text = 'blah';
 
     promise.then(() => {
-      const commitHandler = sandbox.spy();
+      const commitHandler = sinon.spy();
       element.addEventListener('commit', commitHandler);
 
       MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
@@ -192,7 +187,7 @@
 
   test('clear-on-commit behavior (on)', done => {
     let promise;
-    const queryStub = sandbox.spy(() => {
+    const queryStub = sinon.spy(() => {
       promise = Promise.resolve([{name: 'suggestion', value: 0}]);
       return promise;
     });
@@ -202,7 +197,7 @@
     element.clearOnCommit = true;
 
     promise.then(() => {
-      const commitHandler = sandbox.spy();
+      const commitHandler = sinon.spy();
       element.addEventListener('commit', commitHandler);
 
       MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
@@ -215,7 +210,7 @@
   });
 
   test('threshold guards the query', () => {
-    const queryStub = sandbox.spy(() => Promise.resolve([]));
+    const queryStub = sinon.spy(() => Promise.resolve([]));
     element.query = queryStub;
     element.threshold = 2;
     focusOnInput(element);
@@ -226,9 +221,9 @@
   });
 
   test('noDebounce=false debounces the query', () => {
-    const queryStub = sandbox.spy(() => Promise.resolve([]));
+    const queryStub = sinon.spy(() => Promise.resolve([]));
     let callback;
-    const debounceStub = sandbox.stub(element, 'debounce',
+    const debounceStub = sinon.stub(element, 'debounce').callsFake(
         (name, cb) => { callback = cb; });
     element.query = queryStub;
     element.noDebounce = false;
@@ -255,7 +250,7 @@
 
   test('when focused', done => {
     let promise;
-    const queryStub = sandbox.stub()
+    const queryStub = sinon.stub()
         .returns(promise = Promise.resolve([
           {name: 'suggestion', value: 0},
         ]));
@@ -274,7 +269,7 @@
 
   test('when not focused', done => {
     let promise;
-    const queryStub = sandbox.stub()
+    const queryStub = sinon.stub()
         .returns(promise = Promise.resolve([
           {name: 'suggestion', value: 0},
         ]));
@@ -291,7 +286,7 @@
 
   test('suggestions should not carry over', done => {
     let promise;
-    const queryStub = sandbox.stub()
+    const queryStub = sinon.stub()
         .returns(promise = Promise.resolve([
           {name: 'suggestion', value: 0},
         ]));
@@ -309,7 +304,7 @@
 
   test('multi completes only the last part of the query', done => {
     let promise;
-    const queryStub = sandbox.stub()
+    const queryStub = sinon.stub()
         .returns(promise = Promise.resolve([
           {name: 'suggestion', value: 0},
         ]));
@@ -319,7 +314,7 @@
     element.multi = true;
 
     promise.then(() => {
-      const commitHandler = sandbox.spy();
+      const commitHandler = sinon.spy();
       element.addEventListener('commit', commitHandler);
 
       MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
@@ -334,9 +329,9 @@
   test('tabComplete flag functions', () => {
     // commitHandler checks for the commit event, whereas commitSpy checks for
     // the _commit function of the element.
-    const commitHandler = sandbox.spy();
+    const commitHandler = sinon.spy();
     element.addEventListener('commit', commitHandler);
-    const commitSpy = sandbox.spy(element, '_commit');
+    const commitSpy = sinon.spy(element, '_commit');
     element._focused = true;
 
     element._suggestions = ['tunnel snakes rule!'];
@@ -385,8 +380,8 @@
   });
 
   test('_focused flag shows/hides the suggestions', () => {
-    const openStub = sandbox.stub(element.$.suggestions, 'open');
-    const closedStub = sandbox.stub(element.$.suggestions, 'close');
+    const openStub = sinon.stub(element.$.suggestions, 'open');
+    const closedStub = sinon.stub(element.$.suggestions, 'close');
     element._suggestions = ['hello', 'its me'];
     assert.isFalse(openStub.called);
     assert.isTrue(closedStub.calledOnce);
@@ -399,7 +394,7 @@
 
   test('_handleInputCommit with autocomplete hidden does nothing without' +
         'without allowNonSuggestedValues', () => {
-    const commitStub = sandbox.stub(element, '_commit');
+    const commitStub = sinon.stub(element, '_commit');
     element.$.suggestions.isHidden = true;
     element._handleInputCommit();
     assert.isFalse(commitStub.called);
@@ -407,7 +402,7 @@
 
   test('_handleInputCommit with autocomplete hidden with' +
         'allowNonSuggestedValues', () => {
-    const commitStub = sandbox.stub(element, '_commit');
+    const commitStub = sinon.stub(element, '_commit');
     element.allowNonSuggestedValues = true;
     element.$.suggestions.isHidden = true;
     element._handleInputCommit();
@@ -415,7 +410,7 @@
   });
 
   test('_handleInputCommit with autocomplete open calls commit', () => {
-    const commitStub = sandbox.stub(element, '_commit');
+    const commitStub = sinon.stub(element, '_commit');
     element.$.suggestions.isHidden = false;
     element._handleInputCommit();
     assert.isTrue(commitStub.calledOnce);
@@ -423,7 +418,7 @@
 
   test('_handleInputCommit with autocomplete open calls commit' +
         'with allowNonSuggestedValues', () => {
-    const commitStub = sandbox.stub(element, '_commit');
+    const commitStub = sinon.stub(element, '_commit');
     element.allowNonSuggestedValues = true;
     element.$.suggestions.isHidden = false;
     element._handleInputCommit();
@@ -432,7 +427,7 @@
 
   test('issue 8655', () => {
     function makeSuggestion(s) { return {name: s, text: s, value: s}; }
-    const keydownSpy = sandbox.spy(element, '_handleKeydown');
+    const keydownSpy = sinon.spy(element, '_handleKeydown');
     element.setText('file:');
     element._suggestions =
         [makeSuggestion('file:'), makeSuggestion('-file:')];
@@ -455,12 +450,12 @@
     let focusSpy;
 
     setup(() => {
-      commitSpy = sandbox.spy(element, '_commit');
+      commitSpy = sinon.spy(element, '_commit');
     });
 
     test('enter does not call focus', () => {
       element._suggestions = ['sugar bombs'];
-      focusSpy = sandbox.spy(element, 'focus');
+      focusSpy = sinon.spy(element, 'focus');
       MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
           'enter');
       flushAsynchronousOperations();
@@ -471,8 +466,8 @@
     });
 
     test('tab in input, tabComplete = true', () => {
-      focusSpy = sandbox.spy(element, 'focus');
-      const commitHandler = sandbox.stub();
+      focusSpy = sinon.spy(element, 'focus');
+      const commitHandler = sinon.stub();
       element.addEventListener('commit', commitHandler);
       element.tabComplete = true;
       element._suggestions = ['tunnel snakes drool'];
@@ -487,7 +482,7 @@
 
     test('tab in input, tabComplete = false', () => {
       element._suggestions = ['sugar bombs'];
-      focusSpy = sandbox.spy(element, 'focus');
+      focusSpy = sinon.spy(element, 'focus');
       MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
       flushAsynchronousOperations();
 
@@ -501,7 +496,7 @@
       element._focused = true;
       // When tabComplete is false, do not focus.
       element.tabComplete = false;
-      focusSpy = sandbox.spy(element, 'focus');
+      focusSpy = sinon.spy(element, 'focus');
       flush$0();
       assert.isFalse(element.$.suggestions.isHidden);
 
@@ -518,7 +513,7 @@
       element._focused = true;
       // When tabComplete is true, focus.
       element.tabComplete = true;
-      focusSpy = sandbox.spy(element, 'focus');
+      focusSpy = sinon.spy(element, 'focus');
       flush$0();
       assert.isFalse(element.$.suggestions.isHidden);
 
@@ -532,7 +527,7 @@
     });
 
     test('tap on suggestion commits, does not call focus', () => {
-      focusSpy = sandbox.spy(element, 'focus');
+      focusSpy = sinon.spy(element, 'focus');
       element._focused = true;
       element._suggestions = [{name: 'first suggestion'}];
       flush$0();
@@ -548,7 +543,7 @@
   });
 
   test('input-keydown event fired', () => {
-    const listener = sandbox.spy();
+    const listener = sinon.spy();
     element.addEventListener('input-keydown', listener);
     MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
     flushAsynchronousOperations();
@@ -556,8 +551,8 @@
   });
 
   test('enter with modifier does not complete', () => {
-    const handleSpy = sandbox.spy(element, '_handleKeydown');
-    const commitStub = sandbox.stub(element, '_handleInputCommit');
+    const handleSpy = sinon.spy(element, '_handleKeydown');
+    const commitStub = sinon.stub(element, '_handleInputCommit');
     MockInteractions.pressAndReleaseKeyOn(
         element.$.input, 13, 'ctrl', 'enter');
     assert.isTrue(handleSpy.called);
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
deleted file mode 100644
index dddc3d8..0000000
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
+++ /dev/null
@@ -1,209 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-avatar</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-avatar></gr-avatar>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-avatar.js';
-import {pluginLoader} from '../gr-js-api-interface/gr-plugin-loader.js';
-
-suite('gr-avatar tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('methods', () => {
-    assert.equal(
-        element._buildAvatarURL({
-          _account_id: 123,
-        }),
-        '/accounts/123/avatar?s=16');
-    assert.equal(
-        element._buildAvatarURL({
-          email: 'test@example.com',
-        }),
-        '/accounts/test%40example.com/avatar?s=16');
-    assert.equal(
-        element._buildAvatarURL({
-          name: 'John Doe',
-        }),
-        '/accounts/John%20Doe/avatar?s=16');
-    assert.equal(
-        element._buildAvatarURL({
-          username: 'John_Doe',
-        }),
-        '/accounts/John_Doe/avatar?s=16');
-    assert.equal(
-        element._buildAvatarURL({
-          _account_id: 123,
-          avatars: [
-            {
-              url: 'https://cdn.example.com/s12-p/photo.jpg',
-              height: 12,
-            },
-            {
-              url: 'https://cdn.example.com/s16-p/photo.jpg',
-              height: 16,
-            },
-            {
-              url: 'https://cdn.example.com/s100-p/photo.jpg',
-              height: 100,
-            },
-          ],
-        }),
-        'https://cdn.example.com/s16-p/photo.jpg');
-    assert.equal(
-        element._buildAvatarURL({
-          _account_id: 123,
-          avatars: [
-            {
-              url: 'https://cdn.example.com/s95-p/photo.jpg',
-              height: 95,
-            },
-          ],
-        }),
-        '/accounts/123/avatar?s=16');
-    assert.equal(element._buildAvatarURL(undefined), '');
-  });
-
-  test('dom for existing account', () => {
-    assert.isFalse(element.hasAttribute('hidden'));
-
-    sandbox.stub(
-        element,
-        '_getConfig',
-        () => Promise.resolve({plugin: {has_avatars: true}}));
-
-    element.imageSize = 64;
-    element.account = {
-      _account_id: 123,
-    };
-
-    assert.strictEqual(element.style.backgroundImage, '');
-
-    // Emulate plugins loaded.
-    pluginLoader.loadPlugins([]);
-
-    Promise.all([
-      element.$.restAPI.getConfig(),
-      pluginLoader.awaitPluginsLoaded(),
-    ]).then(() => {
-      assert.isFalse(element.hasAttribute('hidden'));
-
-      assert.isTrue(
-          element.style.backgroundImage.includes('/accounts/123/avatar?s=64'));
-    });
-  });
-
-  suite('plugin has avatars', () => {
-    let element;
-    let sandbox;
-
-    setup(() => {
-      sandbox = sinon.sandbox.create();
-
-      stub('gr-avatar', {
-        _getConfig: () => Promise.resolve({plugin: {has_avatars: true}}),
-      });
-
-      element = fixture('basic');
-    });
-
-    teardown(() => {
-      sandbox.restore();
-    });
-
-    test('dom for non available account', () => {
-      assert.isFalse(element.hasAttribute('hidden'));
-
-      // Emulate plugins loaded.
-      pluginLoader.loadPlugins([]);
-
-      return Promise.all([
-        element.$.restAPI.getConfig(),
-        pluginLoader.awaitPluginsLoaded(),
-      ]).then(() => {
-        assert.isTrue(element.hasAttribute('hidden'));
-
-        assert.strictEqual(element.style.backgroundImage, '');
-      });
-    });
-  });
-
-  suite('config not set', () => {
-    let element;
-    let sandbox;
-
-    setup(() => {
-      sandbox = sinon.sandbox.create();
-
-      stub('gr-avatar', {
-        _getConfig: () => Promise.resolve({}),
-      });
-
-      element = fixture('basic');
-    });
-
-    teardown(() => {
-      sandbox.restore();
-    });
-
-    test('avatar hidden when account set', () => {
-      flush(() => {
-        assert.isFalse(element.hasAttribute('hidden'));
-
-        element.imageSize = 64;
-        element.account = {
-          _account_id: 123,
-        };
-        // Emulate plugins loaded.
-        pluginLoader.loadPlugins([]);
-
-        return Promise.all([
-          element.$.restAPI.getConfig(),
-          pluginLoader.awaitPluginsLoaded(),
-        ]).then(() => {
-          assert.isTrue(element.hasAttribute('hidden'));
-        });
-      });
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.js
new file mode 100644
index 0000000..5e43f90
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.js
@@ -0,0 +1,178 @@
+/**
+ * @license
+ * Copyright (C) 2015 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-avatar.js';
+import {pluginLoader} from '../gr-js-api-interface/gr-plugin-loader.js';
+
+const basicFixture = fixtureFromElement('gr-avatar');
+
+suite('gr-avatar tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('methods', () => {
+    assert.equal(
+        element._buildAvatarURL({
+          _account_id: 123,
+        }),
+        '/accounts/123/avatar?s=16');
+    assert.equal(
+        element._buildAvatarURL({
+          email: 'test@example.com',
+        }),
+        '/accounts/test%40example.com/avatar?s=16');
+    assert.equal(
+        element._buildAvatarURL({
+          name: 'John Doe',
+        }),
+        '/accounts/John%20Doe/avatar?s=16');
+    assert.equal(
+        element._buildAvatarURL({
+          username: 'John_Doe',
+        }),
+        '/accounts/John_Doe/avatar?s=16');
+    assert.equal(
+        element._buildAvatarURL({
+          _account_id: 123,
+          avatars: [
+            {
+              url: 'https://cdn.example.com/s12-p/photo.jpg',
+              height: 12,
+            },
+            {
+              url: 'https://cdn.example.com/s16-p/photo.jpg',
+              height: 16,
+            },
+            {
+              url: 'https://cdn.example.com/s100-p/photo.jpg',
+              height: 100,
+            },
+          ],
+        }),
+        'https://cdn.example.com/s16-p/photo.jpg');
+    assert.equal(
+        element._buildAvatarURL({
+          _account_id: 123,
+          avatars: [
+            {
+              url: 'https://cdn.example.com/s95-p/photo.jpg',
+              height: 95,
+            },
+          ],
+        }),
+        '/accounts/123/avatar?s=16');
+    assert.equal(element._buildAvatarURL(undefined), '');
+  });
+
+  suite('config set', () => {
+    setup(() => {
+      stub('gr-avatar', {
+        _getConfig: () => Promise.resolve({plugin: {has_avatars: true}}),
+      });
+      element = basicFixture.instantiate();
+    });
+
+    test('dom for existing account', () => {
+      assert.isFalse(element.hasAttribute('hidden'));
+
+      element.imageSize = 64;
+      element.account = {
+        _account_id: 123,
+      };
+
+      assert.strictEqual(element.style.backgroundImage, '');
+
+      // Emulate plugins loaded.
+      pluginLoader.loadPlugins([]);
+
+      return Promise.all([
+        element.$.restAPI.getConfig(),
+        pluginLoader.awaitPluginsLoaded(),
+      ]).then(() => {
+        assert.isFalse(element.hasAttribute('hidden'));
+
+        assert.isTrue(
+            element.style.backgroundImage.includes(
+                '/accounts/123/avatar?s=64'));
+      });
+    });
+  });
+
+  suite('plugin has avatars', () => {
+    let element;
+
+    setup(() => {
+      stub('gr-avatar', {
+        _getConfig: () => Promise.resolve({plugin: {has_avatars: true}}),
+      });
+
+      element = basicFixture.instantiate();
+    });
+
+    test('dom for non available account', () => {
+      assert.isFalse(element.hasAttribute('hidden'));
+
+      // Emulate plugins loaded.
+      pluginLoader.loadPlugins([]);
+
+      return Promise.all([
+        element.$.restAPI.getConfig(),
+        pluginLoader.awaitPluginsLoaded(),
+      ]).then(() => {
+        assert.isTrue(element.hasAttribute('hidden'));
+
+        assert.strictEqual(element.style.backgroundImage, '');
+      });
+    });
+  });
+
+  suite('config not set', () => {
+    let element;
+
+    setup(() => {
+      stub('gr-avatar', {
+        _getConfig: () => Promise.resolve({}),
+      });
+
+      element = basicFixture.instantiate();
+    });
+
+    test('avatar hidden when account set', async () => {
+      await flush();
+      assert.isTrue(element.hasAttribute('hidden'));
+
+      element.imageSize = 64;
+      element.account = {
+        _account_id: 123,
+      };
+      // Emulate plugins loaded.
+      pluginLoader.loadPlugins([]);
+
+      return Promise.all([
+        element.$.restAPI.getConfig(),
+        pluginLoader.awaitPluginsLoaded(),
+      ]).then(() => {
+        assert.isTrue(element.hasAttribute('hidden'));
+      });
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.js
similarity index 70%
rename from polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
rename to polygerrit-ui/app/elements/shared/gr-button/gr-button_test.js
index dfc3d75..4bc3bea 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.js
@@ -1,62 +1,43 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-button</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-button></gr-button>
-  </template>
-</test-fixture>
-
-<test-fixture id="nested">
-  <template>
-    <div id="test">
-      <gr-button class="testBtn"></gr-button>
-    </div>
-  </template>
-</test-fixture>
-
-<test-fixture id="tabindex">
-  <template>
-    <gr-button tabindex="3"></gr-button>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-button.js';
 import {addListener} from '@polymer/polymer/lib/utils/gestures.js';
 import {appContext} from '../../../services/app-context.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromElement('gr-button');
+
+const nestedFixture = fixtureFromTemplate(html`
+<div id="test">
+  <gr-button class="testBtn"></gr-button>
+</div>
+`);
+
+const tabindexFixture = fixtureFromTemplate(html`
+  <gr-button tabindex="3"></gr-button>
+`);
 
 suite('gr-button tests', () => {
   let element;
-  let sandbox;
 
   const addSpyOn = function(eventName) {
-    const spy = sandbox.spy();
+    const spy = sinon.spy();
     if (eventName == 'tap') {
       addListener(element, eventName, spy);
     } else {
@@ -66,12 +47,7 @@
   };
 
   setup(() => {
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    element = basicFixture.instantiate();
   });
 
   test('disabled is set by disabled', () => {
@@ -118,7 +94,7 @@
   });
 
   test('tabindex should be preserved', () => {
-    element = fixture('tabindex');
+    element = tabindexFixture.instantiate();
     element.disabled = false;
     assert.equal(element.getAttribute('tabindex'), '3');
     element.disabled = true;
@@ -150,14 +126,14 @@
   // Keycodes: 32 for Space, 13 for Enter.
   for (const key of [32, 13]) {
     test('dispatches click event on keycode ' + key, () => {
-      const tapSpy = sandbox.spy();
+      const tapSpy = sinon.spy();
       element.addEventListener('click', tapSpy);
       MockInteractions.pressAndReleaseKeyOn(element, key);
       assert.isTrue(tapSpy.calledOnce);
     });
 
     test('dispatches no click event with modifier on keycode ' + key, () => {
-      const tapSpy = sandbox.spy();
+      const tapSpy = sinon.spy();
       element.addEventListener('click', tapSpy);
       MockInteractions.pressAndReleaseKeyOn(element, key, 'shift');
       MockInteractions.pressAndReleaseKeyOn(element, key, 'ctrl');
@@ -183,7 +159,7 @@
     // Keycodes: 32 for Space, 13 for Enter.
     for (const key of [32, 13]) {
       test('stops click event on keycode ' + key, () => {
-        const tapSpy = sandbox.spy();
+        const tapSpy = sinon.spy();
         element.addEventListener('click', tapSpy);
         MockInteractions.pressAndReleaseKeyOn(element, key);
         assert.isFalse(tapSpy.called);
@@ -194,7 +170,7 @@
   suite('reporting', () => {
     let reportStub;
     setup(() => {
-      reportStub = sandbox.stub(appContext.reportingService,
+      reportStub = sinon.stub(appContext.reportingService,
           'reportInteraction');
       reportStub.reset();
     });
@@ -204,19 +180,20 @@
       assert.isTrue(reportStub.calledOnce);
       assert.equal(reportStub.lastCall.args[0], 'button-click');
       assert.deepEqual(reportStub.lastCall.args[1], {
-        path: 'html>body>test-fixture#basic>gr-button',
+        path: `html>body>test-fixture#${basicFixture.fixtureId}>gr-button`,
       });
     });
 
     test('report event after click on nested', () => {
-      element = fixture('nested');
+      element = nestedFixture.instantiate();
       MockInteractions.click(element.querySelector('gr-button'));
       assert.isTrue(reportStub.calledOnce);
       assert.equal(reportStub.lastCall.args[0], 'button-click');
       assert.deepEqual(reportStub.lastCall.args[1], {
-        path: 'html>body>test-fixture#nested>div#test>gr-button.testBtn',
+        path: `html>body>test-fixture#${nestedFixture.fixtureId}` +
+            `>div#test>gr-button.testBtn`,
       });
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
deleted file mode 100644
index 1ea9071..0000000
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
+++ /dev/null
@@ -1,82 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-change-star</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-change-star></gr-change-star>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-change-star.js';
-suite('gr-change-star tests', () => {
-  let element;
-
-  setup(() => {
-    element = fixture('basic');
-    element.change = {
-      _number: 2,
-      starred: true,
-    };
-  });
-
-  test('star visibility states', () => {
-    element.set('change.starred', true);
-    let icon = element.shadowRoot
-        .querySelector('iron-icon');
-    assert.isTrue(icon.classList.contains('active'));
-    assert.equal(icon.icon, 'gr-icons:star');
-
-    element.set('change.starred', false);
-    icon = element.shadowRoot
-        .querySelector('iron-icon');
-    assert.isFalse(icon.classList.contains('active'));
-    assert.equal(icon.icon, 'gr-icons:star-border');
-  });
-
-  test('starring', done => {
-    element.addEventListener('toggle-star', () => {
-      assert.equal(element.change.starred, true);
-      done();
-    });
-    element.set('change.starred', false);
-    MockInteractions.tap(element.shadowRoot
-        .querySelector('button'));
-  });
-
-  test('unstarring', done => {
-    element.addEventListener('toggle-star', () => {
-      assert.equal(element.change.starred, false);
-      done();
-    });
-    element.set('change.starred', true);
-    MockInteractions.tap(element.shadowRoot
-        .querySelector('button'));
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.js
new file mode 100644
index 0000000..d479ece
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.js
@@ -0,0 +1,68 @@
+/**
+ * @license
+ * Copyright (C) 2015 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-change-star.js';
+
+const basicFixture = fixtureFromElement('gr-change-star');
+
+suite('gr-change-star tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+    element.change = {
+      _number: 2,
+      starred: true,
+    };
+  });
+
+  test('star visibility states', () => {
+    element.set('change.starred', true);
+    let icon = element.shadowRoot
+        .querySelector('iron-icon');
+    assert.isTrue(icon.classList.contains('active'));
+    assert.equal(icon.icon, 'gr-icons:star');
+
+    element.set('change.starred', false);
+    icon = element.shadowRoot
+        .querySelector('iron-icon');
+    assert.isFalse(icon.classList.contains('active'));
+    assert.equal(icon.icon, 'gr-icons:star-border');
+  });
+
+  test('starring', done => {
+    element.addEventListener('toggle-star', () => {
+      assert.equal(element.change.starred, true);
+      done();
+    });
+    element.set('change.starred', false);
+    MockInteractions.tap(element.shadowRoot
+        .querySelector('button'));
+  });
+
+  test('unstarring', done => {
+    element.addEventListener('toggle-star', () => {
+      assert.equal(element.change.starred, false);
+      done();
+    });
+    element.set('change.starred', true);
+    MockInteractions.tap(element.shadowRoot
+        .querySelector('button'));
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.js
similarity index 70%
rename from polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
rename to polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.js
index 806203b..770a21c 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.js
@@ -1,39 +1,25 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-change-status</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-change-status></gr-change-status>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-change-status.js';
+
+const basicFixture = fixtureFromElement('gr-change-status');
+
 const WIP_TOOLTIP = 'This change isn\'t ready to be reviewed or submitted. ' +
     'It will not appear on dashboards unless you are CC\'ed or assigned, ' +
     'and email notifications will be silenced until the review is started.';
@@ -47,15 +33,9 @@
 
 suite('gr-change-status tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    element = basicFixture.instantiate();
   });
 
   test('WIP', () => {
@@ -134,4 +114,4 @@
     assert.isTrue(element.classList.contains('wip'));
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.js
similarity index 90%
rename from polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
rename to polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.js
index e219b22..3837514 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.js
@@ -1,65 +1,40 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2015 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-comment-thread</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-comment-thread></gr-comment-thread>
-  </template>
-</test-fixture>
-
-<test-fixture id="withComment">
-  <template>
-    <gr-comment-thread></gr-comment-thread>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-comment-thread.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {SpecialFilePath} from '../../../constants/constants.js';
 
+const basicFixture = fixtureFromElement('gr-comment-thread');
+
+const withCommentFixture = fixtureFromElement('gr-comment-thread');
+
 suite('gr-comment-thread tests', () => {
   suite('basic test', () => {
     let element;
-    let sandbox;
 
     setup(() => {
-      sandbox = sinon.sandbox.create();
       stub('gr-rest-api-interface', {
         getLoggedIn() { return Promise.resolve(false); },
       });
-      sandbox = sinon.sandbox.create();
-      element = fixture('basic');
-    });
 
-    teardown(() => {
-      sandbox.restore();
+      element = basicFixture.instantiate();
     });
 
     test('comments are sorted correctly', () => {
@@ -132,9 +107,9 @@
         updated: '2015-12-25 15:00:20.396000000',
         __draft: true,
       }];
-      const commentElStub = sandbox.stub(element, '_commentElWithDraftID',
-          () => { return {}; });
-      const addDraftStub = sandbox.stub(element, 'addDraft');
+      const commentElStub = sinon.stub(element, '_commentElWithDraftID')
+          .callsFake(() => { return {}; });
+      const addDraftStub = sinon.stub(element, 'addDraft');
 
       element.addOrEditDraft(123);
 
@@ -144,9 +119,9 @@
 
     test('addOrEditDraft w/o edit draft', () => {
       element.comments = [];
-      const commentElStub = sandbox.stub(element, '_commentElWithDraftID',
-          () => { return {}; });
-      const addDraftStub = sandbox.stub(element, 'addDraft');
+      const commentElStub = sinon.stub(element, '_commentElWithDraftID')
+          .callsFake(() => { return {}; });
+      const addDraftStub = sinon.stub(element, 'addDraft');
 
       element.addOrEditDraft(123);
 
@@ -188,7 +163,7 @@
 
     test('setting project name loads the project config', done => {
       const projectName = 'foo/bar/baz';
-      const getProjectStub = sandbox.stub(element.$.restAPI, 'getProjectConfig')
+      const getProjectStub = sinon.stub(element.$.restAPI, 'getProjectConfig')
           .returns(Promise.resolve({}));
       element.projectName = projectName;
       flush(() => {
@@ -203,7 +178,7 @@
       assert.isNotOk(element.shadowRoot
           .querySelector('.pathInfo'));
 
-      sandbox.stub(GerritNav, 'getUrlForDiffById');
+      sinon.stub(GerritNav, 'getUrlForDiffById');
       element.changeNum = 123;
       element.projectName = 'test project';
       element.path = 'path/to/file';
@@ -253,10 +228,8 @@
 
 suite('comment action tests with unresolved thread', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     stub('gr-rest-api-interface', {
       getLoggedIn() { return Promise.resolve(false); },
       saveDiffDraft() {
@@ -277,7 +250,7 @@
       },
       deleteDiffDraft() { return Promise.resolve({ok: true}); },
     });
-    element = fixture('withComment');
+    element = withCommentFixture.instantiate();
     element.comments = [{
       author: {
         name: 'Mr. Peanutbutter',
@@ -295,14 +268,10 @@
     flushAsynchronousOperations();
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('reply', () => {
     const commentEl = element.shadowRoot
         .querySelector('gr-comment');
-    const reportStub = sandbox.stub(element.reporting,
+    const reportStub = sinon.stub(element.reporting,
         'recordDraftInteraction');
     assert.ok(commentEl);
 
@@ -320,7 +289,7 @@
   test('quote reply', () => {
     const commentEl = element.shadowRoot
         .querySelector('gr-comment');
-    const reportStub = sandbox.stub(element.reporting,
+    const reportStub = sinon.stub(element.reporting,
         'recordDraftInteraction');
     assert.ok(commentEl);
 
@@ -336,7 +305,7 @@
   });
 
   test('quote reply multiline', () => {
-    const reportStub = sandbox.stub(element.reporting,
+    const reportStub = sinon.stub(element.reporting,
         'recordDraftInteraction');
     element.comments = [{
       author: {
@@ -367,7 +336,7 @@
   });
 
   test('ack', done => {
-    const reportStub = sandbox.stub(element.reporting,
+    const reportStub = sinon.stub(element.reporting,
         'recordDraftInteraction');
     element.changeNum = '42';
     element.patchNum = '1';
@@ -390,7 +359,7 @@
   });
 
   test('done', done => {
-    const reportStub = sandbox.stub(element.reporting,
+    const reportStub = sinon.stub(element.reporting,
         'recordDraftInteraction');
     element.changeNum = '42';
     element.patchNum = '1';
@@ -419,7 +388,7 @@
         .querySelector('gr-comment');
     assert.ok(commentEl);
 
-    const saveOrDiscardStub = sandbox.stub();
+    const saveOrDiscardStub = sinon.stub();
     element.addEventListener('thread-changed', saveOrDiscardStub);
     element.shadowRoot
         .querySelector('gr-comment')._fireSave();
@@ -467,7 +436,7 @@
         'it’s pronouced jiff, not giff'));
     flushAsynchronousOperations();
 
-    const saveOrDiscardStub = sandbox.stub();
+    const saveOrDiscardStub = sinon.stub();
     element.addEventListener('thread-changed', saveOrDiscardStub);
     const draftEl =
         dom(element.root).querySelectorAll('gr-comment')[1];
@@ -500,7 +469,7 @@
         const rootId = element.rootId;
         assert.isOk(rootId);
 
-        const saveOrDiscardStub = sandbox.stub();
+        const saveOrDiscardStub = sinon.stub();
         element.addEventListener('thread-changed', saveOrDiscardStub);
         const draftEl =
         dom(element.root).querySelectorAll('gr-comment')[0];
@@ -834,10 +803,8 @@
 
 suite('comment action tests on resolved comments', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     stub('gr-rest-api-interface', {
       getLoggedIn() { return Promise.resolve(false); },
       saveDiffDraft() {
@@ -858,7 +825,7 @@
       },
       deleteDiffDraft() { return Promise.resolve({ok: true}); },
     });
-    element = fixture('withComment');
+    element = withCommentFixture.instantiate();
     element.comments = [{
       author: {
         name: 'Mr. Peanutbutter',
@@ -874,10 +841,6 @@
     flushAsynchronousOperations();
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('ack and done should be hidden', () => {
     element.changeNum = '42';
     element.patchNum = '1';
@@ -903,4 +866,4 @@
     assert.ok(quoteBtn);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
similarity index 90%
rename from polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
rename to polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
index 17c01e9..b227383 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
@@ -1,46 +1,30 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2015 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-comment</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script src="/node_modules/page/page.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-comment></gr-comment>
-  </template>
-</test-fixture>
-
-<test-fixture id="draft">
-  <template>
-    <gr-comment draft="true"></gr-comment>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-comment.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromElement('gr-comment');
+
+const draftFixture = fixtureFromTemplate(html`
+<gr-comment draft="true"></gr-comment>
+`);
+
 function isVisible(el) {
   assert.ok(el);
   return getComputedStyle(el).getPropertyValue('display') !== 'none';
@@ -49,12 +33,14 @@
 suite('gr-comment tests', () => {
   suite('basic tests', () => {
     let element;
-    let sandbox;
+
+    let openOverlaySpy;
+
     setup(() => {
       stub('gr-rest-api-interface', {
         getAccount() { return Promise.resolve(null); },
       });
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.comment = {
         author: {
           name: 'Mr. Peanutbutter',
@@ -65,11 +51,14 @@
         message: 'is this a crossover episode!?',
         updated: '2015-12-08 19:48:33.843000000',
       };
-      sandbox = sinon.sandbox.create();
+
+      openOverlaySpy = sinon.spy(element, '_openOverlay');
     });
 
     teardown(() => {
-      sandbox.restore();
+      openOverlaySpy.getCalls().forEach(call => {
+        call.args[0].remove();
+      });
     });
 
     test('collapsible comments', () => {
@@ -119,8 +108,8 @@
     });
 
     test('message is not retrieved from storage when other edits', done => {
-      const storageStub = sandbox.stub(element.$.storage, 'getDraftComment');
-      const loadSpy = sandbox.spy(element, '_loadLocalDraft');
+      const storageStub = sinon.stub(element.$.storage, 'getDraftComment');
+      const loadSpy = sinon.spy(element, '_loadLocalDraft');
 
       element.changeNum = 1;
       element.patchNum = 1;
@@ -140,8 +129,8 @@
     });
 
     test('message is retrieved from storage when no other edits', done => {
-      const storageStub = sandbox.stub(element.$.storage, 'getDraftComment');
-      const loadSpy = sandbox.spy(element, '_loadLocalDraft');
+      const storageStub = sinon.stub(element.$.storage, 'getDraftComment');
+      const loadSpy = sinon.spy(element, '_loadLocalDraft');
 
       element.changeNum = 1;
       element.patchNum = 1;
@@ -198,8 +187,8 @@
       setup(() => {
         element.editing = true;
         element._messageText = 'test';
-        sandbox.stub(element, '_handleCancel');
-        sandbox.stub(element, '_handleSave');
+        sinon.stub(element, '_handleCancel');
+        sinon.stub(element, '_handleSave');
         flushAsynchronousOperations();
       });
 
@@ -275,9 +264,9 @@
     });
 
     test('delete comment', done => {
-      sandbox.stub(
+      sinon.stub(
           element.$.restAPI, 'deleteComment').returns(Promise.resolve({}));
-      sandbox.spy(element.confirmDeleteOverlay, 'open');
+      sinon.spy(element.confirmDeleteOverlay, 'open');
       element.changeNum = 42;
       element.patchNum = 0xDEADBEEF;
       element._isAdmin = true;
@@ -307,12 +296,12 @@
 
       setup(() => {
         mockEvent = {preventDefault() {}};
-        sandbox.stub(element, 'save')
+        sinon.stub(element, 'save')
             .returns(Promise.resolve({}));
-        sandbox.stub(element, '_discardDraft')
+        sinon.stub(element, '_discardDraft')
             .returns(Promise.resolve({}));
         endStub = sinon.stub();
-        getTimerStub = sandbox.stub(element.reporting, 'getTimer')
+        getTimerStub = sinon.stub(element.reporting, 'getTimer')
             .returns({end: endStub});
       });
 
@@ -336,7 +325,7 @@
 
       test('discard', () => {
         element.comment = {id: 'abc_123'};
-        sandbox.stub(element, '_closeConfirmDiscardOverlay');
+        sinon.stub(element, '_closeConfirmDiscardOverlay');
         return element._handleConfirmDiscard(mockEvent).then(() => {
           assert.isTrue(endStub.calledOnce);
           assert.isTrue(getTimerStub.calledOnce);
@@ -346,7 +335,7 @@
     });
 
     test('edit reports interaction', () => {
-      const reportStub = sandbox.stub(element.reporting,
+      const reportStub = sinon.stub(element.reporting,
           'recordDraftInteraction');
       element.draft = true;
       flushAsynchronousOperations();
@@ -356,7 +345,7 @@
     });
 
     test('discard reports interaction', () => {
-      const reportStub = sandbox.stub(element.reporting,
+      const reportStub = sinon.stub(element.reporting,
           'recordDraftInteraction');
       element.draft = true;
       flushAsynchronousOperations();
@@ -368,7 +357,6 @@
 
   suite('gr-comment draft tests', () => {
     let element;
-    let sandbox;
 
     setup(() => {
       stub('gr-rest-api-interface', {
@@ -397,7 +385,7 @@
       stub('gr-storage', {
         getDraftComment() { return null; },
       });
-      element = fixture('draft');
+      element = draftFixture.instantiate();
       element.changeNum = 42;
       element.patchNum = 1;
       element.editing = false;
@@ -409,11 +397,6 @@
         line: 5,
       };
       element.commentSide = 'right';
-      sandbox = sinon.sandbox.create();
-    });
-
-    teardown(() => {
-      sandbox.restore();
     });
 
     test('button visibility states', () => {
@@ -668,7 +651,7 @@
       assert.isTrue(element.editing);
 
       element._messageText = '';
-      const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment');
+      const eraseMessageDraftSpy = sinon.spy(element, '_eraseDraftComment');
 
       // Save should be disabled on an empty message.
       let disabled = element.shadowRoot
@@ -701,8 +684,8 @@
 
     test('draft discard removes message from storage', done => {
       element._messageText = '';
-      const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment');
-      sandbox.stub(element, '_closeConfirmDiscardOverlay');
+      const eraseMessageDraftSpy = sinon.spy(element, '_eraseDraftComment');
+      sinon.stub(element, '_closeConfirmDiscardOverlay');
 
       element.addEventListener('comment-discard', e => {
         assert.isTrue(eraseMessageDraftSpy.called);
@@ -713,11 +696,11 @@
 
     test('storage is cleared only after save success', () => {
       element._messageText = 'test';
-      const eraseStub = sandbox.stub(element, '_eraseDraftComment');
-      sandbox.stub(element.$.restAPI, 'getResponseObject')
+      const eraseStub = sinon.stub(element, '_eraseDraftComment');
+      sinon.stub(element.$.restAPI, 'getResponseObject')
           .returns(Promise.resolve({}));
 
-      sandbox.stub(element, '_saveDraft').returns(Promise.resolve({ok: false}));
+      sinon.stub(element, '_saveDraft').returns(Promise.resolve({ok: false}));
 
       const savePromise = element.save();
       assert.isFalse(eraseStub.called);
@@ -725,7 +708,7 @@
         assert.isFalse(eraseStub.called);
 
         element._saveDraft.restore();
-        sandbox.stub(element, '_saveDraft')
+        sinon.stub(element, '_saveDraft')
             .returns(Promise.resolve({ok: true}));
         return element.save().then(() => {
           assert.isTrue(eraseStub.called);
@@ -754,8 +737,8 @@
       let mockEvent;
 
       setup(() => {
-        discardStub = sandbox.stub(element, '_discardDraft');
-        overlayStub = sandbox.stub(element, '_openOverlay')
+        discardStub = sinon.stub(element, '_discardDraft');
+        overlayStub = sinon.stub(element, '_openOverlay')
             .returns(Promise.resolve());
         mockEvent = {preventDefault: sinon.stub()};
       });
@@ -776,7 +759,7 @@
     });
 
     test('ctrl+s saves comment', done => {
-      const stub = sinon.stub(element, 'save', () => {
+      const stub = sinon.stub(element, 'save').callsFake(() => {
         assert.isTrue(stub.called);
         stub.restore();
         done();
@@ -792,7 +775,7 @@
 
     test('draft saving/editing', done => {
       const dispatchEventStub = sinon.stub(element, 'dispatchEvent');
-      const cancelDebounce = sandbox.stub(element, 'cancelDebouncer');
+      const cancelDebounce = sinon.stub(element, 'cancelDebouncer');
 
       element.draft = true;
       flushAsynchronousOperations();
@@ -858,7 +841,7 @@
     });
 
     test('draft prevent save when disabled', () => {
-      const saveStub = sandbox.stub(element, 'save').returns(Promise.resolve());
+      const saveStub = sinon.stub(element, 'save').returns(Promise.resolve());
       element.showActions = true;
       element.draft = true;
       flushAsynchronousOperations();
@@ -881,7 +864,7 @@
     });
 
     test('proper event fires on resolve, comment is not saved', done => {
-      const save = sandbox.stub(element, 'save');
+      const save = sinon.stub(element, 'save');
       element.addEventListener('comment-update', e => {
         assert.isTrue(e.detail.comment.unresolved);
         assert.isFalse(save.called);
@@ -892,7 +875,7 @@
     });
 
     test('resolved comment state indicated by checkbox', () => {
-      sandbox.stub(element, 'save');
+      sinon.stub(element, 'save');
       element.comment = {unresolved: false};
       assert.isTrue(element.shadowRoot
           .querySelector('.resolve input').checked);
@@ -903,7 +886,7 @@
 
     test('resolved checkbox saves with tap when !editing', () => {
       element.editing = false;
-      const save = sandbox.stub(element, 'save');
+      const save = sinon.stub(element, 'save');
 
       element.comment = {unresolved: false};
       assert.isTrue(element.shadowRoot
@@ -927,7 +910,7 @@
       });
 
       test('_show{Start,End}Request', () => {
-        const updateStub = sandbox.stub(element, '_updateRequestToast');
+        const updateStub = sinon.stub(element, '_updateRequestToast');
         element._numPendingDraftRequests.number = 1;
 
         element._showStartRequest();
@@ -948,9 +931,9 @@
     });
 
     test('cancelling an unsaved draft discards, persists in storage', () => {
-      const discardSpy = sandbox.spy(element, '_fireDiscard');
-      const storeStub = sandbox.stub(element.$.storage, 'setDraftComment');
-      const eraseStub = sandbox.stub(element.$.storage, 'eraseDraftComment');
+      const discardSpy = sinon.spy(element, '_fireDiscard');
+      const storeStub = sinon.stub(element.$.storage, 'setDraftComment');
+      const eraseStub = sinon.stub(element.$.storage, 'eraseDraftComment');
       element._messageText = 'test text';
       flushAsynchronousOperations();
       element.flushDebouncer('store');
@@ -964,8 +947,8 @@
 
     test('cancelling edit on a saved draft does not store', () => {
       element.comment.id = 'foo';
-      const discardSpy = sandbox.spy(element, '_fireDiscard');
-      const storeStub = sandbox.stub(element.$.storage, 'setDraftComment');
+      const discardSpy = sinon.spy(element, '_fireDiscard');
+      const storeStub = sinon.stub(element.$.storage, 'setDraftComment');
       element._messageText = 'test text';
       flushAsynchronousOperations();
       element.flushDebouncer('store');
@@ -978,7 +961,7 @@
     test('deleting text from saved draft and saving deletes the draft', () => {
       element.comment = {id: 'foo', message: 'test'};
       element._messageText = '';
-      const discardStub = sandbox.stub(element, '_discardDraft');
+      const discardStub = sinon.stub(element, '_discardDraft');
 
       element.save();
       assert.isTrue(discardStub.called);
@@ -1150,19 +1133,18 @@
 
   suite('respectful tips', () => {
     let element;
-    let sandbox;
+
     let clock;
     setup(() => {
       stub('gr-rest-api-interface', {
         getAccount() { return Promise.resolve(null); },
       });
       clock = sinon.useFakeTimers();
-      sandbox = sinon.sandbox.create();
     });
 
     teardown(() => {
       clock.restore();
-      sandbox.restore();
+      sinon.restore();
     });
 
     test('show tip when no cached record', done => {
@@ -1174,7 +1156,7 @@
         setRespectfulTipVisibility() { return respectfulSetStub(); },
       });
       respectfulGetStub.returns(null);
-      element = fixture('draft');
+      element = draftFixture.instantiate();
       // fake random
       element.getRandomNum = () => 0;
       element.comment = {__editing: true};
@@ -1197,7 +1179,7 @@
         setRespectfulTipVisibility(days) { return respectfulSetStub(days); },
       });
       respectfulGetStub.returns(null);
-      element = fixture('draft');
+      element = draftFixture.instantiate();
       // fake random
       element.getRandomNum = () => 0;
       element.comment = {__editing: true};
@@ -1226,7 +1208,7 @@
         setRespectfulTipVisibility() { return respectfulSetStub(); },
       });
       respectfulGetStub.returns(null);
-      element = fixture('draft');
+      element = draftFixture.instantiate();
       // fake random
       element.getRandomNum = () => 3;
       element.comment = {__editing: true};
@@ -1249,7 +1231,7 @@
         setRespectfulTipVisibility() { return respectfulSetStub(); },
       });
       respectfulGetStub.returns(null);
-      element = fixture('draft');
+      element = draftFixture.instantiate();
       // fake random
       element.getRandomNum = () => 0;
       element.comment = {__editing: false};
@@ -1281,7 +1263,7 @@
         setRespectfulTipVisibility() { return respectfulSetStub(); },
       });
       respectfulGetStub.returns({});
-      element = fixture('draft');
+      element = draftFixture.instantiate();
       // fake random
       element.getRandomNum = () => 0;
       element.comment = {__editing: true};
@@ -1296,4 +1278,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.js
similarity index 60%
rename from polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
rename to polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.js
index 398f7f0..58c00da 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.js
@@ -1,59 +1,39 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-copy-clipboard</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-copy-clipboard></gr-copy-clipboard>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-copy-clipboard.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromElement('gr-copy-clipboard');
+
 suite('gr-copy-clipboard tests', () => {
   let element;
-  let sandbox;
 
   setup(done => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.text = `git fetch http://gerrit@localhost:8080/a/test-project
         refs/changes/05/5/1 && git checkout FETCH_HEAD`;
     flushAsynchronousOperations();
     flush(done);
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('copy to clipboard', () => {
-    const clipboardSpy = sandbox.spy(element, '_copyToClipboard');
+    const clipboardSpy = sinon.spy(element, '_copyToClipboard');
     const copyBtn = element.shadowRoot
         .querySelector('.copyToClipboard');
     MockInteractions.tap(copyBtn);
@@ -102,4 +82,4 @@
     assert.isFalse(clickStub.called);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
deleted file mode 100644
index 63435d2..0000000
--- a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
+++ /dev/null
@@ -1,58 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-count-string-formatter</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script type="module">
-import '../../../test/common-test-setup.js';
-import {GrCountStringFormatter} from './gr-count-string-formatter.js';
-
-suite('gr-count-string-formatter tests', () => {
-  test('computeString', () => {
-    const noun = 'unresolved';
-    assert.equal(GrCountStringFormatter.computeString(0, noun), '');
-    assert.equal(GrCountStringFormatter.computeString(1, noun),
-        '1 unresolved');
-    assert.equal(GrCountStringFormatter.computeString(2, noun),
-        '2 unresolved');
-  });
-
-  test('computeShortString', () => {
-    const noun = 'c';
-    assert.equal(GrCountStringFormatter.computeShortString(0, noun), '');
-    assert.equal(GrCountStringFormatter.computeShortString(1, noun), '1c');
-    assert.equal(GrCountStringFormatter.computeShortString(2, noun), '2c');
-  });
-
-  test('computePluralString', () => {
-    const noun = 'comment';
-    assert.equal(GrCountStringFormatter.computePluralString(0, noun), '');
-    assert.equal(GrCountStringFormatter.computePluralString(1, noun),
-        '1 comment');
-    assert.equal(GrCountStringFormatter.computePluralString(2, noun),
-        '2 comments');
-  });
-});
-</script>
-
diff --git a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.js b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.js
new file mode 100644
index 0000000..36637ec
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.js
@@ -0,0 +1,47 @@
+/**
+ * @license
+ * Copyright (C) 2017 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 {GrCountStringFormatter} from './gr-count-string-formatter.js';
+
+suite('gr-count-string-formatter tests', () => {
+  test('computeString', () => {
+    const noun = 'unresolved';
+    assert.equal(GrCountStringFormatter.computeString(0, noun), '');
+    assert.equal(GrCountStringFormatter.computeString(1, noun),
+        '1 unresolved');
+    assert.equal(GrCountStringFormatter.computeString(2, noun),
+        '2 unresolved');
+  });
+
+  test('computeShortString', () => {
+    const noun = 'c';
+    assert.equal(GrCountStringFormatter.computeShortString(0, noun), '');
+    assert.equal(GrCountStringFormatter.computeShortString(1, noun), '1c');
+    assert.equal(GrCountStringFormatter.computeShortString(2, noun), '2c');
+  });
+
+  test('computePluralString', () => {
+    const noun = 'comment';
+    assert.equal(GrCountStringFormatter.computePluralString(0, noun), '');
+    assert.equal(GrCountStringFormatter.computePluralString(1, noun),
+        '1 comment');
+    assert.equal(GrCountStringFormatter.computePluralString(2, noun),
+        '2 comments');
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
index 561746a..bc07d84 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
@@ -30,21 +30,15 @@
 `);
 
 suite('gr-cursor-manager tests', () => {
-  let sandbox;
   let element;
   let list;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     const fixtureElements = basicTestFixutre.instantiate();
     element = fixtureElements[0];
     list = fixtureElements[1];
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('core cursor functionality', () => {
     // The element is initialized into the proper state.
     assert.isArray(element.stops);
@@ -164,8 +158,8 @@
   });
 
   test('opt_noScroll', () => {
-    sandbox.stub(element, '_targetIsVisible', () => false);
-    const scrollStub = sandbox.stub(window, 'scrollTo');
+    sinon.stub(element, '_targetIsVisible').callsFake(() => false);
+    const scrollStub = sinon.stub(window, 'scrollTo');
     element.stops = list.querySelectorAll('li');
     element.scrollMode = 'keep-visible';
 
@@ -207,7 +201,7 @@
   test('focusOnMove prop', () => {
     const listEls = list.querySelectorAll('li');
     for (let i = 0; i < listEls.length; i++) {
-      sandbox.spy(listEls[i], 'focus');
+      sinon.spy(listEls[i], 'focus');
     }
     element.stops = listEls;
     element.setCursor(list.children[0]);
@@ -230,32 +224,32 @@
       // There is a target which has a targetNext
       element.setCursor(list.children[0]);
       element._moveCursor(1);
-      scrollStub = sandbox.stub(window, 'scrollTo');
+      scrollStub = sinon.stub(window, 'scrollTo');
       window.innerHeight = 60;
     });
 
     test('Called when top and bottom not visible', () => {
-      sandbox.stub(element, '_targetIsVisible').returns(false);
+      sinon.stub(element, '_targetIsVisible').returns(false);
       element._scrollToTarget();
       assert.isTrue(scrollStub.called);
     });
 
     test('Not called when top and bottom visible', () => {
-      sandbox.stub(element, '_targetIsVisible').returns(true);
+      sinon.stub(element, '_targetIsVisible').returns(true);
       element._scrollToTarget();
       assert.isFalse(scrollStub.called);
     });
 
     test('Called when top is visible, bottom is not, scroll is lower', () => {
-      const visibleStub = sandbox.stub(element, '_targetIsVisible',
+      const visibleStub = sinon.stub(element, '_targetIsVisible').callsFake(
           () => visibleStub.callCount === 2);
-      sandbox.stub(element, '_getWindowDims').returns({
+      sinon.stub(element, '_getWindowDims').returns({
         scrollX: 123,
         scrollY: 15,
         innerHeight: 1000,
         pageYOffset: 0,
       });
-      sandbox.stub(element, '_calculateScrollToValue').returns(20);
+      sinon.stub(element, '_calculateScrollToValue').returns(20);
       element._scrollToTarget();
       assert.isTrue(scrollStub.called);
       assert.isTrue(scrollStub.calledWithExactly(123, 20));
@@ -263,22 +257,22 @@
     });
 
     test('Called when top is visible, bottom not, scroll is higher', () => {
-      const visibleStub = sandbox.stub(element, '_targetIsVisible',
+      const visibleStub = sinon.stub(element, '_targetIsVisible').callsFake(
           () => visibleStub.callCount === 2);
-      sandbox.stub(element, '_getWindowDims').returns({
+      sinon.stub(element, '_getWindowDims').returns({
         scrollX: 123,
         scrollY: 25,
         innerHeight: 1000,
         pageYOffset: 0,
       });
-      sandbox.stub(element, '_calculateScrollToValue').returns(20);
+      sinon.stub(element, '_calculateScrollToValue').returns(20);
       element._scrollToTarget();
       assert.isFalse(scrollStub.called);
       assert.equal(visibleStub.callCount, 2);
     });
 
     test('_calculateScrollToValue', () => {
-      sandbox.stub(element, '_getWindowDims').returns({
+      sinon.stub(element, '_getWindowDims').returns({
         scrollX: 123,
         scrollY: 25,
         innerHeight: 300,
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js
similarity index 80%
rename from polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
rename to polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js
index 589a157..804c3b7 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js
@@ -1,50 +1,34 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2015 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-date-formatter</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-date-formatter date-str="2015-09-24 23:30:17.033000000"></gr-date-formatter>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-date-formatter.js';
 import {parseDate} from '../../../utils/date-util.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromTemplate(html`
+<gr-date-formatter date-str="2015-09-24 23:30:17.033000000"></gr-date-formatter>
+`);
+
 suite('gr-date-formatter tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-  });
 
-  teardown(() => {
-    sandbox.restore();
   });
 
   /**
@@ -63,7 +47,7 @@
         .toJSON()
         .replace('T', ' ')
         .slice(0, -1);
-    sandbox.useFakeTimers(normalizedDate(nowStr).getTime());
+    sinon.useFakeTimers(normalizedDate(nowStr).getTime());
     element.dateStr = dateStr;
     flush(() => {
       const span = element.shadowRoot
@@ -93,8 +77,8 @@
       date_format: 'STD',
       relative_date_in_change_table: false,
     }).then(() => {
-      element = fixture('basic');
-      sandbox.stub(element, '_getUtcOffsetString').returns('');
+      element = basicFixture.instantiate();
+      sinon.stub(element, '_getUtcOffsetString').returns('');
       return element._loadPreferences();
     }));
 
@@ -142,8 +126,8 @@
       date_format: 'US',
       relative_date_in_change_table: false,
     }).then(() => {
-      element = fixture('basic');
-      sandbox.stub(element, '_getUtcOffsetString').returns('');
+      element = basicFixture.instantiate();
+      sinon.stub(element, '_getUtcOffsetString').returns('');
       return element._loadPreferences();
     }));
 
@@ -178,8 +162,8 @@
       date_format: 'ISO',
       relative_date_in_change_table: false,
     }).then(() => {
-      element = fixture('basic');
-      sandbox.stub(element, '_getUtcOffsetString').returns('');
+      element = basicFixture.instantiate();
+      sinon.stub(element, '_getUtcOffsetString').returns('');
       return element._loadPreferences();
     }));
 
@@ -214,8 +198,8 @@
       date_format: 'EURO',
       relative_date_in_change_table: false,
     }).then(() => {
-      element = fixture('basic');
-      sandbox.stub(element, '_getUtcOffsetString').returns('');
+      element = basicFixture.instantiate();
+      sinon.stub(element, '_getUtcOffsetString').returns('');
       return element._loadPreferences();
     }));
 
@@ -250,8 +234,8 @@
       date_format: 'UK',
       relative_date_in_change_table: false,
     }).then(() => {
-      element = fixture('basic');
-      sandbox.stub(element, '_getUtcOffsetString').returns('');
+      element = basicFixture.instantiate();
+      sinon.stub(element, '_getUtcOffsetString').returns('');
       return element._loadPreferences();
     }));
 
@@ -286,8 +270,8 @@
       stubRestAPI(
           {time_format: 'HHMM_12', date_format: 'STD'}
       ).then(() => {
-        element = fixture('basic');
-        sandbox.stub(element, '_getUtcOffsetString').returns('');
+        element = basicFixture.instantiate();
+        sinon.stub(element, '_getUtcOffsetString').returns('');
         return element._loadPreferences();
       })
     );
@@ -307,8 +291,8 @@
       stubRestAPI(
           {time_format: 'HHMM_12', date_format: 'US'}
       ).then(() => {
-        element = fixture('basic');
-        sandbox.stub(element, '_getUtcOffsetString').returns('');
+        element = basicFixture.instantiate();
+        sinon.stub(element, '_getUtcOffsetString').returns('');
         return element._loadPreferences();
       })
     );
@@ -328,8 +312,8 @@
       stubRestAPI(
           {time_format: 'HHMM_12', date_format: 'ISO'}
       ).then(() => {
-        element = fixture('basic');
-        sandbox.stub(element, '_getUtcOffsetString').returns('');
+        element = basicFixture.instantiate();
+        sinon.stub(element, '_getUtcOffsetString').returns('');
         return element._loadPreferences();
       })
     );
@@ -349,8 +333,8 @@
       stubRestAPI(
           {time_format: 'HHMM_12', date_format: 'EURO'}
       ).then(() => {
-        element = fixture('basic');
-        sandbox.stub(element, '_getUtcOffsetString').returns('');
+        element = basicFixture.instantiate();
+        sinon.stub(element, '_getUtcOffsetString').returns('');
         return element._loadPreferences();
       })
     );
@@ -370,8 +354,8 @@
       stubRestAPI(
           {time_format: 'HHMM_12', date_format: 'UK'}
       ).then(() => {
-        element = fixture('basic');
-        sandbox.stub(element, '_getUtcOffsetString').returns('');
+        element = basicFixture.instantiate();
+        sinon.stub(element, '_getUtcOffsetString').returns('');
         return element._loadPreferences();
       })
     );
@@ -391,8 +375,8 @@
       date_format: 'STD',
       relative_date_in_change_table: true,
     }).then(() => {
-      element = fixture('basic');
-      sandbox.stub(element, '_getUtcOffsetString').returns('');
+      element = basicFixture.instantiate();
+      sinon.stub(element, '_getUtcOffsetString').returns('');
       return element._loadPreferences();
     }));
 
@@ -419,7 +403,7 @@
       date_format: 'US',
       relative_date_in_change_table: true,
     }).then(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       return element._loadPreferences();
     }));
 
@@ -433,7 +417,7 @@
 
   suite('logged out', () => {
     setup(() => stubRestAPI(null).then(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       return element._loadPreferences();
     }));
 
@@ -445,4 +429,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html
deleted file mode 100644
index 1060e82..0000000
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html
+++ /dev/null
@@ -1,111 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-dialog</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-dialog></gr-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-dialog.js';
-import {isHidden} from '../../../test/test-utils.js';
-suite('gr-dialog tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => { sandbox.restore(); });
-
-  test('events', done => {
-    let numEvents = 0;
-    function handler() { if (++numEvents == 2) { done(); } }
-
-    element.addEventListener('confirm', handler);
-    element.addEventListener('cancel', handler);
-
-    MockInteractions.tap(element.shadowRoot
-        .querySelector('gr-button[primary]'));
-    MockInteractions.tap(element.shadowRoot
-        .querySelector('gr-button:not([primary])'));
-  });
-
-  test('confirmOnEnter', () => {
-    element.confirmOnEnter = false;
-    const handleConfirmStub = sandbox.stub(element, '_handleConfirm');
-    const handleKeydownSpy = sandbox.spy(element, '_handleKeydown');
-    MockInteractions.pressAndReleaseKeyOn(element.shadowRoot
-        .querySelector('main'),
-    13, null, 'enter');
-    flushAsynchronousOperations();
-
-    assert.isTrue(handleKeydownSpy.called);
-    assert.isFalse(handleConfirmStub.called);
-
-    element.confirmOnEnter = true;
-    MockInteractions.pressAndReleaseKeyOn(element.shadowRoot
-        .querySelector('main'),
-    13, null, 'enter');
-    flushAsynchronousOperations();
-
-    assert.isTrue(handleConfirmStub.called);
-  });
-
-  test('resetFocus', () => {
-    const focusStub = sandbox.stub(element.$.confirm, 'focus');
-    element.resetFocus();
-    assert.isTrue(focusStub.calledOnce);
-  });
-
-  suite('tooltip', () => {
-    test('tooltip not added by default', () => {
-      assert.isNull(element.$.confirm.getAttribute('has-tooltip'));
-    });
-
-    test('tooltip added if confirm tooltip is passed', done => {
-      element.confirmTooltip = 'confirm tooltip';
-      flush(() => {
-        assert(element.$.confirm.getAttribute('has-tooltip'));
-        done();
-      });
-    });
-  });
-
-  test('empty cancel label hides cancel btn', () => {
-    assert.isFalse(isHidden(element.$.cancel));
-    element.cancelLabel = '';
-    flushAsynchronousOperations();
-
-    assert.isTrue(isHidden(element.$.cancel));
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.js b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.js
new file mode 100644
index 0000000..ce36d7b
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.js
@@ -0,0 +1,93 @@
+/**
+ * @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-dialog.js';
+import {isHidden} from '../../../test/test-utils.js';
+
+const basicFixture = fixtureFromElement('gr-dialog');
+
+suite('gr-dialog tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('events', done => {
+    let numEvents = 0;
+    function handler() { if (++numEvents == 2) { done(); } }
+
+    element.addEventListener('confirm', handler);
+    element.addEventListener('cancel', handler);
+
+    MockInteractions.tap(element.shadowRoot
+        .querySelector('gr-button[primary]'));
+    MockInteractions.tap(element.shadowRoot
+        .querySelector('gr-button:not([primary])'));
+  });
+
+  test('confirmOnEnter', () => {
+    element.confirmOnEnter = false;
+    const handleConfirmStub = sinon.stub(element, '_handleConfirm');
+    const handleKeydownSpy = sinon.spy(element, '_handleKeydown');
+    MockInteractions.pressAndReleaseKeyOn(element.shadowRoot
+        .querySelector('main'),
+    13, null, 'enter');
+    flushAsynchronousOperations();
+
+    assert.isTrue(handleKeydownSpy.called);
+    assert.isFalse(handleConfirmStub.called);
+
+    element.confirmOnEnter = true;
+    MockInteractions.pressAndReleaseKeyOn(element.shadowRoot
+        .querySelector('main'),
+    13, null, 'enter');
+    flushAsynchronousOperations();
+
+    assert.isTrue(handleConfirmStub.called);
+  });
+
+  test('resetFocus', () => {
+    const focusStub = sinon.stub(element.$.confirm, 'focus');
+    element.resetFocus();
+    assert.isTrue(focusStub.calledOnce);
+  });
+
+  suite('tooltip', () => {
+    test('tooltip not added by default', () => {
+      assert.isNull(element.$.confirm.getAttribute('has-tooltip'));
+    });
+
+    test('tooltip added if confirm tooltip is passed', done => {
+      element.confirmTooltip = 'confirm tooltip';
+      flush(() => {
+        assert(element.$.confirm.getAttribute('has-tooltip'));
+        done();
+      });
+    });
+  });
+
+  test('empty cancel label hides cancel btn', () => {
+    assert.isFalse(isHidden(element.$.cancel));
+    element.cancelLabel = '';
+    flushAsynchronousOperations();
+
+    assert.isTrue(isHidden(element.$.cancel));
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.js
similarity index 67%
rename from polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html
rename to polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.js
index 2750d67..ced8c6c 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.js
@@ -1,42 +1,28 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-diff-preferences</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-diff-preferences></gr-diff-preferences>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-diff-preferences.js';
+
+const basicFixture = fixtureFromElement('gr-diff-preferences');
+
 suite('gr-diff-preferences tests', () => {
   let element;
-  let sandbox;
+
   let diffPreferences;
 
   function valueOf(title, fieldsetid) {
@@ -70,13 +56,11 @@
       },
     });
 
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
+    element = basicFixture.instantiate();
+
     return element.loadData();
   });
 
-  teardown(() => { sandbox.restore(); });
-
   test('renders', () => {
     // Rendered with the expected preferences selected.
     assert.equal(valueOf('Context', 'diffPreferences')
@@ -105,7 +89,7 @@
   });
 
   test('save changes', () => {
-    sandbox.stub(element.$.restAPI, 'saveDiffPreferences')
+    sinon.stub(element.$.restAPI, 'saveDiffPreferences')
         .returns(Promise.resolve());
     const showTrailingWhitespaceCheckbox =
         valueOf('Show trailing whitespace', 'diffPreferences')
@@ -121,4 +105,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.js
similarity index 68%
rename from polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
rename to polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.js
index 237fbe0..0f8b97d 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.js
@@ -1,43 +1,29 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-download-commands</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-download-commands></gr-download-commands>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-download-commands.js';
 import {isHidden} from '../../../test/test-utils.js';
+
+const basicFixture = fixtureFromElement('gr-download-commands');
+
 suite('gr-download-commands', () => {
   let element;
-  let sandbox;
+
   const SCHEMES = ['http', 'repo', 'ssh'];
   const COMMANDS = [{
     title: 'Checkout',
@@ -59,16 +45,12 @@
   const SELECTED_SCHEME = 'http';
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-  });
 
-  teardown(() => {
-    sandbox.restore();
   });
 
   suite('unauthenticated', () => {
     setup(done => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.schemes = SCHEMES;
       element.commands = COMMANDS;
       element.selectedScheme = SELECTED_SCHEME;
@@ -77,7 +59,7 @@
     });
 
     test('focusOnCopy', () => {
-      const focusStub = sandbox.stub(element.shadowRoot
+      const focusStub = sinon.stub(element.shadowRoot
           .querySelector('gr-shell-command'),
       'focusOnCopy');
       element.focusOnCopy();
@@ -136,8 +118,8 @@
 
     test('saves scheme to preferences', () => {
       element._loggedIn = true;
-      const savePrefsStub = sandbox.stub(element.$.restAPI, 'savePreferences',
-          () => Promise.resolve());
+      const savePrefsStub = sinon.stub(element.$.restAPI, 'savePreferences')
+          .callsFake(() => Promise.resolve());
 
       flushAsynchronousOperations();
 
@@ -152,4 +134,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.js
similarity index 76%
rename from polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
rename to polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.js
index b64d5f7..8d7de0e 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.js
@@ -1,58 +1,39 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-dropdown-list</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-dropdown-list></gr-dropdown-list>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-dropdown-list.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromElement('gr-dropdown-list');
+
 suite('gr-dropdown-list tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
     stub('gr-rest-api-interface', {
       getConfig() { return Promise.resolve({}); },
     });
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    element = basicFixture.instantiate();
   });
 
   test('tap on trigger opens menu', () => {
-    sandbox.stub(element, '_open', () => { element.$.dropdown.open(); });
+    sinon.stub(element, '_open')
+        .callsFake(() => { element.$.dropdown.open(); });
     assert.isFalse(element.$.dropdown.opened);
     MockInteractions.tap(element.$.trigger);
     assert.isTrue(element.$.dropdown.opened);
@@ -169,4 +150,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.js
similarity index 74%
rename from polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
rename to polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.js
index d17cc1a..d1d9164 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.js
@@ -1,54 +1,34 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-dropdown</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-dropdown></gr-dropdown>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-dropdown.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromElement('gr-dropdown');
+
 suite('gr-dropdown tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
     stub('gr-rest-api-interface', {
       getConfig() { return Promise.resolve({}); },
     });
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    element = basicFixture.instantiate();
   });
 
   test('_computeIsDownload', () => {
@@ -57,8 +37,10 @@
   });
 
   test('tap on trigger opens menu, then closes', () => {
-    sandbox.stub(element, '_open', () => { element.$.dropdown.open(); });
-    sandbox.stub(element, '_close', () => { element.$.dropdown.close(); });
+    sinon.stub(element, '_open')
+        .callsFake(() => { element.$.dropdown.open(); });
+    sinon.stub(element, '_close')
+        .callsFake(() => { element.$.dropdown.close(); });
     assert.isFalse(element.$.dropdown.opened);
     MockInteractions.tap(element.$.trigger);
     assert.isTrue(element.$.dropdown.opened);
@@ -122,8 +104,8 @@
   test('non link items', () => {
     const item0 = {name: 'item one', id: 'foo'};
     element.items = [item0, {name: 'item two', id: 'bar'}];
-    const fooTapped = sandbox.stub();
-    const tapped = sandbox.stub();
+    const fooTapped = sinon.stub();
+    const tapped = sinon.stub();
     element.addEventListener('tap-item-foo', fooTapped);
     element.addEventListener('tap-item', tapped);
     flushAsynchronousOperations();
@@ -138,8 +120,8 @@
     element.items = [{name: 'item one', id: 'foo'}];
     element.disabledIds = ['foo'];
 
-    const stub = sandbox.stub();
-    const tapped = sandbox.stub();
+    const stub = sinon.stub();
+    const tapped = sinon.stub();
     element.addEventListener('tap-item-foo', stub);
     element.addEventListener('tap-item', tapped);
     flushAsynchronousOperations();
@@ -174,7 +156,7 @@
     });
 
     test('down', () => {
-      const stub = sandbox.stub(element.$.cursor, 'next');
+      const stub = sinon.stub(element.$.cursor, 'next');
       assert.isFalse(element.$.dropdown.opened);
       MockInteractions.pressAndReleaseKeyOn(element, 40);
       assert.isTrue(element.$.dropdown.opened);
@@ -183,7 +165,7 @@
     });
 
     test('up', () => {
-      const stub = sandbox.stub(element.$.cursor, 'previous');
+      const stub = sinon.stub(element.$.cursor, 'previous');
       assert.isFalse(element.$.dropdown.opened);
       MockInteractions.pressAndReleaseKeyOn(element, 38);
       assert.isTrue(element.$.dropdown.opened);
@@ -199,10 +181,10 @@
       assert.isTrue(element.$.dropdown.opened);
 
       const el = element.$.cursor.target.querySelector(':not([hidden]) a');
-      const stub = sandbox.stub(el, 'click');
+      const stub = sinon.stub(el, 'click');
       MockInteractions.pressAndReleaseKeyOn(element, 32); // Space
       assert.isTrue(stub.called);
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
deleted file mode 100644
index c50920e..0000000
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
+++ /dev/null
@@ -1,166 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-editable-content</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<!-- Can't use absolute path below for mock-interaction.js.
-Web component tester(wct) has a built-in http server and it serves "/components" directory (which is
-actually /node_modules directory). Also, wct patches some files to load modules from /components.
-With the absolute path, browser tries to load dom-module from 2 different places (/component/... and
-/node_modules/...) though this is actually the same file. This leads to a run-time error.
--->
-<script src="../../../node_modules/iron-test-helpers/mock-interactions.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-editable-content></gr-editable-content>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-editable-content.js';
-suite('gr-editable-content tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
-  });
-
-  teardown(() => { sandbox.restore(); });
-
-  test('save event', done => {
-    element.content = '';
-    element._newContent = 'foo';
-    element.addEventListener('editable-content-save', e => {
-      assert.equal(e.detail.content, 'foo');
-      done();
-    });
-    MockInteractions.tap(element.shadowRoot
-        .querySelector('gr-button[primary]'));
-  });
-
-  test('cancel event', done => {
-    element.addEventListener('editable-content-cancel', () => {
-      done();
-    });
-    MockInteractions.tap(element.shadowRoot
-        .querySelector('gr-button:not([primary])'));
-  });
-
-  test('enabling editing keeps old content', () => {
-    element.content = 'current content';
-    element._newContent = 'old content';
-    element.editing = true;
-    assert.equal(element._newContent, 'old content');
-  });
-
-  test('disabling editing does not update edit field contents', () => {
-    element.content = 'current content';
-    element.editing = true;
-    element._newContent = 'stale content';
-    element.editing = false;
-    assert.equal(element._newContent, 'stale content');
-  });
-
-  test('zero width spaces are removed properly', () => {
-    element.removeZeroWidthSpace = true;
-    element.content = 'R=\u200Btest@google.com';
-    element.editing = true;
-    assert.equal(element._newContent, 'R=test@google.com');
-  });
-
-  suite('editing', () => {
-    setup(() => {
-      element.content = 'current content';
-      element.editing = true;
-    });
-
-    test('save button is disabled initially', () => {
-      assert.isTrue(element.shadowRoot
-          .querySelector('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);
-    });
-  });
-
-  suite('storageKey and related behavior', () => {
-    let dispatchSpy;
-    setup(() => {
-      element.content = 'current content';
-      element.storageKey = 'test';
-      dispatchSpy = sandbox.spy(element, 'dispatchEvent');
-    });
-
-    test('editing toggled to true, has stored data', () => {
-      sandbox.stub(element.$.storage, 'getEditableContentItem')
-          .returns({message: 'stored content'});
-      element.editing = true;
-
-      assert.equal(element._newContent, 'stored content');
-      assert.isTrue(dispatchSpy.called);
-      assert.equal(dispatchSpy.lastCall.args[0].type, 'show-alert');
-    });
-
-    test('editing toggled to true, has no stored data', () => {
-      sandbox.stub(element.$.storage, 'getEditableContentItem')
-          .returns({});
-      element.editing = true;
-
-      assert.equal(element._newContent, 'current content');
-      assert.isFalse(dispatchSpy.called);
-    });
-
-    test('edits are cached', () => {
-      const storeStub =
-          sandbox.stub(element.$.storage, 'setEditableContentItem');
-      const eraseStub =
-          sandbox.stub(element.$.storage, 'eraseEditableContentItem');
-      element.editing = true;
-
-      element._newContent = 'new content';
-      flushAsynchronousOperations();
-      element.flushDebouncer('store');
-
-      assert.isTrue(storeStub.called);
-      assert.deepEqual(
-          [element.storageKey, element._newContent],
-          storeStub.lastCall.args);
-
-      element._newContent = '';
-      flushAsynchronousOperations();
-      element.flushDebouncer('store');
-
-      assert.isTrue(eraseStub.called);
-      assert.deepEqual([element.storageKey], eraseStub.lastCall.args);
-    });
-  });
-});
-</script>
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.js
new file mode 100644
index 0000000..0a9dd79
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.js
@@ -0,0 +1,141 @@
+/**
+ * @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-editable-content.js';
+
+const basicFixture = fixtureFromElement('gr-editable-content');
+
+suite('gr-editable-content tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('save event', done => {
+    element.content = '';
+    element._newContent = 'foo';
+    element.addEventListener('editable-content-save', e => {
+      assert.equal(e.detail.content, 'foo');
+      done();
+    });
+    MockInteractions.tap(element.shadowRoot
+        .querySelector('gr-button[primary]'));
+  });
+
+  test('cancel event', done => {
+    element.addEventListener('editable-content-cancel', () => {
+      done();
+    });
+    MockInteractions.tap(element.shadowRoot
+        .querySelector('gr-button:not([primary])'));
+  });
+
+  test('enabling editing keeps old content', () => {
+    element.content = 'current content';
+    element._newContent = 'old content';
+    element.editing = true;
+    assert.equal(element._newContent, 'old content');
+  });
+
+  test('disabling editing does not update edit field contents', () => {
+    element.content = 'current content';
+    element.editing = true;
+    element._newContent = 'stale content';
+    element.editing = false;
+    assert.equal(element._newContent, 'stale content');
+  });
+
+  test('zero width spaces are removed properly', () => {
+    element.removeZeroWidthSpace = true;
+    element.content = 'R=\u200Btest@google.com';
+    element.editing = true;
+    assert.equal(element._newContent, 'R=test@google.com');
+  });
+
+  suite('editing', () => {
+    setup(() => {
+      element.content = 'current content';
+      element.editing = true;
+    });
+
+    test('save button is disabled initially', () => {
+      assert.isTrue(element.shadowRoot
+          .querySelector('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);
+    });
+  });
+
+  suite('storageKey and related behavior', () => {
+    let dispatchSpy;
+    setup(() => {
+      element.content = 'current content';
+      element.storageKey = 'test';
+      dispatchSpy = sinon.spy(element, 'dispatchEvent');
+    });
+
+    test('editing toggled to true, has stored data', () => {
+      sinon.stub(element.$.storage, 'getEditableContentItem')
+          .returns({message: 'stored content'});
+      element.editing = true;
+
+      assert.equal(element._newContent, 'stored content');
+      assert.isTrue(dispatchSpy.called);
+      assert.equal(dispatchSpy.lastCall.args[0].type, 'show-alert');
+    });
+
+    test('editing toggled to true, has no stored data', () => {
+      sinon.stub(element.$.storage, 'getEditableContentItem')
+          .returns({});
+      element.editing = true;
+
+      assert.equal(element._newContent, 'current content');
+      assert.isFalse(dispatchSpy.called);
+    });
+
+    test('edits are cached', () => {
+      const storeStub =
+          sinon.stub(element.$.storage, 'setEditableContentItem');
+      const eraseStub =
+          sinon.stub(element.$.storage, 'eraseEditableContentItem');
+      element.editing = true;
+
+      element._newContent = 'new content';
+      flushAsynchronousOperations();
+      element.flushDebouncer('store');
+
+      assert.isTrue(storeStub.called);
+      assert.deepEqual(
+          [element.storageKey, element._newContent],
+          storeStub.lastCall.args);
+
+      element._newContent = '';
+      flushAsynchronousOperations();
+      element.flushDebouncer('store');
+
+      assert.isTrue(eraseStub.called);
+      assert.deepEqual([element.storageKey], eraseStub.lastCall.args);
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.js
similarity index 64%
rename from polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
rename to polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.js
index 1d1bce8..8c04aed 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.js
@@ -1,78 +1,55 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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
+import '../../../test/common-test-setup-karma.js';
+import './gr-editable-label.js';
+import {flush as flush$0} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-editable-label</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<!-- Can't use absolute path below for mock-interaction.js.
-Web component tester(wct) has a built-in http server and it serves "/components" directory (which is
-actually /node_modules directory). Also, wct patches some files to load modules from /components.
-With the absolute path, browser tries to load dom-module from 2 different places (/component/... and
-/node_modules/...) though this is actually the same file. This leads to a run-time error.
--->
-<script src="../../../node_modules/iron-test-helpers/mock-interactions.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-editable-label
+const basicFixture = fixtureFromTemplate(html`
+<gr-editable-label
         value="value text"
         placeholder="label text"></gr-editable-label>
-  </template>
-</test-fixture>
+`);
 
-<test-fixture id="no-placeholder">
-  <template>
-    <gr-editable-label value=""></gr-editable-label>
-  </template>
-</test-fixture>
+const noPlaceholderFixture = fixtureFromTemplate(html`
+<gr-editable-label value=""></gr-editable-label>
+`);
 
-<test-fixture id="read-only">
-  <template>
-    <gr-editable-label
+const readOnlyFixture = fixtureFromTemplate(html`
+<gr-editable-label
         read-only
         value="value text"
         placeholder="label text"></gr-editable-label>
-  </template>
-</test-fixture>
+`);
 
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-editable-label.js';
-import {flush as flush$0} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 suite('gr-editable-label tests', () => {
   let element;
   let elementNoPlaceholder;
   let input;
   let label;
-  let sandbox;
 
   setup(done => {
-    element = fixture('basic');
-    elementNoPlaceholder = fixture('no-placeholder');
+    element = basicFixture.instantiate();
+    elementNoPlaceholder = noPlaceholderFixture.instantiate();
 
     label = element.shadowRoot
         .querySelector('label');
-    sandbox = sinon.sandbox.create();
+
     flush(() => {
       // In Polymer 2 inputElement isn't nativeInput anymore
       input = element.$.input.$.nativeInput || element.$.input.inputElement;
@@ -80,17 +57,13 @@
     });
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('element render', () => {
     // The dropdown is closed and the label is visible:
     assert.isFalse(element.$.dropdown.opened);
     assert.isTrue(label.classList.contains('editable'));
     assert.equal(label.textContent, 'value text');
-    const focusSpy = sandbox.spy(input, 'focus');
-    const showSpy = sandbox.spy(element, '_showDropdown');
+    const focusSpy = sinon.spy(input, 'focus');
+    const showSpy = sinon.spy(element, '_showDropdown');
 
     MockInteractions.tap(label);
 
@@ -123,7 +96,7 @@
   });
 
   test('edit value', done => {
-    const editedStub = sandbox.stub();
+    const editedStub = sinon.stub();
     element.addEventListener('changed', editedStub);
     assert.isFalse(element.editing);
 
@@ -148,7 +121,7 @@
   });
 
   test('save button', done => {
-    const editedStub = sandbox.stub();
+    const editedStub = sinon.stub();
     element.addEventListener('changed', editedStub);
     assert.isFalse(element.editing);
 
@@ -173,7 +146,7 @@
   });
 
   test('edit and then escape key', done => {
-    const editedStub = sandbox.stub();
+    const editedStub = sinon.stub();
     element.addEventListener('changed', editedStub);
     assert.isFalse(element.editing);
 
@@ -199,7 +172,7 @@
   });
 
   test('cancel button', done => {
-    const editedStub = sandbox.stub();
+    const editedStub = sinon.stub();
     element.addEventListener('changed', editedStub);
     assert.isFalse(element.editing);
 
@@ -229,7 +202,7 @@
     let label;
 
     setup(() => {
-      element = fixture('read-only');
+      element = readOnlyFixture.instantiate();
       label = element.shadowRoot
           .querySelector('label');
     });
@@ -250,4 +223,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
deleted file mode 100644
index ef31382..0000000
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
+++ /dev/null
@@ -1,124 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-fixed-panel</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<style>
-  /* Prevent horizontal scrolling on page.
-   New version of web-component-tester creates body with margins */
-  body {
-    margin: 0px;
-    padding: 0px;
-  }
-</style>
-
-<test-fixture id="basic">
-  <template>
-    <gr-fixed-panel>
-      <div style="height: 100px"></div>
-    </gr-fixed-panel>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-fixed-panel.js';
-suite('gr-fixed-panel', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-    element.readyForMeasure = true;
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('can be disabled with floatingDisabled', () => {
-    element.floatingDisabled = true;
-    sandbox.stub(element, '_reposition');
-    window.dispatchEvent(new CustomEvent('resize'));
-    element.flushDebouncer('update');
-    assert.isFalse(element._reposition.called);
-  });
-
-  test('header is the height of the content', () => {
-    assert.equal(element.getBoundingClientRect().height, 100);
-  });
-
-  test('scroll triggers _reposition', () => {
-    sandbox.stub(element, '_reposition');
-    window.dispatchEvent(new CustomEvent('scroll'));
-    element.flushDebouncer('update');
-    assert.isTrue(element._reposition.called);
-  });
-
-  suite('_reposition', () => {
-    const getHeaderTop = function() {
-      return element.$.header.style.top;
-    };
-
-    const emulateScrollY = function(distance) {
-      element._getElementTop.returns(element._headerTopInitial - distance);
-      element._updateDebounced();
-      element.flushDebouncer('scroll');
-    };
-
-    setup(() => {
-      element._headerTopInitial = 10;
-      sandbox.stub(element, '_getElementTop')
-          .returns(element._headerTopInitial);
-    });
-
-    test('scrolls header along with document', () => {
-      emulateScrollY(20);
-      // No top property is set when !_headerFloating.
-      assert.equal(getHeaderTop(), '');
-    });
-
-    test('does not stick to the top by default', () => {
-      emulateScrollY(150);
-      // No top property is set when !_headerFloating.
-      assert.equal(getHeaderTop(), '');
-    });
-
-    test('sticks to the top if enabled', () => {
-      element.keepOnScroll = true;
-      emulateScrollY(120);
-      assert.equal(getHeaderTop(), '0px');
-    });
-
-    test('drops a shadow when fixed to the top', () => {
-      element.keepOnScroll = true;
-      emulateScrollY(5);
-      assert.isFalse(element.$.header.classList.contains('fixedAtTop'));
-      emulateScrollY(120);
-      assert.isTrue(element.$.header.classList.contains('fixedAtTop'));
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.js b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.js
new file mode 100644
index 0000000..b9378ba
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.js
@@ -0,0 +1,99 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-fixed-panel.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromTemplate(html`
+<gr-fixed-panel>
+      <div style="height: 100px"></div>
+    </gr-fixed-panel>
+`);
+
+suite('gr-fixed-panel', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+    element.readyForMeasure = true;
+  });
+
+  test('can be disabled with floatingDisabled', () => {
+    element.floatingDisabled = true;
+    sinon.stub(element, '_reposition');
+    window.dispatchEvent(new CustomEvent('resize'));
+    element.flushDebouncer('update');
+    assert.isFalse(element._reposition.called);
+  });
+
+  test('header is the height of the content', () => {
+    assert.equal(element.getBoundingClientRect().height, 100);
+  });
+
+  test('scroll triggers _reposition', () => {
+    sinon.stub(element, '_reposition');
+    window.dispatchEvent(new CustomEvent('scroll'));
+    element.flushDebouncer('update');
+    assert.isTrue(element._reposition.called);
+  });
+
+  suite('_reposition', () => {
+    const getHeaderTop = function() {
+      return element.$.header.style.top;
+    };
+
+    const emulateScrollY = function(distance) {
+      element._getElementTop.returns(element._headerTopInitial - distance);
+      element._updateDebounced();
+      element.flushDebouncer('scroll');
+    };
+
+    setup(() => {
+      element._headerTopInitial = 10;
+      sinon.stub(element, '_getElementTop')
+          .returns(element._headerTopInitial);
+    });
+
+    test('scrolls header along with document', () => {
+      emulateScrollY(20);
+      // No top property is set when !_headerFloating.
+      assert.equal(getHeaderTop(), '');
+    });
+
+    test('does not stick to the top by default', () => {
+      emulateScrollY(150);
+      // No top property is set when !_headerFloating.
+      assert.equal(getHeaderTop(), '');
+    });
+
+    test('sticks to the top if enabled', () => {
+      element.keepOnScroll = true;
+      emulateScrollY(120);
+      assert.equal(getHeaderTop(), '0px');
+    });
+
+    test('drops a shadow when fixed to the top', () => {
+      element.keepOnScroll = true;
+      emulateScrollY(5);
+      assert.isFalse(element.$.header.classList.contains('fixedAtTop'));
+      emulateScrollY(120);
+      assert.isTrue(element.$.header.classList.contains('fixedAtTop'));
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.js
similarity index 89%
rename from polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
rename to polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.js
index 083eac4..fd5a9ba 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.js
@@ -1,42 +1,27 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-editable-label</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-formatted-text></gr-formatted-text>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-formatted-text.js';
+
+const basicFixture = fixtureFromElement('gr-formatted-text');
+
 suite('gr-formatted-text tests', () => {
   let element;
-  let sandbox;
 
   function assertBlock(result, index, type, text) {
     assert.equal(result[index].type, type);
@@ -49,12 +34,7 @@
   }
 
   setup(() => {
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    element = basicFixture.instantiate();
   });
 
   test('parse null undefined and empty', () => {
@@ -409,18 +389,18 @@
   });
 
   test('_computeNodes called without config', () => {
-    const computeNodesSpy = sandbox.spy(element, '_computeNodes');
+    const computeNodesSpy = sinon.spy(element, '_computeNodes');
     element.content = 'some text';
     assert.isTrue(computeNodesSpy.called);
   });
 
   test('_contentOrConfigChanged called with config', () => {
-    const contentStub = sandbox.stub(element, '_contentChanged');
-    const contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged');
+    const contentStub = sinon.stub(element, '_contentChanged');
+    const contentConfigStub = sinon.stub(element, '_contentOrConfigChanged');
     element.content = 'some text';
     element.config = {};
     assert.isTrue(contentStub.called);
     assert.isTrue(contentConfigStub.called);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html
deleted file mode 100644
index a321b43..0000000
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html
+++ /dev/null
@@ -1,96 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2020 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.
--->
-
-<meta name="viewport"
-      content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-hovercard-account</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script src="../../../node_modules/iron-test-helpers/mock-interactions.js" type="module"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-hovercard-account class="hovered"></gr-hovercard-account>
-  </template>
-</test-fixture>
-
-
-<script type="module">
-  import '../../../test/common-test-setup.js';
-  import './gr-hovercard-account.js';
-
-  suite('gr-hovercard-account tests', () => {
-    let element;
-    let sandbox;
-    const ACCOUNT = {
-      email: 'kermit@gmail.com',
-      username: 'kermit',
-      name: 'Kermit The Frog',
-      _account_id: '31415926535',
-    };
-
-    setup(() => {
-      sandbox = sinon.sandbox.create();
-      element = fixture('basic');
-      sandbox.stub(element.$.restAPI, 'getAccount').returns(
-          new Promise(resolve => { '2'; })
-      );
-      element.account = Object.assign({}, ACCOUNT);
-      element.show({});
-      flushAsynchronousOperations();
-    });
-
-    test('account name is shown', () => {
-      assert.equal(element.shadowRoot.querySelector('.name').innerText,
-          'Kermit The Frog');
-    });
-
-    test('_computeText', () => {
-      let account = {_account_id: '1'};
-      const selfAccount = {_account_id: '1'};
-      assert.equal(element._computeText(account, selfAccount), 'Your');
-      account = {_account_id: '2'};
-      assert.equal(element._computeText(account, selfAccount), 'Their');
-    });
-
-    test('account status is not shown if the property is not set', () => {
-      assert.isNull(element.shadowRoot.querySelector('.status'));
-    });
-
-    test('account status is displayed', () => {
-      element.account = Object.assign({status: 'OOO'}, ACCOUNT);
-      flushAsynchronousOperations();
-      assert.equal(element.shadowRoot.querySelector('.status .value').innerText,
-          'OOO');
-    });
-
-    test('voteable div is not shown if the property is not set', () => {
-      assert.isNull(element.shadowRoot.querySelector('.voteable'));
-    });
-
-    test('voteable div is displayed', () => {
-      element.voteableText = 'CodeReview: +2';
-      flushAsynchronousOperations();
-      assert.equal(element.shadowRoot.querySelector('.voteable .value').innerText,
-          element.voteableText);
-    });
-  });
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
new file mode 100644
index 0000000..ea7eb87
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
@@ -0,0 +1,86 @@
+/**
+ * @license
+ * Copyright (C) 2020 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-hovercard-account.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromTemplate(html`
+<gr-hovercard-account class="hovered"></gr-hovercard-account>
+`);
+
+suite('gr-hovercard-account tests', () => {
+  let element;
+
+  const ACCOUNT = {
+    email: 'kermit@gmail.com',
+    username: 'kermit',
+    name: 'Kermit The Frog',
+    _account_id: '31415926535',
+  };
+
+  setup(() => {
+    element = basicFixture.instantiate();
+    sinon.stub(element.$.restAPI, 'getAccount').returns(
+        new Promise(resolve => { '2'; })
+    );
+
+    element.account = Object.assign({}, ACCOUNT);
+    element.show({});
+    flushAsynchronousOperations();
+  });
+
+  teardown(() => {
+    element.hide({});
+  });
+
+  test('account name is shown', () => {
+    assert.equal(element.shadowRoot.querySelector('.name').innerText,
+        'Kermit The Frog');
+  });
+
+  test('_computeText', () => {
+    let account = {_account_id: '1'};
+    const selfAccount = {_account_id: '1'};
+    assert.equal(element._computeText(account, selfAccount), 'Your');
+    account = {_account_id: '2'};
+    assert.equal(element._computeText(account, selfAccount), 'Their');
+  });
+
+  test('account status is not shown if the property is not set', () => {
+    assert.isNull(element.shadowRoot.querySelector('.status'));
+  });
+
+  test('account status is displayed', () => {
+    element.account = Object.assign({status: 'OOO'}, ACCOUNT);
+    flushAsynchronousOperations();
+    assert.equal(element.shadowRoot.querySelector('.status .value').innerText,
+        'OOO');
+  });
+
+  test('voteable div is not shown if the property is not set', () => {
+    assert.isNull(element.shadowRoot.querySelector('.voteable'));
+  });
+
+  test('voteable div is displayed', () => {
+    element.voteableText = 'CodeReview: +2';
+    flushAsynchronousOperations();
+    assert.equal(element.shadowRoot.querySelector('.voteable .value').innerText,
+        element.voteableText);
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.js
similarity index 66%
rename from polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
rename to polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.js
index 21f692d..a7a2c0e 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.js
@@ -1,58 +1,49 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2018 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-hovercard</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<!-- Can't use absolute path below for mock-interaction.js.
-Web component tester(wct) has a built-in http server and it serves "/components" directory (which is
-actually /node_modules directory). Also, wct patches some files to load modules from /components.
-With the absolute path, browser tries to load dom-module from 2 different places (/component/... and
-/node_modules/...) though this is actually the same file. This leads to a run-time error.
--->
-<script src="../../../node_modules/iron-test-helpers/mock-interactions.js"></script>
-
-<button id="foo">Hello</button>
-<test-fixture id="basic">
-  <template>
-    <gr-hovercard for="foo" id="bar"></gr-hovercard>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-hovercard.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+//
+
+const basicFixture = fixtureFromTemplate(html`
+<gr-hovercard for="foo" id="bar"></gr-hovercard>
+`);
+
 suite('gr-hovercard tests', () => {
   let element;
-  let sandbox;
+
+  let button;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+    button = document.createElement('button');
+    button.innerHTML = 'Hello';
+    button.setAttribute('id', 'foo');
+    document.body.appendChild(button);
+
+    element = basicFixture.instantiate();
   });
 
-  teardown(() => { sandbox.restore(); });
+  teardown(() => {
+    element.hide({});
+    button.remove();
+  });
 
   test('updatePosition', () => {
     // Test that the correct style properties have at least been set.
@@ -156,4 +147,4 @@
     button.dispatchEvent(new CustomEvent('mouseenter'));
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.js
similarity index 62%
rename from polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html
rename to polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.js
index a14612b..b46a3b0 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.js
@@ -1,53 +1,36 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-annotation-actions-context</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <div></div>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-js-api-interface.js';
 import {GrAnnotation} from '../../diff/gr-diff-highlight/gr-annotation.js';
 import {GrAnnotationActionsContext} from './gr-annotation-actions-context.js';
 import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
+
 const pluginApi = _testOnly_initGerritPluginApi();
 
 suite('gr-annotation-actions-context tests', () => {
   let instance;
-  let sandbox;
+
   let el;
   let lineNumberEl;
   let plugin;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     pluginApi.install(p => { plugin = p; }, '0.1',
         'http://test.com/plugins/testplugin/static/test.js');
 
@@ -64,11 +47,11 @@
   });
 
   teardown(() => {
-    sandbox.restore();
+    el.remove();
   });
 
   test('test annotateRange', () => {
-    const annotateElementSpy = sandbox.spy(GrAnnotation, 'annotateElement');
+    const annotateElementSpy = sinon.spy(GrAnnotation, 'annotateElement');
     const start = 0;
     const end = 100;
     const cssStyleObject = plugin.styles().css('background-color: #000000');
@@ -101,4 +84,4 @@
     assert.isTrue(lineNumberEl.classList.contains(className));
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.js
similarity index 69%
rename from polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
rename to polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.js
index e72fc63..e819529 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.js
@@ -1,54 +1,42 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-annotation-actions-js-api-js-api</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<test-fixture id="basic">
-  <template>
-    <span hidden id="annotation-span">
-      <label for="annotation-checkbox" id="annotation-label"></label>
-      <iron-input type="checkbox" disabled>
-        <input is="iron-input" type="checkbox" id="annotation-checkbox" disabled>
-      </iron-input>
-    </span>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import '../../change/gr-change-actions/gr-change-actions.js';
 import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromTemplate(html`
+  <span hidden id="annotation-span">
+    <label for="annotation-checkbox" id="annotation-label"></label>
+    <iron-input type="checkbox" disabled>
+      <input is="iron-input" type="checkbox" id="annotation-checkbox" disabled>
+    </iron-input>
+  </span>
+`);
 
 const pluginApi = _testOnly_initGerritPluginApi();
 
 suite('gr-annotation-actions-js-api tests', () => {
   let annotationActions;
-  let sandbox;
+
   let plugin;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     pluginApi.install(p => { plugin = p; }, '0.1',
         'http://test.com/plugins/testplugin/static/test.js');
     annotationActions = plugin.annotationApi();
@@ -56,7 +44,6 @@
 
   teardown(() => {
     annotationActions = null;
-    sandbox.restore();
   });
 
   test('add/get layer', () => {
@@ -89,8 +76,8 @@
     const path2 = '/dummy/path2';
     const annotationLayer1 = annotationActions.getLayer(path1, 1, 2);
     const annotationLayer2 = annotationActions.getLayer(path2, 1, 2);
-    const layer1Spy = sandbox.spy(annotationLayer1, 'notifyListeners');
-    const layer2Spy = sandbox.spy(annotationLayer2, 'notifyListeners');
+    const layer1Spy = sinon.spy(annotationLayer1, 'notifyListeners');
+    const layer2Spy = sinon.spy(annotationLayer2, 'notifyListeners');
 
     let notify;
     let notifyFuncCalled;
@@ -112,8 +99,8 @@
     assert.isFalse(layer2Spy.called);
 
     // Reset spies.
-    layer1Spy.reset();
-    layer2Spy.reset();
+    layer1Spy.resetHistory();
+    layer2Spy.resetHistory();
 
     // Assert that only the 2nd layer is invoked with path2.
     notify(path2, 0, 20, 'left');
@@ -122,9 +109,9 @@
   });
 
   test('toggle checkbox', () => {
-    const fakeEl = {content: fixture('basic')};
-    const hookStub = {onAttached: sandbox.stub()};
-    sandbox.stub(plugin, 'hook').returns(hookStub);
+    const fakeEl = {content: basicFixture.instantiate()};
+    const hookStub = {onAttached: sinon.stub()};
+    sinon.stub(plugin, 'hook').returns(hookStub);
 
     let checkbox;
     let onAttachedFuncCalled = false;
@@ -148,8 +135,8 @@
     // Assert that error is shown if we try to enable checkbox again.
     onAttachedFuncCalled = false;
     annotationActions.enableToggleCheckbox('test label2', onAttachedFunc);
-    const errorStub = sandbox.stub(
-        console, 'error', (msg, err) => undefined);
+    const errorStub = sinon.stub(
+        console, 'error').callsFake((msg, err) => undefined);
     emulateAttached();
     assert.isTrue(
         errorStub.calledWith(
@@ -189,4 +176,4 @@
     assert.equal(listenerCalledTimes, 3);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html
deleted file mode 100644
index d01566a..0000000
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html
+++ /dev/null
@@ -1,85 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2019 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-api-interface</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-js-api-interface.js';
-import {getPluginNameFromUrl} from './gr-api-utils.js';
-
-const PRELOADED_PROTOCOL = 'preloaded:';
-
-suite('gr-api-utils tests', () => {
-  suite('test getPluginNameFromUrl', () => {
-    test('with empty string', () => {
-      assert.equal(getPluginNameFromUrl(''), null);
-    });
-
-    test('with invalid url', () => {
-      assert.equal(getPluginNameFromUrl('test'), null);
-    });
-
-    test('with random invalid url', () => {
-      assert.equal(getPluginNameFromUrl('http://example.com'), null);
-      assert.equal(
-          getPluginNameFromUrl('http://example.com/static/a.html'),
-          null
-      );
-    });
-
-    test('with valid urls', () => {
-      assert.equal(
-          getPluginNameFromUrl('http://example.com/plugins/a.html'),
-          'a'
-      );
-      assert.equal(
-          getPluginNameFromUrl('http://example.com/plugins/a/static/t.html'),
-          'a'
-      );
-    });
-
-    test('with preloaded urls', () => {
-      assert.equal(getPluginNameFromUrl(`${PRELOADED_PROTOCOL}a`), 'a');
-    });
-
-    test('with gerrit-theme override', () => {
-      assert.equal(
-          getPluginNameFromUrl('http://example.com/static/gerrit-theme.html'),
-          'gerrit-theme'
-      );
-    });
-
-    test('with ASSETS_PATH', () => {
-      window.ASSETS_PATH = 'http://cdn.com/2';
-      assert.equal(
-          getPluginNameFromUrl(`${window.ASSETS_PATH}/plugins/a.html`),
-          'a'
-      );
-      window.ASSETS_PATH = undefined;
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.js
new file mode 100644
index 0000000..85c62cb
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.js
@@ -0,0 +1,74 @@
+/**
+ * @license
+ * Copyright (C) 2019 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-js-api-interface.js';
+import {getPluginNameFromUrl} from './gr-api-utils.js';
+
+const PRELOADED_PROTOCOL = 'preloaded:';
+
+suite('gr-api-utils tests', () => {
+  suite('test getPluginNameFromUrl', () => {
+    test('with empty string', () => {
+      assert.equal(getPluginNameFromUrl(''), null);
+    });
+
+    test('with invalid url', () => {
+      assert.equal(getPluginNameFromUrl('test'), null);
+    });
+
+    test('with random invalid url', () => {
+      assert.equal(getPluginNameFromUrl('http://example.com'), null);
+      assert.equal(
+          getPluginNameFromUrl('http://example.com/static/a.html'),
+          null
+      );
+    });
+
+    test('with valid urls', () => {
+      assert.equal(
+          getPluginNameFromUrl('http://example.com/plugins/a.html'),
+          'a'
+      );
+      assert.equal(
+          getPluginNameFromUrl('http://example.com/plugins/a/static/t.html'),
+          'a'
+      );
+    });
+
+    test('with preloaded urls', () => {
+      assert.equal(getPluginNameFromUrl(`${PRELOADED_PROTOCOL}a`), 'a');
+    });
+
+    test('with gerrit-theme override', () => {
+      assert.equal(
+          getPluginNameFromUrl('http://example.com/static/gerrit-theme.html'),
+          'gerrit-theme'
+      );
+    });
+
+    test('with ASSETS_PATH', () => {
+      window.ASSETS_PATH = 'http://cdn.com/2';
+      assert.equal(
+          getPluginNameFromUrl(`${window.ASSETS_PATH}/plugins/a.html`),
+          'a'
+      );
+      window.ASSETS_PATH = undefined;
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
similarity index 81%
rename from polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
rename to polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
index 1d5e423..231830bd 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
@@ -1,50 +1,32 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-change-actions-js-api</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<!--
-This must refer to the element this interface is wrapping around. Otherwise
-breaking changes to gr-change-actions won’t be noticed.
--->
-
-<test-fixture id="basic">
-  <template>
-    <gr-change-actions></gr-change-actions>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import '../../change/gr-change-actions/gr-change-actions.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {resetPlugins} from '../../../test/test-utils.js';
 import {pluginLoader} from './gr-plugin-loader.js';
 import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
+
+const basicFixture = fixtureFromElement('gr-change-actions');
+
 const pluginApi = _testOnly_initGerritPluginApi();
 
-suite('gr-js-api-interface tests', () => {
+suite('gr-change-actions-js-api-interface tests', () => {
   let element;
   let changeActions;
   let plugin;
@@ -65,7 +47,7 @@
       // Mimic all plugins loaded.
       pluginLoader.loadPlugins([]);
       changeActions = plugin.changeActions();
-      element = fixture('basic');
+      element = basicFixture.instantiate();
     });
 
     teardown(() => {
@@ -83,7 +65,7 @@
   suite('normal init', () => {
     setup(() => {
       resetPlugins();
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       sinon.stub(element, '_editStatusChanged');
       element.change = {};
       element._hasKnownChainState = false;
@@ -229,4 +211,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
deleted file mode 100644
index 0360f85..0000000
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
+++ /dev/null
@@ -1,125 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-change-reply-js-api</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<!--
-This must refer to the element this interface is wrapping around. Otherwise
-breaking changes to gr-reply-dialog won’t be noticed.
--->
-
-<test-fixture id="basic">
-  <template>
-    <gr-reply-dialog></gr-reply-dialog>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import '../../change/gr-reply-dialog/gr-reply-dialog.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
-
-const pluginApi = _testOnly_initGerritPluginApi();
-
-suite('gr-change-reply-js-api tests', () => {
-  let element;
-  let sandbox;
-  let changeReply;
-  let plugin;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({}); },
-      getAccount() { return Promise.resolve(null); },
-    });
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  suite('early init', () => {
-    setup(() => {
-      pluginApi.install(p => { plugin = p; }, '0.1',
-          'http://test.com/plugins/testplugin/static/test.js');
-      changeReply = plugin.changeReply();
-      element = fixture('basic');
-    });
-
-    teardown(() => {
-      changeReply = null;
-    });
-
-    test('works', () => {
-      sandbox.stub(element, 'getLabelValue').returns('+123');
-      assert.equal(changeReply.getLabelValue('My-Label'), '+123');
-
-      sandbox.stub(element, 'setLabelValue');
-      changeReply.setLabelValue('My-Label', '+1337');
-      assert.isTrue(
-          element.setLabelValue.calledWithExactly('My-Label', '+1337'));
-
-      sandbox.stub(element, 'send');
-      changeReply.send(false);
-      assert.isTrue(element.send.calledWithExactly(false));
-
-      sandbox.stub(element, 'setPluginMessage');
-      changeReply.showMessage('foobar');
-      assert.isTrue(element.setPluginMessage.calledWithExactly('foobar'));
-    });
-  });
-
-  suite('normal init', () => {
-    setup(() => {
-      element = fixture('basic');
-      pluginApi.install(p => { plugin = p; }, '0.1',
-          'http://test.com/plugins/testplugin/static/test.js');
-      changeReply = plugin.changeReply();
-    });
-
-    teardown(() => {
-      changeReply = null;
-    });
-
-    test('works', () => {
-      sandbox.stub(element, 'getLabelValue').returns('+123');
-      assert.equal(changeReply.getLabelValue('My-Label'), '+123');
-
-      sandbox.stub(element, 'setLabelValue');
-      changeReply.setLabelValue('My-Label', '+1337');
-      assert.isTrue(
-          element.setLabelValue.calledWithExactly('My-Label', '+1337'));
-
-      sandbox.stub(element, 'send');
-      changeReply.send(false);
-      assert.isTrue(element.send.calledWithExactly(false));
-
-      sandbox.stub(element, 'setPluginMessage');
-      changeReply.showMessage('foobar');
-      assert.isTrue(element.setPluginMessage.calledWithExactly('foobar'));
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.js
new file mode 100644
index 0000000..8f41b39
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.js
@@ -0,0 +1,101 @@
+/**
+ * @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 '../../change/gr-reply-dialog/gr-reply-dialog.js';
+import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
+
+const basicFixture = fixtureFromElement('gr-reply-dialog');
+
+const pluginApi = _testOnly_initGerritPluginApi();
+
+suite('gr-change-reply-js-api tests', () => {
+  let element;
+
+  let changeReply;
+  let plugin;
+
+  setup(() => {
+    stub('gr-rest-api-interface', {
+      getConfig() { return Promise.resolve({}); },
+      getAccount() { return Promise.resolve(null); },
+    });
+  });
+
+  suite('early init', () => {
+    setup(() => {
+      pluginApi.install(p => { plugin = p; }, '0.1',
+          'http://test.com/plugins/testplugin/static/test.js');
+      changeReply = plugin.changeReply();
+      element = basicFixture.instantiate();
+    });
+
+    teardown(() => {
+      changeReply = null;
+    });
+
+    test('works', () => {
+      sinon.stub(element, 'getLabelValue').returns('+123');
+      assert.equal(changeReply.getLabelValue('My-Label'), '+123');
+
+      sinon.stub(element, 'setLabelValue');
+      changeReply.setLabelValue('My-Label', '+1337');
+      assert.isTrue(
+          element.setLabelValue.calledWithExactly('My-Label', '+1337'));
+
+      sinon.stub(element, 'send');
+      changeReply.send(false);
+      assert.isTrue(element.send.calledWithExactly(false));
+
+      sinon.stub(element, 'setPluginMessage');
+      changeReply.showMessage('foobar');
+      assert.isTrue(element.setPluginMessage.calledWithExactly('foobar'));
+    });
+  });
+
+  suite('normal init', () => {
+    setup(() => {
+      element = basicFixture.instantiate();
+      pluginApi.install(p => { plugin = p; }, '0.1',
+          'http://test.com/plugins/testplugin/static/test.js');
+      changeReply = plugin.changeReply();
+    });
+
+    teardown(() => {
+      changeReply = null;
+    });
+
+    test('works', () => {
+      sinon.stub(element, 'getLabelValue').returns('+123');
+      assert.equal(changeReply.getLabelValue('My-Label'), '+123');
+
+      sinon.stub(element, 'setLabelValue');
+      changeReply.setLabelValue('My-Label', '+1337');
+      assert.isTrue(
+          element.setLabelValue.calledWithExactly('My-Label', '+1337'));
+
+      sinon.stub(element, 'send');
+      changeReply.send(false);
+      assert.isTrue(element.send.calledWithExactly(false));
+
+      sinon.stub(element, 'setPluginMessage');
+      changeReply.showMessage('foobar');
+      assert.isTrue(element.setPluginMessage.calledWithExactly('foobar'));
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html
deleted file mode 100644
index 2d87497..0000000
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html
+++ /dev/null
@@ -1,105 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2019 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-api-interface</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-js-api-interface></gr-js-api-interface>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-js-api-interface.js';
-import {pluginLoader} from './gr-plugin-loader.js';
-import {resetPlugins} from '../../../test/test-utils.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
-
-const pluginApi = _testOnly_initGerritPluginApi();
-
-suite('gr-gerrit tests', () => {
-  let element;
-  let sandbox;
-  let sendStub;
-
-  setup(() => {
-    window.clock = sinon.useFakeTimers();
-    sandbox = sinon.sandbox.create();
-    sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
-    stub('gr-rest-api-interface', {
-      getAccount() {
-        return Promise.resolve({name: 'Judy Hopps'});
-      },
-      send(...args) {
-        return sendStub(...args);
-      },
-    });
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    window.clock.restore();
-    sandbox.restore();
-    element._removeEventCallbacks();
-    resetPlugins();
-  });
-
-  suite('proxy methods', () => {
-    test('Gerrit._isPluginEnabled proxy to pluginLoader', () => {
-      const stubFn = sandbox.stub();
-      sandbox.stub(
-          pluginLoader,
-          'isPluginEnabled',
-          (...args) => stubFn(...args)
-      );
-      pluginApi._isPluginEnabled('test_plugin');
-      assert.isTrue(stubFn.calledWith('test_plugin'));
-    });
-
-    test('Gerrit._isPluginLoaded proxy to pluginLoader', () => {
-      const stubFn = sandbox.stub();
-      sandbox.stub(
-          pluginLoader,
-          'isPluginLoaded',
-          (...args) => stubFn(...args)
-      );
-      pluginApi._isPluginLoaded('test_plugin');
-      assert.isTrue(stubFn.calledWith('test_plugin'));
-    });
-
-    test('Gerrit._isPluginPreloaded proxy to pluginLoader', () => {
-      const stubFn = sandbox.stub();
-      sandbox.stub(
-          pluginLoader,
-          'isPluginPreloaded',
-          (...args) => stubFn(...args)
-      );
-      pluginApi._isPluginPreloaded('test_plugin');
-      assert.isTrue(stubFn.calledWith('test_plugin'));
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.js
new file mode 100644
index 0000000..e5b32f7
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.js
@@ -0,0 +1,87 @@
+/**
+ * @license
+ * Copyright (C) 2019 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-js-api-interface.js';
+import {pluginLoader} from './gr-plugin-loader.js';
+import {resetPlugins} from '../../../test/test-utils.js';
+import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
+
+const basicFixture = fixtureFromElement('gr-js-api-interface');
+
+const pluginApi = _testOnly_initGerritPluginApi();
+
+suite('gr-gerrit tests', () => {
+  let element;
+
+  let sendStub;
+
+  setup(() => {
+    window.clock = sinon.useFakeTimers();
+
+    sendStub = sinon.stub().returns(Promise.resolve({status: 200}));
+    stub('gr-rest-api-interface', {
+      getAccount() {
+        return Promise.resolve({name: 'Judy Hopps'});
+      },
+      send(...args) {
+        return sendStub(...args);
+      },
+    });
+    element = basicFixture.instantiate();
+  });
+
+  teardown(() => {
+    window.clock.restore();
+    element._removeEventCallbacks();
+    resetPlugins();
+  });
+
+  suite('proxy methods', () => {
+    test('Gerrit._isPluginEnabled proxy to pluginLoader', () => {
+      const stubFn = sinon.stub();
+      sinon.stub(
+          pluginLoader,
+          'isPluginEnabled')
+          .callsFake((...args) => stubFn(...args)
+          );
+      pluginApi._isPluginEnabled('test_plugin');
+      assert.isTrue(stubFn.calledWith('test_plugin'));
+    });
+
+    test('Gerrit._isPluginLoaded proxy to pluginLoader', () => {
+      const stubFn = sinon.stub();
+      sinon.stub(
+          pluginLoader,
+          'isPluginLoaded')
+          .callsFake((...args) => stubFn(...args));
+      pluginApi._isPluginLoaded('test_plugin');
+      assert.isTrue(stubFn.calledWith('test_plugin'));
+    });
+
+    test('Gerrit._isPluginPreloaded proxy to pluginLoader', () => {
+      const stubFn = sinon.stub();
+      sinon.stub(
+          pluginLoader,
+          'isPluginPreloaded')
+          .callsFake((...args) => stubFn(...args));
+      pluginApi._isPluginPreloaded('test_plugin');
+      assert.isTrue(stubFn.calledWith('test_plugin'));
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
similarity index 84%
rename from polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
rename to polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
index ea1ac91..69d635b 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
@@ -1,38 +1,21 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-api-interface</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-js-api-interface></gr-js-api-interface>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-js-api-interface.js';
 import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import {GrPopupInterface} from '../../plugins/gr-popup-interface/gr-popup-interface.js';
@@ -42,13 +25,15 @@
 import {pluginLoader} from './gr-plugin-loader.js';
 import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
 
+const basicFixture = fixtureFromElement('gr-js-api-interface');
+
 const pluginApi = _testOnly_initGerritPluginApi();
 
 suite('gr-js-api-interface tests', () => {
   let element;
   let plugin;
   let errorStub;
-  let sandbox;
+
   let getResponseObjectStub;
   let sendStub;
 
@@ -58,9 +43,9 @@
 
   setup(() => {
     window.clock = sinon.useFakeTimers();
-    sandbox = sinon.sandbox.create();
-    getResponseObjectStub = sandbox.stub().returns(Promise.resolve());
-    sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
+
+    getResponseObjectStub = sinon.stub().returns(Promise.resolve());
+    sendStub = sinon.stub().returns(Promise.resolve({status: 200}));
     stub('gr-rest-api-interface', {
       getAccount() {
         return Promise.resolve({name: 'Judy Hopps'});
@@ -70,8 +55,8 @@
         return sendStub(...args);
       },
     });
-    element = fixture('basic');
-    errorStub = sandbox.stub(console, 'error');
+    element = basicFixture.instantiate();
+    errorStub = sinon.stub(console, 'error');
     pluginApi.install(p => { plugin = p; }, '0.1',
         'http://test.com/plugins/testplugin/static/test.js');
     pluginLoader.loadPlugins([]);
@@ -79,7 +64,6 @@
 
   teardown(() => {
     window.clock.restore();
-    sandbox.restore();
     element._removeEventCallbacks();
     plugin = null;
   });
@@ -242,7 +226,7 @@
       _number: 42,
       revisions: {def: {_number: 2}, abc: {_number: 1}},
     };
-    const spy = sandbox.spy();
+    const spy = sinon.spy();
     pluginLoader.loadPlugins(['plugins/test.html']);
     plugin.on(element.EventType.SHOW_CHANGE, spy);
     element.handleEvent(element.EventType.SHOW_CHANGE,
@@ -350,7 +334,7 @@
 
   test('getLoggedIn', done => {
     // fake fetch for authCheck
-    sandbox.stub(window, 'fetch', () => Promise.resolve({status: 204}));
+    sinon.stub(window, 'fetch').callsFake(() => Promise.resolve({status: 204}));
     plugin.restApi().getLoggedIn()
         .then(loggedIn => {
           assert.isTrue(loggedIn);
@@ -371,7 +355,7 @@
 
   test('getAdminMenuLinks', () => {
     const links = [{text: 'a', url: 'b'}, {text: 'c', url: 'd'}];
-    const getCallbacksStub = sandbox.stub(element, '_getEventCallbacks')
+    const getCallbacksStub = sinon.stub(element, '_getEventCallbacks')
         .returns([
           {getMenuLinks: () => [links[0]]},
           {getMenuLinks: () => [links[1]]},
@@ -387,7 +371,7 @@
     let baseUrlPlugin;
 
     setup(() => {
-      sandbox.stub(BaseUrlBehavior, 'getBaseUrl').returns('/r');
+      sinon.stub(BaseUrlBehavior, 'getBaseUrl').returns('/r');
 
       pluginApi.install(p => { baseUrlPlugin = p; }, '0.1',
           'http://test.com/r/plugins/baseurlplugin/static/test.js');
@@ -410,7 +394,7 @@
     });
 
     test('popup(moduleName) creates popup with component', () => {
-      const openStub = sandbox.stub(GrPopupInterface.prototype, 'open',
+      const openStub = sinon.stub(GrPopupInterface.prototype, 'open').callsFake(
           function() {
             // Arrow function can't be used here, because we want to
             // get properties from the instance of GrPopupInterface
@@ -426,7 +410,7 @@
     test('deprecated.popup(element) creates popup with element', () => {
       const el = document.createElement('div');
       el.textContent = 'some text here';
-      const openStub = sandbox.stub(GrPopupInterface.prototype, 'open');
+      const openStub = sinon.stub(GrPopupInterface.prototype, 'open');
       openStub.returns(Promise.resolve({
         _getElement() {
           return document.createElement('div');
@@ -445,15 +429,15 @@
       change = {};
       revision = {};
       actionDetails = {__key: 'some'};
-      sandbox.stub(plugin, 'on').callsArgWith(1, change, revision);
-      sandbox.stub(plugin, 'changeActions').returns({
-        addTapListener: sandbox.stub().callsArg(1),
+      sinon.stub(plugin, 'on').callsArgWith(1, change, revision);
+      sinon.stub(plugin, 'changeActions').returns({
+        addTapListener: sinon.stub().callsArg(1),
         getActionDetails: () => actionDetails,
       });
     });
 
     test('returns GrPluginActionContext', () => {
-      const stub = sandbox.stub();
+      const stub = sinon.stub();
       plugin.deprecated.onAction('change', 'foo', ctx => {
         assert.isTrue(ctx instanceof GrPluginActionContext);
         assert.strictEqual(ctx.change, change);
@@ -466,7 +450,7 @@
     });
 
     test('other actions', () => {
-      const stub = sandbox.stub();
+      const stub = sinon.stub();
       plugin.deprecated.onAction('project', 'foo', stub);
       plugin.deprecated.onAction('edit', 'foo', stub);
       plugin.deprecated.onAction('branch', 'foo', stub);
@@ -476,7 +460,7 @@
 
   suite('screen', () => {
     test('screenUrl()', () => {
-      sandbox.stub(BaseUrlBehavior, 'getBaseUrl').returns('/base');
+      sinon.stub(BaseUrlBehavior, 'getBaseUrl').returns('/base');
       assert.equal(
           plugin.screenUrl(),
           `${location.origin}/base/x/testplugin`
@@ -488,9 +472,9 @@
     });
 
     test('deprecated works', () => {
-      const stub = sandbox.stub();
-      const hookStub = {onAttached: sandbox.stub()};
-      sandbox.stub(plugin, 'hook').returns(hookStub);
+      const stub = sinon.stub();
+      const hookStub = {onAttached: sinon.stub()};
+      sinon.stub(plugin, 'hook').returns(hookStub);
       plugin.deprecated.screen('foo', stub);
       assert.isTrue(plugin.hook.calledWith('testplugin-screen-foo'));
       const fakeEl = {style: {display: ''}};
@@ -500,7 +484,7 @@
     });
 
     test('works', () => {
-      sandbox.stub(plugin, 'registerCustomComponent');
+      sinon.stub(plugin, 'registerCustomComponent');
       plugin.screen('foo', 'some-module');
       assert.isTrue(plugin.registerCustomComponent.calledWith(
           'testplugin-screen-foo', 'some-module'));
@@ -513,8 +497,8 @@
 
     setup(()=> {
       fakeEl = {change: {}, revision: {}};
-      const hookStub = {onAttached: sandbox.stub()};
-      sandbox.stub(plugin, 'hook').returns(hookStub);
+      const hookStub = {onAttached: sinon.stub()};
+      sinon.stub(plugin, 'hook').returns(hookStub);
       emulateAttached = () => hookStub.onAttached.callArgWith(0, fakeEl);
     });
 
@@ -528,7 +512,7 @@
       ['CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK', 'change-metadata-item'],
     ].forEach(([panelName, endpointName]) => {
       test(`deprecated.panel works for ${panelName}`, () => {
-        const callback = sandbox.stub();
+        const callback = sinon.stub();
         plugin.deprecated.panel(panelName, callback);
         assert.isTrue(plugin.hook.calledWith(endpointName));
         emulateAttached();
@@ -553,15 +537,15 @@
     });
 
     test('plugin.deprecated.settingsScreen() works', () => {
-      const hookStub = {onAttached: sandbox.stub()};
-      sandbox.stub(plugin, 'hook').returns(hookStub);
+      const hookStub = {onAttached: sinon.stub()};
+      sinon.stub(plugin, 'hook').returns(hookStub);
       const fakeSettings = {};
-      fakeSettings.title = sandbox.stub().returns(fakeSettings);
-      fakeSettings.token = sandbox.stub().returns(fakeSettings);
-      fakeSettings.module = sandbox.stub().returns(fakeSettings);
-      fakeSettings.build = sandbox.stub().returns(hookStub);
-      sandbox.stub(plugin, 'settings').returns(fakeSettings);
-      const callback = sandbox.stub();
+      fakeSettings.title = sinon.stub().returns(fakeSettings);
+      fakeSettings.token = sinon.stub().returns(fakeSettings);
+      fakeSettings.module = sinon.stub().returns(fakeSettings);
+      fakeSettings.build = sinon.stub().returns(hookStub);
+      sinon.stub(plugin, 'settings').returns(fakeSettings);
+      const callback = sinon.stub();
 
       plugin.deprecated.settingsScreen('path', 'menu', callback);
       assert.isTrue(fakeSettings.title.calledWith('menu'));
@@ -574,7 +558,7 @@
         style: {
           display: '',
         },
-        querySelector: sandbox.stub().returns(fakeBody),
+        querySelector: sinon.stub().returns(fakeBody),
       };
       // Emulate settings screen attached
       hookStub.onAttached.callArgWith(0, fakeEl);
@@ -585,4 +569,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
deleted file mode 100644
index 08c784ab..0000000
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
+++ /dev/null
@@ -1,163 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-plugin-action-context</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <div></div>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-js-api-interface.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {GrPluginActionContext} from './gr-plugin-action-context.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
-
-const pluginApi = _testOnly_initGerritPluginApi();
-
-suite('gr-plugin-action-context tests', () => {
-  let instance;
-  let sandbox;
-  let plugin;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    pluginApi.install(p => { plugin = p; }, '0.1',
-        'http://test.com/plugins/testplugin/static/test.js');
-    instance = new GrPluginActionContext(plugin);
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('popup() and hide()', () => {
-    const popupApiStub = {
-      close: sandbox.stub(),
-    };
-    sandbox.stub(plugin.deprecated, 'popup').returns(popupApiStub);
-    const el = {};
-    instance.popup(el);
-    assert.isTrue(instance.plugin.deprecated.popup.calledWith(el));
-
-    instance.hide();
-    assert.isTrue(popupApiStub.close.called);
-  });
-
-  test('textfield', () => {
-    assert.equal(instance.textfield().tagName, 'PAPER-INPUT');
-  });
-
-  test('br', () => {
-    assert.equal(instance.br().tagName, 'BR');
-  });
-
-  test('msg', () => {
-    const el = instance.msg('foobar');
-    assert.equal(el.tagName, 'GR-LABEL');
-    assert.equal(el.textContent, 'foobar');
-  });
-
-  test('div', () => {
-    const el1 = document.createElement('span');
-    el1.textContent = 'foo';
-    const el2 = document.createElement('div');
-    el2.textContent = 'bar';
-    const div = instance.div(el1, el2);
-    assert.equal(div.tagName, 'DIV');
-    assert.equal(div.textContent, 'foobar');
-  });
-
-  test('button', done => {
-    const clickStub = sandbox.stub();
-    const button = instance.button('foo', {onclick: clickStub});
-    // If you don't attach a Polymer element to the DOM, then the ready()
-    // callback will not be called and then e.g. this.$ is undefined.
-    dom(document.body).appendChild(button);
-    MockInteractions.tap(button);
-    flush(() => {
-      assert.isTrue(clickStub.called);
-      assert.equal(button.textContent, 'foo');
-      done();
-    });
-  });
-
-  test('checkbox', () => {
-    const el = instance.checkbox();
-    assert.equal(el.tagName, 'INPUT');
-    assert.equal(el.type, 'checkbox');
-  });
-
-  test('label', () => {
-    const fakeMsg = {};
-    const fakeCheckbox = {};
-    sandbox.stub(instance, 'div');
-    sandbox.stub(instance, 'msg').returns(fakeMsg);
-    instance.label(fakeCheckbox, 'foo');
-    assert.isTrue(instance.div.calledWithExactly(fakeCheckbox, fakeMsg));
-  });
-
-  test('call', () => {
-    instance.action = {
-      method: 'METHOD',
-      __key: 'key',
-      __url: '/changes/1/revisions/2/foo~bar',
-    };
-    const sendStub = sandbox.stub().returns(Promise.resolve());
-    sandbox.stub(plugin, 'restApi').returns({
-      send: sendStub,
-    });
-    const payload = {foo: 'foo'};
-    const successStub = sandbox.stub();
-    instance.call(payload, successStub);
-    assert.isTrue(sendStub.calledWith(
-        'METHOD', '/changes/1/revisions/2/foo~bar', payload));
-  });
-
-  test('call error', done => {
-    instance.action = {
-      method: 'METHOD',
-      __key: 'key',
-      __url: '/changes/1/revisions/2/foo~bar',
-    };
-    const sendStub = sandbox.stub().returns(Promise.reject(new Error('boom')));
-    sandbox.stub(plugin, 'restApi').returns({
-      send: sendStub,
-    });
-    const errorStub = sandbox.stub();
-    document.addEventListener('show-alert', errorStub);
-    instance.call();
-    flush(() => {
-      assert.isTrue(errorStub.calledOnce);
-      assert.equal(errorStub.args[0][0].detail.message,
-          'Plugin network error: Error: boom');
-      done();
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.js
new file mode 100644
index 0000000..dde3c04
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.js
@@ -0,0 +1,152 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-js-api-interface.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {GrPluginActionContext} from './gr-plugin-action-context.js';
+import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
+
+const pluginApi = _testOnly_initGerritPluginApi();
+
+suite('gr-plugin-action-context tests', () => {
+  let instance;
+
+  let plugin;
+
+  setup(() => {
+    pluginApi.install(p => { plugin = p; }, '0.1',
+        'http://test.com/plugins/testplugin/static/test.js');
+    instance = new GrPluginActionContext(plugin);
+  });
+
+  test('popup() and hide()', () => {
+    const popupApiStub = {
+      close: sinon.stub(),
+    };
+    sinon.stub(plugin.deprecated, 'popup').returns(popupApiStub);
+    const el = {};
+    instance.popup(el);
+    assert.isTrue(instance.plugin.deprecated.popup.calledWith(el));
+
+    instance.hide();
+    assert.isTrue(popupApiStub.close.called);
+  });
+
+  test('textfield', () => {
+    assert.equal(instance.textfield().tagName, 'PAPER-INPUT');
+  });
+
+  test('br', () => {
+    assert.equal(instance.br().tagName, 'BR');
+  });
+
+  test('msg', () => {
+    const el = instance.msg('foobar');
+    assert.equal(el.tagName, 'GR-LABEL');
+    assert.equal(el.textContent, 'foobar');
+  });
+
+  test('div', () => {
+    const el1 = document.createElement('span');
+    el1.textContent = 'foo';
+    const el2 = document.createElement('div');
+    el2.textContent = 'bar';
+    const div = instance.div(el1, el2);
+    assert.equal(div.tagName, 'DIV');
+    assert.equal(div.textContent, 'foobar');
+  });
+
+  suite('button', () => {
+    let clickStub;
+    let button;
+    setup(() => {
+      clickStub = sinon.stub();
+      button = instance.button('foo', {onclick: clickStub});
+      // If you don't attach a Polymer element to the DOM, then the ready()
+      // callback will not be called and then e.g. this.$ is undefined.
+      dom(document.body).appendChild(button);
+    });
+
+    test('click', done => {
+      MockInteractions.tap(button);
+      flush(() => {
+        assert.isTrue(clickStub.called);
+        assert.equal(button.textContent, 'foo');
+        done();
+      });
+    });
+
+    teardown(() => {
+      button.remove();
+    });
+  });
+
+  test('checkbox', () => {
+    const el = instance.checkbox();
+    assert.equal(el.tagName, 'INPUT');
+    assert.equal(el.type, 'checkbox');
+  });
+
+  test('label', () => {
+    const fakeMsg = {};
+    const fakeCheckbox = {};
+    sinon.stub(instance, 'div');
+    sinon.stub(instance, 'msg').returns(fakeMsg);
+    instance.label(fakeCheckbox, 'foo');
+    assert.isTrue(instance.div.calledWithExactly(fakeCheckbox, fakeMsg));
+  });
+
+  test('call', () => {
+    instance.action = {
+      method: 'METHOD',
+      __key: 'key',
+      __url: '/changes/1/revisions/2/foo~bar',
+    };
+    const sendStub = sinon.stub().returns(Promise.resolve());
+    sinon.stub(plugin, 'restApi').returns({
+      send: sendStub,
+    });
+    const payload = {foo: 'foo'};
+    const successStub = sinon.stub();
+    instance.call(payload, successStub);
+    assert.isTrue(sendStub.calledWith(
+        'METHOD', '/changes/1/revisions/2/foo~bar', payload));
+  });
+
+  test('call error', done => {
+    instance.action = {
+      method: 'METHOD',
+      __key: 'key',
+      __url: '/changes/1/revisions/2/foo~bar',
+    };
+    const sendStub = sinon.stub().returns(Promise.reject(new Error('boom')));
+    sinon.stub(plugin, 'restApi').returns({
+      send: sendStub,
+    });
+    const errorStub = sinon.stub();
+    document.addEventListener('show-alert', errorStub);
+    instance.call();
+    flush(() => {
+      assert.isTrue(errorStub.calledOnce);
+      assert.equal(errorStub.args[0][0].detail.message,
+          'Plugin network error: Error: boom');
+      done();
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.js
index e6767bc..f1af433 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.js
@@ -25,14 +25,12 @@
 const pluginApi = _testOnly_initGerritPluginApi();
 
 suite('gr-plugin-endpoints tests', () => {
-  let sandbox;
   let instance;
   let pluginFoo;
   let pluginBar;
   let domHook;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     domHook = {};
     instance = new GrPluginEndpoints();
     pluginApi.install(p => { pluginFoo = p; }, '0.1',
@@ -57,12 +55,11 @@
           domHook,
         }
     );
-    sandbox.stub(pluginLoader, 'arePluginsLoaded').returns(true);
-    sandbox.spy(instance, 'importUrl');
+    sinon.stub(pluginLoader, 'arePluginsLoaded').returns(true);
+    sinon.spy(instance, 'importUrl');
   });
 
   teardown(() => {
-    sandbox.restore();
     resetPlugins();
   });
 
@@ -134,7 +131,7 @@
   });
 
   test('onNewEndpoint', () => {
-    const newModuleStub = sandbox.stub();
+    const newModuleStub = sinon.stub();
     instance.onNewEndpoint('a-place', newModuleStub);
     instance.registerModule(
         pluginFoo,
@@ -176,4 +173,4 @@
       },
     ]);
   });
-});
\ No newline at end of file
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js
similarity index 78%
rename from polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
rename to polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js
index f10916d..e8839d6 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js
@@ -1,38 +1,21 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-plugin-host</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-js-api-interface></gr-js-api-interface>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-js-api-interface.js';
 import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import {PRELOADED_PROTOCOL, PLUGIN_LOADING_TIMEOUT_MS} from './gr-api-utils.js';
@@ -41,19 +24,21 @@
 import {_testOnly_flushPreinstalls} from './gr-gerrit.js';
 import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
 
+const basicFixture = fixtureFromElement('gr-js-api-interface');
+
 const pluginApi = _testOnly_initGerritPluginApi();
 
 suite('gr-plugin-loader tests', () => {
   let plugin;
-  let sandbox;
+
   let url;
   let sendStub;
   let pluginLoader;
 
   setup(() => {
     window.clock = sinon.useFakeTimers();
-    sandbox = sinon.sandbox.create();
-    sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
+
+    sendStub = sinon.stub().returns(Promise.resolve({status: 200}));
     stub('gr-rest-api-interface', {
       getAccount() {
         return Promise.resolve({name: 'Judy Hopps'});
@@ -63,13 +48,12 @@
       },
     });
     pluginLoader = _testOnly_resetPluginLoader();
-    sandbox.stub(document.body, 'appendChild');
-    fixture('basic');
+    sinon.stub(document.body, 'appendChild');
+    basicFixture.instantiate();
     url = window.location.origin;
   });
 
   teardown(() => {
-    sandbox.restore();
     window.clock.restore();
     resetPlugins();
   });
@@ -88,20 +72,20 @@
     assert.doesNotThrow(() => {
       _testOnly_flushPreinstalls();
     });
-    window.Gerrit.flushPreinstalls = sandbox.stub();
+    window.Gerrit.flushPreinstalls = sinon.stub();
     _testOnly_flushPreinstalls();
     assert.isTrue(window.Gerrit.flushPreinstalls.calledOnce);
     delete window.Gerrit.flushPreinstalls;
   });
 
   test('versioning', () => {
-    const callback = sandbox.spy();
+    const callback = sinon.spy();
     pluginApi.install(callback, '0.0pre-alpha');
     assert(callback.notCalled);
   });
 
   test('report pluginsLoaded', done => {
-    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+    const pluginsLoadedStub = sinon.stub(pluginLoader._getReporting(),
         'pluginsLoaded');
     pluginsLoadedStub.reset();
     window.Gerrit._loadPlugins([]);
@@ -130,10 +114,10 @@
   });
 
   test('plugins installed successfully', done => {
-    sandbox.stub(pluginLoader, '_loadJsPlugin', url => {
+    sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
       pluginApi.install(() => void 0, undefined, url);
     });
-    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+    const pluginsLoadedStub = sinon.stub(pluginLoader._getReporting(),
         'pluginsLoaded');
 
     const plugins = [
@@ -150,7 +134,7 @@
   });
 
   test('isPluginEnabled and isPluginLoaded', done => {
-    sandbox.stub(pluginLoader, '_loadJsPlugin', url => {
+    sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
       pluginApi.install(() => void 0, undefined, url);
     });
 
@@ -180,10 +164,10 @@
       'http://test.com/plugins/bar/static/test.js',
     ];
 
-    const alertStub = sandbox.stub();
+    const alertStub = sinon.stub();
     document.addEventListener('show-alert', alertStub);
 
-    sandbox.stub(pluginLoader, '_loadJsPlugin', url => {
+    sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
       pluginApi.install(() => {
         if (url === plugins[0]) {
           throw new Error('failed');
@@ -191,7 +175,7 @@
       }, undefined, url);
     });
 
-    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+    const pluginsLoadedStub = sinon.stub(pluginLoader._getReporting(),
         'pluginsLoaded');
 
     pluginLoader.loadPlugins(plugins);
@@ -210,10 +194,10 @@
       'http://test.com/plugins/bar/static/test.js',
     ];
 
-    const alertStub = sandbox.stub();
+    const alertStub = sinon.stub();
     document.addEventListener('show-alert', alertStub);
 
-    sandbox.stub(pluginLoader, '_loadJsPlugin', url => {
+    sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
       pluginApi.install(() => {
         if (url === plugins[0]) {
           throw new Error('failed');
@@ -221,7 +205,7 @@
       }, undefined, url);
     });
 
-    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+    const pluginsLoadedStub = sinon.stub(pluginLoader._getReporting(),
         'pluginsLoaded');
 
     pluginLoader.loadPlugins(plugins);
@@ -245,16 +229,16 @@
       'http://test.com/plugins/bar/static/test.js',
     ];
 
-    const alertStub = sandbox.stub();
+    const alertStub = sinon.stub();
     document.addEventListener('show-alert', alertStub);
 
-    sandbox.stub(pluginLoader, '_loadJsPlugin', url => {
+    sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
       pluginApi.install(() => {
         throw new Error('failed');
       }, undefined, url);
     });
 
-    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+    const pluginsLoadedStub = sinon.stub(pluginLoader._getReporting(),
         'pluginsLoaded');
 
     pluginLoader.loadPlugins(plugins);
@@ -273,15 +257,15 @@
       'http://test.com/plugins/bar/static/test.js',
     ];
 
-    const alertStub = sandbox.stub();
+    const alertStub = sinon.stub();
     document.addEventListener('show-alert', alertStub);
 
-    sandbox.stub(pluginLoader, '_loadJsPlugin', url => {
+    sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
       pluginApi.install(() => {
       }, url === plugins[0] ? '' : 'alpha', url);
     });
 
-    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+    const pluginsLoadedStub = sinon.stub(pluginLoader._getReporting(),
         'pluginsLoaded');
 
     pluginLoader.loadPlugins(plugins);
@@ -295,10 +279,10 @@
   });
 
   test('multiple assets for same plugin installed successfully', done => {
-    sandbox.stub(pluginLoader, '_loadJsPlugin', url => {
+    sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
       pluginApi.install(() => void 0, undefined, url);
     });
-    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+    const pluginsLoadedStub = sinon.stub(pluginLoader._getReporting(),
         'pluginsLoaded');
 
     const plugins = [
@@ -319,19 +303,19 @@
     let importHtmlPluginStub;
     let loadJsPluginStub;
     setup(() => {
-      importHtmlPluginStub = sandbox.stub();
-      sandbox.stub(pluginLoader, '_loadHtmlPlugin', url => {
+      importHtmlPluginStub = sinon.stub();
+      sinon.stub(pluginLoader, '_loadHtmlPlugin').callsFake( url => {
         importHtmlPluginStub(url);
       });
-      loadJsPluginStub = sandbox.stub();
-      sandbox.stub(pluginLoader, '_createScriptTag', url => {
+      loadJsPluginStub = sinon.stub();
+      sinon.stub(pluginLoader, '_createScriptTag').callsFake( url => {
         loadJsPluginStub(url);
       });
     });
 
     test('invalid plugin path', () => {
-      const failToLoadStub = sandbox.stub();
-      sandbox.stub(pluginLoader, '_failToLoad', (...args) => {
+      const failToLoadStub = sinon.stub();
+      sinon.stub(pluginLoader, '_failToLoad').callsFake((...args) => {
         failToLoadStub(...args);
       });
 
@@ -364,7 +348,7 @@
 
     test('relative path should honor getBaseUrl', () => {
       const testUrl = '/test';
-      sandbox.stub(BaseUrlBehavior, 'getBaseUrl', () => testUrl);
+      sinon.stub(BaseUrlBehavior, 'getBaseUrl').callsFake(() => testUrl);
 
       pluginLoader.loadPlugins([
         'foo/bar.js',
@@ -405,12 +389,12 @@
     let loadJsPluginStub;
     setup(() => {
       window.ASSETS_PATH = 'https://cdn.com';
-      importHtmlPluginStub = sandbox.stub();
-      sandbox.stub(pluginLoader, '_loadHtmlPlugin', url => {
+      importHtmlPluginStub = sinon.stub();
+      sinon.stub(pluginLoader, '_loadHtmlPlugin').callsFake( url => {
         importHtmlPluginStub(url);
       });
-      loadJsPluginStub = sandbox.stub();
-      sandbox.stub(pluginLoader, '_createScriptTag', url => {
+      loadJsPluginStub = sinon.stub();
+      sinon.stub(pluginLoader, '_createScriptTag').callsFake( url => {
         loadJsPluginStub(url);
       });
     });
@@ -486,7 +470,7 @@
         installed = true;
       }
     }
-    sandbox.stub(pluginLoader, '_loadJsPlugin', url => {
+    sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
       pluginApi.install(() => pluginCallback(url), undefined, url);
     });
 
@@ -502,13 +486,16 @@
   });
 
   suite('preloaded plugins', () => {
+    teardown(() => {
+      window.Gerrit._preloadedPlugins = null;
+    });
     test('skips preloaded plugins when load plugins', () => {
-      const importHtmlPluginStub = sandbox.stub();
-      sandbox.stub(pluginLoader, '_importHtmlPlugin', url => {
+      const importHtmlPluginStub = sinon.stub();
+      sinon.stub(pluginLoader, '_importHtmlPlugin').callsFake( url => {
         importHtmlPluginStub(url);
       });
-      const loadJsPluginStub = sandbox.stub();
-      sandbox.stub(pluginLoader, '_loadJsPlugin', url => {
+      const loadJsPluginStub = sinon.stub();
+      sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
         loadJsPluginStub(url);
       });
 
@@ -534,11 +521,10 @@
       assert.isTrue(
           pluginLoader.isPluginPreloaded(PRELOADED_PROTOCOL + 'baz')
       );
-      window.Gerrit._preloadedPlugins = null;
     });
 
     test('preloaded plugins are installed', () => {
-      const installStub = sandbox.stub();
+      const installStub = sinon.stub();
       window.Gerrit._preloadedPlugins = {foo: installStub};
       pluginLoader.installPreloadedPlugins();
       assert.isTrue(installStub.called);
@@ -555,4 +541,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js
index d84cd834..31ff8ee 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js
@@ -17,6 +17,10 @@
 
 let restApi;
 
+export function _testOnlyResetRestApi() {
+  restApi = null;
+}
+
 function getRestApi() {
   if (!restApi) {
     restApi = document.createElement('gr-rest-api-interface');
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.js
similarity index 72%
rename from polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
rename to polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.js
index fcc3b669..53aaa1e 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.js
@@ -1,31 +1,21 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-plugin-rest-api</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-js-api-interface.js';
 import {GrPluginRestApi} from './gr-plugin-rest-api.js';
 import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
@@ -34,22 +24,21 @@
 
 suite('gr-plugin-rest-api tests', () => {
   let instance;
-  let sandbox;
+
   let getResponseObjectStub;
   let sendStub;
   let restApiStub;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    getResponseObjectStub = sandbox.stub().returns(Promise.resolve());
-    sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
+    getResponseObjectStub = sinon.stub().returns(Promise.resolve());
+    sendStub = sinon.stub().returns(Promise.resolve({status: 200}));
     restApiStub = {
       getAccount: () => Promise.resolve({name: 'Judy Hopps'}),
       getResponseObject: getResponseObjectStub,
       send: sendStub,
-      getLoggedIn: sandbox.stub(),
-      getVersion: sandbox.stub(),
-      getConfig: sandbox.stub(),
+      getLoggedIn: sinon.stub(),
+      getVersion: sinon.stub(),
+      getConfig: sinon.stub(),
     };
     stub('gr-rest-api-interface', Object.keys(restApiStub).reduce((a, k) => {
       a[k] = (...args) => restApiStub[k](...args);
@@ -60,10 +49,6 @@
     instance = new GrPluginRestApi();
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('fetch', () => {
     const payload = {foo: 'foo'};
     return instance.fetch('HTTP_METHOD', '/url', payload).then(r => {
@@ -157,4 +142,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.js
similarity index 81%
rename from polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
rename to polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.js
index b97b5b3..ee9fb13 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.js
@@ -1,59 +1,41 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2018 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-label-info</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-label-info></gr-label-info>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-label-info.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {isHidden} from '../../../test/test-utils.js';
+
+const basicFixture = fixtureFromElement('gr-label-info');
+
 suite('gr-account-link tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
+    element = basicFixture.instantiate();
+
     // Needed to trigger computed bindings.
     element.account = {};
     element.change = {labels: {}};
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   suite('remove reviewer votes', () => {
     setup(() => {
-      sandbox.stub(element, '_computeValueTooltip').returns('');
+      sinon.stub(element, '_computeValueTooltip').returns('');
       element.account = {
         _account_id: 1,
         name: 'bojack',
@@ -91,7 +73,7 @@
 
     test('deletes votes', () => {
       const deleteResponse = Promise.resolve({ok: true});
-      const deleteStub = sandbox.stub(
+      const deleteStub = sinon.stub(
           element.$.restAPI, 'deleteVote').returns(deleteResponse);
 
       element.change.removable_reviewers = [element.account];
@@ -246,4 +228,4 @@
         .querySelector('.placeholder')));
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html
deleted file mode 100644
index 99a038e..0000000
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html
+++ /dev/null
@@ -1,62 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 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.
--->
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-labeled-autocomplete</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-labeled-autocomplete></gr-labeled-autocomplete>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-labeled-autocomplete.js';
-suite('gr-labeled-autocomplete tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => { sandbox.restore(); });
-
-  test('tapping trigger focuses autocomplete', () => {
-    const e = {stopPropagation: () => undefined};
-    sandbox.stub(e, 'stopPropagation');
-    sandbox.stub(element.$.autocomplete, 'focus');
-    element._handleTriggerClick(e);
-    assert.isTrue(e.stopPropagation.calledOnce);
-    assert.isTrue(element.$.autocomplete.focus.calledOnce);
-  });
-
-  test('setText', () => {
-    sandbox.stub(element.$.autocomplete, 'setText');
-    element.setText('foo-bar');
-    assert.isTrue(element.$.autocomplete.setText.calledWith('foo-bar'));
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.js b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.js
new file mode 100644
index 0000000..3e904a2
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.js
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright (C) 2018 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-labeled-autocomplete.js';
+
+const basicFixture = fixtureFromElement('gr-labeled-autocomplete');
+
+suite('gr-labeled-autocomplete tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('tapping trigger focuses autocomplete', () => {
+    const e = {stopPropagation: () => undefined};
+    sinon.stub(e, 'stopPropagation');
+    sinon.stub(element.$.autocomplete, 'focus');
+    element._handleTriggerClick(e);
+    assert.isTrue(e.stopPropagation.calledOnce);
+    assert.isTrue(element.$.autocomplete.focus.calledOnce);
+  });
+
+  test('setText', () => {
+    sinon.stub(element.$.autocomplete, 'setText');
+    element.setText('foo-bar');
+    assert.isTrue(element.$.autocomplete.setText.calledWith('foo-bar'));
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.js b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.js
index 6208252..4231d71 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.js
@@ -21,16 +21,14 @@
 const basicFixture = fixtureFromElement('gr-lib-loader');
 
 suite('gr-lib-loader tests', () => {
-  let sandbox;
   let element;
   let resolveLoad;
   let loadStub;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     element = basicFixture.instantiate();
 
-    loadStub = sandbox.stub(element, '_loadScript', () =>
+    loadStub = sinon.stub(element, '_loadScript').callsFake(() =>
       new Promise(resolve => resolveLoad = resolve)
     );
 
@@ -42,7 +40,6 @@
     if (window.hljs) {
       delete window.hljs;
     }
-    sandbox.restore();
 
     // Because the element state is a singleton, clean it up.
     element._hljsState.configured = false;
@@ -51,7 +48,7 @@
   });
 
   test('only load once', done => {
-    sandbox.stub(element, '_getHLJSUrl').returns('');
+    sinon.stub(element, '_getHLJSUrl').returns('');
     const firstCallHandler = sinon.stub();
     element.getHLJS().then(firstCallHandler);
 
@@ -116,7 +113,7 @@
       let root;
 
       setup(() => {
-        sandbox.stub(element, '_getLibRoot', () => root);
+        sinon.stub(element, '_getLibRoot').callsFake(() => root);
       });
 
       test('with no root', () => {
@@ -130,4 +127,4 @@
       });
     });
   });
-});
\ No newline at end of file
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html
deleted file mode 100644
index 889b786..0000000
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html
+++ /dev/null
@@ -1,103 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-limited-text</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-limited-text></gr-limited-text>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-limited-text.js';
-suite('gr-limited-text tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('_updateTitle', () => {
-    const updateSpy = sandbox.spy(element, '_updateTitle');
-    element.text = 'abc 123';
-    flushAsynchronousOperations();
-    assert.isTrue(updateSpy.calledOnce);
-    assert.isNotOk(element.getAttribute('title'));
-    assert.isFalse(element.hasTooltip);
-
-    element.limit = 10;
-    flushAsynchronousOperations();
-    assert.isTrue(updateSpy.calledTwice);
-    assert.isNotOk(element.getAttribute('title'));
-    assert.isFalse(element.hasTooltip);
-
-    element.limit = 3;
-    flushAsynchronousOperations();
-    assert.isTrue(updateSpy.calledThrice);
-    assert.equal(element.getAttribute('title'), 'abc 123');
-    assert.isTrue(element.hasTooltip);
-
-    element.tooltipLimit = 3;
-    flushAsynchronousOperations();
-    assert.equal(element.getAttribute('title'), 'abc');
-
-    element.tooltipLimit = 1024;
-    element.limit = 100;
-    flushAsynchronousOperations();
-    assert.equal(updateSpy.callCount, 6);
-    assert.isNotOk(element.getAttribute('title'));
-    assert.isFalse(element.hasTooltip);
-
-    element.limit = null;
-    flushAsynchronousOperations();
-    assert.equal(updateSpy.callCount, 7);
-    assert.isNotOk(element.getAttribute('title'));
-    assert.isFalse(element.hasTooltip);
-  });
-
-  test('_computeDisplayText', () => {
-    assert.equal(element._computeDisplayText('foo bar', 100), 'foo bar');
-    assert.equal(element._computeDisplayText('foo bar', 4), 'foo…');
-    assert.equal(element._computeDisplayText('foo bar', null), 'foo bar');
-  });
-
-  test('when disable tooltip', () => {
-    sandbox.spy(element, '_updateTitle');
-    element.text = 'abcdefghijklmn';
-    element.disableTooltip = true;
-    element.limit = 10;
-    flushAsynchronousOperations();
-    assert.equal(element.getAttribute('title'), null);
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.js b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.js
new file mode 100644
index 0000000..dda3324
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.js
@@ -0,0 +1,83 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-limited-text.js';
+
+const basicFixture = fixtureFromElement('gr-limited-text');
+
+suite('gr-limited-text tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('_updateTitle', () => {
+    const updateSpy = sinon.spy(element, '_updateTitle');
+    element.text = 'abc 123';
+    flushAsynchronousOperations();
+    assert.isTrue(updateSpy.calledOnce);
+    assert.isNotOk(element.getAttribute('title'));
+    assert.isFalse(element.hasTooltip);
+
+    element.limit = 10;
+    flushAsynchronousOperations();
+    assert.isTrue(updateSpy.calledTwice);
+    assert.isNotOk(element.getAttribute('title'));
+    assert.isFalse(element.hasTooltip);
+
+    element.limit = 3;
+    flushAsynchronousOperations();
+    assert.isTrue(updateSpy.calledThrice);
+    assert.equal(element.getAttribute('title'), 'abc 123');
+    assert.isTrue(element.hasTooltip);
+
+    element.tooltipLimit = 3;
+    flushAsynchronousOperations();
+    assert.equal(element.getAttribute('title'), 'abc');
+
+    element.tooltipLimit = 1024;
+    element.limit = 100;
+    flushAsynchronousOperations();
+    assert.equal(updateSpy.callCount, 6);
+    assert.isNotOk(element.getAttribute('title'));
+    assert.isFalse(element.hasTooltip);
+
+    element.limit = null;
+    flushAsynchronousOperations();
+    assert.equal(updateSpy.callCount, 7);
+    assert.isNotOk(element.getAttribute('title'));
+    assert.isFalse(element.hasTooltip);
+  });
+
+  test('_computeDisplayText', () => {
+    assert.equal(element._computeDisplayText('foo bar', 100), 'foo bar');
+    assert.equal(element._computeDisplayText('foo bar', 4), 'foo…');
+    assert.equal(element._computeDisplayText('foo bar', null), 'foo bar');
+  });
+
+  test('when disable tooltip', () => {
+    sinon.spy(element, '_updateTitle');
+    element.text = 'abcdefghijklmn';
+    element.disableTooltip = true;
+    element.limit = 10;
+    flushAsynchronousOperations();
+    assert.equal(element.getAttribute('title'), null);
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
deleted file mode 100644
index c8de3df..0000000
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
+++ /dev/null
@@ -1,65 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-linked-chip</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<!-- Can't use absolute path below for mock-interaction.js.
-Web component tester(wct) has a built-in http server and it serves "/components" directory (which is
-actually /node_modules directory). Also, wct patches some files to load modules from /components.
-With the absolute path, browser tries to load dom-module from 2 different places (/component/... and
-/node_modules/...) though this is actually the same file. This leads to a run-time error.
--->
-<script src="../../../node_modules/iron-test-helpers/mock-interactions.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-linked-chip></gr-linked-chip>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-linked-chip.js';
-suite('gr-linked-chip tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('remove fired', () => {
-    const spy = sandbox.spy();
-    element.addEventListener('remove', spy);
-    flushAsynchronousOperations();
-    MockInteractions.tap(element.$.remove);
-    assert.isTrue(spy.called);
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.js b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.js
new file mode 100644
index 0000000..b111e84
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.js
@@ -0,0 +1,38 @@
+/**
+ * @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-linked-chip.js';
+
+const basicFixture = fixtureFromElement('gr-linked-chip');
+
+suite('gr-linked-chip tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('remove fired', () => {
+    const spy = sinon.spy();
+    element.addEventListener('remove', spy);
+    flushAsynchronousOperations();
+    MockInteractions.tap(element.$.remove);
+    assert.isTrue(spy.called);
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.js
similarity index 88%
rename from polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
rename to polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.js
index 4fa4390..a295d1a 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.js
@@ -1,52 +1,42 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2015 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-linked-text</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-linked-text>
-      <div id="output"></div>
-    </gr-linked-text>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-linked-text.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromTemplate(html`
+<gr-linked-text>
+      <div id="output"></div>
+    </gr-linked-text>
+`);
 
 suite('gr-linked-text tests', () => {
   let element;
-  let sandbox;
+
+  let originalCanonicalPath;
 
   setup(() => {
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
-    sandbox.stub(GerritNav, 'mapCommentlinks', x => x);
+    originalCanonicalPath = window.CANONICAL_PATH;
+    element = basicFixture.instantiate();
+
+    sinon.stub(GerritNav, 'mapCommentlinks').value( x => x);
     element.config = {
       ph: {
         match: '([Bb]ug|[Ii]ssue)\\s*#?(\\d+)',
@@ -90,7 +80,7 @@
   });
 
   teardown(() => {
-    sandbox.restore();
+    window.CANONICAL_PATH = originalCanonicalPath;
   });
 
   test('URL pattern was parsed and linked.', () => {
@@ -366,11 +356,11 @@
   });
 
   test('_contentOrConfigChanged called with config', () => {
-    const contentStub = sandbox.stub(element, '_contentChanged');
-    const contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged');
+    const contentStub = sinon.stub(element, '_contentChanged');
+    const contentConfigStub = sinon.stub(element, '_contentOrConfigChanged');
     element.content = 'some text';
     assert.isTrue(contentStub.called);
     assert.isTrue(contentConfigStub.called);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.js
similarity index 68%
rename from polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
rename to polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.js
index 55aab82..93451ff 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.js
@@ -1,52 +1,31 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-list-view</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/node_modules/page/page.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-list-view></gr-list-view>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-list-view.js';
 import page from 'page/page.mjs';
 
+const basicFixture = fixtureFromElement('gr-list-view');
+
 suite('gr-list-view tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    element = basicFixture.instantiate();
   });
 
   test('_computeNavLink', () => {
@@ -55,7 +34,7 @@
     let filter = 'test';
     const path = '/admin/projects';
 
-    sandbox.stub(element, 'getBaseUrl', () => '');
+    sinon.stub(element, 'getBaseUrl').callsFake(() => '');
 
     assert.equal(
         element._computeNavLink(offset, 1, projectsPerPage, filter, path),
@@ -81,7 +60,7 @@
 
   test('_onValueChange', done => {
     element.path = '/admin/projects';
-    sandbox.stub(page, 'show', url => {
+    sinon.stub(page, 'show').callsFake( url => {
       assert.equal(url, '/admin/projects/q/filter:test');
       done();
     });
@@ -89,7 +68,7 @@
   });
 
   test('_filterChanged not reload when swap between falsy values', () => {
-    sandbox.stub(element, '_debounceReload');
+    sinon.stub(element, '_debounceReload');
     element.filter = null;
     element.filter = undefined;
     element.filter = '';
@@ -137,7 +116,7 @@
   });
 
   test('fires create clicked event when button tapped', () => {
-    const clickHandler = sandbox.stub();
+    const clickHandler = sinon.stub();
     element.addEventListener('create-clicked', clickHandler);
     element.createNew = true;
     flushAsynchronousOperations();
@@ -148,7 +127,7 @@
   test('next/prev links change when path changes', () => {
     const BRANCHES_PATH = '/path/to/branches';
     const TAGS_PATH = '/path/to/tags';
-    sandbox.stub(element, '_computeNavLink');
+    sinon.stub(element, '_computeNavLink');
     element.offset = 0;
     element.itemsPerPage = 25;
     element.filter = '';
@@ -163,4 +142,4 @@
     assert.equal(element._computePage(50, 25), 3);
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html
deleted file mode 100644
index d43c739..0000000
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html
+++ /dev/null
@@ -1,91 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-overlay</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/page/page.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-overlay>
-      <div>content</div>
-    </gr-overlay>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-overlay.js';
-suite('gr-overlay tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('events are fired on fullscreen view', done => {
-    sandbox.stub(element, '_isMobile').returns(true);
-    const openHandler = sandbox.stub();
-    const closeHandler = sandbox.stub();
-    element.addEventListener('fullscreen-overlay-opened', openHandler);
-    element.addEventListener('fullscreen-overlay-closed', closeHandler);
-
-    element.open().then(() => {
-      assert.isTrue(element._isMobile.called);
-      assert.isTrue(element._fullScreenOpen);
-      assert.isTrue(openHandler.called);
-
-      element._close();
-      assert.isFalse(element._fullScreenOpen);
-      assert.isTrue(closeHandler.called);
-      done();
-    });
-  });
-
-  test('events are not fired on desktop view', done => {
-    sandbox.stub(element, '_isMobile').returns(false);
-    const openHandler = sandbox.stub();
-    const closeHandler = sandbox.stub();
-    element.addEventListener('fullscreen-overlay-opened', openHandler);
-    element.addEventListener('fullscreen-overlay-closed', closeHandler);
-
-    element.open().then(() => {
-      assert.isTrue(element._isMobile.called);
-      assert.isFalse(element._fullScreenOpen);
-      assert.isFalse(openHandler.called);
-
-      element._close();
-      assert.isFalse(element._fullScreenOpen);
-      assert.isFalse(closeHandler.called);
-      done();
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.js
new file mode 100644
index 0000000..f3c591b
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.js
@@ -0,0 +1,73 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-overlay.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromTemplate(html`
+<gr-overlay>
+      <div>content</div>
+    </gr-overlay>
+`);
+
+suite('gr-overlay tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('events are fired on fullscreen view', done => {
+    sinon.stub(element, '_isMobile').returns(true);
+    const openHandler = sinon.stub();
+    const closeHandler = sinon.stub();
+    element.addEventListener('fullscreen-overlay-opened', openHandler);
+    element.addEventListener('fullscreen-overlay-closed', closeHandler);
+
+    element.open().then(() => {
+      assert.isTrue(element._isMobile.called);
+      assert.isTrue(element._fullScreenOpen);
+      assert.isTrue(openHandler.called);
+
+      element._close();
+      assert.isFalse(element._fullScreenOpen);
+      assert.isTrue(closeHandler.called);
+      done();
+    });
+  });
+
+  test('events are not fired on desktop view', done => {
+    sinon.stub(element, '_isMobile').returns(false);
+    const openHandler = sinon.stub();
+    const closeHandler = sinon.stub();
+    element.addEventListener('fullscreen-overlay-opened', openHandler);
+    element.addEventListener('fullscreen-overlay-closed', closeHandler);
+
+    element.open().then(() => {
+      assert.isTrue(element._isMobile.called);
+      assert.isFalse(element._fullScreenOpen);
+      assert.isFalse(openHandler.called);
+
+      element._close();
+      assert.isFalse(element._fullScreenOpen);
+      assert.isFalse(closeHandler.called);
+      done();
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html
deleted file mode 100644
index a54d7a3..0000000
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html
+++ /dev/null
@@ -1,89 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-page-nav</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/page/page.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-page-nav>
-      <ul>
-        <li>item</li>
-      </ul>
-    </gr-page-nav>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-page-nav.js';
-suite('gr-page-nav tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-    flushAsynchronousOperations();
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('header is not pinned just below top', () => {
-    sandbox.stub(element, '_getOffsetParent', () => 0);
-    sandbox.stub(element, '_getOffsetTop', () => 10);
-    sandbox.stub(element, '_getScrollY', () => 5);
-    element._handleBodyScroll();
-    assert.isFalse(element.$.nav.classList.contains('pinned'));
-  });
-
-  test('header is pinned when scroll down the page', () => {
-    sandbox.stub(element, '_getOffsetParent', () => 0);
-    sandbox.stub(element, '_getOffsetTop', () => 10);
-    sandbox.stub(element, '_getScrollY', () => 25);
-    window.scrollY = 100;
-    element._handleBodyScroll();
-    assert.isTrue(element.$.nav.classList.contains('pinned'));
-  });
-
-  test('header is not pinned just below top with header set', () => {
-    element._headerHeight = 20;
-    sandbox.stub(element, '_getScrollY', () => 15);
-    window.scrollY = 100;
-    element._handleBodyScroll();
-    assert.isFalse(element.$.nav.classList.contains('pinned'));
-  });
-
-  test('header is pinned when scroll down the page with header set', () => {
-    element._headerHeight = 20;
-    sandbox.stub(element, '_getScrollY', () => 25);
-    window.scrollY = 100;
-    element._handleBodyScroll();
-    assert.isTrue(element.$.nav.classList.contains('pinned'));
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.js b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.js
new file mode 100644
index 0000000..2e40b27
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.js
@@ -0,0 +1,71 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-page-nav.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromTemplate(html`
+<gr-page-nav>
+      <ul>
+        <li>item</li>
+      </ul>
+    </gr-page-nav>
+`);
+
+suite('gr-page-nav tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+    flushAsynchronousOperations();
+  });
+
+  test('header is not pinned just below top', () => {
+    sinon.stub(element, '_getOffsetParent').callsFake(() => 0);
+    sinon.stub(element, '_getOffsetTop').callsFake(() => 10);
+    sinon.stub(element, '_getScrollY').callsFake(() => 5);
+    element._handleBodyScroll();
+    assert.isFalse(element.$.nav.classList.contains('pinned'));
+  });
+
+  test('header is pinned when scroll down the page', () => {
+    sinon.stub(element, '_getOffsetParent').callsFake(() => 0);
+    sinon.stub(element, '_getOffsetTop').callsFake(() => 10);
+    sinon.stub(element, '_getScrollY').callsFake(() => 25);
+    window.scrollY = 100;
+    element._handleBodyScroll();
+    assert.isTrue(element.$.nav.classList.contains('pinned'));
+  });
+
+  test('header is not pinned just below top with header set', () => {
+    element._headerHeight = 20;
+    sinon.stub(element, '_getScrollY').callsFake(() => 15);
+    window.scrollY = 100;
+    element._handleBodyScroll();
+    assert.isFalse(element.$.nav.classList.contains('pinned'));
+  });
+
+  test('header is pinned when scroll down the page with header set', () => {
+    element._headerHeight = 20;
+    sinon.stub(element, '_getScrollY').callsFake(() => 25);
+    window.scrollY = 100;
+    element._handleBodyScroll();
+    assert.isTrue(element.$.nav.classList.contains('pinned'));
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.js
similarity index 68%
rename from polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html
rename to polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.js
index 67b82f9..bfdbe41 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.js
@@ -1,52 +1,35 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2018 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.
+ */
 
-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.
--->
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-repo-branch-picker</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-repo-branch-picker></gr-repo-branch-picker>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-repo-branch-picker.js';
+
+const basicFixture = fixtureFromElement('gr-repo-branch-picker');
+
 suite('gr-repo-branch-picker tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+    element = basicFixture.instantiate();
   });
 
-  teardown(() => { sandbox.restore(); });
-
   suite('_getRepoSuggestions', () => {
     setup(() => {
-      sandbox.stub(element.$.restAPI, 'getRepos')
+      sinon.stub(element.$.restAPI, 'getRepos')
           .returns(Promise.resolve([
             {
               id: 'plugins%2Favatars-external',
@@ -82,7 +65,7 @@
 
   suite('_getRepoBranchesSuggestions', () => {
     setup(() => {
-      sandbox.stub(element.$.restAPI, 'getRepoBranches')
+      sinon.stub(element.$.restAPI, 'getRepoBranches')
           .returns(Promise.resolve([
             {ref: 'refs/heads/stable-2.10'},
             {ref: 'refs/heads/stable-2.11'},
@@ -140,4 +123,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.js
similarity index 86%
rename from polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
rename to polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.js
index f3abdb0..af2efae 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.js
@@ -1,53 +1,37 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-auth</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import {Auth, authService} from './gr-auth.js';
 import {appContext} from '../../../services/app-context.js';
 
 suite('gr-auth', () => {
   let auth;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     auth = authService;
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   suite('Auth class methods', () => {
     let fakeFetch;
     setup(() => {
       auth = new Auth();
-      fakeFetch = sandbox.stub(window, 'fetch');
+      fakeFetch = sinon.stub(window, 'fetch');
     });
 
     test('auth-check returns 403', done => {
@@ -93,7 +77,7 @@
     setup(() => {
       auth = new Auth();
       clock = sinon.useFakeTimers();
-      fakeFetch = sandbox.stub(window, 'fetch');
+      fakeFetch = sinon.stub(window, 'fetch');
     });
 
     test('cache auth-check result', done => {
@@ -228,7 +212,7 @@
 
   suite('default (xsrf token header)', () => {
     setup(() => {
-      sandbox.stub(window, 'fetch').returns(Promise.resolve({ok: true}));
+      sinon.stub(window, 'fetch').returns(Promise.resolve({ok: true}));
     });
 
     test('GET', done => {
@@ -241,7 +225,7 @@
     });
 
     test('POST', done => {
-      sandbox.stub(auth, '_getCookie')
+      sinon.stub(auth, '_getCookie')
           .withArgs('XSRF_TOKEN')
           .returns('foobar');
       auth.fetch('/url', {method: 'POST'}).then(() => {
@@ -256,7 +240,7 @@
 
   suite('cors (access token)', () => {
     setup(() => {
-      sandbox.stub(window, 'fetch').returns(Promise.resolve({ok: true}));
+      sinon.stub(window, 'fetch').returns(Promise.resolve({ok: true}));
     });
 
     let getToken;
@@ -269,14 +253,14 @@
     };
 
     setup(() => {
-      getToken = sandbox.stub();
+      getToken = sinon.stub();
       getToken.returns(Promise.resolve(makeToken()));
       auth.setup(getToken);
     });
 
     test('base url support', done => {
       const baseUrl = 'http://foo';
-      sandbox.stub(BaseUrlBehavior, 'getBaseUrl').returns(baseUrl);
+      sinon.stub(BaseUrlBehavior, 'getBaseUrl').returns(baseUrl);
       auth.fetch(baseUrl + '/url', {bar: 'bar'}).then(() => {
         const [url] = fetch.lastCall.args;
         assert.equal(url, 'http://foo/a/url?access_token=zbaz');
@@ -313,7 +297,7 @@
     });
 
     test('getToken refreshes token', done => {
-      sandbox.stub(auth, '_isTokenValid');
+      sinon.stub(auth, '_isTokenValid');
       auth._isTokenValid
           .onFirstCall().returns(true)
           .onSecondCall()
@@ -391,4 +375,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.js
similarity index 62%
rename from polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
rename to polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.js
index cfa164f..e5217a4 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.js
@@ -1,36 +1,25 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-etag-decorator</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import {GrEtagDecorator} from './gr-etag-decorator.js';
 
 suite('gr-etag-decorator', () => {
   let etag;
-  let sandbox;
 
   const fakeRequest = (opt_etag, opt_status) => {
     const headers = new Headers();
@@ -42,14 +31,9 @@
   };
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     etag = new GrEtagDecorator();
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
-
   test('exists', () => {
     assert.isOk(etag);
   });
@@ -96,4 +80,4 @@
     assert.strictEqual(etag.getCachedPayload('/foo'), 'new payload');
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 9861a03..e792c4d 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -51,6 +51,38 @@
 const ANONYMIZED_REVISION_BASE_URL = ANONYMIZED_CHANGE_BASE_URL +
     '/revisions/*';
 
+let siteBasedCache = new SiteBasedCache(); // Shared across instances.
+let fetchPromisesCache = new FetchPromisesCache(); // Shared across instances.
+let pendingRequest = {}; // Shared across instances.
+let grEtagDecorator = new GrEtagDecorator; // Shared across instances.
+let projectLookup = {}; // Shared across instances.
+
+export function _testOnlyResetGrRestApiSharedObjects() {
+  for (const key in fetchPromisesCache._data) {
+    if (fetchPromisesCache._data.hasOwnProperty(key)) {
+      // reject already fulfilled promise does nothing
+      fetchPromisesCache._data[key].reject();
+    }
+  }
+
+  for (const key in pendingRequest) {
+    if (!pendingRequest.hasOwnProperty(key)) {
+      continue;
+    }
+    for (const req of pendingRequest[key]) {
+      // reject already fulfilled promise does nothing
+      req.reject();
+    }
+  }
+
+  siteBasedCache = new SiteBasedCache();
+  fetchPromisesCache = new FetchPromisesCache();
+  pendingRequest = {};
+  grEtagDecorator = new GrEtagDecorator;
+  projectLookup = {};
+  authService.clearCache();
+}
+
 /**
  * @extends PolymerElement
  */
@@ -89,26 +121,26 @@
     return {
       _cache: {
         type: Object,
-        value: new SiteBasedCache(), // Shared across instances.
+        value: siteBasedCache, // Shared across instances.
       },
       _sharedFetchPromises: {
         type: Object,
-        value: new FetchPromisesCache(), // Shared across instances.
+        value: fetchPromisesCache, // Shared across instances.
       },
       _pendingRequests: {
         type: Object,
-        value: {}, // Intentional to share the object across instances.
+        value: pendingRequest, // Intentional to share the object across instances.
       },
       _etags: {
         type: Object,
-        value: new GrEtagDecorator(), // Share across instances.
+        value: grEtagDecorator, // Share across instances.
       },
       /**
        * Used to maintain a mapping of changeNums to project names.
        */
       _projectLookup: {
         type: Object,
-        value: {}, // Intentional to share the object across instances.
+        value: projectLookup, // Intentional to share the object across instances.
       },
     };
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js
similarity index 84%
rename from polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
rename to polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js
index 0a51d26..639b768 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js
@@ -1,86 +1,73 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-rest-api-interface</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-rest-api-interface></gr-rest-api-interface>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-rest-api-interface.js';
 import {mockPromise} from '../../../test/test-utils.js';
 import {GrReviewerUpdatesParser} from './gr-reviewer-updates-parser.js';
 import {authService} from './gr-auth.js';
 
+const basicFixture = fixtureFromElement('gr-rest-api-interface');
+
 suite('gr-rest-api-interface tests', () => {
   let element;
-  let sandbox;
+
   let ctr = 0;
+  let originalCanonicalPath;
 
   setup(() => {
     // Modify CANONICAL_PATH to effectively reset cache.
     ctr += 1;
+    originalCanonicalPath = window.CANONICAL_PATH;
     window.CANONICAL_PATH = `test${ctr}`;
 
-    sandbox = sinon.sandbox.create();
     const testJSON = ')]}\'\n{"hello": "bonjour"}';
-    sandbox.stub(window, 'fetch').returns(Promise.resolve({
+    sinon.stub(window, 'fetch').returns(Promise.resolve({
       ok: true,
       text() {
         return Promise.resolve(testJSON);
       },
     }));
     // fake auth
-    sandbox.stub(authService, 'authCheck').returns(Promise.resolve(true));
-    element = fixture('basic');
+    sinon.stub(authService, 'authCheck').returns(Promise.resolve(true));
+    element = basicFixture.instantiate();
     element._projectLookup = {};
   });
 
   teardown(() => {
-    sandbox.restore();
+    window.CANONICAL_PATH = originalCanonicalPath;
   });
 
   test('parent diff comments are properly grouped', done => {
-    sandbox.stub(element._restApiHelper, 'fetchJSON', () => Promise.resolve({
-      '/COMMIT_MSG': [],
-      'sieve.go': [
-        {
-          updated: '2017-02-03 22:32:28.000000000',
-          message: 'this isn’t quite right',
-        },
-        {
-          side: 'PARENT',
-          message: 'how did this work in the first place?',
-          updated: '2017-02-03 22:33:28.000000000',
-        },
-      ],
-    }));
+    sinon.stub(element._restApiHelper, 'fetchJSON')
+        .callsFake(() => Promise.resolve({
+          '/COMMIT_MSG': [],
+          'sieve.go': [
+            {
+              updated: '2017-02-03 22:32:28.000000000',
+              message: 'this isn’t quite right',
+            },
+            {
+              side: 'PARENT',
+              message: 'how did this work in the first place?',
+              updated: '2017-02-03 22:33:28.000000000',
+            },
+          ],
+        }));
     element._getDiffComments('42', '', 'PARENT', 1, 'sieve.go').then(
         obj => {
           assert.equal(obj.baseComments.length, 1);
@@ -206,9 +193,9 @@
   });
 
   test('differing patch diff comments are properly grouped', done => {
-    sandbox.stub(element, 'getFromProjectLookup')
+    sinon.stub(element, 'getFromProjectLookup')
         .returns(Promise.resolve('test'));
-    sandbox.stub(element._restApiHelper, 'fetchJSON', request => {
+    sinon.stub(element._restApiHelper, 'fetchJSON').callsFake( request => {
       const url = request.url;
       if (url === '/changes/test~42/revisions/1') {
         return Promise.resolve({
@@ -323,7 +310,7 @@
   });
 
   test('server error', done => {
-    const getResponseObjectStub = sandbox.stub(element, 'getResponseObject');
+    const getResponseObjectStub = sinon.stub(element, 'getResponseObject');
     window.fetch.returns(Promise.resolve({ok: false}));
     const serverErrorEventPromise = new Promise(resolve => {
       element.addEventListener('server-error', resolve);
@@ -337,8 +324,8 @@
   });
 
   test('legacy n,z key in change url is replaced', async () => {
-    sandbox.stub(element, 'getConfig', async () => { return {}; });
-    const stub = sandbox.stub(element._restApiHelper, 'fetchJSON')
+    sinon.stub(element, 'getConfig').callsFake( async () => { return {}; });
+    const stub = sinon.stub(element._restApiHelper, 'fetchJSON')
         .returns(Promise.resolve([]));
     await element.getChanges(1, null, 'n,z');
     assert.equal(stub.lastCall.args[0].params.S, 0);
@@ -346,7 +333,7 @@
 
   test('saveDiffPreferences invalidates cache line', () => {
     const cacheKey = '/accounts/self/preferences.diff';
-    const sendStub = sandbox.stub(element._restApiHelper, 'send');
+    const sendStub = sinon.stub(element._restApiHelper, 'send');
     element._cache.set(cacheKey, {tab_size: 4});
     element.saveDiffPreferences({tab_size: 8});
     assert.isTrue(sendStub.called);
@@ -356,8 +343,8 @@
   test('getAccount when resp is null does not add anything to the cache',
       done => {
         const cacheKey = '/accounts/self/detail';
-        const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
-            () => Promise.resolve());
+        const stub = sinon.stub(element._restApiHelper, 'fetchCacheURL')
+            .callsFake(() => Promise.resolve());
 
         element.getAccount().then(() => {
           assert.isTrue(stub.called);
@@ -372,8 +359,8 @@
   test('getAccount does not add to the cache when resp.status is 403',
       done => {
         const cacheKey = '/accounts/self/detail';
-        const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
-            () => Promise.resolve());
+        const stub = sinon.stub(element._restApiHelper, 'fetchCacheURL')
+            .callsFake(() => Promise.resolve());
 
         element.getAccount().then(() => {
           assert.isTrue(stub.called);
@@ -386,7 +373,7 @@
 
   test('getAccount when resp is successful', done => {
     const cacheKey = '/accounts/self/detail';
-    const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
+    const stub = sinon.stub(element._restApiHelper, 'fetchCacheURL').callsFake(
         () => Promise.resolve());
 
     element.getAccount().then(response => {
@@ -400,12 +387,13 @@
   });
 
   const preferenceSetup = function(testJSON, loggedIn, smallScreen) {
-    sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(loggedIn));
-    sandbox.stub(element, '_isNarrowScreen', () => smallScreen);
-    sandbox.stub(
+    sinon.stub(element, 'getLoggedIn')
+        .callsFake(() => Promise.resolve(loggedIn));
+    sinon.stub(element, '_isNarrowScreen').callsFake(() => smallScreen);
+    sinon.stub(
         element._restApiHelper,
-        'fetchCacheURL',
-        () => Promise.resolve(testJSON));
+        'fetchCacheURL')
+        .callsFake(() => Promise.resolve(testJSON));
   };
 
   test('getPreferences returns correctly on small screens logged in',
@@ -468,14 +456,14 @@
       });
 
   test('savPreferences normalizes download scheme', () => {
-    const sendStub = sandbox.stub(element._restApiHelper, 'send');
+    const sendStub = sinon.stub(element._restApiHelper, 'send');
     element.savePreferences({download_scheme: 'HTTP'});
     assert.isTrue(sendStub.called);
     assert.equal(sendStub.lastCall.args[0].body.download_scheme, 'http');
   });
 
   test('getDiffPreferences returns correct defaults', done => {
-    sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(false));
+    sinon.stub(element, 'getLoggedIn').callsFake(() => Promise.resolve(false));
 
     element.getDiffPreferences().then(obj => {
       assert.equal(obj.auto_hide_diff_table_header, true);
@@ -497,14 +485,14 @@
   });
 
   test('saveDiffPreferences set show_tabs to false', () => {
-    const sendStub = sandbox.stub(element._restApiHelper, 'send');
+    const sendStub = sinon.stub(element._restApiHelper, 'send');
     element.saveDiffPreferences({show_tabs: false});
     assert.isTrue(sendStub.called);
     assert.equal(sendStub.lastCall.args[0].body.show_tabs, false);
   });
 
   test('getEditPreferences returns correct defaults', done => {
-    sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(false));
+    sinon.stub(element, 'getLoggedIn').callsFake(() => Promise.resolve(false));
 
     element.getEditPreferences().then(obj => {
       assert.equal(obj.auto_close_brackets, false);
@@ -528,14 +516,14 @@
   });
 
   test('saveEditPreferences set show_tabs to false', () => {
-    const sendStub = sandbox.stub(element._restApiHelper, 'send');
+    const sendStub = sinon.stub(element._restApiHelper, 'send');
     element.saveEditPreferences({show_tabs: false});
     assert.isTrue(sendStub.called);
     assert.equal(sendStub.lastCall.args[0].body.show_tabs, false);
   });
 
   test('confirmEmail', () => {
-    const sendStub = sandbox.spy(element._restApiHelper, 'send');
+    const sendStub = sinon.spy(element._restApiHelper, 'send');
     element.confirmEmail('foo');
     assert.isTrue(sendStub.calledOnce);
     assert.equal(sendStub.lastCall.args[0].method, 'PUT');
@@ -545,7 +533,7 @@
   });
 
   test('setAccountStatus', () => {
-    const sendStub = sandbox.stub(element._restApiHelper, 'send')
+    const sendStub = sinon.stub(element._restApiHelper, 'send')
         .returns(Promise.resolve('OOO'));
     element._cache.set('/accounts/self/detail', {});
     return element.setAccountStatus('OOO').then(() => {
@@ -564,7 +552,8 @@
   suite('draft comments', () => {
     test('_sendDiffDraftRequest pending requests tracked', () => {
       const obj = element._pendingRequests;
-      sandbox.stub(element, '_getChangeURLAndSend', () => mockPromise());
+      sinon.stub(element, '_getChangeURLAndSend')
+          .callsFake(() => mockPromise());
       assert.notOk(element.hasPendingDiffDrafts());
 
       element._sendDiffDraftRequest(null, null, null, {});
@@ -586,8 +575,8 @@
     suite('_failForCreate200', () => {
       test('_sendDiffDraftRequest checks for 200 on create', () => {
         const sendPromise = Promise.resolve();
-        sandbox.stub(element, '_getChangeURLAndSend').returns(sendPromise);
-        const failStub = sandbox.stub(element, '_failForCreate200')
+        sinon.stub(element, '_getChangeURLAndSend').returns(sendPromise);
+        const failStub = sinon.stub(element, '_failForCreate200')
             .returns(Promise.resolve());
         return element._sendDiffDraftRequest('PUT', 123, 4, {}).then(() => {
           assert.isTrue(failStub.calledOnce);
@@ -596,9 +585,9 @@
       });
 
       test('_sendDiffDraftRequest no checks for 200 on non create', () => {
-        sandbox.stub(element, '_getChangeURLAndSend')
+        sinon.stub(element, '_getChangeURLAndSend')
             .returns(Promise.resolve());
-        const failStub = sandbox.stub(element, '_failForCreate200')
+        const failStub = sinon.stub(element, '_failForCreate200')
             .returns(Promise.resolve());
         return element._sendDiffDraftRequest('PUT', 123, 4, {id: '123'})
             .then(() => {
@@ -650,9 +639,9 @@
     const change_num = '1';
     const file_name = 'index.php';
     const file_contents = '<?php';
-    sandbox.stub(element._restApiHelper, 'send').returns(
+    sinon.stub(element._restApiHelper, 'send').returns(
         Promise.resolve([change_num, file_name, file_contents]));
-    sandbox.stub(element, 'getResponseObject')
+    sinon.stub(element, 'getResponseObject')
         .returns(Promise.resolve([change_num, file_name, file_contents]));
     element._cache.set('/changes/' + change_num + '/edit/' + file_name, {});
     return element.saveChangeEdit(change_num, file_name, file_contents)
@@ -671,9 +660,9 @@
     element._projectLookup = {1: 'test'};
     const change_num = '1';
     const message = 'this is a commit message';
-    sandbox.stub(element._restApiHelper, 'send').returns(
+    sinon.stub(element._restApiHelper, 'send').returns(
         Promise.resolve([change_num, message]));
-    sandbox.stub(element, 'getResponseObject')
+    sinon.stub(element, 'getResponseObject')
         .returns(Promise.resolve([change_num, message]));
     element._cache.set('/changes/' + change_num + '/message', {});
     return element.putChangeCommitMessage(change_num, message).then(() => {
@@ -690,9 +679,9 @@
     element._projectLookup = {1: 'test'};
     const change_num = '1';
     const messageId = 'abc';
-    sandbox.stub(element._restApiHelper, 'send').returns(
+    sinon.stub(element._restApiHelper, 'send').returns(
         Promise.resolve([change_num, messageId]));
-    sandbox.stub(element, 'getResponseObject')
+    sinon.stub(element, 'getResponseObject')
         .returns(Promise.resolve([change_num, messageId]));
     return element.deleteChangeCommitMessage(change_num, messageId).then(() => {
       assert.isTrue(element._restApiHelper.send.calledOnce);
@@ -706,7 +695,7 @@
   });
 
   test('startWorkInProgress', () => {
-    const sendStub = sandbox.stub(element, '_getChangeURLAndSend')
+    const sendStub = sinon.stub(element, '_getChangeURLAndSend')
         .returns(Promise.resolve('ok'));
     element.startWorkInProgress('42');
     assert.isTrue(sendStub.calledOnce);
@@ -727,7 +716,7 @@
   });
 
   test('startReview', () => {
-    const sendStub = sandbox.stub(element, '_getChangeURLAndSend')
+    const sendStub = sinon.stub(element, '_getChangeURLAndSend')
         .returns(Promise.resolve({}));
     element.startReview('42', {message: 'Please review.'});
     assert.isTrue(sendStub.calledOnce);
@@ -740,7 +729,7 @@
   });
 
   test('deleteComment', () => {
-    const sendStub = sandbox.stub(element, '_getChangeURLAndSend')
+    const sendStub = sinon.stub(element, '_getChangeURLAndSend')
         .returns(Promise.resolve('some response'));
     return element.deleteComment('foo', 'bar', '01234', 'removal reason')
         .then(response => {
@@ -757,7 +746,7 @@
   });
 
   test('createRepo encodes name', () => {
-    const sendStub = sandbox.stub(element._restApiHelper, 'send')
+    const sendStub = sinon.stub(element._restApiHelper, 'send')
         .returns(Promise.resolve());
     return element.createRepo({name: 'x/y'}).then(() => {
       assert.isTrue(sendStub.calledOnce);
@@ -766,7 +755,7 @@
   });
 
   test('queryChangeFiles', () => {
-    const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+    const fetchStub = sinon.stub(element, '_getChangeURLAndFetch')
         .returns(Promise.resolve());
     return element.queryChangeFiles('42', 'edit', 'test/path.js').then(() => {
       assert.equal(fetchStub.lastCall.args[0].changeNum, '42');
@@ -818,7 +807,7 @@
     let fetchCacheURLStub;
     setup(() => {
       fetchCacheURLStub =
-          sandbox.stub(element._restApiHelper, 'fetchCacheURL');
+          sinon.stub(element._restApiHelper, 'fetchCacheURL');
     });
 
     test('normal use', () => {
@@ -905,7 +894,7 @@
     let fetchCacheURLStub;
     setup(() => {
       fetchCacheURLStub =
-          sandbox.stub(element._restApiHelper, 'fetchCacheURL');
+          sinon.stub(element._restApiHelper, 'fetchCacheURL');
     });
 
     test('normal use', () => {
@@ -934,13 +923,13 @@
   });
 
   test('gerrit auth is used', () => {
-    sandbox.stub(authService, 'fetch').returns(Promise.resolve());
+    sinon.stub(authService, 'fetch').returns(Promise.resolve());
     element._restApiHelper.fetchJSON({url: 'foo'});
     assert(authService.fetch.called);
   });
 
   test('getSuggestedAccounts does not return _fetchJSON', () => {
-    const _fetchJSONSpy = sandbox.spy(element._restApiHelper, 'fetchJSON');
+    const _fetchJSONSpy = sinon.spy(element._restApiHelper, 'fetchJSON');
     return element.getSuggestedAccounts().then(accts => {
       assert.isFalse(_fetchJSONSpy.called);
       assert.equal(accts.length, 0);
@@ -948,8 +937,8 @@
   });
 
   test('_fetchJSON gets called by getSuggestedAccounts', () => {
-    const _fetchJSONStub = sandbox.stub(element._restApiHelper, 'fetchJSON',
-        () => Promise.resolve());
+    const _fetchJSONStub = sinon.stub(element._restApiHelper, 'fetchJSON')
+        .callsFake(() => Promise.resolve());
     return element.getSuggestedAccounts('own').then(() => {
       assert.deepEqual(_fetchJSONStub.lastCall.args[0].params, {
         q: 'own',
@@ -963,15 +952,15 @@
       let toHexStub;
 
       setup(() => {
-        toHexStub = sandbox.stub(element, 'listChangesOptionsToHex',
+        toHexStub = sinon.stub(element, 'listChangesOptionsToHex').callsFake(
             options => 'deadbeef');
-        sandbox.stub(element, '_getChangeDetail',
+        sinon.stub(element, '_getChangeDetail').callsFake(
             async (changeNum, options) => { return {changeNum, options}; });
       });
 
       test('signed pushes disabled', async () => {
         const {PUSH_CERTIFICATES} = element.ListChangesOption;
-        sandbox.stub(element, 'getConfig', async () => { return {}; });
+        sinon.stub(element, 'getConfig').callsFake( async () => { return {}; });
         const {changeNum, options} = await element.getChangeDetail(123);
         assert.strictEqual(123, changeNum);
         assert.strictEqual('deadbeef', options);
@@ -981,7 +970,7 @@
 
       test('signed pushes enabled', async () => {
         const {PUSH_CERTIFICATES} = element.ListChangesOption;
-        sandbox.stub(element, 'getConfig', async () => {
+        sinon.stub(element, 'getConfig').callsFake( async () => {
           return {receive: {enable_signed_push: true}};
         });
         const {changeNum, options} = await element.getChangeDetail(123);
@@ -993,7 +982,7 @@
     });
 
     test('GrReviewerUpdatesParser.parse is used', () => {
-      sandbox.stub(GrReviewerUpdatesParser, 'parse').returns(
+      sinon.stub(GrReviewerUpdatesParser, 'parse').returns(
           Promise.resolve('foo'));
       return element.getChangeDetail(42).then(result => {
         assert.isTrue(GrReviewerUpdatesParser.parse.calledOnce);
@@ -1007,8 +996,8 @@
       const expectedUrl =
           window.CANONICAL_PATH + '/changes/test~4321/detail?'+
           '0=5&1=1&2=6&3=7&4=1&5=4';
-      sandbox.stub(element._etags, 'getOptions');
-      sandbox.stub(element._etags, 'collect');
+      sinon.stub(element._etags, 'getOptions');
+      sinon.stub(element._etags, 'collect');
       return element._getChangeDetail(changeNum, '516714').then(() => {
         assert.isTrue(element._etags.getOptions.calledWithExactly(
             expectedUrl));
@@ -1018,9 +1007,9 @@
 
     test('_getChangeDetail calls errFn on 500', () => {
       const errFn = sinon.stub();
-      sandbox.stub(element, 'getChangeActionURL')
+      sinon.stub(element, 'getChangeActionURL')
           .returns(Promise.resolve(''));
-      sandbox.stub(element._restApiHelper, 'fetchRawJSON')
+      sinon.stub(element._restApiHelper, 'fetchRawJSON')
           .returns(Promise.resolve({ok: false, status: 500}));
       return element._getChangeDetail(123, '516714', errFn).then(() => {
         assert.isTrue(errFn.called);
@@ -1028,13 +1017,13 @@
     });
 
     test('_getChangeDetail populates _projectLookup', () => {
-      sandbox.stub(element, 'getChangeActionURL')
+      sinon.stub(element, 'getChangeActionURL')
           .returns(Promise.resolve(''));
-      sandbox.stub(element._restApiHelper, 'fetchRawJSON')
+      sinon.stub(element._restApiHelper, 'fetchRawJSON')
           .returns(Promise.resolve({ok: true}));
 
       const mockResponse = {_number: 1, project: 'test'};
-      sandbox.stub(element._restApiHelper, 'readResponsePayload')
+      sinon.stub(element._restApiHelper, 'readResponsePayload')
           .returns(Promise.resolve({
             parsed: mockResponse,
             raw: JSON.stringify(mockResponse),
@@ -1056,16 +1045,16 @@
         const mockResponse = {foo: 'bar', baz: 42};
         mockResponseSerial = element.JSON_PREFIX +
             JSON.stringify(mockResponse);
-        sandbox.stub(element._restApiHelper, 'urlWithParams')
+        sinon.stub(element._restApiHelper, 'urlWithParams')
             .returns(requestUrl);
-        sandbox.stub(element, 'getChangeActionURL')
+        sinon.stub(element, 'getChangeActionURL')
             .returns(Promise.resolve(requestUrl));
-        collectSpy = sandbox.spy(element._etags, 'collect');
-        getPayloadSpy = sandbox.spy(element._etags, 'getCachedPayload');
+        collectSpy = sinon.spy(element._etags, 'collect');
+        getPayloadSpy = sinon.spy(element._etags, 'getCachedPayload');
       });
 
       test('contributes to cache', () => {
-        sandbox.stub(element._restApiHelper, 'fetchRawJSON')
+        sinon.stub(element._restApiHelper, 'fetchRawJSON')
             .returns(Promise.resolve({
               text: () => Promise.resolve(mockResponseSerial),
               status: 200,
@@ -1081,7 +1070,7 @@
       });
 
       test('uses cache on HTTP 304', () => {
-        sandbox.stub(element._restApiHelper, 'fetchRawJSON')
+        sinon.stub(element._restApiHelper, 'fetchRawJSON')
             .returns(Promise.resolve({
               text: () => Promise.resolve(mockResponseSerial),
               status: 304,
@@ -1103,7 +1092,7 @@
 
   suite('getFromProjectLookup', () => {
     test('getChange fails', () => {
-      sandbox.stub(element, 'getChange')
+      sinon.stub(element, 'getChange')
           .returns(Promise.resolve(null));
       return element.getFromProjectLookup().then(val => {
         assert.strictEqual(val, undefined);
@@ -1112,7 +1101,7 @@
     });
 
     test('getChange succeeds, no project', () => {
-      sandbox.stub(element, 'getChange').returns(Promise.resolve(null));
+      sinon.stub(element, 'getChange').returns(Promise.resolve(null));
       return element.getFromProjectLookup().then(val => {
         assert.strictEqual(val, undefined);
         assert.deepEqual(element._projectLookup, {});
@@ -1120,7 +1109,7 @@
     });
 
     test('getChange succeeds with project', () => {
-      sandbox.stub(element, 'getChange')
+      sinon.stub(element, 'getChange')
           .returns(Promise.resolve({project: 'project'}));
       return element.getFromProjectLookup('test').then(val => {
         assert.equal(val, 'project');
@@ -1131,7 +1120,7 @@
 
   suite('getChanges populates _projectLookup', () => {
     test('multiple queries', () => {
-      sandbox.stub(element._restApiHelper, 'fetchJSON')
+      sinon.stub(element._restApiHelper, 'fetchJSON')
           .returns(Promise.resolve([
             [
               {_number: 1, project: 'test'},
@@ -1151,7 +1140,7 @@
     });
 
     test('no query', () => {
-      sandbox.stub(element._restApiHelper, 'fetchJSON')
+      sinon.stub(element._restApiHelper, 'fetchJSON')
           .returns(Promise.resolve([
             {_number: 1, project: 'test'},
             {_number: 2, project: 'test'},
@@ -1171,7 +1160,7 @@
 
   test('_getChangeURLAndFetch', () => {
     element._projectLookup = {1: 'test'};
-    const fetchStub = sandbox.stub(element._restApiHelper, 'fetchJSON')
+    const fetchStub = sinon.stub(element._restApiHelper, 'fetchJSON')
         .returns(Promise.resolve());
     const req = {changeNum: 1, endpoint: '/test', patchNum: 1};
     return element._getChangeURLAndFetch(req).then(() => {
@@ -1182,7 +1171,7 @@
 
   test('_getChangeURLAndSend', () => {
     element._projectLookup = {1: 'test'};
-    const sendStub = sandbox.stub(element._restApiHelper, 'send')
+    const sendStub = sinon.stub(element._restApiHelper, 'send')
         .returns(Promise.resolve());
 
     const req = {
@@ -1220,7 +1209,7 @@
   });
 
   test('setChangeTopic', () => {
-    const sendSpy = sandbox.spy(element, '_getChangeURLAndSend');
+    const sendSpy = sinon.spy(element, '_getChangeURLAndSend');
     return element.setChangeTopic(123, 'foo-bar').then(() => {
       assert.isTrue(sendSpy.calledOnce);
       assert.deepEqual(sendSpy.lastCall.args[0].body, {topic: 'foo-bar'});
@@ -1228,7 +1217,7 @@
   });
 
   test('setChangeHashtag', () => {
-    const sendSpy = sandbox.spy(element, '_getChangeURLAndSend');
+    const sendSpy = sinon.spy(element, '_getChangeURLAndSend');
     return element.setChangeHashtag(123, 'foo-bar').then(() => {
       assert.isTrue(sendSpy.calledOnce);
       assert.equal(sendSpy.lastCall.args[0].body, 'foo-bar');
@@ -1236,7 +1225,7 @@
   });
 
   test('generateAccountHttpPassword', () => {
-    const sendSpy = sandbox.spy(element._restApiHelper, 'send');
+    const sendSpy = sinon.spy(element._restApiHelper, 'send');
     return element.generateAccountHttpPassword().then(() => {
       assert.isTrue(sendSpy.calledOnce);
       assert.deepEqual(sendSpy.lastCall.args[0].body, {generate: true});
@@ -1245,7 +1234,7 @@
 
   suite('getChangeFiles', () => {
     test('patch only', () => {
-      const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+      const fetchStub = sinon.stub(element, '_getChangeURLAndFetch')
           .returns(Promise.resolve());
       const range = {basePatchNum: 'PARENT', patchNum: 2};
       return element.getChangeFiles(123, range).then(() => {
@@ -1256,7 +1245,7 @@
     });
 
     test('simple range', () => {
-      const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+      const fetchStub = sinon.stub(element, '_getChangeURLAndFetch')
           .returns(Promise.resolve());
       const range = {basePatchNum: 4, patchNum: 5};
       return element.getChangeFiles(123, range).then(() => {
@@ -1269,7 +1258,7 @@
     });
 
     test('parent index', () => {
-      const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+      const fetchStub = sinon.stub(element, '_getChangeURLAndFetch')
           .returns(Promise.resolve());
       const range = {basePatchNum: -3, patchNum: 5};
       return element.getChangeFiles(123, range).then(() => {
@@ -1284,7 +1273,7 @@
 
   suite('getDiff', () => {
     test('patchOnly', () => {
-      const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+      const fetchStub = sinon.stub(element, '_getChangeURLAndFetch')
           .returns(Promise.resolve());
       return element.getDiff(123, 'PARENT', 2, 'foo/bar.baz').then(() => {
         assert.isTrue(fetchStub.calledOnce);
@@ -1296,7 +1285,7 @@
     });
 
     test('simple range', () => {
-      const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+      const fetchStub = sinon.stub(element, '_getChangeURLAndFetch')
           .returns(Promise.resolve());
       return element.getDiff(123, 4, 5, 'foo/bar.baz').then(() => {
         assert.isTrue(fetchStub.calledOnce);
@@ -1308,7 +1297,7 @@
     });
 
     test('parent index', () => {
-      const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+      const fetchStub = sinon.stub(element, '_getChangeURLAndFetch')
           .returns(Promise.resolve());
       return element.getDiff(123, -3, 5, 'foo/bar.baz').then(() => {
         assert.isTrue(fetchStub.calledOnce);
@@ -1321,7 +1310,7 @@
   });
 
   test('getDashboard', () => {
-    const fetchCacheURLStub = sandbox.stub(element._restApiHelper,
+    const fetchCacheURLStub = sinon.stub(element._restApiHelper,
         'fetchCacheURL');
     element.getDashboard('gerrit/project', 'default:main');
     assert.isTrue(fetchCacheURLStub.calledOnce);
@@ -1331,7 +1320,7 @@
   });
 
   test('getFileContent', () => {
-    sandbox.stub(element, '_getChangeURLAndSend')
+    sinon.stub(element, '_getChangeURLAndSend')
         .returns(Promise.resolve({
           ok: 'true',
           headers: {
@@ -1343,7 +1332,7 @@
           },
         }));
 
-    sandbox.stub(element, 'getResponseObject')
+    sinon.stub(element, 'getResponseObject')
         .returns(Promise.resolve('new content'));
 
     const edit = element.getFileContent('1', 'tst/path', 'EDIT').then(res => {
@@ -1366,8 +1355,8 @@
       done();
     };
     element.addEventListener('server-error', handler);
-    sandbox.stub(authService, 'fetch').returns(Promise.resolve(res));
-    sandbox.stub(element, '_changeBaseURL').returns(Promise.resolve(''));
+    sinon.stub(authService, 'fetch').returns(Promise.resolve(res));
+    sinon.stub(element, '_changeBaseURL').returns(Promise.resolve(''));
     element.getFileContent('1', 'tst/path', '1').then(() => {
       flushAsynchronousOperations();
 
@@ -1378,9 +1367,9 @@
 
   test('getChangeFilesOrEditFiles is edit-sensitive', () => {
     const fn = element.getChangeOrEditFiles.bind(element);
-    const getChangeFilesStub = sandbox.stub(element, 'getChangeFiles')
+    const getChangeFilesStub = sinon.stub(element, 'getChangeFiles')
         .returns(Promise.resolve({}));
-    const getChangeEditFilesStub = sandbox.stub(element, 'getChangeEditFiles')
+    const getChangeEditFilesStub = sinon.stub(element, 'getChangeEditFiles')
         .returns(Promise.resolve({}));
 
     return fn('1', {patchNum: 'edit'}).then(() => {
@@ -1394,13 +1383,13 @@
   });
 
   test('_fetch forwards request and logs', () => {
-    const logStub = sandbox.stub(element._restApiHelper, '_logCall');
+    const logStub = sinon.stub(element._restApiHelper, '_logCall');
     const response = {status: 404, text: sinon.stub()};
     const url = 'my url';
     const fetchOptions = {method: 'DELETE'};
-    sandbox.stub(element._auth, 'fetch').returns(Promise.resolve(response));
+    sinon.stub(element._auth, 'fetch').returns(Promise.resolve(response));
     const startTime = 123;
-    sandbox.stub(Date, 'now').returns(startTime);
+    sinon.stub(Date, 'now').returns(startTime);
     const req = {url, fetchOptions};
     return element._restApiHelper.fetch(req).then(() => {
       assert.isTrue(logStub.calledOnce);
@@ -1410,7 +1399,7 @@
   });
 
   test('_logCall only reports requests with anonymized URLss', () => {
-    sandbox.stub(Date, 'now').returns(200);
+    sinon.stub(Date, 'now').returns(200);
     const handler = sinon.stub();
     element.addEventListener('rpc-log', handler);
 
@@ -1424,10 +1413,10 @@
   });
 
   test('saveChangeStarred', async () => {
-    sandbox.stub(element, 'getFromProjectLookup')
+    sinon.stub(element, 'getFromProjectLookup')
         .returns(Promise.resolve('test'));
     const sendStub =
-        sandbox.stub(element._restApiHelper, 'send').returns(Promise.resolve());
+        sinon.stub(element._restApiHelper, 'send').returns(Promise.resolve());
 
     await element.saveChangeStarred(123, true);
     assert.isTrue(sendStub.calledOnce);
@@ -1446,4 +1435,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.js
similarity index 73%
rename from polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html
rename to polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.js
index 32d2166..8acba73 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.js
@@ -1,47 +1,37 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2019 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-rest-api-helper</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<script type="module">
-import '../../../../test/common-test-setup.js';
+import '../../../../test/common-test-setup-karma.js';
 import {SiteBasedCache} from './gr-rest-api-helper.js';
 import {FetchPromisesCache, GrRestApiHelper} from './gr-rest-api-helper.js';
 import {authService} from '../gr-auth.js';
 
 suite('gr-rest-api-helper tests', () => {
   let helper;
-  let sandbox;
+
   let cache;
   let fetchPromisesCache;
+  let originalCanonicalPath;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     cache = new SiteBasedCache();
     fetchPromisesCache = new FetchPromisesCache();
 
+    originalCanonicalPath = window.CANONICAL_PATH;
     window.CANONICAL_PATH = 'testhelper';
 
     const mockRestApiInterface = {
@@ -50,7 +40,7 @@
     };
 
     const testJSON = ')]}\'\n{"hello": "bonjour"}';
-    sandbox.stub(window, 'fetch').returns(Promise.resolve({
+    sinon.stub(window, 'fetch').returns(Promise.resolve({
       ok: true,
       text() {
         return Promise.resolve(testJSON);
@@ -62,12 +52,12 @@
   });
 
   teardown(() => {
-    sandbox.restore();
+    window.CANONICAL_PATH = originalCanonicalPath;
   });
 
   suite('fetchJSON()', () => {
     test('Sets header to accept application/json', () => {
-      const authFetchStub = sandbox.stub(helper._auth, 'fetch')
+      const authFetchStub = sinon.stub(helper._auth, 'fetch')
           .returns(Promise.resolve());
       helper.fetchJSON({url: '/dummy/url'});
       assert.isTrue(authFetchStub.called);
@@ -76,7 +66,7 @@
     });
 
     test('Use header option accept when provided', () => {
-      const authFetchStub = sandbox.stub(helper._auth, 'fetch')
+      const authFetchStub = sinon.stub(helper._auth, 'fetch')
           .returns(Promise.resolve());
       const headers = new Headers();
       headers.append('Accept', '*/*');
@@ -97,7 +87,7 @@
 
   test('cached results', done => {
     let n = 0;
-    sandbox.stub(helper, 'fetchJSON', () => Promise.resolve(++n));
+    sinon.stub(helper, 'fetchJSON').callsFake(() => Promise.resolve(++n));
     const promises = [];
     promises.push(helper.fetchCacheURL('/foo'));
     promises.push(helper.fetchCacheURL('/foo'));
@@ -171,4 +161,4 @@
         });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.js
similarity index 85%
rename from polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
rename to polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.js
index fa54d6b..f408a97 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.js
@@ -1,53 +1,38 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-reviewer-updates-parser</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import {GrReviewerUpdatesParser} from './gr-reviewer-updates-parser.js';
 import {parseDate} from '../../../utils/date-util.js';
 
 suite('gr-reviewer-updates-parser tests', () => {
-  let sandbox;
   let instance;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-  });
 
-  teardown(() => {
-    sandbox.restore();
   });
 
   test('ignores changes without messages', () => {
     const change = {};
-    sandbox.stub(
+    sinon.stub(
         GrReviewerUpdatesParser.prototype, '_filterRemovedMessages');
-    sandbox.stub(
+    sinon.stub(
         GrReviewerUpdatesParser.prototype, '_groupUpdates');
-    sandbox.stub(
+    sinon.stub(
         GrReviewerUpdatesParser.prototype, '_formatUpdates');
     assert.strictEqual(GrReviewerUpdatesParser.parse(change), change);
     assert.isFalse(
@@ -62,11 +47,11 @@
     const change = {
       messages: [],
     };
-    sandbox.stub(
+    sinon.stub(
         GrReviewerUpdatesParser.prototype, '_filterRemovedMessages');
-    sandbox.stub(
+    sinon.stub(
         GrReviewerUpdatesParser.prototype, '_groupUpdates');
-    sandbox.stub(
+    sinon.stub(
         GrReviewerUpdatesParser.prototype, '_formatUpdates');
     assert.strictEqual(GrReviewerUpdatesParser.parse(change), change);
     assert.isFalse(
@@ -82,11 +67,11 @@
       messages: [],
       reviewer_updates: [],
     };
-    sandbox.stub(
+    sinon.stub(
         GrReviewerUpdatesParser.prototype, '_filterRemovedMessages');
-    sandbox.stub(
+    sinon.stub(
         GrReviewerUpdatesParser.prototype, '_groupUpdates');
-    sandbox.stub(
+    sinon.stub(
         GrReviewerUpdatesParser.prototype, '_formatUpdates');
     assert.strictEqual(GrReviewerUpdatesParser.parse(change), change);
     assert.isFalse(
@@ -303,4 +288,4 @@
     assert.equal(updates[3].date, tplus(500));
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.js
similarity index 60%
rename from polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
rename to polygerrit-ui/app/elements/shared/gr-select/gr-select_test.js
index 670f383..c697850 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.js
@@ -1,59 +1,46 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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
+import '../../../test/common-test-setup-karma.js';
+import './gr-select.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-select</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-select>
+const basicFixture = fixtureFromTemplate(html`
+<gr-select>
       <select>
         <option value="1">One</option>
         <option value="2">Two</option>
         <option value="3">Three</option>
       </select>
     </gr-select>
-  </template>
-</test-fixture>
+`);
 
-<test-fixture id="noOptions">
-  <template>
-    <gr-select>
+const noOptionsFixture = fixtureFromTemplate(html`
+<gr-select>
       <select>
       </select>
     </gr-select>
-  </template>
-</test-fixture>
+`);
 
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-select.js';
 suite('gr-select tests', () => {
   let element;
 
   setup(() => {
-    element = fixture('basic');
+    element = basicFixture.instantiate();
   });
 
   test('bindValue must be set to the first option value', () => {
@@ -109,7 +96,7 @@
     let element;
 
     setup(() => {
-      element = fixture('noOptions');
+      element = noOptionsFixture.instantiate();
     });
 
     test('bindValue must not be changed', () => {
@@ -117,4 +104,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html
deleted file mode 100644
index ee0b64f..0000000
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html
+++ /dev/null
@@ -1,61 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2018 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-shell-command</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-shell-command></gr-shell-command>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-shell-command.js';
-suite('gr-shell-command tests', () => {
-  let element;
-  let sandbox;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-    element.text = `git fetch http://gerrit@localhost:8080/a/test-project
-        refs/changes/05/5/1 && git checkout FETCH_HEAD`;
-    flushAsynchronousOperations();
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('focusOnCopy', () => {
-    const focusStub = sandbox.stub(element.shadowRoot
-        .querySelector('gr-copy-clipboard'),
-    'focusOnCopy');
-    element.focusOnCopy();
-    assert.isTrue(focusStub.called);
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.js b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.js
new file mode 100644
index 0000000..5e20717
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.js
@@ -0,0 +1,41 @@
+/**
+ * @license
+ * Copyright (C) 2018 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-shell-command.js';
+
+const basicFixture = fixtureFromElement('gr-shell-command');
+
+suite('gr-shell-command tests', () => {
+  let element;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+    element.text = `git fetch http://gerrit@localhost:8080/a/test-project
+        refs/changes/05/5/1 && git checkout FETCH_HEAD`;
+    flushAsynchronousOperations();
+  });
+
+  test('focusOnCopy', () => {
+    const focusStub = sinon.stub(element.shadowRoot
+        .querySelector('gr-copy-clipboard'),
+    'focusOnCopy');
+    element.focusOnCopy();
+    assert.isTrue(focusStub.called);
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.js
similarity index 75%
rename from polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
rename to polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.js
index b560c56..99f953f 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.js
@@ -1,41 +1,27 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-storage</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-storage></gr-storage>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-storage.js';
+
+const basicFixture = fixtureFromElement('gr-storage');
+
 suite('gr-storage tests', () => {
   let element;
-  let sandbox;
 
   function mockStorage(opt_quotaExceeded) {
     return {
@@ -50,13 +36,11 @@
   }
 
   setup(() => {
-    element = fixture('basic');
-    sandbox = sinon.sandbox.create();
+    element = basicFixture.instantiate();
+
     element._storage = mockStorage();
   });
 
-  teardown(() => sandbox.restore());
-
   test('storing, retrieving and erasing drafts', () => {
     const changeNum = 1234;
     const patchNum = 5;
@@ -106,7 +90,7 @@
     // Make sure that the call to cleanup doesn't get throttled.
     element._lastCleanup = 0;
 
-    const cleanupSpy = sandbox.spy(element, '_cleanupItems');
+    const cleanupSpy = sinon.spy(element, '_cleanupItems');
 
     // Create a message with a timestamp that is a second behind the max age.
     element._storage.setItem(key, JSON.stringify({
@@ -166,7 +150,7 @@
   });
 
   test('editable content items', () => {
-    const cleanupStub = sandbox.stub(element, '_cleanupItems');
+    const cleanupStub = sinon.stub(element, '_cleanupItems');
     const key = 'testKey';
     const computedKey = element._getEditableContentKey(key);
     // Key correctly computed.
@@ -192,4 +176,4 @@
     assert.isNotOk(element._storage.getItem(computedKey));
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.js
similarity index 80%
rename from polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
rename to polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.js
index bcf1207..2aa7697 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.js
@@ -1,63 +1,40 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-textarea</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-textarea></gr-textarea>
-  </template>
-</test-fixture>
-
-<test-fixture id="monospace">
-  <template>
-    <gr-textarea monospace="true"></gr-textarea>
-  </template>
-</test-fixture>
-
-<test-fixture id="hideBorder">
-  <template>
-    <gr-textarea hide-border="true"></gr-textarea>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-textarea.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromElement('gr-textarea');
+
+const monospaceFixture = fixtureFromTemplate(html`
+<gr-textarea monospace="true"></gr-textarea>
+`);
+
+const hideBorderFixture = fixtureFromTemplate(html`
+<gr-textarea hide-border="true"></gr-textarea>
+`);
+
 suite('gr-textarea tests', () => {
   let element;
-  let sandbox;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
-    element = fixture('basic');
-    sandbox.stub(element.reporting, 'reportInteraction');
-  });
-
-  teardown(() => {
-    sandbox.restore();
+    element = basicFixture.instantiate();
+    sinon.stub(element.reporting, 'reportInteraction');
   });
 
   test('monospace is set properly', () => {
@@ -154,7 +131,7 @@
         // Since selectionStart is on Chrome set always on end of text, we
         // stub it to 1
         const text = ': hello';
-        sandbox.stub(element.$, 'textarea', {
+        sinon.stub(element.$, 'textarea').value( {
           selectionStart: 1,
           value: text,
           textarea: {
@@ -169,7 +146,7 @@
         assert.equal(element._currentSearchString, '');
       });
   test('emoji selector closes when text changes before the colon', () => {
-    const resetStub = sandbox.stub(element, '_resetEmojiDropdown');
+    const resetStub = sinon.stub(element, '_resetEmojiDropdown');
     MockInteractions.focus(element.$.textarea);
     flushAsynchronousOperations();
     element.$.textarea.selectionStart = 10;
@@ -189,7 +166,7 @@
   });
 
   test('_resetEmojiDropdown', () => {
-    const closeSpy = sandbox.spy(element, 'closeDropdown');
+    const closeSpy = sinon.spy(element, 'closeDropdown');
     element._resetEmojiDropdown();
     assert.equal(element._currentSearchString, '');
     assert.isTrue(element._hideAutocomplete);
@@ -203,7 +180,7 @@
 
   test('_determineSuggestions', () => {
     const emojiText = 'tear';
-    const formatSpy = sandbox.spy(element, '_formatSuggestions');
+    const formatSpy = sinon.spy(element, '_formatSuggestions');
     element._determineSuggestions(emojiText);
     assert.isTrue(formatSpy.called);
     assert.isTrue(formatSpy.lastCall.calledWithExactly(
@@ -244,7 +221,7 @@
   });
 
   test('emoji dropdown is closed when iron-overlay-closed is fired', () => {
-    const resetSpy = sandbox.spy(element, '_resetEmojiDropdown');
+    const resetSpy = sinon.spy(element, '_resetEmojiDropdown');
     element.$.emojiSuggestions.dispatchEvent(
         new CustomEvent('dropdown-closed', {
           composed: true, bubbles: true,
@@ -273,7 +250,7 @@
     }
 
     test('escape key', () => {
-      const resetSpy = sandbox.spy(element, '_resetEmojiDropdown');
+      const resetSpy = sinon.spy(element, '_resetEmojiDropdown');
       MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
       assert.isFalse(resetSpy.called);
       setupDropdown();
@@ -283,7 +260,7 @@
     });
 
     test('up key', () => {
-      const upSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorUp');
+      const upSpy = sinon.spy(element.$.emojiSuggestions, 'cursorUp');
       MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
       assert.isFalse(upSpy.called);
       setupDropdown();
@@ -292,7 +269,7 @@
     });
 
     test('down key', () => {
-      const downSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorDown');
+      const downSpy = sinon.spy(element.$.emojiSuggestions, 'cursorDown');
       MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
       assert.isFalse(downSpy.called);
       setupDropdown();
@@ -301,7 +278,7 @@
     });
 
     test('enter key', () => {
-      const enterSpy = sandbox.spy(element.$.emojiSuggestions,
+      const enterSpy = sinon.spy(element.$.emojiSuggestions,
           'getCursorTarget');
       MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
       assert.isFalse(enterSpy.called);
@@ -313,7 +290,7 @@
     });
 
     test('enter key - ignored on just colon without more information', () => {
-      const enterSpy = sandbox.spy(element.$.emojiSuggestions,
+      const enterSpy = sinon.spy(element.$.emojiSuggestions,
           'getCursorTarget');
       MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
       assert.isFalse(enterSpy.called);
@@ -335,15 +312,9 @@
   // properties before ready() is called.
 
     let element;
-    let sandbox;
 
     setup(() => {
-      sandbox = sinon.sandbox.create();
-      element = fixture('monospace');
-    });
-
-    teardown(() => {
-      sandbox.restore();
+      element = monospaceFixture.instantiate();
     });
 
     test('monospace is set properly', () => {
@@ -359,15 +330,9 @@
   // properties before ready() is called.
 
     let element;
-    let sandbox;
 
     setup(() => {
-      sandbox = sinon.sandbox.create();
-      element = fixture('hideBorder');
-    });
-
-    teardown(() => {
-      sandbox.restore();
+      element = hideBorderFixture.instantiate();
     });
 
     test('hideBorder is set properly', () => {
@@ -375,4 +340,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
deleted file mode 100644
index a8fc18a..0000000
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
+++ /dev/null
@@ -1,61 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-storage</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-tooltip-content>
-    </gr-tooltip-content>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-tooltip-content.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-suite('gr-tooltip-content tests', () => {
-  let element;
-  setup(() => {
-    element = fixture('basic');
-  });
-
-  test('icon is not visible by default', () => {
-    assert.equal(dom(element.root)
-        .querySelector('iron-icon').hidden, true);
-  });
-
-  test('position-below attribute is reflected', () => {
-    assert.isFalse(element.hasAttribute('position-below'));
-    element.positionBelow = true;
-    assert.isTrue(element.hasAttribute('position-below'));
-  });
-
-  test('icon is visible with showIcon property', () => {
-    element.showIcon = true;
-    assert.equal(dom(element.root)
-        .querySelector('iron-icon').hidden, false);
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js
new file mode 100644
index 0000000..f905eaa
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js
@@ -0,0 +1,51 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-tooltip-content.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromTemplate(html`
+<gr-tooltip-content>
+    </gr-tooltip-content>
+`);
+
+suite('gr-tooltip-content tests', () => {
+  let element;
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('icon is not visible by default', () => {
+    assert.equal(dom(element.root)
+        .querySelector('iron-icon').hidden, true);
+  });
+
+  test('position-below attribute is reflected', () => {
+    assert.isFalse(element.hasAttribute('position-below'));
+    element.positionBelow = true;
+    assert.isTrue(element.hasAttribute('position-below'));
+  });
+
+  test('icon is visible with showIcon property', () => {
+    element.showIcon = true;
+    assert.equal(dom(element.root)
+        .querySelector('iron-icon').hidden, false);
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html
deleted file mode 100644
index b69d945..0000000
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html
+++ /dev/null
@@ -1,66 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-storage</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-tooltip>
-    </gr-tooltip>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-tooltip.js';
-suite('gr-tooltip tests', () => {
-  let element;
-  setup(() => {
-    element = fixture('basic');
-  });
-
-  test('max-width is respected if set', () => {
-    element.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit' +
-        ', sed do eiusmod tempor incididunt ut labore et dolore magna aliqua';
-    element.maxWidth = '50px';
-    assert.equal(getComputedStyle(element).width, '50px');
-  });
-
-  test('the correct arrow is displayed', () => {
-    assert.equal(getComputedStyle(element.shadowRoot
-        .querySelector('.arrowPositionBelow')).display,
-    'none');
-    assert.notEqual(getComputedStyle(element.shadowRoot
-        .querySelector('.arrowPositionAbove'))
-        .display, 'none');
-    element.positionBelow = true;
-    assert.notEqual(getComputedStyle(element.shadowRoot
-        .querySelector('.arrowPositionBelow'))
-        .display, 'none');
-    assert.equal(getComputedStyle(element.shadowRoot
-        .querySelector('.arrowPositionAbove'))
-        .display, 'none');
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.js b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.js
new file mode 100644
index 0000000..b5f068c
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.js
@@ -0,0 +1,56 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-tooltip.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromTemplate(html`
+<gr-tooltip>
+    </gr-tooltip>
+`);
+
+suite('gr-tooltip tests', () => {
+  let element;
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('max-width is respected if set', () => {
+    element.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit' +
+        ', sed do eiusmod tempor incididunt ut labore et dolore magna aliqua';
+    element.maxWidth = '50px';
+    assert.equal(getComputedStyle(element).width, '50px');
+  });
+
+  test('the correct arrow is displayed', () => {
+    assert.equal(getComputedStyle(element.shadowRoot
+        .querySelector('.arrowPositionBelow')).display,
+    'none');
+    assert.notEqual(getComputedStyle(element.shadowRoot
+        .querySelector('.arrowPositionAbove'))
+        .display, 'none');
+    element.positionBelow = true;
+    assert.notEqual(getComputedStyle(element.shadowRoot
+        .querySelector('.arrowPositionBelow'))
+        .display, 'none');
+    assert.equal(getComputedStyle(element.shadowRoot
+        .querySelector('.arrowPositionAbove'))
+        .display, 'none');
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html b/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html
deleted file mode 100644
index 2d89b30..0000000
--- a/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html
+++ /dev/null
@@ -1,90 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>revision-info</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './revision-info.js';
-import {RevisionInfo} from './revision-info.js';
-suite('revision-info tests', () => {
-  let mockChange;
-
-  setup(() => {
-    mockChange = {
-      revisions: {
-        r1: {_number: 1, commit: {parents: [
-          {commit: 'p1'},
-          {commit: 'p2'},
-          {commit: 'p3'},
-        ]}},
-        r2: {_number: 2, commit: {parents: [
-          {commit: 'p1'},
-          {commit: 'p4'},
-        ]}},
-        r3: {_number: 3, commit: {parents: [{commit: 'p5'}]}},
-        r4: {_number: 4, commit: {parents: [
-          {commit: 'p2'},
-          {commit: 'p3'},
-        ]}},
-        r5: {_number: 5, commit: {parents: [
-          {commit: 'p5'},
-          {commit: 'p2'},
-          {commit: 'p3'},
-        ]}},
-      },
-    };
-  });
-
-  test('getMaxParents', () => {
-    const ri = new RevisionInfo(mockChange);
-    assert.equal(ri.getMaxParents(), 3);
-  });
-
-  test('getParentCountMap', () => {
-    const ri = new RevisionInfo(mockChange);
-    assert.deepEqual(ri.getParentCountMap(), {1: 3, 2: 2, 3: 1, 4: 2, 5: 3});
-  });
-
-  test('getParentCount', () => {
-    const ri = new RevisionInfo(mockChange);
-    assert.deepEqual(ri.getParentCount(1), 3);
-    assert.deepEqual(ri.getParentCount(3), 1);
-  });
-
-  test('getParentCount', () => {
-    const ri = new RevisionInfo(mockChange);
-    assert.deepEqual(ri.getParentCount(1), 3);
-    assert.deepEqual(ri.getParentCount(3), 1);
-  });
-
-  test('getParentId', () => {
-    const ri = new RevisionInfo(mockChange);
-    assert.deepEqual(ri.getParentId(1, 2), 'p3');
-    assert.deepEqual(ri.getParentId(2, 1), 'p4');
-    assert.deepEqual(ri.getParentId(3, 0), 'p5');
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.js b/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.js
new file mode 100644
index 0000000..7d0dd4f
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.js
@@ -0,0 +1,79 @@
+/**
+ * @license
+ * Copyright (C) 2017 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 './revision-info.js';
+import {RevisionInfo} from './revision-info.js';
+suite('revision-info tests', () => {
+  let mockChange;
+
+  setup(() => {
+    mockChange = {
+      revisions: {
+        r1: {_number: 1, commit: {parents: [
+          {commit: 'p1'},
+          {commit: 'p2'},
+          {commit: 'p3'},
+        ]}},
+        r2: {_number: 2, commit: {parents: [
+          {commit: 'p1'},
+          {commit: 'p4'},
+        ]}},
+        r3: {_number: 3, commit: {parents: [{commit: 'p5'}]}},
+        r4: {_number: 4, commit: {parents: [
+          {commit: 'p2'},
+          {commit: 'p3'},
+        ]}},
+        r5: {_number: 5, commit: {parents: [
+          {commit: 'p5'},
+          {commit: 'p2'},
+          {commit: 'p3'},
+        ]}},
+      },
+    };
+  });
+
+  test('getMaxParents', () => {
+    const ri = new RevisionInfo(mockChange);
+    assert.equal(ri.getMaxParents(), 3);
+  });
+
+  test('getParentCountMap', () => {
+    const ri = new RevisionInfo(mockChange);
+    assert.deepEqual(ri.getParentCountMap(), {1: 3, 2: 2, 3: 1, 4: 2, 5: 3});
+  });
+
+  test('getParentCount', () => {
+    const ri = new RevisionInfo(mockChange);
+    assert.deepEqual(ri.getParentCount(1), 3);
+    assert.deepEqual(ri.getParentCount(3), 1);
+  });
+
+  test('getParentCount', () => {
+    const ri = new RevisionInfo(mockChange);
+    assert.deepEqual(ri.getParentCount(1), 3);
+    assert.deepEqual(ri.getParentCount(3), 1);
+  });
+
+  test('getParentId', () => {
+    const ri = new RevisionInfo(mockChange);
+    assert.deepEqual(ri.getParentId(1, 2), 'p3');
+    assert.deepEqual(ri.getParentId(2, 1), 'p4');
+    assert.deepEqual(ri.getParentId(3, 0), 'p5');
+  });
+});
+
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init_test.html b/polygerrit-ui/app/embed/gr-diff-app-context-init_test.html
deleted file mode 100644
index 3aed13f..0000000
--- a/polygerrit-ui/app/embed/gr-diff-app-context-init_test.html
+++ /dev/null
@@ -1,42 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2020 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<script type="module">
-  import '../test/common-test-setup.js';
-  import {appContext} from '../services/app-context.js';
-  import {initDiffAppContext} from './gr-diff-app-context-init.js';
-  suite('gr diff app context initializer tests', () => {
-    setup(() => {
-      initDiffAppContext();
-    });
-
-    test('all services initialized and are singletons', () => {
-      Object.keys(appContext).forEach(serviceName => {
-        const service = appContext[serviceName];
-        assert.isNotNull(service);
-        const service2 = appContext[serviceName];
-        assert.strictEqual(service, service2);
-      });
-    });
-  });
-</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init_test.js b/polygerrit-ui/app/embed/gr-diff-app-context-init_test.js
new file mode 100644
index 0000000..832c931
--- /dev/null
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init_test.js
@@ -0,0 +1,35 @@
+/**
+ * @license
+ * Copyright (C) 2020 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 {appContext} from '../services/app-context.js';
+import {initDiffAppContext} from './gr-diff-app-context-init.js';
+suite('gr diff app context initializer tests', () => {
+  setup(() => {
+    initDiffAppContext();
+  });
+
+  test('all services initialized and are singletons', () => {
+    Object.keys(appContext).forEach(serviceName => {
+      const service = appContext[serviceName];
+      assert.isNotNull(service);
+      const service2 = appContext[serviceName];
+      assert.strictEqual(service, service2);
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
index 83860d5..1492ad8 100644
--- a/polygerrit-ui/app/rules.bzl
+++ b/polygerrit-ui/app/rules.bzl
@@ -82,63 +82,3 @@
             "zip -qr $$ROOT/$@ *",
         ]),
     )
-
-def _wct_test(name, srcs, split_index, split_count):
-    """Macro to define single WCT suite
-
-    Defines a private macro for a portion of test files with split_index.
-    The actual split happens in test/tests.js file
-
-    Args:
-        name: name of generated sh_test
-        srcs: source files
-        split_index: index WCT suite. Must be less than split_count
-        split_count: total number of WCT suites
-    """
-    str_index = str(split_index)
-    config_json = struct(splitIndex = split_index, splitCount = split_count).to_json()
-    native.sh_test(
-        name = name,
-        size = "enormous",
-        srcs = ["wct_test.sh"],
-        args = [
-            "$(location @ui_dev_npm//web-component-tester/bin:wct)",
-            config_json,
-        ],
-        data = [
-            "@ui_dev_npm//web-component-tester/bin:wct",
-        ] + srcs,
-        # Should not run sandboxed.
-        tags = [
-            "local",
-            "manual",
-            "wct",
-        ],
-    )
-
-def wct_suite(name, srcs, split_count):
-    """Define test suites for WCT tests.
-
-    All tests files are split to split_count WCT suites
-
-    Args:
-        name: rule name. The macro create a test suite rule with the name name+"_test"
-        srcs: source files
-        split_count: number of sh_test (i.e. WCT suites)
-    """
-    tests = []
-    for i in range(split_count):
-        test_name = "wct_test_" + str(i)
-        _wct_test(test_name, srcs, i, split_count)
-        tests.append(test_name)
-
-    native.test_suite(
-        name = name + "_test",
-        tests = tests,
-        # Setup tags for suite as well.
-        # This excludes tests from the wildcard expansion (//...)
-        tags = [
-            "local",
-            "manual",
-        ],
-    )
diff --git a/polygerrit-ui/app/run_test.sh b/polygerrit-ui/app/run_test.sh
index a5231a1..2ca1118 100755
--- a/polygerrit-ui/app/run_test.sh
+++ b/polygerrit-ui/app/run_test.sh
@@ -6,13 +6,6 @@
     bazel_bin=bazel
 fi
 
-# WCT tests are not hermetic, and need extra environment variables.
-# TODO(hanwen): does $DISPLAY even work on OSX?
 ${bazel_bin} test \
-      --test_env="HOME=$HOME" \
-      --test_env="WCT_ARGS=${WCT_ARGS}" \
-      --test_env="DISPLAY=${DISPLAY}" \
-      --test_env="WCT_HEADLESS_MODE=${WCT_HEADLESS_MODE}" \
       "$@" \
-      //polygerrit-ui/app:wct_test \
       //polygerrit-ui:karma_test
diff --git a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.js
similarity index 80%
rename from polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html
rename to polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.js
index 818ddaa..94d96ad 100644
--- a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html
+++ b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.js
@@ -1,31 +1,21 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2019 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-display-name-utils</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script type="module">
-import '../../test/common-test-setup.js';
+import '../../test/common-test-setup-karma.js';
 import {GrDisplayNameUtils} from './gr-display-name-utils.js';
 
 suite('gr-display-name-utils tests', () => {
@@ -200,4 +190,4 @@
     assert.equal(GrDisplayNameUtils._accountEmail(undefined), '');
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html
deleted file mode 100644
index 80d3590..0000000
--- a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html
+++ /dev/null
@@ -1,97 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2019 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-email-suggestions-provider</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../test/common-test-setup.js';
-import '../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import {GrEmailSuggestionsProvider} from './gr-email-suggestions-provider.js';
-
-suite('GrEmailSuggestionsProvider tests', () => {
-  let sandbox;
-  let restAPI;
-  let provider;
-  const account1 = {
-    name: 'Some name',
-    email: 'some@example.com',
-  };
-  const account2 = {
-    email: 'other@example.com',
-    _account_id: 3,
-  };
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({}); },
-    });
-    restAPI = fixture('basic');
-    provider = new GrEmailSuggestionsProvider(restAPI);
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('getSuggestions', done => {
-    const getSuggestedAccountsStub =
-        sandbox.stub(restAPI, 'getSuggestedAccounts')
-            .returns(Promise.resolve([account1, account2]));
-
-    provider.getSuggestions('Some input').then(res => {
-      assert.deepEqual(res, [account1, account2]);
-      assert.isTrue(getSuggestedAccountsStub.calledOnce);
-      assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
-      done();
-    });
-  });
-
-  test('makeSuggestionItem', () => {
-    assert.deepEqual(provider.makeSuggestionItem(account1), {
-      name: 'Some name <some@example.com>',
-      value: {
-        account: account1,
-        count: 1,
-      },
-    });
-
-    assert.deepEqual(provider.makeSuggestionItem(account2), {
-      name: 'other@example.com <other@example.com>',
-      value: {
-        account: account2,
-        count: 1,
-      },
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.js b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.js
new file mode 100644
index 0000000..7c40b7a
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.js
@@ -0,0 +1,78 @@
+/**
+ * @license
+ * Copyright (C) 2019 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 '../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import {GrEmailSuggestionsProvider} from './gr-email-suggestions-provider.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromTemplate(html`
+<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`);
+
+suite('GrEmailSuggestionsProvider tests', () => {
+  let restAPI;
+  let provider;
+  const account1 = {
+    name: 'Some name',
+    email: 'some@example.com',
+  };
+  const account2 = {
+    email: 'other@example.com',
+    _account_id: 3,
+  };
+
+  setup(() => {
+    stub('gr-rest-api-interface', {
+      getConfig() { return Promise.resolve({}); },
+    });
+    restAPI = basicFixture.instantiate();
+    provider = new GrEmailSuggestionsProvider(restAPI);
+  });
+
+  test('getSuggestions', done => {
+    const getSuggestedAccountsStub =
+        sinon.stub(restAPI, 'getSuggestedAccounts')
+            .returns(Promise.resolve([account1, account2]));
+
+    provider.getSuggestions('Some input').then(res => {
+      assert.deepEqual(res, [account1, account2]);
+      assert.isTrue(getSuggestedAccountsStub.calledOnce);
+      assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
+      done();
+    });
+  });
+
+  test('makeSuggestionItem', () => {
+    assert.deepEqual(provider.makeSuggestionItem(account1), {
+      name: 'Some name <some@example.com>',
+      value: {
+        account: account1,
+        count: 1,
+      },
+    });
+
+    assert.deepEqual(provider.makeSuggestionItem(account2), {
+      name: 'other@example.com <other@example.com>',
+      value: {
+        account: account2,
+        count: 1,
+      },
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html
deleted file mode 100644
index 2111b7e..0000000
--- a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html
+++ /dev/null
@@ -1,105 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2019 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-group-suggestions-provider</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../test/common-test-setup.js';
-import '../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import {GrGroupSuggestionsProvider} from './gr-group-suggestions-provider.js';
-
-suite('GrGroupSuggestionsProvider tests', () => {
-  let sandbox;
-  let restAPI;
-  let provider;
-  const group1 = {
-    name: 'Some name',
-    id: 1,
-  };
-  const group2 = {
-    name: 'Other name',
-    id: 3,
-    url: 'abcd',
-  };
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({}); },
-    });
-    restAPI = fixture('basic');
-    provider = new GrGroupSuggestionsProvider(restAPI);
-  });
-
-  teardown(() => {
-    sandbox.restore();
-  });
-
-  test('getSuggestions', done => {
-    const getSuggestedAccountsStub =
-        sandbox.stub(restAPI, 'getSuggestedGroups')
-            .returns(Promise.resolve({
-              'Some name': {id: 1},
-              'Other name': {id: 3, url: 'abcd'},
-            }));
-
-    provider.getSuggestions('Some input').then(res => {
-      assert.deepEqual(res, [group1, group2]);
-      assert.isTrue(getSuggestedAccountsStub.calledOnce);
-      assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
-      done();
-    });
-  });
-
-  test('makeSuggestionItem', () => {
-    assert.deepEqual(provider.makeSuggestionItem(group1), {
-      name: 'Some name',
-      value: {
-        group: {
-          name: 'Some name',
-          id: 1,
-        },
-      },
-    });
-
-    assert.deepEqual(provider.makeSuggestionItem(group2), {
-      name: 'Other name',
-      value: {
-        group: {
-          name: 'Other name',
-          id: 3,
-        },
-      },
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.js b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.js
new file mode 100644
index 0000000..0939f76
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.js
@@ -0,0 +1,86 @@
+/**
+ * @license
+ * Copyright (C) 2019 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 '../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import {GrGroupSuggestionsProvider} from './gr-group-suggestions-provider.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromTemplate(html`
+<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`);
+
+suite('GrGroupSuggestionsProvider tests', () => {
+  let restAPI;
+  let provider;
+  const group1 = {
+    name: 'Some name',
+    id: 1,
+  };
+  const group2 = {
+    name: 'Other name',
+    id: 3,
+    url: 'abcd',
+  };
+
+  setup(() => {
+    stub('gr-rest-api-interface', {
+      getConfig() { return Promise.resolve({}); },
+    });
+    restAPI = basicFixture.instantiate();
+    provider = new GrGroupSuggestionsProvider(restAPI);
+  });
+
+  test('getSuggestions', done => {
+    const getSuggestedAccountsStub =
+        sinon.stub(restAPI, 'getSuggestedGroups')
+            .returns(Promise.resolve({
+              'Some name': {id: 1},
+              'Other name': {id: 3, url: 'abcd'},
+            }));
+
+    provider.getSuggestions('Some input').then(res => {
+      assert.deepEqual(res, [group1, group2]);
+      assert.isTrue(getSuggestedAccountsStub.calledOnce);
+      assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
+      done();
+    });
+  });
+
+  test('makeSuggestionItem', () => {
+    assert.deepEqual(provider.makeSuggestionItem(group1), {
+      name: 'Some name',
+      value: {
+        group: {
+          name: 'Some name',
+          id: 1,
+        },
+      },
+    });
+
+    assert.deepEqual(provider.makeSuggestionItem(group2), {
+      name: 'Other name',
+      value: {
+        group: {
+          name: 'Other name',
+          id: 3,
+        },
+      },
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.js
similarity index 79%
rename from polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html
rename to polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.js
index 8774d48..fba4b26 100644
--- a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.js
@@ -1,44 +1,31 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2019 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-reviewer-suggestions-provider</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../test/common-test-setup.js';
+import '../../test/common-test-setup-karma.js';
 import '../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import {GrDisplayNameUtils} from '../gr-display-name-utils/gr-display-name-utils.js';
 import {GrReviewerSuggestionsProvider, SUGGESTIONS_PROVIDERS_USERS_TYPES} from './gr-reviewer-suggestions-provider.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const basicFixture = fixtureFromTemplate(html`
+<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`);
 
 suite('GrReviewerSuggestionsProvider tests', () => {
-  let sandbox;
   let _nextAccountId = 0;
   const makeAccount = function(opt_status) {
     const accountId = ++_nextAccountId;
@@ -91,7 +78,7 @@
       getConfig() { return Promise.resolve({}); },
     });
 
-    restAPI = fixture('basic');
+    restAPI = basicFixture.instantiate();
     change = {
       _number: 42,
       owner,
@@ -100,13 +87,10 @@
         REVIEWER: [existingReviewer2],
       },
     };
-    sandbox = sinon.sandbox.create();
+
     return flush(done);
   });
 
-  teardown(() => {
-    sandbox.restore();
-  });
   suite('allowAnyUser set to false', () => {
     setup(done => {
       provider = GrReviewerSuggestionsProvider.create(restAPI, change._number,
@@ -180,7 +164,7 @@
           value: {account, count: 1},
         });
 
-        sandbox.stub(GrDisplayNameUtils, '_accountEmail',
+        sinon.stub(GrDisplayNameUtils, '_accountEmail').callsFake(
             () => '');
 
         suggestion = provider.makeSuggestionItem(account3);
@@ -219,10 +203,10 @@
 
     test('getChangeSuggestedReviewers is used', done => {
       const suggestReviewerStub =
-          sandbox.stub(restAPI, 'getChangeSuggestedReviewers')
+          sinon.stub(restAPI, 'getChangeSuggestedReviewers')
               .returns(Promise.resolve([]));
       const suggestAccountStub =
-          sandbox.stub(restAPI, 'getSuggestedAccounts')
+          sinon.stub(restAPI, 'getSuggestedAccounts')
               .returns(Promise.resolve([]));
 
       provider.getSuggestions('').then(() => {
@@ -243,10 +227,10 @@
 
     test('getSuggestedAccounts is used', done => {
       const suggestReviewerStub =
-          sandbox.stub(restAPI, 'getChangeSuggestedReviewers')
+          sinon.stub(restAPI, 'getChangeSuggestedReviewers')
               .returns(Promise.resolve([]));
       const suggestAccountStub =
-          sandbox.stub(restAPI, 'getSuggestedAccounts')
+          sinon.stub(restAPI, 'getSuggestedAccounts')
               .returns(Promise.resolve([]));
 
       provider.getSuggestions('').then(() => {
@@ -258,4 +242,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/services/app-context-init_test.html b/polygerrit-ui/app/services/app-context-init_test.html
deleted file mode 100644
index f5dc7d1..0000000
--- a/polygerrit-ui/app/services/app-context-init_test.html
+++ /dev/null
@@ -1,43 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2020 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<script type="module">
-  import '../test/common-test-setup.js';
-  import {appContext} from './app-context.js';
-  import {initAppContext} from './app-context-init.js';
-  suite('app context initializer tests', () => {
-    setup(() => {
-      initAppContext();
-    });
-
-    test('all services initialized and are singletons', () => {
-      Object.keys(appContext).forEach(serviceName => {
-        const service = appContext[serviceName];
-        assert.isNotNull(service);
-        const service2 = appContext[serviceName];
-        assert.strictEqual(service, service2);
-      });
-    });
-  });
-</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/services/app-context-init_test.js b/polygerrit-ui/app/services/app-context-init_test.js
new file mode 100644
index 0000000..9d22ec2
--- /dev/null
+++ b/polygerrit-ui/app/services/app-context-init_test.js
@@ -0,0 +1,35 @@
+/**
+ * @license
+ * Copyright (C) 2020 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 {appContext} from './app-context.js';
+import {initAppContext} from './app-context-init.js';
+suite('app context initializer tests', () => {
+  setup(() => {
+    initAppContext();
+  });
+
+  test('all services initialized and are singletons', () => {
+    Object.keys(appContext).forEach(serviceName => {
+      const service = appContext[serviceName];
+      assert.isNotNull(service);
+      const service2 = appContext[serviceName];
+      assert.strictEqual(service, service2);
+    });
+  });
+});
+
diff --git a/polygerrit-ui/app/services/flags_test.html b/polygerrit-ui/app/services/flags_test.html
deleted file mode 100644
index 51efb0d..0000000
--- a/polygerrit-ui/app/services/flags_test.html
+++ /dev/null
@@ -1,44 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2020 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<script>
-  window.ENABLED_EXPERIMENTS = ['a', 'a'];
-</script>
-
-<script type="module">
-  import '../test/common-test-setup.js';
-  import {FlagsService} from './flags.js';
-  suite('flags tests', () => {
-    const flags = new FlagsService();
-
-    test('isEnabled', () => {
-      assert.equal(flags.isEnabled('a'), true);
-      assert.equal(flags.isEnabled('random'), false);
-    });
-
-    test('enabledExperiments', () => {
-      assert.deepEqual(flags.enabledExperiments, ['a']);
-    });
-  });
-</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/services/flags_test.js b/polygerrit-ui/app/services/flags_test.js
new file mode 100644
index 0000000..ae1033e
--- /dev/null
+++ b/polygerrit-ui/app/services/flags_test.js
@@ -0,0 +1,44 @@
+/**
+ * @license
+ * Copyright (C) 2020 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 {FlagsService} from './flags.js';
+
+suite('flags tests', () => {
+  let originalEnabledExperiments;
+  let flags;
+
+  suiteSetup(() => {
+    originalEnabledExperiments = window.ENABLED_EXPERIMENTS;
+    window.ENABLED_EXPERIMENTS = ['a', 'a'];
+    flags = new FlagsService();
+  });
+
+  suiteTeardown(() => {
+    window.ENABLED_EXPERIMENTS = originalEnabledExperiments;
+  });
+
+  test('isEnabled', () => {
+    assert.equal(flags.isEnabled('a'), true);
+    assert.equal(flags.isEnabled('random'), false);
+  });
+
+  test('enabledExperiments', () => {
+    assert.deepEqual(flags.enabledExperiments, ['a']);
+  });
+});
+
diff --git a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.html b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js
similarity index 69%
rename from polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.html
rename to polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js
index ef2c539..1cdd6e3 100644
--- a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.html
+++ b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js
@@ -1,56 +1,37 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2019 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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-api-interface</title>
-
-<script src="../../../node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-js-api-interface></gr-js-api-interface>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../test/common-test-setup.js';
+import '../../test/common-test-setup-karma.js';
 import '../../elements/shared/gr-js-api-interface/gr-js-api-interface.js';
 import {EventEmitter} from './gr-event-interface.js';
 import {_testOnly_initGerritPluginApi} from '../../elements/shared/gr-js-api-interface/gr-gerrit.js';
 
+const basicFixture = fixtureFromElement('gr-js-api-interface');
+
 const pluginApi = _testOnly_initGerritPluginApi();
 
 suite('gr-event-interface tests', () => {
-  let sandbox;
-
   setup(() => {
-    sandbox = sinon.sandbox.create();
-  });
 
-  teardown(() => {
-    sandbox.restore();
   });
 
   suite('test on Gerrit', () => {
     setup(() => {
-      fixture('basic');
+      basicFixture.instantiate();
       pluginApi.removeAllListeners();
     });
 
@@ -149,4 +130,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock_test.html b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock_test.html
deleted file mode 100644
index e33a214..0000000
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock_test.html
+++ /dev/null
@@ -1,39 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2020 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<script type="module">
-  import '../../test/common-test-setup.js';
-  import {GrReporting} from './gr-reporting.js';
-  import {grReportingMock} from './gr-reporting_mock.js';
-  suite('gr-reporting_mock tests', () => {
-    test('mocks all public methods', () => {
-      const methods = Object.getOwnPropertyNames(GrReporting.prototype)
-          .filter(name => typeof GrReporting.prototype[name] === 'function')
-          .filter(name => !name.startsWith('_') && name !== 'constructor')
-          .sort();
-      const mockMethods = Object.getOwnPropertyNames(grReportingMock)
-          .sort();
-      assert.deepEqual(methods, mockMethods);
-    });
-  });
-</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock_test.js b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock_test.js
new file mode 100644
index 0000000..7c70e10
--- /dev/null
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock_test.js
@@ -0,0 +1,32 @@
+/**
+ * @license
+ * Copyright (C) 2020 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 {GrReporting} from './gr-reporting.js';
+import {grReportingMock} from './gr-reporting_mock.js';
+suite('gr-reporting_mock tests', () => {
+  test('mocks all public methods', () => {
+    const methods = Object.getOwnPropertyNames(GrReporting.prototype)
+        .filter(name => typeof GrReporting.prototype[name] === 'function')
+        .filter(name => !name.startsWith('_') && name !== 'constructor')
+        .sort();
+    const mockMethods = Object.getOwnPropertyNames(grReportingMock)
+        .sort();
+    assert.deepEqual(methods, mockMethods);
+  });
+});
+
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js
similarity index 83%
rename from polygerrit-ui/app/services/gr-reporting/gr-reporting_test.html
rename to polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js
index e309c8c..01ba3cb 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.html
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js
@@ -1,52 +1,39 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-reporting</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-services-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<script type="module">
-import '../../test/common-test-setup.js';
+import '../../test/common-test-setup-karma.js';
 import {GrReporting, DEFAULT_STARTUP_TIMERS, initErrorReporter} from './gr-reporting.js';
 import {appContext} from '../app-context.js';
 suite('gr-reporting tests', () => {
   let service;
-  let sandbox;
+
   let clock;
   let fakePerformance;
 
   const NOW_TIME = 100;
 
   setup(() => {
-    sandbox = sinon.sandbox.create();
     clock = sinon.useFakeTimers(NOW_TIME);
     service = new GrReporting(appContext.flagsService);
     service._baselines = Object.assign({}, DEFAULT_STARTUP_TIMERS);
-    sandbox.stub(service, 'reporter');
+    sinon.stub(service, 'reporter');
   });
 
   teardown(() => {
-    sandbox.restore();
     clock.restore();
   });
 
@@ -56,9 +43,8 @@
       loadEventEnd: 2,
     };
     fakePerformance.toJSON = () => fakePerformance;
-    sinon.stub(service, 'performanceTiming',
-        {get() { return fakePerformance; }});
-    sandbox.stub(window.performance, 'now').returns(42);
+    sinon.stub(service, 'performanceTiming').get(() => fakePerformance);
+    sinon.stub(window.performance, 'now').returns(42);
     service.appStarted();
     assert.isTrue(
         service.reporter.calledWithMatch(
@@ -73,7 +59,7 @@
   });
 
   test('WebComponentsReady', () => {
-    sandbox.stub(window.performance, 'now').returns(42);
+    sinon.stub(window.performance, 'now').returns(42);
     service.timeEnd('WebComponentsReady');
     assert.isTrue(service.reporter.calledWithMatch(
         'timing-report', 'UI Latency', 'WebComponentsReady', 42
@@ -82,7 +68,7 @@
 
   test('beforeLocationChanged', () => {
     service._baselines['garbage'] = 'monster';
-    sandbox.stub(service, 'time');
+    sinon.stub(service, 'time');
     service.beforeLocationChanged();
     assert.isTrue(service.time.calledWithExactly('DashboardDisplayed'));
     assert.isTrue(service.time.calledWithExactly('ChangeDisplayed'));
@@ -93,7 +79,7 @@
   });
 
   test('changeDisplayed', () => {
-    sandbox.spy(service, 'timeEnd');
+    sinon.spy(service, 'timeEnd');
     service.changeDisplayed();
     assert.isFalse(service.timeEnd.calledWith('ChangeDisplayed'));
     assert.isTrue(service.timeEnd.calledWith('StartupChangeDisplayed'));
@@ -102,7 +88,7 @@
   });
 
   test('changeFullyLoaded', () => {
-    sandbox.spy(service, 'timeEnd');
+    sinon.spy(service, 'timeEnd');
     service.changeFullyLoaded();
     assert.isFalse(
         service.timeEnd.calledWithExactly('ChangeFullyLoaded'));
@@ -113,7 +99,7 @@
   });
 
   test('diffViewDisplayed', () => {
-    sandbox.spy(service, 'timeEnd');
+    sinon.spy(service, 'timeEnd');
     service.diffViewDisplayed();
     assert.isFalse(service.timeEnd.calledWith('DiffViewDisplayed'));
     assert.isTrue(service.timeEnd.calledWith('StartupDiffViewDisplayed'));
@@ -122,7 +108,7 @@
   });
 
   test('fileListDisplayed', () => {
-    sandbox.spy(service, 'timeEnd');
+    sinon.spy(service, 'timeEnd');
     service.fileListDisplayed();
     assert.isFalse(
         service.timeEnd.calledWithExactly('FileListDisplayed'));
@@ -133,7 +119,7 @@
   });
 
   test('dashboardDisplayed', () => {
-    sandbox.spy(service, 'timeEnd');
+    sinon.spy(service, 'timeEnd');
     service.dashboardDisplayed();
     assert.isFalse(service.timeEnd.calledWith('DashboardDisplayed'));
     assert.isTrue(service.timeEnd.calledWith('StartupDashboardDisplayed'));
@@ -142,8 +128,8 @@
   });
 
   test('dashboardDisplayed details', () => {
-    sandbox.spy(service, 'timeEnd');
-    sandbox.stub(window, 'performance', {
+    sinon.spy(service, 'timeEnd');
+    sinon.stub(window, 'performance').value( {
       memory: {
         usedJSHeapSize: 1024 * 1024,
       },
@@ -186,8 +172,8 @@
     };
 
     setup(() => {
-      sandbox.spy(service, 'timeEnd');
-      nowStub = sandbox.stub(window.performance, 'now');
+      sinon.spy(service, 'timeEnd');
+      nowStub = sinon.stub(window.performance, 'now');
       visibilityStateStub = {
         value: value => {
           Object.defineProperty(document, 'visibilityState',
@@ -239,7 +225,7 @@
       visibilityStateStub.value('visible');
       nowStub.returns(15);
       service.beforeLocationChanged();
-      service.timeEnd.reset();
+      service.timeEnd.resetHistory();
       service.dashboardDisplayed();
       assert.isTrue(
           service.timeEnd.calledWithMatch('DashboardDisplayed',
@@ -249,7 +235,7 @@
   });
 
   test('time and timeEnd', () => {
-    const nowStub = sandbox.stub(window.performance, 'now').returns(0);
+    const nowStub = sinon.stub(window.performance, 'now').returns(0);
     service.time('foo');
     nowStub.returns(1);
     service.time('bar');
@@ -266,7 +252,7 @@
   });
 
   test('timer object', () => {
-    const nowStub = sandbox.stub(window.performance, 'now').returns(100);
+    const nowStub = sinon.stub(window.performance, 'now').returns(100);
     const timer = service.getTimer('foo-bar');
     nowStub.returns(150);
     timer.end();
@@ -284,7 +270,7 @@
   });
 
   test('timer object maximum', () => {
-    const nowStub = sandbox.stub(window.performance, 'now').returns(100);
+    const nowStub = sinon.stub(window.performance, 'now').returns(100);
     const timer = service.getTimer('foo-bar').withMaximum(100);
     nowStub.returns(150);
     timer.end();
@@ -298,8 +284,8 @@
 
   test('recordDraftInteraction', () => {
     const key = 'TimeBetweenDraftActions';
-    const nowStub = sandbox.stub(window.performance, 'now').returns(100);
-    const timingStub = sandbox.stub(service, '_reportTiming');
+    const nowStub = sinon.stub(window.performance, 'now').returns(100);
+    const timingStub = sinon.stub(service, '_reportTiming');
     service.recordDraftInteraction();
     assert.isFalse(timingStub.called);
 
@@ -321,7 +307,7 @@
   });
 
   test('timeEndWithAverage', () => {
-    const nowStub = sandbox.stub(window.performance, 'now').returns(0);
+    const nowStub = sinon.stub(window.performance, 'now').returns(0);
     nowStub.returns(1000);
     service.time('foo');
     nowStub.returns(1100);
@@ -342,7 +328,7 @@
 
   test('reportInteraction', () => {
     service.reporter.restore();
-    sandbox.spy(service, '_reportEvent');
+    sinon.spy(service, '_reportEvent');
     service.pluginsLoaded(); // so we don't cache
     service.reportInteraction('button-click', {name: 'sendReply'});
     assert.isTrue(service._reportEvent.getCall(2).calledWithMatch(
@@ -356,9 +342,9 @@
 
   test('report start time', () => {
     service.reporter.restore();
-    sandbox.stub(window.performance, 'now').returns(42);
-    sandbox.spy(service, '_reportEvent');
-    const dispatchStub = sandbox.spy(document, 'dispatchEvent');
+    sinon.stub(window.performance, 'now').returns(42);
+    sinon.spy(service, '_reportEvent');
+    const dispatchStub = sinon.spy(document, 'dispatchEvent');
     service.pluginsLoaded();
     service.time('timeAction');
     service.timeEnd('timeAction');
@@ -377,11 +363,11 @@
   suite('plugins', () => {
     setup(() => {
       service.reporter.restore();
-      sandbox.stub(service, '_reportEvent');
+      sinon.stub(service, '_reportEvent');
     });
 
     test('pluginsLoaded reports time', () => {
-      sandbox.stub(window.performance, 'now').returns(42);
+      sinon.stub(window.performance, 'now').returns(42);
       service.pluginsLoaded();
       assert.isTrue(service._reportEvent.calledWithMatch(
           {
@@ -463,7 +449,7 @@
           this.handlers[type] = handler;
         },
       };
-      sandbox.stub(console, 'error');
+      sinon.stub(console, 'error');
       Object.defineProperty(appContext, 'reportingService', {
         get() {
           return service;
@@ -510,4 +496,4 @@
     });
   });
 });
-</script>
+
diff --git a/polygerrit-ui/app/test/common-test-setup-karma.js b/polygerrit-ui/app/test/common-test-setup-karma.js
index 4fd3b03..cc934fc 100644
--- a/polygerrit-ui/app/test/common-test-setup-karma.js
+++ b/polygerrit-ui/app/test/common-test-setup-karma.js
@@ -14,11 +14,47 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 import './common-test-setup.js';
 import '@polymer/test-fixture/test-fixture.js';
 import 'chai/chai.js';
 self.assert = window.chai.assert;
+self.expect = window.chai.expect;
+
+window.addEventListener('error', e => {
+  // For uncaught error mochajs doesn't print the full stack trace.
+  // We should print it ourselves.
+  console.error(e.error.stack.toString());
+});
+
+let originalOnBeforeUnload;
+
+suiteSetup(() => {
+  // This suiteSetup() method is called only once before all tests
+
+  // Can't use window.addEventListener("beforeunload",...) here,
+  // the handler is raised too late.
+  originalOnBeforeUnload = window.onbeforeunload;
+  window.onbeforeunload = e => {
+    // If a test reloads a page, we can't prevent it.
+    // However we can print earror and the stack trace with assert.fail
+    try {
+      throw new Error();
+    } catch (e) {
+      console.error('Page reloading attempt detected.');
+      console.error(e.stack.toString());
+    }
+    originalOnBeforeUnload(e);
+  };
+});
+
+suiteTeardown(() => {
+  // This suiteTeardown() method is called only once after all tests
+  window.onbeforeunload = originalOnBeforeUnload;
+});
+
+// Tests can use fake timers (sandbox.useFakeTimers)
+// Keep the original one for use in test utils methods.
+const nativeSetTimeout = window.setTimeout;
 
 /**
  * Triggers a flush of any pending events, observations, etc and calls you back
@@ -33,10 +69,10 @@
   // (https://github.com/Polymer/polymer-dev/issues/851)
   window.Polymer.dom.flush();
   if (callback) {
-    window.setTimeout(callback, 0);
+    nativeSetTimeout(callback, 0);
   } else {
     return new Promise(resolve => {
-      window.setTimeout(resolve, 0);
+      nativeSetTimeout(resolve, 0);
     });
   }
 }
diff --git a/polygerrit-ui/app/test/common-test-setup.js b/polygerrit-ui/app/test/common-test-setup.js
index b274169..36e27ba 100644
--- a/polygerrit-ui/app/test/common-test-setup.js
+++ b/polygerrit-ui/app/test/common-test-setup.js
@@ -18,20 +18,19 @@
 // TODO(dmfilippov): remove bundled-polymer.js imports when the following issue
 // https://github.com/Polymer/polymer-resin/issues/9 is resolved.
 import '../scripts/bundled-polymer.js';
-
+import './test-app-context-init.js';
 import 'polymer-resin/standalone/polymer-resin.js';
 import '@polymer/iron-test-helpers/iron-test-helpers.js';
 import './test-router.js';
 import {SafeTypes} from '../behaviors/safe-types-behavior/safe-types-behavior.js';
-import {appContext} from '../services/app-context.js';
-import {initAppContext} from '../services/app-context-init.js';
 import {_testOnly_resetPluginLoader} from '../elements/shared/gr-js-api-interface/gr-plugin-loader.js';
-import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock.js';
-
-// Returns true if tests run under the Karma
-function isKarmaTest() {
-  return window.__karma__ !== undefined;
-}
+import {_testOnlyResetRestApi} from '../elements/shared/gr-js-api-interface/gr-plugin-rest-api.js';
+import {_testOnlyResetGrRestApiSharedObjects} from '../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import {TestKeyboardShortcutBinder} from './test-utils';
+import {flushDebouncers} from '@polymer/polymer/lib/utils/debounce';
+import {_testOnly_getShortcutManagerInstance} from '../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import sinon from 'sinon/pkg/sinon-esm.js';
+window.sinon = sinon;
 
 security.polymer_resin.install({
   allowedIdentifierPrefixes: [''],
@@ -49,47 +48,27 @@
   safeTypesBridge: SafeTypes.safeTypesBridge,
 });
 
-// Default implementations of 'fixture' and 'stub' methods in
-// web-component-tester are incorrect. Default methods calls mocha teardown
-// method to register cleanup actions. Each call to the teardown method adds
-// additional 'afterEach' hook to a suite.
-// As a result, if a suite's setup(..) method calls fixture(..) or stub(..)
-// method, then additional afterEach hook is registered before each test.
-// In overall, afterEach hook is called testCount^2 instead of testCount.
-// When tests runs with the wct test runner, the runner adds listener for
-// the 'afterEach' and tries to make some UI and log udpates. These updates
-// are quite heavy, and after about 40-50 tests each test waste 0.5-1seconds.
-//
-// Our implementation uses global teardown to clean up everything. mocha calls
-// global teardown after each test. The cleanups array stores all functions
-// which must be called after a test ends.
-//
-// Note, that fixture(...) and stub(..) methods are registered different by
-// WCT. This is why these methods implemented slightly different here.
 const cleanups = [];
-if (isKarmaTest() || !window.fixture) {
-  // For karma always set our implementation
-  // (karma doesn't provide the fixture method)
-  window.fixture = function(fixtureId, model) {
-    // This method is inspired by WCT method
-    cleanups.push(() => document.getElementById(fixtureId).restore());
-    return document.getElementById(fixtureId).create(model);
-  };
-} else {
-  // The following error is important for WCT tests.
-  // If window.fixture already installed by WCT at this point, WCT tests
-  // performance decreases rapidly.
-  // It allows to catch performance problems earlier.
-  throw new Error('window.fixture must be set before wct sets it');
-}
 
-// On the first call to the setup, WCT installs window.fixture
-// and window.stub methods
+// For karma always set our implementation
+// (karma doesn't provide the fixture method)
+window.fixture = function(fixtureId, model) {
+  // This method is inspired by web-component-tester method
+  cleanups.push(() => document.getElementById(fixtureId).restore());
+  return document.getElementById(fixtureId).create(model);
+};
+
 setup(() => {
   // If the following asserts fails - then window.stub is
   // overwritten by some other code.
   assert.equal(cleanups.length, 0);
-
+  // The following calls is nessecary to avoid influence of previously executed
+  // tests.
+  TestKeyboardShortcutBinder.push();
+  const mgr = _testOnly_getShortcutManagerInstance();
+  assert.equal(mgr.activeHosts.size, 0);
+  assert.equal(mgr.listeners.size, 0);
+  document.getSelection().removeAllRanges();
   const pl = _testOnly_resetPluginLoader();
   // For testing, always init with empty plugin list
   // Since when serve in gr-app, we always retrieve the list
@@ -99,46 +78,68 @@
   // You still can manually call _testOnly_resetPluginLoader
   // to reset this behavior if you need to test something specific.
   pl.loadPlugins([]);
+  _testOnlyResetGrRestApiSharedObjects();
+  _testOnlyResetRestApi();
 });
 
-if (isKarmaTest() || window.stub) {
-  // For karma always set our implementation
-  // (karma doesn't provide the stub method)
-  window.stub = function(tagName, implementation) {
-    // This method is inspired by WCT method
-    const proto = document.createElement(tagName).constructor.prototype;
-    const stubs = Object.keys(implementation)
-        .map(key => sinon.stub(proto, key, implementation[key]));
-    cleanups.push(() => {
-      stubs.forEach(stub => {
-        stub.restore();
-      });
+// For karma always set our implementation
+// (karma doesn't provide the stub method)
+window.stub = function(tagName, implementation) {
+  // This method is inspired by web-component-tester method
+  const proto = document.createElement(tagName).constructor.prototype;
+  const stubs = Object.keys(implementation)
+      .map(key => sinon.stub(proto, key).callsFake(implementation[key]));
+  cleanups.push(() => {
+    stubs.forEach(stub => {
+      stub.restore();
     });
-  };
-} else {
-  // The following error is important for WCT tests.
-  // If window.fixture already installed by WCT at this point, WCT tests
-  // performance decreases rapidly.
-  // It allows to catch performance problems earlier.
-  throw new Error('window.stub must be set after wct sets it');
-}
-
-initAppContext();
-function setMock(serviceName, setupMock) {
-  Object.defineProperty(appContext, serviceName, {
-    get() {
-      return setupMock;
-    },
   });
+};
+
+// Very simple function to catch unexpected elements in documents body.
+// It can't catch everything, but in most cases it is enough.
+function checkChildAllowed(element) {
+  const allowedTags = ['SCRIPT', 'IRON-A11Y-ANNOUNCER'];
+  if (allowedTags.includes(element.tagName)) {
+    return;
+  }
+  if (element.tagName === 'TEST-FIXTURE') {
+    if (element.children.length == 0 ||
+        (element.children.length == 1 &&
+        element.children[0].tagName === 'TEMPLATE')) {
+      return;
+    }
+    assert.fail(`Test fixture
+        ${element.outerHTML}` +
+        `isn't resotred after the test is finished. Please ensure that ` +
+        `restore() method is called for this test-fixture. Usually the call` +
+        `happens automatically.`);
+    return;
+  }
+  if (element.tagName === 'DIV' && element.id === 'gr-hovercard-container' &&
+      element.childNodes.length === 0) {
+    return;
+  }
+  assert.fail(
+      `The following node remains in document after the test:
+      ${element.tagName}
+      Outer HTML:
+      ${element.outerHTML},
+      Stack trace:
+      ${element.stackTrace}`);
 }
-setMock('reportingService', grReportingMock);
+function checkGlobalSpace() {
+  for (const child of document.body.children) {
+    checkChildAllowed(child);
+  }
+}
 
 teardown(() => {
-  // WCT incorrectly uses teardown method in the 'fixture' and 'stub'
-  // implementations. This leads to slowdown WCT tests after each tests.
-  // I.e. more tests in a file - longer it takes.
-  // For example, gr-file-list_test.html takes approx 40 second without
-  // a fix and 10 seconds with our implementation of fixture and stub.
+  sinon.restore();
   cleanups.forEach(cleanup => cleanup());
   cleanups.splice(0);
+  TestKeyboardShortcutBinder.pop();
+  checkGlobalSpace();
+  // Clean Polymer debouncer queue, so next tests will not be affected.
+  flushDebouncers();
 });
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
deleted file mode 100644
index 63df0be..0000000
--- a/polygerrit-ui/app/test/index.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!-- Copyright (C) 2020 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. -->
-
-<!DOCTYPE html>
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>Elements Test Runner</title>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/node_modules/web-component-tester/browser.js"></script>
-<style>
-  /* Prevent horizontal scrolling on page.
-   New version of web-component-tester creates very narrow iframe */
-  #subsuites {
-    width: 1500px !important;
-  }
-</style>
-<script type="module">
-    import {config, testsPerFileString} from './suite_conf.js';
-    import {getSuiteTests} from './tests.js';
-    WCT.loadSuites(
-        getSuiteTests(testsPerFileString, config.splitIndex, config.splitCount));
-</script>
diff --git a/polygerrit-ui/app/test/mocks/comment-api.js b/polygerrit-ui/app/test/mocks/comment-api.js
index 77c72d4..f2ca48c 100644
--- a/polygerrit-ui/app/test/mocks/comment-api.js
+++ b/polygerrit-ui/app/test/mocks/comment-api.js
@@ -19,11 +19,13 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 
+/**
+ * This is an "abstract" class for tests. The descendant must define a template
+ * for this element and a tagName - see createCommentApiMockWithTemplateElement below
+ */
 class CommentApiMock extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
-  static get is() { return 'comment-api-mock'; }
-
   static get properties() {
     return {
       _changeComments: Object,
@@ -52,4 +54,18 @@
   }
 }
 
-customElements.define(CommentApiMock.is, CommentApiMock);
+/**
+ * Creates a new element which is descendant of CommentApiMock with specified
+ * template. Additionally, the method registers a tagName for this element.
+ *
+ * Each tagName must be a unique accross all tests.
+ */
+export function createCommentApiMockWithTemplateElement(tagName, template) {
+  const elementClass = class extends CommentApiMock {
+    static get is() { return tagName; }
+
+    static get template() { return template; }
+  };
+  customElements.define(tagName, elementClass);
+  return elementClass;
+}
diff --git a/polygerrit-ui/app/test/suite_conf.js b/polygerrit-ui/app/test/suite_conf.js
deleted file mode 100644
index 82870fe..0000000
--- a/polygerrit-ui/app/test/suite_conf.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 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.
- */
-
-/**
- * This file is an example of the content.
- * The real file is generated by the wct_test.sh script.
- * Content of this file doesn't affect wct tests.
- * Generated files contains all information to split test files between different tests
- */
-
-export const config = {
-  splitIndex: 0, // Index for split (wct_suite creates several sh_test, each split has its own index)
-  splitCount: 1, // Defines the number of splits
-};
-
-/**
- * testsPerFileString contains information about number of tests in each file
- * This information is not precise. It is used to split test files between WCT suites more evenly.
- */
-export const testsPerFileString = `
-./elements/change-list/gr-repo-header/gr-repo-header_test.html:1
-./elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html:25
-./elements/gr-app_test.html:4
-./behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html:13
-./behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html:19
-`;
diff --git a/polygerrit-ui/app/test/test-app-context-init.js b/polygerrit-ui/app/test/test-app-context-init.js
new file mode 100644
index 0000000..6fffdba
--- /dev/null
+++ b/polygerrit-ui/app/test/test-app-context-init.js
@@ -0,0 +1,32 @@
+/**
+ * @license
+ * Copyright (C) 2020 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.
+ */
+
+// Init app context before any other imports
+import {initAppContext} from '../services/app-context-init.js';
+import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock';
+import {appContext} from '../services/app-context.js';
+
+initAppContext();
+
+function setMock(serviceName, setupMock) {
+  Object.defineProperty(appContext, serviceName, {
+    get() {
+      return setupMock;
+    },
+  });
+}
+setMock('reportingService', grReportingMock);
diff --git a/polygerrit-ui/app/test/test-utils.js b/polygerrit-ui/app/test/test-utils.js
index 284343a..aaab295 100644
--- a/polygerrit-ui/app/test/test-utils.js
+++ b/polygerrit-ui/app/test/test-utils.js
@@ -18,6 +18,7 @@
 import {_testOnly_resetPluginLoader} from '../elements/shared/gr-js-api-interface/gr-plugin-loader.js';
 import {testOnly_resetInternalState} from '../elements/shared/gr-js-api-interface/gr-api-utils.js';
 import {_testOnly_resetEndpoints} from '../elements/shared/gr-js-api-interface/gr-plugin-endpoints.js';
+import {KeyboardShortcutBinder, _testOnly_getShortcutManagerInstance} from '../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 
 export const mockPromise = () => {
   let res;
@@ -29,6 +30,37 @@
 };
 export const isHidden = el => getComputedStyle(el).display === 'none';
 
+// Some tests/elements can define its own binding. We want to restore bindings
+// at the end of the test. The TestKeyboardShortcutBinder store bindings in
+// stack, so it is possible to override bindings in nested suites.
+export class TestKeyboardShortcutBinder {
+  static push() {
+    if (!this.stack) {
+      this.stack = [];
+    }
+    const testBinder = new TestKeyboardShortcutBinder();
+    this.stack.push(testBinder);
+    return KeyboardShortcutBinder;
+  }
+
+  static pop() {
+    this.stack.pop()._restoreShortcuts();
+  }
+
+  constructor() {
+    this._originalBinding = new Map(
+        _testOnly_getShortcutManagerInstance().bindings);
+  }
+
+  _restoreShortcuts() {
+    const bindings = _testOnly_getShortcutManagerInstance().bindings;
+    bindings.clear();
+    this._originalBinding.forEach((value, key) => {
+      bindings.set(key, value);
+    });
+  }
+}
+
 // Provide reset plugins function to clear installed plugins between tests.
 // No gr-app found (running tests)
 export const resetPlugins = () => {
diff --git a/polygerrit-ui/app/test/tests.js b/polygerrit-ui/app/test/tests.js
deleted file mode 100644
index b0b8fee..0000000
--- a/polygerrit-ui/app/test/tests.js
+++ /dev/null
@@ -1,327 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 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.
- */
-
-const testFiles = [];
-const scriptsPath = '../scripts/';
-const elementsPath = '../elements/';
-const behaviorsPath = '../behaviors/';
-const servicesPath = '../services/';
-
-// Elements tests.
-/* eslint-disable max-len */
-const elements = [
-  // This seemed to be flaky when it was farther down the list. Keep at the
-  // beginning.
-  'gr-app_test.html',
-  'admin/gr-admin-group-list/gr-admin-group-list_test.html',
-  'admin/gr-admin-view/gr-admin-view_test.html',
-  'admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html',
-  'admin/gr-create-change-dialog/gr-create-change-dialog_test.html',
-  'admin/gr-create-group-dialog/gr-create-group-dialog_test.html',
-  'admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html',
-  'admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html',
-  'admin/gr-group-audit-log/gr-group-audit-log_test.html',
-  'admin/gr-group-members/gr-group-members_test.html',
-  'admin/gr-group/gr-group_test.html',
-  'admin/gr-permission/gr-permission_test.html',
-  'admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html',
-  'admin/gr-plugin-list/gr-plugin-list_test.html',
-  'admin/gr-repo-access/gr-repo-access_test.html',
-  'admin/gr-repo-commands/gr-repo-commands_test.html',
-  'admin/gr-repo-dashboards/gr-repo-dashboards_test.html',
-  'admin/gr-repo-detail-list/gr-repo-detail-list_test.html',
-  'admin/gr-repo-list/gr-repo-list_test.html',
-  'admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html',
-  'admin/gr-repo/gr-repo_test.html',
-  'admin/gr-rule-editor/gr-rule-editor_test.html',
-  'change-list/gr-change-list-item/gr-change-list-item_test.html',
-  'change-list/gr-change-list-view/gr-change-list-view_test.html',
-  'change-list/gr-change-list/gr-change-list_test.html',
-  'change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html',
-  'change-list/gr-create-change-help/gr-create-change-help_test.html',
-  'change-list/gr-dashboard-view/gr-dashboard-view_test.html',
-  'change-list/gr-repo-header/gr-repo-header_test.html',
-  'change-list/gr-user-header/gr-user-header_test.html',
-  'change/gr-change-actions/gr-change-actions_test.html',
-  'change/gr-change-requirements/gr-change-requirements_test.html',
-  'change/gr-comment-list/gr-comment-list_test.html',
-  'change/gr-commit-info/gr-commit-info_test.html',
-  'change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html',
-  'change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html',
-  'change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html',
-  'change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html',
-  'change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html',
-  'change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html',
-  'change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.html',
-  'change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html',
-  'change/gr-download-dialog/gr-download-dialog_test.html',
-  'change/gr-file-list-header/gr-file-list-header_test.html',
-  'change/gr-file-list/gr-file-list_test.html',
-  'change/gr-included-in-dialog/gr-included-in-dialog_test.html',
-  'change/gr-label-score-row/gr-label-score-row_test.html',
-  'change/gr-label-scores/gr-label-scores_test.html',
-  'change/gr-message/gr-message_test.html',
-  'change/gr-messages-list/gr-messages-list_test.html',
-  'change/gr-messages-list/gr-messages-list-experimental_test.html',
-  'change/gr-related-changes-list/gr-related-changes-list_test.html',
-  'change/gr-reply-dialog/gr-reply-dialog_test.html',
-  'change/gr-reviewer-list/gr-reviewer-list_test.html',
-  'change/gr-thread-list/gr-thread-list_test.html',
-  'change/gr-upload-help-dialog/gr-upload-help-dialog_test.html',
-  'core/gr-account-dropdown/gr-account-dropdown_test.html',
-  'core/gr-error-dialog/gr-error-dialog_test.html',
-  'core/gr-error-manager/gr-error-manager_test.html',
-  'core/gr-key-binding-display/gr-key-binding-display_test.html',
-  'core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html',
-  'core/gr-main-header/gr-main-header_test.html',
-  'core/gr-navigation/gr-navigation_test.html',
-  'core/gr-router/gr-router_test.html',
-  'core/gr-search-bar/gr-search-bar_test.html',
-  'core/gr-smart-search/gr-smart-search_test.html',
-  'diff/gr-comment-api/gr-comment-api_test.html',
-  'diff/gr-coverage-layer/gr-coverage-layer_test.html',
-  'diff/gr-diff-builder/gr-diff-builder-element_test.html',
-  'diff/gr-diff-builder/gr-diff-builder-unified_test.html',
-  'diff/gr-diff-cursor/gr-diff-cursor_test.html',
-  'diff/gr-diff-highlight/gr-annotation_test.html',
-  'diff/gr-diff-highlight/gr-diff-highlight_test.html',
-  'diff/gr-diff-host/gr-diff-host_test.html',
-  'diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html',
-  'diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.html',
-  'diff/gr-diff-processor/gr-diff-processor_test.html',
-  'diff/gr-diff-selection/gr-diff-selection_test.html',
-  'diff/gr-diff-view/gr-diff-view_test.html',
-  'diff/gr-diff/gr-diff-group_test.html',
-  'diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html',
-  'diff/gr-patch-range-select/gr-patch-range-select_test.html',
-  'diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html',
-  'diff/gr-selection-action-box/gr-selection-action-box_test.html',
-  'diff/gr-syntax-layer/gr-syntax-layer_test.html',
-  'documentation/gr-documentation-search/gr-documentation-search_test.html',
-  'edit/gr-default-editor/gr-default-editor_test.html',
-  'edit/gr-edit-controls/gr-edit-controls_test.html',
-  'edit/gr-edit-file-controls/gr-edit-file-controls_test.html',
-  'edit/gr-editor-view/gr-editor-view_test.html',
-  'plugins/gr-admin-api/gr-admin-api_test.html',
-  'plugins/gr-styles-api/gr-styles-api_test.html',
-  'plugins/gr-attribute-helper/gr-attribute-helper_test.html',
-  'plugins/gr-dom-hooks/gr-dom-hooks_test.html',
-  'plugins/gr-event-helper/gr-event-helper_test.html',
-  'plugins/gr-plugin-host/gr-plugin-host_test.html',
-  'plugins/gr-popup-interface/gr-plugin-popup_test.html',
-  'plugins/gr-popup-interface/gr-popup-interface_test.html',
-  'plugins/gr-repo-api/gr-repo-api_test.html',
-  'plugins/gr-settings-api/gr-settings-api_test.html',
-  'plugins/gr-theme-api/gr-theme-api_test.html',
-  'settings/gr-account-info/gr-account-info_test.html',
-  'settings/gr-agreements-list/gr-agreements-list_test.html',
-  'settings/gr-change-table-editor/gr-change-table-editor_test.html',
-  'settings/gr-cla-view/gr-cla-view_test.html',
-  'settings/gr-edit-preferences/gr-edit-preferences_test.html',
-  'settings/gr-email-editor/gr-email-editor_test.html',
-  'settings/gr-gpg-editor/gr-gpg-editor_test.html',
-  'settings/gr-group-list/gr-group-list_test.html',
-  'settings/gr-http-password/gr-http-password_test.html',
-  'settings/gr-identities/gr-identities_test.html',
-  'settings/gr-menu-editor/gr-menu-editor_test.html',
-  'settings/gr-registration-dialog/gr-registration-dialog_test.html',
-  'settings/gr-ssh-editor/gr-ssh-editor_test.html',
-  'settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html',
-  'shared/gr-account-entry/gr-account-entry_test.html',
-  'shared/gr-account-label/gr-account-label_test.html',
-  'shared/gr-account-list/gr-account-list_test.html',
-  'shared/gr-account-link/gr-account-link_test.html',
-  'shared/gr-alert/gr-alert_test.html',
-  'shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html',
-  'shared/gr-avatar/gr-avatar_test.html',
-  'shared/gr-button/gr-button_test.html',
-  'shared/gr-change-star/gr-change-star_test.html',
-  'shared/gr-change-status/gr-change-status_test.html',
-  'shared/gr-comment-thread/gr-comment-thread_test.html',
-  'shared/gr-comment/gr-comment_test.html',
-  'shared/gr-copy-clipboard/gr-copy-clipboard_test.html',
-  'shared/gr-count-string-formatter/gr-count-string-formatter_test.html',
-  'shared/gr-date-formatter/gr-date-formatter_test.html',
-  'shared/gr-dialog/gr-dialog_test.html',
-  'shared/gr-diff-preferences/gr-diff-preferences_test.html',
-  'shared/gr-download-commands/gr-download-commands_test.html',
-  'shared/gr-dropdown/gr-dropdown_test.html',
-  'shared/gr-dropdown-list/gr-dropdown-list_test.html',
-  'shared/gr-editable-content/gr-editable-content_test.html',
-  'shared/gr-editable-label/gr-editable-label_test.html',
-  'shared/gr-formatted-text/gr-formatted-text_test.html',
-  'shared/gr-hovercard/gr-hovercard_test.html',
-  'shared/gr-hovercard-account/gr-hovercard-account_test.html',
-  'shared/gr-js-api-interface/gr-annotation-actions-context_test.html',
-  'shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html',
-  'shared/gr-js-api-interface/gr-change-actions-js-api_test.html',
-  'shared/gr-js-api-interface/gr-change-reply-js-api_test.html',
-  'shared/gr-js-api-interface/gr-api-utils_test.html',
-  'shared/gr-js-api-interface/gr-js-api-interface_test.html',
-  'shared/gr-js-api-interface/gr-gerrit_test.html',
-  'shared/gr-js-api-interface/gr-plugin-action-context_test.html',
-  'shared/gr-js-api-interface/gr-plugin-loader_test.html',
-  'shared/gr-js-api-interface/gr-plugin-rest-api_test.html',
-  'shared/gr-fixed-panel/gr-fixed-panel_test.html',
-  'shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html',
-  'shared/gr-label-info/gr-label-info_test.html',
-  'shared/gr-limited-text/gr-limited-text_test.html',
-  'shared/gr-linked-chip/gr-linked-chip_test.html',
-  'shared/gr-linked-text/gr-linked-text_test.html',
-  'shared/gr-list-view/gr-list-view_test.html',
-  'shared/gr-overlay/gr-overlay_test.html',
-  'shared/gr-page-nav/gr-page-nav_test.html',
-  'shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html',
-  'shared/gr-rest-api-interface/gr-auth_test.html',
-  'shared/gr-rest-api-interface/gr-etag-decorator_test.html',
-  'shared/gr-rest-api-interface/gr-rest-api-interface_test.html',
-  'shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html',
-  'shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html',
-  'shared/gr-select/gr-select_test.html',
-  'shared/gr-shell-command/gr-shell-command_test.html',
-  'shared/gr-storage/gr-storage_test.html',
-  'shared/gr-textarea/gr-textarea_test.html',
-  'shared/gr-tooltip-content/gr-tooltip-content_test.html',
-  'shared/gr-tooltip/gr-tooltip_test.html',
-  'shared/revision-info/revision-info_test.html',
-];
-/* eslint-enable max-len */
-for (let file of elements) {
-  file = elementsPath + file;
-  testFiles.push(file);
-}
-
-// Behaviors tests.
-/* eslint-disable max-len */
-const behaviors = [
-  'docs-url-behavior/docs-url-behavior_test.html',
-  'dom-util-behavior/dom-util-behavior_test.html',
-  'keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html',
-  'rest-client-behavior/rest-client-behavior_test.html',
-  'gr-access-behavior/gr-access-behavior_test.html',
-  'gr-admin-nav-behavior/gr-admin-nav-behavior_test.html',
-  'gr-change-table-behavior/gr-change-table-behavior_test.html',
-  'gr-list-view-behavior/gr-list-view-behavior_test.html',
-  'gr-display-name-behavior/gr-display-name-behavior_test.html',
-  'gr-patch-set-behavior/gr-patch-set-behavior_test.html',
-  'gr-path-list-behavior/gr-path-list-behavior_test.html',
-  'gr-tooltip-behavior/gr-tooltip-behavior_test.html',
-  'gr-url-encoding-behavior/gr-url-encoding-behavior_test.html',
-  'safe-types-behavior/safe-types-behavior_test.html',
-];
-/* eslint-enable max-len */
-for (let file of behaviors) {
-  // Behaviors do not utilize the DOM, so no shadow DOM test is necessary.
-  file = behaviorsPath + file;
-  testFiles.push(file);
-}
-
-const scripts = [
-  'gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html',
-  'gr-group-suggestions-provider/gr-group-suggestions-provider_test.html',
-  'gr-display-name-utils/gr-display-name-utils_test.html',
-  'gr-email-suggestions-provider/gr-email-suggestions-provider_test.html',
-];
-/* eslint-enable max-len */
-for (let file of scripts) {
-  file = scriptsPath + file;
-  testFiles.push(file);
-}
-
-const services = [
-  'app-context-init_test.html',
-  'flags_test.html',
-  'gr-reporting/gr-reporting_test.html',
-  'gr-reporting/gr-reporting_mock_test.html',
-  'gr-event-interface/gr-event-interface_test.html',
-];
-for (let file of services) {
-  file = servicesPath + file;
-  testFiles.push(file);
-}
-
-// embed test
-testFiles.push('../embed/gr-diff-app-context-init_test.html');
-
-/**
- * Converts multiline string to a map<file_name, test_count>.
- *
- * @param {number} testsPerFileString - multiline input string in the following format:
- *   fileName1:test_count1
- *   fileName2:test_count2
- *   ...
- *   fileName3:test_count3
- * @return Object<string, number> - key is the test file name, value is the number of tests
- */
-function parseTestsPerFileString(testsPerFileString) {
-  return testsPerFileString.split('\n').map(s => s.trim().replace('./', '../'))
-      .reduce((acc, fileAndCount) => {
-        const [file, countStr] = fileAndCount.split(':');
-        acc[file] = parseInt(countStr);
-        return acc;
-      }, {});
-}
-
-const defaultTestsPerFile = [];
-
-function getBucketWithMinTests(buckets) {
-  let minBucket = buckets[0];
-  for (let i = 1; i < buckets.length; i++) {
-    if (buckets[i].count < minBucket.count) {
-      minBucket = buckets[i];
-    }
-  }
-  return minBucket;
-}
-
-/**
- * Split testFiles among all buckets. The greedy algorithm is used,
- * because we don't need accurate splitting
- */
-function splitTestsByBuckets(buckets, testsPerFile) {
-  for (const testFile of testFiles) {
-    const testsInFile = testsPerFile[testFile] ?
-      testsPerFile[testFile] : defaultTestsPerFile;
-    const minBucket = getBucketWithMinTests(buckets);
-    minBucket.count += testsInFile;
-    minBucket.items.push(testFile);
-  }
-}
-
-/**
- * Returns list of test files for specified splitIndex
- *
- * @param {string} testsPerFileString - information about number of tests in each file
- *  (see suite_conf.js for exact format)
- * @param {number} splitIndex - index of split to return (0<=splitIndex<splitCount)
- * @param {number} splitCount - total number of splits
- * @return Array<string> - list of test files
- */
-export function getSuiteTests(testsPerFileString, splitIndex, splitCount) {
-  const testsPerFile = parseTestsPerFileString(testsPerFileString);
-  const buckets = [];
-  for (let i = 0; i < splitCount; i++) {
-    buckets.push({count: 0, items: []});
-  }
-  // TODO(dmfilippov): split tests by buckets only once
-  // This doesn't affect overall performance, so we can keep it
-  // while we have only small amounts of test files.
-  splitTestsByBuckets(buckets, testsPerFile);
-  console.log(buckets);
-  return buckets[splitIndex].items;
-}
-
diff --git a/polygerrit-ui/app/wct.conf.js b/polygerrit-ui/app/wct.conf.js
deleted file mode 100644
index 1a9300e..0000000
--- a/polygerrit-ui/app/wct.conf.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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.
- */
-
-/*
-For some reason wct tries to install selenium into its node_modules
-directory on first run. If you've installed into /usr/local and
-aren't running wct as root, you're screwed. Turning this option off
-through skipSeleniumInstall seems to still work, so there's that.
-
-Sauce tests are disabled by default in order to run local tests
-only.  Run it with (saucelabs.com account required; free for open
-source): ./polygerrit-ui/app/run_test.sh --test_arg=--plugin --test_arg=sauce
-*/
-
-const headless = 'WCT_HEADLESS_MODE' in process.env ?
-  process.env['WCT_HEADLESS_MODE'] === '1' : false;
-
-const headlessBrowserOptions = {
-  chrome: ['start-maximized', 'headless', 'disable-gpu', 'no-sandbox'],
-  firefox: ['-headless'],
-};
-
-const defaultBrowserOptions = {
-  chrome: ['start-maximized'],
-  firefox: [],
-};
-
-module.exports = {
-  suites: ['test'],
-  npm: true,
-  moduleResolution: 'node',
-  wctPackageName: 'wct-browser-legacy',
-  plugins: {
-    local: {
-      skipSeleniumInstall: true,
-      browserOptions: headless ? headlessBrowserOptions : defaultBrowserOptions,
-    },
-    sauce: {
-      disabled: true,
-      browsers: [
-        'OS X 10.12/chrome',
-        'Windows 10/chrome',
-        'Linux/firefox',
-        'OS X 10.12/safari',
-        'Windows 10/microsoftedge',
-      ],
-    },
-  },
-};
diff --git a/polygerrit-ui/app/wct_test.sh b/polygerrit-ui/app/wct_test.sh
deleted file mode 100755
index 829a507..0000000
--- a/polygerrit-ui/app/wct_test.sh
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/bin/sh
-
-set -ex
-root_dir=$(pwd)
-t=$TEST_TMPDIR
-export JSON_CONFIG=$2
-
-mkdir -p $t/node_modules
-# WCT doesn't implement node module resolution.
-# WCT uses only node_module/ directory from current directory when looking for a module
-# So, it is impossible to make hierarchical node_modules. Instead, we copy
-# all node_modules to one directory.
-cp -R -L ./external/ui_dev_npm/node_modules/* $t/node_modules
-
-# Copy ui_npm, so it will override ui_dev_npm modules (in case of conflicts)
-# Because browser always requests specific exact files (i.e. not a directory),
-# it always receives file from ui_npm. It can broke WCT itself but luckily it works.
-cp -R -L ./external/ui_npm/node_modules/* $t/node_modules
-
-cp -R -L ./polygerrit-ui/app/* $t/
-
-export PATH="$(dirname $NPM):$PATH"
-
-cd $t
-echo "export const config=$JSON_CONFIG;" > ./test/suite_conf.js
-echo "export const testsPerFileString=\`" >> ./test/suite_conf.js
-# Count number of tests in each file.
-# We don't need accurate data, use simplest method
-# TODO(dmfilippov): collect data only once
-# In the current implementation, the same data is collected for each split,
-# It takes less than a second which many times less than the overall wct test time
-grep -rnw '.' --include=\*_test.html -e "test(" -c >> ./test/suite_conf.js
-echo "\`;" >>./test/suite_conf.js
-
-# If wct doesn't receive any parameters, it fails (can't find files)
-# Pass --config-file as a parameter to have some arguments in command line
-$root_dir/$1 --config-file wct.conf.js ${WCT_ARGS}
diff --git a/polygerrit-ui/package.json b/polygerrit-ui/package.json
index 527763b..ea039d5 100644
--- a/polygerrit-ui/package.json
+++ b/polygerrit-ui/package.json
@@ -15,11 +15,7 @@
     "karma-mocha-reporter": "^2.2.5",
     "lodash": "^4.17.15",
     "mocha": "^7.1.1",
-    "wct-browser-legacy": "^1.0.2",
-    "web-component-tester": "^6.9.2"
-  },
-  "scripts": {
-    "postinstall": "selenium-standalone install"
+    "sinon": "^9.0.2"
   },
   "license": "Apache-2.0",
   "private": true
diff --git a/polygerrit-ui/yarn.lock b/polygerrit-ui/yarn.lock
index 5ad28b6..d73e1d0 100644
--- a/polygerrit-ui/yarn.lock
+++ b/polygerrit-ui/yarn.lock
@@ -18,27 +18,6 @@
     invariant "^2.2.4"
     semver "^5.5.0"
 
-"@babel/core@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.8.3.tgz#30b0ebb4dd1585de6923a0b4d179e0b9f5d82941"
-  integrity sha512-4XFkf8AwyrEG7Ziu3L2L0Cv+WyY47Tcsp70JFmpftbAA1K7YL/sgE9jh9HyNj08Y/U50ItUchpN0w6HxAoX1rA==
-  dependencies:
-    "@babel/code-frame" "^7.8.3"
-    "@babel/generator" "^7.8.3"
-    "@babel/helpers" "^7.8.3"
-    "@babel/parser" "^7.8.3"
-    "@babel/template" "^7.8.3"
-    "@babel/traverse" "^7.8.3"
-    "@babel/types" "^7.8.3"
-    convert-source-map "^1.7.0"
-    debug "^4.1.0"
-    gensync "^1.0.0-beta.1"
-    json5 "^2.1.0"
-    lodash "^4.17.13"
-    resolve "^1.3.2"
-    semver "^5.4.1"
-    source-map "^0.5.0"
-
 "@babel/core@^7.9.0":
   version "7.9.0"
   resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e"
@@ -61,16 +40,6 @@
     semver "^5.4.1"
     source-map "^0.5.0"
 
-"@babel/generator@^7.0.0-beta.42", "@babel/generator@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.3.tgz#0e22c005b0a94c1c74eafe19ef78ce53a4d45c03"
-  integrity sha512-WjoPk8hRpDRqqzRpvaR8/gDUPkrnOOeuT2m8cNICJtZH6mwaCo3v0OKMI7Y6SM1pBtyijnLtAL0HDi41pf41ug==
-  dependencies:
-    "@babel/types" "^7.8.3"
-    jsesc "^2.5.1"
-    lodash "^4.17.13"
-    source-map "^0.5.0"
-
 "@babel/generator@^7.4.0", "@babel/generator@^7.9.0":
   version "7.9.4"
   resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.4.tgz#12441e90c3b3c4159cdecf312075bf1a8ce2dbce"
@@ -81,6 +50,16 @@
     lodash "^4.17.13"
     source-map "^0.5.0"
 
+"@babel/generator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.3.tgz#0e22c005b0a94c1c74eafe19ef78ce53a4d45c03"
+  integrity sha512-WjoPk8hRpDRqqzRpvaR8/gDUPkrnOOeuT2m8cNICJtZH6mwaCo3v0OKMI7Y6SM1pBtyijnLtAL0HDi41pf41ug==
+  dependencies:
+    "@babel/types" "^7.8.3"
+    jsesc "^2.5.1"
+    lodash "^4.17.13"
+    source-map "^0.5.0"
+
 "@babel/helper-annotate-as-pure@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee"
@@ -96,15 +75,6 @@
     "@babel/helper-explode-assignable-expression" "^7.8.3"
     "@babel/types" "^7.8.3"
 
-"@babel/helper-call-delegate@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.8.3.tgz#de82619898aa605d409c42be6ffb8d7204579692"
-  integrity sha512-6Q05px0Eb+N4/GTyKPPvnkig7Lylw+QzihMpws9iiZQv7ZImf84ZsZpQH7QoWN4n4tm81SnSzPgHw2qtO0Zf3A==
-  dependencies:
-    "@babel/helper-hoist-variables" "^7.8.3"
-    "@babel/traverse" "^7.8.3"
-    "@babel/types" "^7.8.3"
-
 "@babel/helper-compilation-targets@^7.8.7":
   version "7.8.7"
   resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.7.tgz#dac1eea159c0e4bd46e309b5a1b04a66b53c1dde"
@@ -187,18 +157,6 @@
   dependencies:
     "@babel/types" "^7.8.3"
 
-"@babel/helper-module-transforms@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.8.3.tgz#d305e35d02bee720fbc2c3c3623aa0c316c01590"
-  integrity sha512-C7NG6B7vfBa/pwCOshpMbOYUmrYQDfCpVL/JCRu0ek8B5p8kue1+BCXpg2vOYs7w5ACB9GTOBYQ5U6NwrMg+3Q==
-  dependencies:
-    "@babel/helper-module-imports" "^7.8.3"
-    "@babel/helper-simple-access" "^7.8.3"
-    "@babel/helper-split-export-declaration" "^7.8.3"
-    "@babel/template" "^7.8.3"
-    "@babel/types" "^7.8.3"
-    lodash "^4.17.13"
-
 "@babel/helper-module-transforms@^7.9.0":
   version "7.9.0"
   resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz#43b34dfe15961918707d247327431388e9fe96e5"
@@ -292,15 +250,6 @@
     "@babel/traverse" "^7.8.3"
     "@babel/types" "^7.8.3"
 
-"@babel/helpers@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.8.3.tgz#382fbb0382ce7c4ce905945ab9641d688336ce85"
-  integrity sha512-LmU3q9Pah/XyZU89QvBgGt+BCsTPoQa+73RxAQh8fb8qkDyIfeQnmgs+hvzhTCKTzqOyk7JTkS3MS1S8Mq5yrQ==
-  dependencies:
-    "@babel/template" "^7.8.3"
-    "@babel/traverse" "^7.8.3"
-    "@babel/types" "^7.8.3"
-
 "@babel/helpers@^7.9.0":
   version "7.9.2"
   resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.9.2.tgz#b42a81a811f1e7313b88cba8adc66b3d9ae6c09f"
@@ -329,14 +278,7 @@
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.3.tgz#790874091d2001c9be6ec426c2eed47bc7679081"
   integrity sha512-/V72F4Yp/qmHaTALizEm9Gf2eQHV3QyTL3K0cNfijwnMnb1L+LDlAubb/ZnSdGAVzVSWakujHYs1I26x66sMeQ==
 
-"@babel/plugin-external-helpers@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-external-helpers/-/plugin-external-helpers-7.8.3.tgz#5a94164d9af393b2820a3cdc407e28ebf237de4b"
-  integrity sha512-mx0WXDDiIl5DwzMtzWGRSPugXi9BxROS05GQrhLNbEamhBiicgn994ibwkyiBH+6png7bm/yA7AUsvHyCXi4Vw==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
-
-"@babel/plugin-proposal-async-generator-functions@^7.0.0", "@babel/plugin-proposal-async-generator-functions@^7.8.3":
+"@babel/plugin-proposal-async-generator-functions@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz#bad329c670b382589721b27540c7d288601c6e6f"
   integrity sha512-NZ9zLv848JsV3hs8ryEh7Uaz/0KsmPLqv0+PdkDJL1cJy0K4kOCFa8zc1E3mp+RHPQcpdfb/6GovEsW4VDrOMw==
@@ -377,14 +319,6 @@
     "@babel/helper-plugin-utils" "^7.8.3"
     "@babel/plugin-syntax-numeric-separator" "^7.8.3"
 
-"@babel/plugin-proposal-object-rest-spread@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.8.3.tgz#eb5ae366118ddca67bed583b53d7554cad9951bb"
-  integrity sha512-8qvuPwU/xxUCt78HocNlv0mXXo0wdh9VT1R04WU8HGOfaOob26pF+9P5/lYjN/q7DHOX1bvX60hnhOvuQUJdbA==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
-    "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
-
 "@babel/plugin-proposal-object-rest-spread@^7.9.0":
   version "7.9.0"
   resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.0.tgz#a28993699fc13df165995362693962ba6b061d6f"
@@ -417,7 +351,7 @@
     "@babel/helper-create-regexp-features-plugin" "^7.8.8"
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-syntax-async-generators@^7.0.0", "@babel/plugin-syntax-async-generators@^7.8.0":
+"@babel/plugin-syntax-async-generators@^7.8.0":
   version "7.8.4"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
   integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==
@@ -431,14 +365,14 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-syntax-dynamic-import@^7.0.0", "@babel/plugin-syntax-dynamic-import@^7.8.0", "@babel/plugin-syntax-dynamic-import@^7.8.3":
+"@babel/plugin-syntax-dynamic-import@^7.8.0", "@babel/plugin-syntax-dynamic-import@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3"
   integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-import-meta@^7.0.0", "@babel/plugin-syntax-import-meta@^7.8.3":
+"@babel/plugin-syntax-import-meta@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.8.3.tgz#230afff79d3ccc215b5944b438e4e266daf3d84d"
   integrity sha512-vYiGd4wQ9gx0Lngb7+bPCwQXGK/PR6FeTIJ+TIOlq+OfOKG/kCAOO2+IBac3oMM9qV7/fU76hfcqxUaLKZf1hQ==
@@ -466,7 +400,7 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.8.0":
+"@babel/plugin-syntax-object-rest-spread@^7.8.0":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
   integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
@@ -494,14 +428,14 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-arrow-functions@^7.0.0", "@babel/plugin-transform-arrow-functions@^7.8.3":
+"@babel/plugin-transform-arrow-functions@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz#82776c2ed0cd9e1a49956daeb896024c9473b8b6"
   integrity sha512-0MRF+KC8EqH4dbuITCWwPSzsyO3HIWWlm30v8BbbpOrS1B++isGxPnnuq/IZvOX5J2D/p7DQalQm+/2PnlKGxg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-async-to-generator@^7.0.0", "@babel/plugin-transform-async-to-generator@^7.8.3":
+"@babel/plugin-transform-async-to-generator@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.8.3.tgz#4308fad0d9409d71eafb9b1a6ee35f9d64b64086"
   integrity sha512-imt9tFLD9ogt56Dd5CI/6XgpukMwd/fLGSrix2httihVe7LOGVPhyhMh1BU5kDM7iHD08i8uUtmV2sWaBFlHVQ==
@@ -510,14 +444,14 @@
     "@babel/helper-plugin-utils" "^7.8.3"
     "@babel/helper-remap-async-to-generator" "^7.8.3"
 
-"@babel/plugin-transform-block-scoped-functions@^7.0.0", "@babel/plugin-transform-block-scoped-functions@^7.8.3":
+"@babel/plugin-transform-block-scoped-functions@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.8.3.tgz#437eec5b799b5852072084b3ae5ef66e8349e8a3"
   integrity sha512-vo4F2OewqjbB1+yaJ7k2EJFHlTP3jR634Z9Cj9itpqNjuLXvhlVxgnjsHsdRgASR8xYDrx6onw4vW5H6We0Jmg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-block-scoping@^7.0.0", "@babel/plugin-transform-block-scoping@^7.8.3":
+"@babel/plugin-transform-block-scoping@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.8.3.tgz#97d35dab66857a437c166358b91d09050c868f3a"
   integrity sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w==
@@ -525,20 +459,6 @@
     "@babel/helper-plugin-utils" "^7.8.3"
     lodash "^4.17.13"
 
-"@babel/plugin-transform-classes@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.8.3.tgz#46fd7a9d2bb9ea89ce88720477979fe0d71b21b8"
-  integrity sha512-SjT0cwFJ+7Rbr1vQsvphAHwUHvSUPmMjMU/0P59G8U2HLFqSa082JO7zkbDNWs9kH/IUqpHI6xWNesGf8haF1w==
-  dependencies:
-    "@babel/helper-annotate-as-pure" "^7.8.3"
-    "@babel/helper-define-map" "^7.8.3"
-    "@babel/helper-function-name" "^7.8.3"
-    "@babel/helper-optimise-call-expression" "^7.8.3"
-    "@babel/helper-plugin-utils" "^7.8.3"
-    "@babel/helper-replace-supers" "^7.8.3"
-    "@babel/helper-split-export-declaration" "^7.8.3"
-    globals "^11.1.0"
-
 "@babel/plugin-transform-classes@^7.9.0":
   version "7.9.2"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.9.2.tgz#8603fc3cc449e31fdbdbc257f67717536a11af8d"
@@ -553,20 +473,13 @@
     "@babel/helper-split-export-declaration" "^7.8.3"
     globals "^11.1.0"
 
-"@babel/plugin-transform-computed-properties@^7.0.0", "@babel/plugin-transform-computed-properties@^7.8.3":
+"@babel/plugin-transform-computed-properties@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.8.3.tgz#96d0d28b7f7ce4eb5b120bb2e0e943343c86f81b"
   integrity sha512-O5hiIpSyOGdrQZRQ2ccwtTVkgUDBBiCuK//4RJ6UfePllUTCENOzKxfh6ulckXKc0DixTFLCfb2HVkNA7aDpzA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-destructuring@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.8.3.tgz#20ddfbd9e4676906b1056ee60af88590cc7aaa0b"
-  integrity sha512-H4X646nCkiEcHZUZaRkhE2XVsoz0J/1x3VVujnn96pSoGCtKPA99ZZA+va+gK+92Zycd6OBKCD8tDb/731bhgQ==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
-
 "@babel/plugin-transform-destructuring@^7.8.3":
   version "7.8.8"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.8.8.tgz#fadb2bc8e90ccaf5658de6f8d4d22ff6272a2f4b"
@@ -582,14 +495,14 @@
     "@babel/helper-create-regexp-features-plugin" "^7.8.3"
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-duplicate-keys@^7.0.0", "@babel/plugin-transform-duplicate-keys@^7.8.3":
+"@babel/plugin-transform-duplicate-keys@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.8.3.tgz#8d12df309aa537f272899c565ea1768e286e21f1"
   integrity sha512-s8dHiBUbcbSgipS4SMFuWGqCvyge5V2ZeAWzR6INTVC3Ltjig/Vw1G2Gztv0vU/hRG9X8IvKvYdoksnUfgXOEQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-exponentiation-operator@^7.0.0", "@babel/plugin-transform-exponentiation-operator@^7.8.3":
+"@babel/plugin-transform-exponentiation-operator@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.8.3.tgz#581a6d7f56970e06bf51560cd64f5e947b70d7b7"
   integrity sha512-zwIpuIymb3ACcInbksHaNcR12S++0MDLKkiqXHl3AzpgdKlFNhog+z/K0+TGW+b0w5pgTq4H6IwV/WhxbGYSjQ==
@@ -597,13 +510,6 @@
     "@babel/helper-builder-binary-assignment-operator-visitor" "^7.8.3"
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-for-of@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.8.3.tgz#15f17bce2fc95c7d59a24b299e83e81cedc22e18"
-  integrity sha512-ZjXznLNTxhpf4Q5q3x1NsngzGA38t9naWH8Gt+0qYZEJAcvPI9waSStSh56u19Ofjr7QmD0wUsQ8hw8s/p1VnA==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
-
 "@babel/plugin-transform-for-of@^7.9.0":
   version "7.9.0"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.9.0.tgz#0f260e27d3e29cd1bb3128da5e76c761aa6c108e"
@@ -611,7 +517,7 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-function-name@^7.0.0", "@babel/plugin-transform-function-name@^7.8.3":
+"@babel/plugin-transform-function-name@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.8.3.tgz#279373cb27322aaad67c2683e776dfc47196ed8b"
   integrity sha512-rO/OnDS78Eifbjn5Py9v8y0aR+aSYhDhqAwVfsTl0ERuMZyr05L1aFSCJnbv2mmsLkit/4ReeQ9N2BgLnOcPCQ==
@@ -619,14 +525,7 @@
     "@babel/helper-function-name" "^7.8.3"
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-instanceof@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-instanceof/-/plugin-transform-instanceof-7.8.3.tgz#a44d7d71590da36be7429573300618aefd784c3d"
-  integrity sha512-c/jB6Ebe2u17hxo+rce6PDgbkuHyfcJOleqgHYttnvMrCsxVwUnYsMq7GhxXekzUQsv9IImhv6YICKihpen+Ag==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
-
-"@babel/plugin-transform-literals@^7.0.0", "@babel/plugin-transform-literals@^7.8.3":
+"@babel/plugin-transform-literals@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.8.3.tgz#aef239823d91994ec7b68e55193525d76dbd5dc1"
   integrity sha512-3Tqf8JJ/qB7TeldGl+TT55+uQei9JfYaregDcEAyBZ7akutriFrt6C/wLYIer6OYhleVQvH/ntEhjE/xMmy10A==
@@ -640,15 +539,6 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-modules-amd@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.8.3.tgz#65606d44616b50225e76f5578f33c568a0b876a5"
-  integrity sha512-MadJiU3rLKclzT5kBH4yxdry96odTUwuqrZM+GllFI/VhxfPz+k9MshJM+MwhfkCdxxclSbSBbUGciBngR+kEQ==
-  dependencies:
-    "@babel/helper-module-transforms" "^7.8.3"
-    "@babel/helper-plugin-utils" "^7.8.3"
-    babel-plugin-dynamic-import-node "^2.3.0"
-
 "@babel/plugin-transform-modules-amd@^7.9.0":
   version "7.9.0"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.9.0.tgz#19755ee721912cf5bb04c07d50280af3484efef4"
@@ -700,7 +590,7 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-object-super@^7.0.0", "@babel/plugin-transform-object-super@^7.8.3":
+"@babel/plugin-transform-object-super@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.8.3.tgz#ebb6a1e7a86ffa96858bd6ac0102d65944261725"
   integrity sha512-57FXk+gItG/GejofIyLIgBKTas4+pEU47IXKDBWFTxdPd7F80H8zybyAY7UoblVfBhBGs2EKM+bJUu2+iUYPDQ==
@@ -708,15 +598,6 @@
     "@babel/helper-plugin-utils" "^7.8.3"
     "@babel/helper-replace-supers" "^7.8.3"
 
-"@babel/plugin-transform-parameters@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.8.3.tgz#7890576a13b17325d8b7d44cb37f21dc3bbdda59"
-  integrity sha512-/pqngtGb54JwMBZ6S/D3XYylQDFtGjWrnoCF4gXZOUpFV/ujbxnoNGNvDGu6doFWRPBveE72qTx/RRU44j5I/Q==
-  dependencies:
-    "@babel/helper-call-delegate" "^7.8.3"
-    "@babel/helper-get-function-arity" "^7.8.3"
-    "@babel/helper-plugin-utils" "^7.8.3"
-
 "@babel/plugin-transform-parameters@^7.8.7":
   version "7.9.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.9.3.tgz#3028d0cc20ddc733166c6e9c8534559cee09f54a"
@@ -732,13 +613,6 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-regenerator@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.3.tgz#b31031e8059c07495bf23614c97f3d9698bc6ec8"
-  integrity sha512-qt/kcur/FxrQrzFR432FGZznkVAjiyFtCOANjkAKwCbt465L6ZCiUQh2oMYGU3Wo8LRFJxNDFwWn106S5wVUNA==
-  dependencies:
-    regenerator-transform "^0.14.0"
-
 "@babel/plugin-transform-regenerator@^7.8.7":
   version "7.8.7"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.7.tgz#5e46a0dca2bee1ad8285eb0527e6abc9c37672f8"
@@ -753,21 +627,21 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-shorthand-properties@^7.0.0", "@babel/plugin-transform-shorthand-properties@^7.8.3":
+"@babel/plugin-transform-shorthand-properties@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.8.3.tgz#28545216e023a832d4d3a1185ed492bcfeac08c8"
   integrity sha512-I9DI6Odg0JJwxCHzbzW08ggMdCezoWcuQRz3ptdudgwaHxTjxw5HgdFJmZIkIMlRymL6YiZcped4TTCB0JcC8w==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-spread@^7.0.0", "@babel/plugin-transform-spread@^7.8.3":
+"@babel/plugin-transform-spread@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.8.3.tgz#9c8ffe8170fdfb88b114ecb920b82fb6e95fe5e8"
   integrity sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-sticky-regex@^7.0.0", "@babel/plugin-transform-sticky-regex@^7.8.3":
+"@babel/plugin-transform-sticky-regex@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.8.3.tgz#be7a1290f81dae767475452199e1f76d6175b100"
   integrity sha512-9Spq0vGCD5Bb4Z/ZXXSK5wbbLFMG085qd2vhL1JYu1WcQ5bXqZBAYRzU1d+p79GcHs2szYv5pVQCX13QgldaWw==
@@ -775,7 +649,7 @@
     "@babel/helper-plugin-utils" "^7.8.3"
     "@babel/helper-regex" "^7.8.3"
 
-"@babel/plugin-transform-template-literals@^7.0.0", "@babel/plugin-transform-template-literals@^7.8.3":
+"@babel/plugin-transform-template-literals@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.8.3.tgz#7bfa4732b455ea6a43130adc0ba767ec0e402a80"
   integrity sha512-820QBtykIQOLFT8NZOcTRJ1UNuztIELe4p9DCgvj4NK+PwluSJ49we7s9FB1HIGNIYT7wFUJ0ar2QpCDj0escQ==
@@ -783,13 +657,6 @@
     "@babel/helper-annotate-as-pure" "^7.8.3"
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-typeof-symbol@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.3.tgz#5cffb216fb25c8c64ba6bf5f76ce49d3ab079f4d"
-  integrity sha512-3TrkKd4LPqm4jHs6nPtSDI/SV9Cm5PRJkHLUgTcqRQQTMAZ44ZaAdDZJtvWFSaRcvT0a1rTmJ5ZA5tDKjleF3g==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
-
 "@babel/plugin-transform-typeof-symbol@^7.8.4":
   version "7.8.4"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.4.tgz#ede4062315ce0aaf8a657a920858f1a2f35fc412"
@@ -797,7 +664,7 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-unicode-regex@^7.0.0", "@babel/plugin-transform-unicode-regex@^7.8.3":
+"@babel/plugin-transform-unicode-regex@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz#0cef36e3ba73e5c57273effb182f46b91a1ecaad"
   integrity sha512-+ufgJjYdmWfSQ+6NS9VGUR2ns8cjJjYbrbi11mZBTaWm+Fui/ncTLFF28Ei1okavY+xkojGr1eJxNsWYeA5aZw==
@@ -907,21 +774,6 @@
     "@babel/parser" "^7.8.3"
     "@babel/types" "^7.8.3"
 
-"@babel/traverse@^7.0.0", "@babel/traverse@^7.0.0-beta.42", "@babel/traverse@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.8.3.tgz#a826215b011c9b4f73f3a893afbc05151358bf9a"
-  integrity sha512-we+a2lti+eEImHmEXp7bM9cTxGzxPmBiVJlLVD+FuuQMeeO7RaDbutbgeheDkw+Xe3mCfJHnGOWLswT74m2IPg==
-  dependencies:
-    "@babel/code-frame" "^7.8.3"
-    "@babel/generator" "^7.8.3"
-    "@babel/helper-function-name" "^7.8.3"
-    "@babel/helper-split-export-declaration" "^7.8.3"
-    "@babel/parser" "^7.8.3"
-    "@babel/types" "^7.8.3"
-    debug "^4.1.0"
-    globals "^11.1.0"
-    lodash "^4.17.13"
-
 "@babel/traverse@^7.4.3", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0":
   version "7.9.0"
   resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.0.tgz#d3882c2830e513f4fe4cec9fe76ea1cc78747892"
@@ -937,14 +789,20 @@
     globals "^11.1.0"
     lodash "^4.17.13"
 
-"@babel/types@^7.0.0-beta.42", "@babel/types@^7.8.3":
+"@babel/traverse@^7.8.3":
   version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c"
-  integrity sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.8.3.tgz#a826215b011c9b4f73f3a893afbc05151358bf9a"
+  integrity sha512-we+a2lti+eEImHmEXp7bM9cTxGzxPmBiVJlLVD+FuuQMeeO7RaDbutbgeheDkw+Xe3mCfJHnGOWLswT74m2IPg==
   dependencies:
-    esutils "^2.0.2"
+    "@babel/code-frame" "^7.8.3"
+    "@babel/generator" "^7.8.3"
+    "@babel/helper-function-name" "^7.8.3"
+    "@babel/helper-split-export-declaration" "^7.8.3"
+    "@babel/parser" "^7.8.3"
+    "@babel/types" "^7.8.3"
+    debug "^4.1.0"
+    globals "^11.1.0"
     lodash "^4.17.13"
-    to-fast-properties "^2.0.0"
 
 "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.8.6", "@babel/types@^7.9.0":
   version "7.9.0"
@@ -955,6 +813,15 @@
     lodash "^4.17.13"
     to-fast-properties "^2.0.0"
 
+"@babel/types@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c"
+  integrity sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==
+  dependencies:
+    esutils "^2.0.2"
+    lodash "^4.17.13"
+    to-fast-properties "^2.0.0"
+
 "@open-wc/building-utils@^2.16.1":
   version "2.16.1"
   resolved "https://registry.yarnpkg.com/@open-wc/building-utils/-/building-utils-2.16.1.tgz#093d74881b996fe9497d628cdf55b6757422d894"
@@ -1002,11 +869,6 @@
     portfinder "^1.0.21"
     request "^2.88.0"
 
-"@polymer/esm-amd-loader@^1.0.0":
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/@polymer/esm-amd-loader/-/esm-amd-loader-1.0.4.tgz#4e77f2f59b29b01e0ad02aa83d33716cddc5f9f9"
-  integrity sha512-h+hqYkL+tQV/y2ESD5gFXMl5z4cC+XY1jTlBeGSBaTcj3VbB5OBEScbvRXm63NcEbBneQQYbHfBAXAkF9i9wIA==
-
 "@polymer/iron-test-helpers@^3.0.1":
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/@polymer/iron-test-helpers/-/iron-test-helpers-3.0.1.tgz#ec2b9c6567e2967a191b3d800a04b1167b2d1394"
@@ -1021,21 +883,6 @@
   dependencies:
     "@webcomponents/shadycss" "^1.9.1"
 
-"@polymer/sinonjs@^1.14.1":
-  version "1.17.1"
-  resolved "https://registry.yarnpkg.com/@polymer/sinonjs/-/sinonjs-1.17.1.tgz#e47d3785b7d0e8c29feb97f7e924b0fc597e2e9b"
-  integrity sha512-/U8F/cOTrbF2iVVYgINYmvKbtbexs+89Q3v8AaHADRYabTg7aOZGOb0RyWpOI+sUJt04kj63U4FwMhzW5r4wZA==
-
-"@polymer/test-fixture@^0.0.3":
-  version "0.0.3"
-  resolved "https://registry.yarnpkg.com/@polymer/test-fixture/-/test-fixture-0.0.3.tgz#4443752697d4d9293bbc412ea0b5e4d341f149d9"
-  integrity sha1-REN1JpfU2Sk7vEEuoLXk00HxSdk=
-
-"@polymer/test-fixture@^3.0.0-pre.1":
-  version "3.0.0-pre.21"
-  resolved "https://registry.yarnpkg.com/@polymer/test-fixture/-/test-fixture-3.0.0-pre.21.tgz#85152207cb0bf57caebc191c80bb0fdb6952614e"
-  integrity sha512-IxzUe6YzaORzUksafHAXHprV29YncOJgr0+1zNAifl0/f+cb5iAd4IWUrnsnVFHG5UGTLjvis5RgV6vvIZPDrA==
-
 "@polymer/test-fixture@^4.0.2":
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/@polymer/test-fixture/-/test-fixture-4.0.2.tgz#2f4777ecdcfb22ee000db35a05e0edf27c722c19"
@@ -1059,279 +906,52 @@
   dependencies:
     estree-walker "^1.0.1"
 
-"@types/babel-generator@^6.25.1":
-  version "6.25.3"
-  resolved "https://registry.yarnpkg.com/@types/babel-generator/-/babel-generator-6.25.3.tgz#8f06caa12d0595a0538560abe771966d77d29286"
-  integrity sha512-pGgnuxVddKcYIc+VJkRDop7gxLhqclNKBdlsm/5Vp8d+37pQkkDK7fef8d9YYImRzw9xcojEPc18pUYnbxmjqA==
+"@sinonjs/commons@^1", "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.7.2":
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d"
+  integrity sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q==
   dependencies:
-    "@types/babel-types" "*"
+    type-detect "4.0.8"
 
-"@types/babel-traverse@^6.25.2", "@types/babel-traverse@^6.25.3":
-  version "6.25.5"
-  resolved "https://registry.yarnpkg.com/@types/babel-traverse/-/babel-traverse-6.25.5.tgz#6d293cf7523e48b524faa7b86dc3c488191484e5"
-  integrity sha512-WrMbwmu+MWf8FiUMbmVOGkc7bHPzndUafn1CivMaBHthBBoo0VNIcYk1KV71UovYguhsNOwf3UF5oRmkkGOU3w==
+"@sinonjs/fake-timers@^6.0.0", "@sinonjs/fake-timers@^6.0.1":
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40"
+  integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==
   dependencies:
-    "@types/babel-types" "*"
+    "@sinonjs/commons" "^1.7.0"
 
-"@types/babel-types@*":
-  version "7.0.7"
-  resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.7.tgz#667eb1640e8039436028055737d2b9986ee336e3"
-  integrity sha512-dBtBbrc+qTHy1WdfHYjBwRln4+LWqASWakLHsWHR2NWHIFkv4W3O070IGoGLEBrJBvct3r0L1BUPuvURi7kYUQ==
-
-"@types/babel-types@^6.25.1":
-  version "6.25.2"
-  resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-6.25.2.tgz#5c57f45973e4f13742dbc5273dd84cffe7373a9e"
-  integrity sha512-+3bMuktcY4a70a0KZc8aPJlEOArPuAKQYHU5ErjkOqGJdx8xuEEVK6nWogqigBOJ8nKPxRpyCUDTCPmZ3bUxGA==
-
-"@types/babylon@^6.16.2":
-  version "6.16.5"
-  resolved "https://registry.yarnpkg.com/@types/babylon/-/babylon-6.16.5.tgz#1c5641db69eb8cdf378edd25b4be7754beeb48b4"
-  integrity sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w==
+"@sinonjs/formatio@^5.0.1":
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-5.0.1.tgz#f13e713cb3313b1ab965901b01b0828ea6b77089"
+  integrity sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==
   dependencies:
-    "@types/babel-types" "*"
+    "@sinonjs/commons" "^1"
+    "@sinonjs/samsam" "^5.0.2"
 
-"@types/bluebird@*":
-  version "3.5.29"
-  resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.29.tgz#7cd933c902c4fc83046517a1bef973886d00bdb6"
-  integrity sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw==
-
-"@types/body-parser@*":
-  version "1.17.1"
-  resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.1.tgz#18fcf61768fb5c30ccc508c21d6fd2e8b3bf7897"
-  integrity sha512-RoX2EZjMiFMjZh9lmYrwgoP9RTpAjSHiJxdp4oidAQVO02T7HER3xj9UKue5534ULWeqVEkujhWcyvUce+d68w==
+"@sinonjs/samsam@^5.0.2", "@sinonjs/samsam@^5.0.3":
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-5.0.3.tgz#86f21bdb3d52480faf0892a480c9906aa5a52938"
+  integrity sha512-QucHkc2uMJ0pFGjJUDP3F9dq5dx8QIaqISl9QgwLOh6P9yv877uONPGXh/OH/0zmM3tW1JjuJltAZV2l7zU+uQ==
   dependencies:
-    "@types/connect" "*"
-    "@types/node" "*"
+    "@sinonjs/commons" "^1.6.0"
+    lodash.get "^4.4.2"
+    type-detect "^4.0.8"
 
-"@types/chai-subset@^1.3.0":
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94"
-  integrity sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==
-  dependencies:
-    "@types/chai" "*"
+"@sinonjs/text-encoding@^0.7.1":
+  version "0.7.1"
+  resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5"
+  integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==
 
-"@types/chai@*":
-  version "4.2.7"
-  resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.7.tgz#1c8c25cbf6e59ffa7d6b9652c78e547d9a41692d"
-  integrity sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g==
-
-"@types/chalk@^0.4.30":
-  version "0.4.31"
-  resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-0.4.31.tgz#a31d74241a6b1edbb973cf36d97a2896834a51f9"
-  integrity sha1-ox10JBprHtu5c8822XooloNKUfk=
-
-"@types/clean-css@*":
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/@types/clean-css/-/clean-css-4.2.1.tgz#cb0134241ec5e6ede1b5344bc829668fd9871a8d"
-  integrity sha512-A1HQhQ0hkvqqByJMgg+Wiv9p9XdoYEzuwm11SVo1mX2/4PSdhjcrUlilJQoqLscIheC51t1D5g+EFWCXZ2VTQQ==
-  dependencies:
-    "@types/node" "*"
-
-"@types/clone@^0.1.30":
-  version "0.1.30"
-  resolved "https://registry.yarnpkg.com/@types/clone/-/clone-0.1.30.tgz#e7365648c1b42136a59c7d5040637b3b5c83b614"
-  integrity sha1-5zZWSMG0ITalnH1QQGN7O1yDthQ=
-
-"@types/compression@^0.0.33":
-  version "0.0.33"
-  resolved "https://registry.yarnpkg.com/@types/compression/-/compression-0.0.33.tgz#95dc733a2339aa846381d7f1377792d2553dc27d"
-  integrity sha1-ldxzOiM5qoRjgdfxN3eS0lU9wn0=
-  dependencies:
-    "@types/express" "*"
-
-"@types/connect@*":
-  version "3.4.33"
-  resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546"
-  integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==
-  dependencies:
-    "@types/node" "*"
-
-"@types/content-type@^1.1.0":
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/@types/content-type/-/content-type-1.1.3.tgz#3688bd77fc12f935548eef102a4e34c512b03a07"
-  integrity sha512-pv8VcFrZ3fN93L4rTNIbbUzdkzjEyVMp5mPVjsFfOYTDOZMZiZ8P1dhu+kEv3faYyKzZgLlSvnyQNFg+p/v5ug==
-
-"@types/cssbeautify@^0.3.1":
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/@types/cssbeautify/-/cssbeautify-0.3.1.tgz#8e0bee8f7decb952250da0caebe05e30591c17ef"
-  integrity sha1-jgvuj33suVIlDaDK6+BeMFkcF+8=
-
-"@types/doctrine@^0.0.1":
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/@types/doctrine/-/doctrine-0.0.1.tgz#b999f2d9f7b43cabe0a1a2f39bc203bc7dcada9d"
-  integrity sha1-uZny2fe0PKvgoaLzm8IDvH3K2p0=
-
-"@types/escape-html@0.0.20":
-  version "0.0.20"
-  resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-0.0.20.tgz#cae698714dd61ebee5ab3f2aeb9a34ba1011735a"
-  integrity sha512-6dhZJLbA7aOwkYB2GDGdIqJ20wmHnkDzaxV9PJXe7O02I2dSFTERzRB6JrX6cWKaS+VqhhY7cQUMCbO5kloFUw==
-
-"@types/estree@*":
-  version "0.0.42"
-  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.42.tgz#8d0c1f480339efedb3e46070e22dd63e0430dd11"
-  integrity sha512-K1DPVvnBCPxzD+G51/cxVIoc2X8uUVl1zpJeE6iKcgHMj4+tbat5Xu4TjV7v2QSDbIeAfLi2hIk+u2+s0MlpUQ==
-
-"@types/events@*":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
-  integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
-
-"@types/expect@^1.20.4":
-  version "1.20.4"
-  resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5"
-  integrity sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==
-
-"@types/express-serve-static-core@*":
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.2.tgz#f6f41fa35d42e79dbf6610eccbb2637e6008a0cf"
-  integrity sha512-El9yMpctM6tORDAiBwZVLMcxoTMcqqRO9dVyYcn7ycLWbvR8klrDn8CAOwRfZujZtWD7yS/mshTdz43jMOejbg==
-  dependencies:
-    "@types/node" "*"
-    "@types/range-parser" "*"
-
-"@types/express@*", "@types/express@^4.0.30", "@types/express@^4.0.36":
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.2.tgz#a0fb7a23d8855bac31bc01d5a58cadd9b2173e6c"
-  integrity sha512-5mHFNyavtLoJmnusB8OKJ5bshSzw+qkMIBAobLrIM48HJvunFva9mOa6aBwh64lBFyNwBbs0xiEFuj4eU/NjCA==
-  dependencies:
-    "@types/body-parser" "*"
-    "@types/express-serve-static-core" "*"
-    "@types/serve-static" "*"
-
-"@types/freeport@^1.0.19":
-  version "1.0.21"
-  resolved "https://registry.yarnpkg.com/@types/freeport/-/freeport-1.0.21.tgz#73f6543ed67d3ca3fff97b985591598b7092066f"
-  integrity sha1-c/ZUPtZ9PKP/+XuYVZFZi3CSBm8=
-
-"@types/glob-stream@*":
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/@types/glob-stream/-/glob-stream-6.1.0.tgz#7ede8a33e59140534f8d8adfb8ac9edfb31897bc"
-  integrity sha512-RHv6ZQjcTncXo3thYZrsbAVwoy4vSKosSWhuhuQxLOTv74OJuFQxXkmUuZCr3q9uNBEVCvIzmZL/FeRNbHZGUg==
-  dependencies:
-    "@types/glob" "*"
-    "@types/node" "*"
-
-"@types/glob@*":
-  version "7.1.1"
-  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
-  integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==
-  dependencies:
-    "@types/events" "*"
-    "@types/minimatch" "*"
-    "@types/node" "*"
-
-"@types/gulp-if@0.0.33":
-  version "0.0.33"
-  resolved "https://registry.yarnpkg.com/@types/gulp-if/-/gulp-if-0.0.33.tgz#edece22b7925d9a6db5f9c8c0d7882aa776fb678"
-  integrity sha512-J5lzff21X7r1x/4hSzn02GgIUEyjCqYIXZ9GgGBLhbsD3RiBdqwnkFWgF16/0jO5rcVZ52Zp+6MQMQdvIsWuKg==
-  dependencies:
-    "@types/node" "*"
-    "@types/vinyl" "*"
-
-"@types/html-minifier@^3.5.1":
-  version "3.5.3"
-  resolved "https://registry.yarnpkg.com/@types/html-minifier/-/html-minifier-3.5.3.tgz#5276845138db2cebc54c789e0aaf87621a21e84f"
-  integrity sha512-j1P/4PcWVVCPEy5lofcHnQ6BtXz9tHGiFPWzqm7TtGuWZEfCHEP446HlkSNc9fQgNJaJZ6ewPtp2aaFla/Uerg==
-  dependencies:
-    "@types/clean-css" "*"
-    "@types/relateurl" "*"
-    "@types/uglify-js" "*"
-
-"@types/is-windows@^0.2.0":
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/@types/is-windows/-/is-windows-0.2.0.tgz#6f24ee48731d31168ea510610d6dd15e5fc9c6ff"
-  integrity sha1-byTuSHMdMRaOpRBhDW3RXl/Jxv8=
-
-"@types/launchpad@^0.6.0":
-  version "0.6.0"
-  resolved "https://registry.yarnpkg.com/@types/launchpad/-/launchpad-0.6.0.tgz#37296109b7f277f6e6c5fd7e0c0706bc918fbb51"
-  integrity sha1-NylhCbfyd/bmxf1+DAcGvJGPu1E=
-
-"@types/mime@*", "@types/mime@^2.0.0":
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d"
-  integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==
-
-"@types/minimatch@*", "@types/minimatch@^3.0.1", "@types/minimatch@^3.0.3":
+"@types/minimatch@^3.0.3":
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
   integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
 
-"@types/mz@0.0.29":
-  version "0.0.29"
-  resolved "https://registry.yarnpkg.com/@types/mz/-/mz-0.0.29.tgz#bc24728c649973f1c7851e9033f9ce525668c27b"
-  integrity sha1-vCRyjGSZc/HHhR6QM/nOUlZowns=
-  dependencies:
-    "@types/bluebird" "*"
-    "@types/node" "*"
-
-"@types/mz@0.0.31":
-  version "0.0.31"
-  resolved "https://registry.yarnpkg.com/@types/mz/-/mz-0.0.31.tgz#a4d80c082fefe71e40a7c0f07d1e6555bbbc7b52"
-  integrity sha1-pNgMCC/v5x5Ap8DwfR5lVbu8e1I=
-  dependencies:
-    "@types/node" "*"
-
 "@types/node@*":
   version "13.1.8"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-13.1.8.tgz#1d590429fe8187a02707720ecf38a6fe46ce294b"
   integrity sha512-6XzyyNM9EKQW4HKuzbo/CkOIjn/evtCmsU+MUM1xDfJ+3/rNjBttM1NgN7AOQvN6tP1Sl1D1PIKMreTArnxM9A==
 
-"@types/node@^4.0.30":
-  version "4.9.4"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-4.9.4.tgz#75ef91733afaa856b01e12da6ecf48aa9d5e221f"
-  integrity sha512-nKoiCZ87x6+fs26bNHjy07AQt6f46nFEitGH0P9JmWbY6tEyum6LLfLf7SIsKFh4DnBWsyUM2gYhaQAt+aA0Sw==
-
-"@types/opn@^3.0.28":
-  version "3.0.28"
-  resolved "https://registry.yarnpkg.com/@types/opn/-/opn-3.0.28.tgz#097d0d1c9b5749573a5d96df132387bb6d02118a"
-  integrity sha1-CX0NHJtXSVc6XZbfEyOHu20CEYo=
-  dependencies:
-    "@types/node" "*"
-
-"@types/parse5@^2.2.34":
-  version "2.2.34"
-  resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-2.2.34.tgz#e3870a10e82735a720f62d71dcd183ba78ef3a9d"
-  integrity sha1-44cKEOgnNacg9i1x3NGDunjvOp0=
-  dependencies:
-    "@types/node" "*"
-
-"@types/path-is-inside@^1.0.0":
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/@types/path-is-inside/-/path-is-inside-1.0.0.tgz#02d6ff38975d684bdec96204494baf9f29f0e17f"
-  integrity sha512-hfnXRGugz+McgX2jxyy5qz9sB21LRzlGn24zlwN2KEgoPtEvjzNRrLtUkOOebPDPZl3Rq7ywKxYvylVcEZDnEw==
-
-"@types/pem@^1.8.1":
-  version "1.9.5"
-  resolved "https://registry.yarnpkg.com/@types/pem/-/pem-1.9.5.tgz#cd5548b5e0acb4b41a9e21067e9fcd8c57089c99"
-  integrity sha512-C0txxEw8B7DCoD85Ko7SEvzUogNd5VDJ5/YBG8XUcacsOGqxr5Oo4g3OUAfdEDUbhXanwUoVh/ZkMFw77FGPQQ==
-  dependencies:
-    "@types/node" "*"
-
-"@types/range-parser@*":
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
-  integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
-
-"@types/relateurl@*":
-  version "0.2.28"
-  resolved "https://registry.yarnpkg.com/@types/relateurl/-/relateurl-0.2.28.tgz#6bda7db8653fa62643f5ee69e9f69c11a392e3a6"
-  integrity sha1-a9p9uGU/piZD9e5p6facEaOS46Y=
-
-"@types/resolve@0.0.6":
-  version "0.0.6"
-  resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.6.tgz#0bd2f236c2e1cebb98b79885df57edd71a8d770e"
-  integrity sha512-g+Rg8uMWY76oYTyaL+m7ZcblqF/oj7pE6uEUyACluJx4zcop1Lk14qQiocdEkEVMDFm6DmKpxJhsER+ZuTwG3g==
-  dependencies:
-    "@types/node" "*"
-
-"@types/resolve@0.0.7":
-  version "0.0.7"
-  resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.7.tgz#b299c13be8d712b1b502fb14a084252acef84f4d"
-  integrity sha512-GPewdjkb0Q76o459qgp6pBLzJj/bD3oveS2kfLhIkZ9U3t3AFKtl5DlFB6lGTw0iZmcmxoGC8lpLW3NNJKrN9A==
-  dependencies:
-    "@types/node" "*"
-
 "@types/resolve@0.0.8":
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194"
@@ -1339,69 +959,6 @@
   dependencies:
     "@types/node" "*"
 
-"@types/serve-static@*", "@types/serve-static@^1.7.31":
-  version "1.13.3"
-  resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1"
-  integrity sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==
-  dependencies:
-    "@types/express-serve-static-core" "*"
-    "@types/mime" "*"
-
-"@types/spdy@^3.4.1":
-  version "3.4.4"
-  resolved "https://registry.yarnpkg.com/@types/spdy/-/spdy-3.4.4.tgz#3282fd4ad8c4603aa49f7017dd520a08a345b2bc"
-  integrity sha512-N9LBlbVRRYq6HgYpPkqQc3a9HJ/iEtVZToW6xlTtJiMhmRJ7jJdV7TaZQJw/Ve/1ePUsQiCTDc4JMuzzag94GA==
-  dependencies:
-    "@types/node" "*"
-
-"@types/ua-parser-js@^0.7.31":
-  version "0.7.33"
-  resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.33.tgz#4a92089511574e12928a7cb6b99a01831acd1dd7"
-  integrity sha512-ngUKcHnytUodUCL7C6EZ+lVXUjTMQb+9p/e1JjV5tN9TVzS98lHozWEFRPY1QcCdwFeMsmVWfZ3DPPT/udCyIw==
-
-"@types/uglify-js@*":
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082"
-  integrity sha512-SudIN9TRJ+v8g5pTG8RRCqfqTMNqgWCKKd3vtynhGzkIIjxaicNAMuY5TRadJ6tzDu3Dotf3ngaMILtmOdmWEQ==
-  dependencies:
-    source-map "^0.6.1"
-
-"@types/uuid@^3.4.3":
-  version "3.4.6"
-  resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.6.tgz#d2c4c48eb85a757bf2927f75f939942d521e3016"
-  integrity sha512-cCdlC/1kGEZdEglzOieLDYBxHsvEOIg7kp/2FYyVR9Pxakq+Qf/inL3RKQ+PA8gOlI/NnL+fXmQH12nwcGzsHw==
-  dependencies:
-    "@types/node" "*"
-
-"@types/vinyl-fs@^2.4.8":
-  version "2.4.11"
-  resolved "https://registry.yarnpkg.com/@types/vinyl-fs/-/vinyl-fs-2.4.11.tgz#b98119b8bb2494141eaf649b09fbfeb311161206"
-  integrity sha512-2OzQSfIr9CqqWMGqmcERE6Hnd2KY3eBVtFaulVo3sJghplUcaeMdL9ZjEiljcQQeHjheWY9RlNmumjIAvsBNaA==
-  dependencies:
-    "@types/glob-stream" "*"
-    "@types/node" "*"
-    "@types/vinyl" "*"
-
-"@types/vinyl@*", "@types/vinyl@^2.0.0":
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.4.tgz#9a7a8071c8d14d3a95d41ebe7135babe4ad5995a"
-  integrity sha512-2o6a2ixaVI2EbwBPg1QYLGQoHK56p/8X/sGfKbFC8N6sY9lfjsMf/GprtkQkSya0D4uRiutRZ2BWj7k3JvLsAQ==
-  dependencies:
-    "@types/expect" "^1.20.4"
-    "@types/node" "*"
-
-"@types/whatwg-url@^6.4.0":
-  version "6.4.0"
-  resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-6.4.0.tgz#1e59b8c64bc0dbdf66d037cf8449d1c3d5270237"
-  integrity sha512-tonhlcbQ2eho09am6RHnHOgvtDfDYINd5rgxD+2YSkKENooVCFsWizJz139MQW/PV8FfClyKrNe9ZbdHrSCxGg==
-  dependencies:
-    "@types/node" "*"
-
-"@types/which@^1.3.1":
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/@types/which/-/which-1.3.2.tgz#9c246fc0c93ded311c8512df2891fb41f6227fdf"
-  integrity sha512-8oDqyLC7eD4HM307boe2QWKyuzdzWBj56xI/imSl2cpL+U3tCMaTAkMJ4ee5JBZ/FsOJlvRGeIShiZDAl1qERA==
-
 "@webcomponents/shadycss@^1.9.1":
   version "1.9.4"
   resolved "https://registry.yarnpkg.com/@webcomponents/shadycss/-/shadycss-1.9.4.tgz#4f9d8ea1526bab084c60b53d4854dc39fdb2bb48"
@@ -1412,16 +969,6 @@
   resolved "https://registry.yarnpkg.com/@webcomponents/shadycss/-/shadycss-1.9.6.tgz#a8c5db867e49200a05cf8d5008029c09b7861979"
   integrity sha512-5fFjvP0jQJZoXK6YzYeYcIDGJ5oEsdjr1L9VaYLw5yxNd4aRz4srMpwCwldeNG0A6Hvr9igbG7fCsBeiiCXd7A==
 
-"@webcomponents/webcomponentsjs@^1.0.7":
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-1.3.3.tgz#5bb82a0d3210c836bd4623e13a4a93145cb9dc27"
-  integrity sha512-eLH04VBMpuZGzBIhOnUjECcQPEPcmfhWEijW9u1B5I+2PPYdWf3vWUExdDxu4Y3GljRSTCOlWnGtS9tpzmXMyQ==
-
-"@webcomponents/webcomponentsjs@^2.0.0":
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.4.1.tgz#7baadec56ed2fd79b94ddfd509132d8c0c295c5c"
-  integrity sha512-7jxBb+KoWncKb/JGFyTY40PjV4yRx2zd35ZLuvRP+6WndJDL7X32ZIZ7bN3sSQIl+NzJkCo7chfXJyzn+6WZaQ==
-
 "@webcomponents/webcomponentsjs@^2.4.0":
   version "2.4.3"
   resolved "https://registry.yarnpkg.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.4.3.tgz#384f4f6d54563ba465fb4df21fe89e78a76fc530"
@@ -1432,7 +979,7 @@
   resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.4.0.tgz#0d5eb58e522a461774af8086414f68e1dda7a6c4"
   integrity sha512-3ZFfCRfDzx3GFjO6RAkYx81lPGpUS20ISxux9gLxuKnqafNcFQo59+IoZqpO2WvQlyc287B62HDnDdNYRmlvWA==
 
-accepts@^1.3.5, accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
+accepts@^1.3.5, accepts@~1.3.4:
   version "1.3.7"
   resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
   integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
@@ -1445,45 +992,11 @@
   resolved "https://registry.yarnpkg.com/accessibility-developer-tools/-/accessibility-developer-tools-2.12.0.tgz#3da0cce9d6ec6373964b84f35db7cfc3df7ab514"
   integrity sha1-PaDM6dbsY3OWS4TzXbfPw996tRQ=
 
-acorn-jsx@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
-  integrity sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=
-  dependencies:
-    acorn "^3.0.4"
-
-acorn@^3.0.4:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
-  integrity sha1-ReN/s56No/JbruP/U2niu18iAXo=
-
-acorn@^5.5.0:
-  version "5.7.3"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
-  integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
-
-acorn@^7.1.0:
-  version "7.1.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c"
-  integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==
-
-adm-zip@~0.4.3:
-  version "0.4.13"
-  resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.13.tgz#597e2f8cc3672151e1307d3e95cddbc75672314a"
-  integrity sha512-fERNJX8sOXfel6qCBCMPvZLzENBEhZTzKqg6vrOW5pvoEaQuJhRU4ndTAh6lHOxn1I6jnz2NHra56ZODM751uw==
-
 after@0.8.2:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
   integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=
 
-agent-base@^4.3.0:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
-  integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==
-  dependencies:
-    es6-promisify "^5.0.0"
-
 ajv@^6.5.5:
   version "6.11.0"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.11.0.tgz#c3607cbc8ae392d8a5a536f25b21f8e5f3f87fe9"
@@ -1494,23 +1007,11 @@
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
-ansi-align@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f"
-  integrity sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=
-  dependencies:
-    string-width "^2.0.0"
-
 ansi-colors@3.2.3:
   version "3.2.3"
   resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813"
   integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==
 
-ansi-regex@^2.0.0:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
-  integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
-
 ansi-regex@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
@@ -1521,11 +1022,6 @@
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
   integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
 
-ansi-styles@^2.2.1:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
-  integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=
-
 ansi-styles@^3.2.0, ansi-styles@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
@@ -1533,11 +1029,6 @@
   dependencies:
     color-convert "^1.9.0"
 
-ansi-styles@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178"
-  integrity sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=
-
 any-promise@^1.0.0, any-promise@^1.1.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
@@ -1551,40 +1042,6 @@
     normalize-path "^3.0.0"
     picomatch "^2.0.4"
 
-append-field@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56"
-  integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=
-
-archiver-utils@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2"
-  integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==
-  dependencies:
-    glob "^7.1.4"
-    graceful-fs "^4.2.0"
-    lazystream "^1.0.0"
-    lodash.defaults "^4.2.0"
-    lodash.difference "^4.5.0"
-    lodash.flatten "^4.4.0"
-    lodash.isplainobject "^4.0.6"
-    lodash.union "^4.6.0"
-    normalize-path "^3.0.0"
-    readable-stream "^2.0.0"
-
-archiver@^3.0.0:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/archiver/-/archiver-3.1.1.tgz#9db7819d4daf60aec10fe86b16cb9258ced66ea0"
-  integrity sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg==
-  dependencies:
-    archiver-utils "^2.1.0"
-    async "^2.6.3"
-    buffer-crc32 "^0.2.1"
-    glob "^7.1.4"
-    readable-stream "^3.4.0"
-    tar-stream "^2.1.0"
-    zip-stream "^2.1.2"
-
 argparse@^1.0.7:
   version "1.0.10"
   resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@@ -1592,35 +1049,6 @@
   dependencies:
     sprintf-js "~1.0.2"
 
-arr-diff@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
-  integrity sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=
-  dependencies:
-    arr-flatten "^1.0.1"
-
-arr-diff@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
-  integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=
-
-arr-flatten@^1.0.1, arr-flatten@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
-  integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
-
-arr-union@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
-  integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
-
-array-back@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/array-back/-/array-back-2.0.0.tgz#6877471d51ecc9c9bfa6136fb6c7d5fe69748022"
-  integrity sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==
-  dependencies:
-    typical "^2.6.1"
-
 array-back@^3.0.1:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0"
@@ -1631,26 +1059,6 @@
   resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.1.tgz#9b80312935a52062e1a233a9c7abeb5481b30e90"
   integrity sha512-Z/JnaVEXv+A9xabHzN43FiiiWEE7gPCRXMrVmRm00tWbjZRul1iHm7ECzlyNq1p4a4ATXz+G9FJ3GqGOkOV3fg==
 
-array-find-index@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
-  integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=
-
-array-flatten@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
-  integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
-
-array-unique@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53"
-  integrity sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=
-
-array-unique@^0.3.2:
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
-  integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
-
 arraybuffer.slice@~0.0.7:
   version "0.0.7"
   resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675"
@@ -1673,48 +1081,28 @@
   resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
   integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
 
-assertion-error@^1.0.1, assertion-error@^1.1.0:
+assertion-error@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
   integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==
 
-assign-symbols@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
-  integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
-
 async-limiter@~1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
   integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
 
-async@^1.5.2:
-  version "1.5.2"
-  resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
-  integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=
-
-async@^2.0.0, async@^2.0.1, async@^2.1.2, async@^2.4.1, async@^2.6.1, async@^2.6.2, async@^2.6.3:
+async@^2.6.2:
   version "2.6.3"
   resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
   integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
   dependencies:
     lodash "^4.17.14"
 
-async@~0.2.9:
-  version "0.2.10"
-  resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
-  integrity sha1-trvgsGdLnXGXCMo43owjfLUmw9E=
-
 asynckit@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
   integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
 
-atob@^2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
-  integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
-
 aws-sign2@~0.7.0:
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@@ -1725,71 +1113,6 @@
   resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
   integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
 
-babel-code-frame@^6.26.0:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
-  integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=
-  dependencies:
-    chalk "^1.1.3"
-    esutils "^2.0.2"
-    js-tokens "^3.0.2"
-
-babel-generator@^6.26.1:
-  version "6.26.1"
-  resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90"
-  integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==
-  dependencies:
-    babel-messages "^6.23.0"
-    babel-runtime "^6.26.0"
-    babel-types "^6.26.0"
-    detect-indent "^4.0.0"
-    jsesc "^1.3.0"
-    lodash "^4.17.4"
-    source-map "^0.5.7"
-    trim-right "^1.0.1"
-
-babel-helper-evaluate-path@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/babel-helper-evaluate-path/-/babel-helper-evaluate-path-0.5.0.tgz#a62fa9c4e64ff7ea5cea9353174ef023a900a67c"
-  integrity sha512-mUh0UhS607bGh5wUMAQfOpt2JX2ThXMtppHRdRU1kL7ZLRWIXxoV2UIV1r2cAeeNeU1M5SB5/RSUgUxrK8yOkA==
-
-babel-helper-flip-expressions@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/babel-helper-flip-expressions/-/babel-helper-flip-expressions-0.4.3.tgz#3696736a128ac18bc25254b5f40a22ceb3c1d3fd"
-  integrity sha1-NpZzahKKwYvCUlS19AoizrPB0/0=
-
-babel-helper-is-nodes-equiv@^0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/babel-helper-is-nodes-equiv/-/babel-helper-is-nodes-equiv-0.0.1.tgz#34e9b300b1479ddd98ec77ea0bbe9342dfe39684"
-  integrity sha1-NOmzALFHnd2Y7HfqC76TQt/jloQ=
-
-babel-helper-is-void-0@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/babel-helper-is-void-0/-/babel-helper-is-void-0-0.4.3.tgz#7d9c01b4561e7b95dbda0f6eee48f5b60e67313e"
-  integrity sha1-fZwBtFYee5Xb2g9u7kj1tg5nMT4=
-
-babel-helper-mark-eval-scopes@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/babel-helper-mark-eval-scopes/-/babel-helper-mark-eval-scopes-0.4.3.tgz#d244a3bef9844872603ffb46e22ce8acdf551562"
-  integrity sha1-0kSjvvmESHJgP/tG4izorN9VFWI=
-
-babel-helper-remove-or-void@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/babel-helper-remove-or-void/-/babel-helper-remove-or-void-0.4.3.tgz#a4f03b40077a0ffe88e45d07010dee241ff5ae60"
-  integrity sha1-pPA7QAd6D/6I5F0HAQ3uJB/1rmA=
-
-babel-helper-to-multiple-sequence-expressions@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/babel-helper-to-multiple-sequence-expressions/-/babel-helper-to-multiple-sequence-expressions-0.5.0.tgz#a3f924e3561882d42fcf48907aa98f7979a4588d"
-  integrity sha512-m2CvfDW4+1qfDdsrtf4dwOslQC3yhbgyBFptncp4wvtdrDHqueW7slsYv4gArie056phvQFhT2nRcGS4bnm6mA==
-
-babel-messages@^6.23.0:
-  version "6.23.0"
-  resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e"
-  integrity sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=
-  dependencies:
-    babel-runtime "^6.22.0"
-
 babel-plugin-dynamic-import-node@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f"
@@ -1807,213 +1130,6 @@
     istanbul-lib-instrument "^3.3.0"
     test-exclude "^5.2.3"
 
-babel-plugin-minify-builtins@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-builtins/-/babel-plugin-minify-builtins-0.5.0.tgz#31eb82ed1a0d0efdc31312f93b6e4741ce82c36b"
-  integrity sha512-wpqbN7Ov5hsNwGdzuzvFcjgRlzbIeVv1gMIlICbPj0xkexnfoIDe7q+AZHMkQmAE/F9R5jkrB6TLfTegImlXag==
-
-babel-plugin-minify-constant-folding@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-constant-folding/-/babel-plugin-minify-constant-folding-0.5.0.tgz#f84bc8dbf6a561e5e350ff95ae216b0ad5515b6e"
-  integrity sha512-Vj97CTn/lE9hR1D+jKUeHfNy+m1baNiJ1wJvoGyOBUx7F7kJqDZxr9nCHjO/Ad+irbR3HzR6jABpSSA29QsrXQ==
-  dependencies:
-    babel-helper-evaluate-path "^0.5.0"
-
-babel-plugin-minify-dead-code-elimination@^0.5.1:
-  version "0.5.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-dead-code-elimination/-/babel-plugin-minify-dead-code-elimination-0.5.1.tgz#1a0c68e44be30de4976ca69ffc535e08be13683f"
-  integrity sha512-x8OJOZIrRmQBcSqxBcLbMIK8uPmTvNWPXH2bh5MDCW1latEqYiRMuUkPImKcfpo59pTUB2FT7HfcgtG8ZlR5Qg==
-  dependencies:
-    babel-helper-evaluate-path "^0.5.0"
-    babel-helper-mark-eval-scopes "^0.4.3"
-    babel-helper-remove-or-void "^0.4.3"
-    lodash "^4.17.11"
-
-babel-plugin-minify-flip-comparisons@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-flip-comparisons/-/babel-plugin-minify-flip-comparisons-0.4.3.tgz#00ca870cb8f13b45c038b3c1ebc0f227293c965a"
-  integrity sha1-AMqHDLjxO0XAOLPB68DyJyk8llo=
-  dependencies:
-    babel-helper-is-void-0 "^0.4.3"
-
-babel-plugin-minify-guarded-expressions@^0.4.3, babel-plugin-minify-guarded-expressions@^0.4.4:
-  version "0.4.4"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-guarded-expressions/-/babel-plugin-minify-guarded-expressions-0.4.4.tgz#818960f64cc08aee9d6c75bec6da974c4d621135"
-  integrity sha512-RMv0tM72YuPPfLT9QLr3ix9nwUIq+sHT6z8Iu3sLbqldzC1Dls8DPCywzUIzkTx9Zh1hWX4q/m9BPoPed9GOfA==
-  dependencies:
-    babel-helper-evaluate-path "^0.5.0"
-    babel-helper-flip-expressions "^0.4.3"
-
-babel-plugin-minify-infinity@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-infinity/-/babel-plugin-minify-infinity-0.4.3.tgz#dfb876a1b08a06576384ef3f92e653ba607b39ca"
-  integrity sha1-37h2obCKBldjhO8/kuZTumB7Oco=
-
-babel-plugin-minify-mangle-names@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-mangle-names/-/babel-plugin-minify-mangle-names-0.5.0.tgz#bcddb507c91d2c99e138bd6b17a19c3c271e3fd3"
-  integrity sha512-3jdNv6hCAw6fsX1p2wBGPfWuK69sfOjfd3zjUXkbq8McbohWy23tpXfy5RnToYWggvqzuMOwlId1PhyHOfgnGw==
-  dependencies:
-    babel-helper-mark-eval-scopes "^0.4.3"
-
-babel-plugin-minify-numeric-literals@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-numeric-literals/-/babel-plugin-minify-numeric-literals-0.4.3.tgz#8e4fd561c79f7801286ff60e8c5fd9deee93c0bc"
-  integrity sha1-jk/VYcefeAEob/YOjF/Z3u6TwLw=
-
-babel-plugin-minify-replace@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-replace/-/babel-plugin-minify-replace-0.5.0.tgz#d3e2c9946c9096c070efc96761ce288ec5c3f71c"
-  integrity sha512-aXZiaqWDNUbyNNNpWs/8NyST+oU7QTpK7J9zFEFSA0eOmtUNMU3fczlTTTlnCxHmq/jYNFEmkkSG3DDBtW3Y4Q==
-
-babel-plugin-minify-simplify@^0.5.1:
-  version "0.5.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-simplify/-/babel-plugin-minify-simplify-0.5.1.tgz#f21613c8b95af3450a2ca71502fdbd91793c8d6a"
-  integrity sha512-OSYDSnoCxP2cYDMk9gxNAed6uJDiDz65zgL6h8d3tm8qXIagWGMLWhqysT6DY3Vs7Fgq7YUDcjOomhVUb+xX6A==
-  dependencies:
-    babel-helper-evaluate-path "^0.5.0"
-    babel-helper-flip-expressions "^0.4.3"
-    babel-helper-is-nodes-equiv "^0.0.1"
-    babel-helper-to-multiple-sequence-expressions "^0.5.0"
-
-babel-plugin-minify-type-constructors@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-type-constructors/-/babel-plugin-minify-type-constructors-0.4.3.tgz#1bc6f15b87f7ab1085d42b330b717657a2156500"
-  integrity sha1-G8bxW4f3qxCF1CszC3F2V6IVZQA=
-  dependencies:
-    babel-helper-is-void-0 "^0.4.3"
-
-babel-plugin-transform-inline-consecutive-adds@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-inline-consecutive-adds/-/babel-plugin-transform-inline-consecutive-adds-0.4.3.tgz#323d47a3ea63a83a7ac3c811ae8e6941faf2b0d1"
-  integrity sha1-Mj1Ho+pjqDp6w8gRro5pQfrysNE=
-
-babel-plugin-transform-member-expression-literals@^6.9.4:
-  version "6.9.4"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-member-expression-literals/-/babel-plugin-transform-member-expression-literals-6.9.4.tgz#37039c9a0c3313a39495faac2ff3a6b5b9d038bf"
-  integrity sha1-NwOcmgwzE6OUlfqsL/OmtbnQOL8=
-
-babel-plugin-transform-merge-sibling-variables@^6.9.4:
-  version "6.9.4"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-merge-sibling-variables/-/babel-plugin-transform-merge-sibling-variables-6.9.4.tgz#85b422fc3377b449c9d1cde44087203532401dae"
-  integrity sha1-hbQi/DN3tEnJ0c3kQIcgNTJAHa4=
-
-babel-plugin-transform-minify-booleans@^6.9.4:
-  version "6.9.4"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-minify-booleans/-/babel-plugin-transform-minify-booleans-6.9.4.tgz#acbb3e56a3555dd23928e4b582d285162dd2b198"
-  integrity sha1-rLs+VqNVXdI5KOS1gtKFFi3SsZg=
-
-babel-plugin-transform-property-literals@^6.9.4:
-  version "6.9.4"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-property-literals/-/babel-plugin-transform-property-literals-6.9.4.tgz#98c1d21e255736573f93ece54459f6ce24985d39"
-  integrity sha1-mMHSHiVXNlc/k+zlRFn2ziSYXTk=
-  dependencies:
-    esutils "^2.0.2"
-
-babel-plugin-transform-regexp-constructors@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-regexp-constructors/-/babel-plugin-transform-regexp-constructors-0.4.3.tgz#58b7775b63afcf33328fae9a5f88fbd4fb0b4965"
-  integrity sha1-WLd3W2OvzzMyj66aX4j71PsLSWU=
-
-babel-plugin-transform-remove-console@^6.9.4:
-  version "6.9.4"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-remove-console/-/babel-plugin-transform-remove-console-6.9.4.tgz#b980360c067384e24b357a588d807d3c83527780"
-  integrity sha1-uYA2DAZzhOJLNXpYjYB9PINSd4A=
-
-babel-plugin-transform-remove-debugger@^6.9.4:
-  version "6.9.4"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-remove-debugger/-/babel-plugin-transform-remove-debugger-6.9.4.tgz#42b727631c97978e1eb2d199a7aec84a18339ef2"
-  integrity sha1-QrcnYxyXl44estGZp67IShgznvI=
-
-babel-plugin-transform-remove-undefined@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-remove-undefined/-/babel-plugin-transform-remove-undefined-0.5.0.tgz#80208b31225766c630c97fa2d288952056ea22dd"
-  integrity sha512-+M7fJYFaEE/M9CXa0/IRkDbiV3wRELzA1kKQFCJ4ifhrzLKn/9VCCgj9OFmYWwBd8IB48YdgPkHYtbYq+4vtHQ==
-  dependencies:
-    babel-helper-evaluate-path "^0.5.0"
-
-babel-plugin-transform-simplify-comparison-operators@^6.9.4:
-  version "6.9.4"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-simplify-comparison-operators/-/babel-plugin-transform-simplify-comparison-operators-6.9.4.tgz#f62afe096cab0e1f68a2d753fdf283888471ceb9"
-  integrity sha1-9ir+CWyrDh9ootdT/fKDiIRxzrk=
-
-babel-plugin-transform-undefined-to-void@^6.9.4:
-  version "6.9.4"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-undefined-to-void/-/babel-plugin-transform-undefined-to-void-6.9.4.tgz#be241ca81404030678b748717322b89d0c8fe280"
-  integrity sha1-viQcqBQEAwZ4t0hxcyK4nQyP4oA=
-
-babel-preset-minify@^0.5.0:
-  version "0.5.1"
-  resolved "https://registry.yarnpkg.com/babel-preset-minify/-/babel-preset-minify-0.5.1.tgz#25f5d0bce36ec818be80338d0e594106e21eaa9f"
-  integrity sha512-1IajDumYOAPYImkHbrKeiN5AKKP9iOmRoO2IPbIuVp0j2iuCcj0n7P260z38siKMZZ+85d3mJZdtW8IgOv+Tzg==
-  dependencies:
-    babel-plugin-minify-builtins "^0.5.0"
-    babel-plugin-minify-constant-folding "^0.5.0"
-    babel-plugin-minify-dead-code-elimination "^0.5.1"
-    babel-plugin-minify-flip-comparisons "^0.4.3"
-    babel-plugin-minify-guarded-expressions "^0.4.4"
-    babel-plugin-minify-infinity "^0.4.3"
-    babel-plugin-minify-mangle-names "^0.5.0"
-    babel-plugin-minify-numeric-literals "^0.4.3"
-    babel-plugin-minify-replace "^0.5.0"
-    babel-plugin-minify-simplify "^0.5.1"
-    babel-plugin-minify-type-constructors "^0.4.3"
-    babel-plugin-transform-inline-consecutive-adds "^0.4.3"
-    babel-plugin-transform-member-expression-literals "^6.9.4"
-    babel-plugin-transform-merge-sibling-variables "^6.9.4"
-    babel-plugin-transform-minify-booleans "^6.9.4"
-    babel-plugin-transform-property-literals "^6.9.4"
-    babel-plugin-transform-regexp-constructors "^0.4.3"
-    babel-plugin-transform-remove-console "^6.9.4"
-    babel-plugin-transform-remove-debugger "^6.9.4"
-    babel-plugin-transform-remove-undefined "^0.5.0"
-    babel-plugin-transform-simplify-comparison-operators "^6.9.4"
-    babel-plugin-transform-undefined-to-void "^6.9.4"
-    lodash "^4.17.11"
-
-babel-runtime@^6.22.0, babel-runtime@^6.26.0:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
-  integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
-  dependencies:
-    core-js "^2.4.0"
-    regenerator-runtime "^0.11.0"
-
-babel-traverse@^6.26.0:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
-  integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=
-  dependencies:
-    babel-code-frame "^6.26.0"
-    babel-messages "^6.23.0"
-    babel-runtime "^6.26.0"
-    babel-types "^6.26.0"
-    babylon "^6.18.0"
-    debug "^2.6.8"
-    globals "^9.18.0"
-    invariant "^2.2.2"
-    lodash "^4.17.4"
-
-babel-types@^6.26.0:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
-  integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=
-  dependencies:
-    babel-runtime "^6.26.0"
-    esutils "^2.0.2"
-    lodash "^4.17.4"
-    to-fast-properties "^1.0.3"
-
-babylon@^6.18.0:
-  version "6.18.0"
-  resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
-  integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==
-
-babylon@^7.0.0-beta.42:
-  version "7.0.0-beta.47"
-  resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.47.tgz#6d1fa44f0abec41ab7c780481e62fd9aafbdea80"
-  integrity sha512-+rq2cr4GDhtToEzKFD6KZZMDBXhjFAr9JjPw9pAppZACeEWqNM294j+NdBzkSHYXwzzBmVjZ3nEVJlOhbR2gOQ==
-
 backo2@1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
@@ -2029,39 +1145,11 @@
   resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
   integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg=
 
-base64-js@1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1"
-  integrity sha1-o5mS1yNYSBGYK+XikLtqU9hnAPE=
-
-base64-js@^1.0.2:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
-  integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
-
 base64id@1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6"
   integrity sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=
 
-base64id@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6"
-  integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==
-
-base@^0.11.1:
-  version "0.11.2"
-  resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
-  integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==
-  dependencies:
-    cache-base "^1.0.1"
-    class-utils "^0.3.5"
-    component-emitter "^1.2.1"
-    define-property "^1.0.0"
-    isobject "^3.0.1"
-    mixin-deep "^1.2.0"
-    pascalcase "^0.1.1"
-
 bcrypt-pbkdf@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
@@ -2081,21 +1169,6 @@
   resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
   integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
 
-bl@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.0.tgz#e1a574cdf528e4053019bb800b041c0ac88da493"
-  integrity sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==
-  dependencies:
-    readable-stream "^2.3.5"
-    safe-buffer "^5.1.1"
-
-bl@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88"
-  integrity sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A==
-  dependencies:
-    readable-stream "^3.0.1"
-
 blob@0.0.5:
   version "0.0.5"
   resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683"
@@ -2106,7 +1179,7 @@
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
   integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
 
-body-parser@1.19.0, body-parser@^1.16.1, body-parser@^1.17.2:
+body-parser@^1.16.1:
   version "1.19.0"
   resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
   integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
@@ -2122,30 +1195,6 @@
     raw-body "2.4.0"
     type-is "~1.6.17"
 
-bower-config@^1.4.0, bower-config@^1.4.1:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/bower-config/-/bower-config-1.4.1.tgz#85fd9df367c2b8dbbd0caa4c5f2bad40cd84c2cc"
-  integrity sha1-hf2d82fCuNu9DKpMXyutQM2Ewsw=
-  dependencies:
-    graceful-fs "^4.1.3"
-    mout "^1.0.0"
-    optimist "^0.6.1"
-    osenv "^0.1.3"
-    untildify "^2.1.0"
-
-boxen@^1.2.1:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
-  integrity sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==
-  dependencies:
-    ansi-align "^2.0.0"
-    camelcase "^4.0.0"
-    chalk "^2.0.1"
-    cli-boxes "^1.0.0"
-    string-width "^2.0.0"
-    term-size "^1.2.0"
-    widest-line "^2.0.0"
-
 brace-expansion@^1.1.7:
   version "1.1.11"
   resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -2154,31 +1203,6 @@
     balanced-match "^1.0.0"
     concat-map "0.0.1"
 
-braces@^1.8.2:
-  version "1.8.5"
-  resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7"
-  integrity sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=
-  dependencies:
-    expand-range "^1.8.1"
-    preserve "^0.2.0"
-    repeat-element "^1.1.2"
-
-braces@^2.3.1:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
-  integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==
-  dependencies:
-    arr-flatten "^1.1.0"
-    array-unique "^0.3.2"
-    extend-shallow "^2.0.1"
-    fill-range "^4.0.0"
-    isobject "^3.0.1"
-    repeat-element "^1.1.2"
-    snapdragon "^0.8.1"
-    snapdragon-node "^2.0.1"
-    split-string "^3.0.2"
-    to-regex "^3.0.1"
-
 braces@^3.0.2, braces@~3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
@@ -2186,19 +1210,6 @@
   dependencies:
     fill-range "^7.0.1"
 
-browser-capabilities@^1.0.0:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/browser-capabilities/-/browser-capabilities-1.1.4.tgz#a6bd657a07a134532ad66c722b8949904478b973"
-  integrity sha512-BezMQhbQklxjRQpZZQ8tnbzEo6AldUwMh8/PeWt5/CTBSwByQRXZEAK2fbnEahQ4poeeaI0suAYRq25A1YGOmw==
-  dependencies:
-    "@types/ua-parser-js" "^0.7.31"
-    ua-parser-js "^0.7.15"
-
-browser-stdout@1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f"
-  integrity sha1-81HTKWnTL6XXpVZxVCY9korjvR8=
-
 browser-stdout@1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
@@ -2223,13 +1234,6 @@
     node-releases "^1.1.53"
     pkg-up "^2.0.0"
 
-browserstack@^1.2.0:
-  version "1.5.3"
-  resolved "https://registry.yarnpkg.com/browserstack/-/browserstack-1.5.3.tgz#93ab48799a12ef99dbd074dd595410ddb196a7ac"
-  integrity sha512-AO+mECXsW4QcqC9bxwM29O7qWa7bJT94uBFzeb5brylIQwawuEziwq20dPYbins95GlWzOawgyDNdjYAo32EKg==
-  dependencies:
-    https-proxy-agent "^2.2.1"
-
 buffer-alloc-unsafe@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
@@ -2243,11 +1247,6 @@
     buffer-alloc-unsafe "^1.1.0"
     buffer-fill "^1.0.0"
 
-buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3:
-  version "0.2.13"
-  resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
-  integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
-
 buffer-fill@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
@@ -2258,52 +1257,16 @@
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
   integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
 
-buffer@^5.1.0:
-  version "5.4.3"
-  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.3.tgz#3fbc9c69eb713d323e3fc1a895eee0710c072115"
-  integrity sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==
-  dependencies:
-    base64-js "^1.0.2"
-    ieee754 "^1.1.4"
-
 builtin-modules@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.1.0.tgz#aad97c15131eb76b65b50ef208e7584cd76a7484"
   integrity sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==
 
-busboy@^0.2.11:
-  version "0.2.14"
-  resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453"
-  integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=
-  dependencies:
-    dicer "0.2.5"
-    readable-stream "1.1.x"
-
-bytes@3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
-  integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
-
 bytes@3.1.0, bytes@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
   integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
 
-cache-base@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
-  integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==
-  dependencies:
-    collection-visit "^1.0.0"
-    component-emitter "^1.2.1"
-    get-value "^2.0.6"
-    has-value "^1.0.0"
-    isobject "^3.0.1"
-    set-value "^2.0.0"
-    to-object-path "^0.3.0"
-    union-value "^1.0.0"
-    unset-value "^1.0.0"
-
 cache-content-type@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c"
@@ -2317,7 +1280,7 @@
   resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20"
   integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA=
 
-camel-case@3.0.x, camel-case@^3.0.0:
+camel-case@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73"
   integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=
@@ -2325,36 +1288,11 @@
     no-case "^2.2.0"
     upper-case "^1.1.1"
 
-camelcase-keys@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
-  integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc=
-  dependencies:
-    camelcase "^2.0.0"
-    map-obj "^1.0.0"
-
-camelcase@^2.0.0:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
-  integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=
-
-camelcase@^4.0.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
-  integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
-
 camelcase@^5.0.0, camelcase@^5.3.1:
   version "5.3.1"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
   integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
 
-cancel-token@^0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/cancel-token/-/cancel-token-0.1.1.tgz#c18197674bb1c84c1d6933ebf15d8d5a5ce79b4f"
-  integrity sha1-wYGXZ0uxyEwdaTPr8V2NWlznm08=
-  dependencies:
-    "@types/node" "^4.0.30"
-
 caniuse-api@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
@@ -2370,25 +1308,11 @@
   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001038.tgz#44da3cbca2ab6cb6aa83d1be5d324e17f141caff"
   integrity sha512-zii9quPo96XfOiRD4TrfYGs+QsGZpb2cGiMAzPjtf/hpFgB6zCPZgJb7I1+EATeMw/o+lG8FyRAnI+CWStHcaQ==
 
-capture-stack-trace@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d"
-  integrity sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==
-
 caseless@~0.12.0:
   version "0.12.0"
   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
   integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
 
-chai@^3.5.0:
-  version "3.5.0"
-  resolved "https://registry.yarnpkg.com/chai/-/chai-3.5.0.tgz#4d02637b067fe958bdbfdd3a40ec56fef7373247"
-  integrity sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=
-  dependencies:
-    assertion-error "^1.0.1"
-    deep-eql "^0.1.3"
-    type-detect "^1.0.0"
-
 chai@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5"
@@ -2401,18 +1325,7 @@
     pathval "^1.1.0"
     type-detect "^4.0.5"
 
-chalk@^1.1.1, chalk@^1.1.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
-  integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
-  dependencies:
-    ansi-styles "^2.2.1"
-    escape-string-regexp "^1.0.2"
-    has-ansi "^2.0.0"
-    strip-ansi "^3.0.0"
-    supports-color "^2.0.0"
-
-chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.2:
   version "2.4.2"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
   integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -2421,20 +1334,6 @@
     escape-string-regexp "^1.0.5"
     supports-color "^5.3.0"
 
-chalk@~0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f"
-  integrity sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=
-  dependencies:
-    ansi-styles "~1.0.0"
-    has-color "~0.1.0"
-    strip-ansi "~0.1.0"
-
-charenc@~0.0.1:
-  version "0.0.2"
-  resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
-  integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=
-
 check-error@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
@@ -2470,28 +1369,6 @@
   optionalDependencies:
     fsevents "~2.1.2"
 
-ci-info@^1.5.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497"
-  integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==
-
-class-utils@^0.3.5:
-  version "0.3.6"
-  resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
-  integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==
-  dependencies:
-    arr-union "^3.1.0"
-    define-property "^0.2.5"
-    isobject "^3.0.0"
-    static-extend "^0.1.1"
-
-clean-css@4.2.x:
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.1.tgz#2d411ef76b8569b6d0c84068dabe85b0aa5e5c17"
-  integrity sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==
-  dependencies:
-    source-map "~0.6.0"
-
 clean-css@^4.2.1:
   version "4.2.3"
   resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
@@ -2499,16 +1376,6 @@
   dependencies:
     source-map "~0.6.0"
 
-cleankill@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/cleankill/-/cleankill-2.0.0.tgz#59830dfc8b411d53dc72ad09d45a78ea33161a91"
-  integrity sha1-WYMN/ItBHVPccq0J1Fp46jMWGpE=
-
-cli-boxes@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
-  integrity sha1-T6kXw+WclKAEzWH47lCdplFocUM=
-
 cliui@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
@@ -2518,17 +1385,7 @@
     strip-ansi "^5.2.0"
     wrap-ansi "^5.1.0"
 
-clone-stats@^0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1"
-  integrity sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=
-
-clone@^1.0.0:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
-  integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
-
-clone@^2.0.0, clone@^2.1.0, clone@^2.1.2:
+clone@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
   integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
@@ -2538,15 +1395,7 @@
   resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
   integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
 
-collection-visit@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
-  integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=
-  dependencies:
-    map-visit "^1.0.0"
-    object-visit "^1.0.0"
-
-color-convert@^1.9.0, color-convert@^1.9.1:
+color-convert@^1.9.0:
   version "1.9.3"
   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
   integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@@ -2558,45 +1407,11 @@
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
   integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
 
-color-name@^1.0.0:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
-  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
-
-color-string@^1.5.2:
-  version "1.5.3"
-  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc"
-  integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==
-  dependencies:
-    color-name "^1.0.0"
-    simple-swizzle "^0.2.2"
-
-color@3.0.x:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/color/-/color-3.0.0.tgz#d920b4328d534a3ac8295d68f7bd4ba6c427be9a"
-  integrity sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==
-  dependencies:
-    color-convert "^1.9.1"
-    color-string "^1.5.2"
-
-colornames@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/colornames/-/colornames-1.1.1.tgz#f8889030685c7c4ff9e2a559f5077eb76a816f96"
-  integrity sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=
-
-colors@^1.1.0, colors@^1.2.1:
+colors@^1.1.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
   integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
 
-colorspace@1.1.x:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.2.tgz#e0128950d082b86a2168580796a0aa5d6c68d8c5"
-  integrity sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==
-  dependencies:
-    color "3.0.x"
-    text-hex "1.0.x"
-
 combined-stream@^1.0.6, combined-stream@~1.0.6:
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
@@ -2614,16 +1429,6 @@
     lodash.camelcase "^4.3.0"
     typical "^4.0.0"
 
-command-line-usage@^5.0.5:
-  version "5.0.5"
-  resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-5.0.5.tgz#5f25933ffe6dedd983c635d38a21d7e623fda357"
-  integrity sha512-d8NrGylA5oCXSbGoKz05FkehDAzSmIm4K03S5VDh4d5lZAtTWfc3D1RuETtuQCn8129nYfJfDdF7P/lwcz1BlA==
-  dependencies:
-    array-back "^2.0.0"
-    chalk "^2.4.1"
-    table-layout "^0.4.3"
-    typical "^2.6.1"
-
 command-line-usage@^6.1.0:
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.0.tgz#f28376a3da3361ff3d36cfd31c3c22c9a64c7cb6"
@@ -2634,28 +1439,11 @@
     table-layout "^1.0.0"
     typical "^5.2.0"
 
-commander@2.17.x:
-  version "2.17.1"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
-  integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
-
-commander@2.9.0:
-  version "2.9.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
-  integrity sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=
-  dependencies:
-    graceful-readlink ">= 1.0.0"
-
 commander@^2.19.0, commander@^2.20.0, commander@~2.20.3:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
 
-commander@~2.19.0:
-  version "2.19.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
-  integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==
-
 component-bind@1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1"
@@ -2666,73 +1454,23 @@
   resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
   integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
 
-component-emitter@^1.2.1:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
-  integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
-
 component-inherit@0.0.3:
   version "0.0.3"
   resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
   integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=
 
-compress-commons@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-2.1.1.tgz#9410d9a534cf8435e3fbbb7c6ce48de2dc2f0610"
-  integrity sha512-eVw6n7CnEMFzc3duyFVrQEuY1BlHR3rYsSztyG32ibGMW722i3C6IizEGMFmfMU+A+fALvBIwxN3czffTcdA+Q==
-  dependencies:
-    buffer-crc32 "^0.2.13"
-    crc32-stream "^3.0.1"
-    normalize-path "^3.0.0"
-    readable-stream "^2.3.6"
-
-compressible@^2.0.0, compressible@~2.0.16:
+compressible@^2.0.0:
   version "2.0.18"
   resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
   integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
   dependencies:
     mime-db ">= 1.43.0 < 2"
 
-compression@^1.6.2:
-  version "1.7.4"
-  resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f"
-  integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==
-  dependencies:
-    accepts "~1.3.5"
-    bytes "3.0.0"
-    compressible "~2.0.16"
-    debug "2.6.9"
-    on-headers "~1.0.2"
-    safe-buffer "5.1.2"
-    vary "~1.1.2"
-
 concat-map@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
   integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
 
-concat-stream@^1.5.2:
-  version "1.6.2"
-  resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
-  integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
-  dependencies:
-    buffer-from "^1.0.0"
-    inherits "^2.0.3"
-    readable-stream "^2.2.2"
-    typedarray "^0.0.6"
-
-configstore@^3.0.0:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f"
-  integrity sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==
-  dependencies:
-    dot-prop "^4.1.0"
-    graceful-fs "^4.1.2"
-    make-dir "^1.0.0"
-    unique-string "^1.0.0"
-    write-file-atomic "^2.0.0"
-    xdg-basedir "^3.0.0"
-
 connect@^3.6.0:
   version "3.7.0"
   resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8"
@@ -2743,40 +1481,30 @@
     parseurl "~1.3.3"
     utils-merge "1.0.1"
 
-content-disposition@0.5.3, content-disposition@~0.5.2:
+content-disposition@~0.5.2:
   version "0.5.3"
   resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
   integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
   dependencies:
     safe-buffer "5.1.2"
 
-content-type@^1.0.2, content-type@^1.0.4, content-type@~1.0.4:
+content-type@^1.0.4, content-type@~1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
   integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
 
-convert-source-map@^1.1.1, convert-source-map@^1.7.0:
+convert-source-map@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
   integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
   dependencies:
     safe-buffer "~5.1.1"
 
-cookie-signature@1.0.6:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
-  integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
-
 cookie@0.3.1:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
   integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
 
-cookie@0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
-  integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
-
 cookies@~0.8.0:
   version "0.8.0"
   resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90"
@@ -2785,11 +1513,6 @@
     depd "~2.0.0"
     keygrip "~1.1.0"
 
-copy-descriptor@^0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
-  integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
-
 core-js-bundle@^3.6.0:
   version "3.6.4"
   resolved "https://registry.yarnpkg.com/core-js-bundle/-/core-js-bundle-3.6.4.tgz#d4e098323c035f4a1b61f00db0b8def04c243920"
@@ -2803,99 +1526,11 @@
     browserslist "^4.8.3"
     semver "7.0.0"
 
-core-js@^2.4.0:
-  version "2.6.11"
-  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
-  integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
-
-core-util-is@1.0.2, core-util-is@~1.0.0:
+core-util-is@1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
   integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
 
-cors@^2.8.4:
-  version "2.8.5"
-  resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
-  integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
-  dependencies:
-    object-assign "^4"
-    vary "^1"
-
-crc32-stream@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-3.0.1.tgz#cae6eeed003b0e44d739d279de5ae63b171b4e85"
-  integrity sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w==
-  dependencies:
-    crc "^3.4.4"
-    readable-stream "^3.4.0"
-
-crc@^3.4.4:
-  version "3.8.0"
-  resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6"
-  integrity sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==
-  dependencies:
-    buffer "^5.1.0"
-
-create-error-class@^3.0.0:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6"
-  integrity sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=
-  dependencies:
-    capture-stack-trace "^1.0.0"
-
-cross-spawn@^5.0.1:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
-  integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=
-  dependencies:
-    lru-cache "^4.0.1"
-    shebang-command "^1.2.0"
-    which "^1.2.9"
-
-cross-spawn@^6.0.5:
-  version "6.0.5"
-  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
-  integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
-  dependencies:
-    nice-try "^1.0.4"
-    path-key "^2.0.1"
-    semver "^5.5.0"
-    shebang-command "^1.2.0"
-    which "^1.2.9"
-
-crypt@~0.0.1:
-  version "0.0.2"
-  resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
-  integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=
-
-crypto-random-string@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
-  integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=
-
-css-slam@^2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/css-slam/-/css-slam-2.1.2.tgz#3d35b1922cb3e0002a45c89ab189492508c493e5"
-  integrity sha512-cObrY+mhFEmepWpua6EpMrgRNTQ0eeym+kvR0lukI6hDEzK7F8himEDS4cJ9+fPHCoArTzVrrR0d+oAUbTR1NA==
-  dependencies:
-    command-line-args "^5.0.2"
-    command-line-usage "^5.0.5"
-    dom5 "^3.0.0"
-    parse5 "^4.0.0"
-    shady-css-parser "^0.1.0"
-
-cssbeautify@^0.3.1:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/cssbeautify/-/cssbeautify-0.3.1.tgz#12dd1f734035c2e6faca67dcbdcef74e42811397"
-  integrity sha1-Et0fc0A1wub6ymfcvc73TkKBE5c=
-
-currently-unhandled@^0.4.1:
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
-  integrity sha1-mI3zP+qxke95mmE2nddsF635V+o=
-  dependencies:
-    array-find-index "^1.0.1"
-
 custom-event@~1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
@@ -2918,14 +1553,7 @@
   resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131"
   integrity sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==
 
-debug@2.6.8:
-  version "2.6.8"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
-  integrity sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=
-  dependencies:
-    ms "2.0.0"
-
-debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8:
+debug@2.6.9:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
   integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
@@ -2939,7 +1567,7 @@
   dependencies:
     ms "^2.1.1"
 
-debug@^4.1.0, debug@^4.1.1, debug@~4.1.0:
+debug@^4.1.0, debug@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
   integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
@@ -2953,23 +1581,11 @@
   dependencies:
     ms "2.0.0"
 
-decamelize@^1.1.2, decamelize@^1.2.0:
+decamelize@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
   integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
 
-decode-uri-component@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
-  integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
-
-deep-eql@^0.1.3:
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2"
-  integrity sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=
-  dependencies:
-    type-detect "0.1.1"
-
 deep-eql@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df"
@@ -2982,7 +1598,7 @@
   resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
   integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=
 
-deep-extend@^0.6.0, deep-extend@~0.6.0:
+deep-extend@~0.6.0:
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
   integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
@@ -2999,28 +1615,6 @@
   dependencies:
     object-keys "^1.0.12"
 
-define-property@^0.2.5:
-  version "0.2.5"
-  resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
-  integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=
-  dependencies:
-    is-descriptor "^0.1.0"
-
-define-property@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
-  integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY=
-  dependencies:
-    is-descriptor "^1.0.0"
-
-define-property@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
-  integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==
-  dependencies:
-    is-descriptor "^1.0.2"
-    isobject "^3.0.1"
-
 delayed-stream@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
@@ -3041,66 +1635,25 @@
   resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
   integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
 
-destroy@^1.0.4, destroy@~1.0.4:
+destroy@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
   integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
 
-detect-file@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7"
-  integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=
-
-detect-indent@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
-  integrity sha1-920GQ1LN9Docts5hnE7jqUdd4gg=
-  dependencies:
-    repeating "^2.0.0"
-
-detect-node@^2.0.3:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
-  integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
-
 di@^0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
   integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=
 
-diagnostics@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/diagnostics/-/diagnostics-1.1.1.tgz#cab6ac33df70c9d9a727490ae43ac995a769b22a"
-  integrity sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==
-  dependencies:
-    colorspace "1.1.x"
-    enabled "1.0.x"
-    kuler "1.0.x"
-
-dicer@0.2.5:
-  version "0.2.5"
-  resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f"
-  integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=
-  dependencies:
-    readable-stream "1.1.x"
-    streamsearch "0.1.2"
-
-diff@3.2.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9"
-  integrity sha1-yc45Okt8vQsFinJck98pkCeGj/k=
-
-diff@3.5.0, diff@^3.1.0:
+diff@3.5.0:
   version "3.5.0"
   resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
   integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
 
-doctrine@^2.0.2:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
-  integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==
-  dependencies:
-    esutils "^2.0.2"
+diff@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
+  integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
 
 dom-serialize@^2.2.0:
   version "2.2.1"
@@ -3112,51 +1665,6 @@
     extend "^3.0.0"
     void-elements "^2.0.0"
 
-dom-urls@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/dom-urls/-/dom-urls-1.1.0.tgz#001ddf81628cd1e706125c7176f53ccec55d918e"
-  integrity sha1-AB3fgWKM0ecGElxxdvU8zsVdkY4=
-  dependencies:
-    urijs "^1.16.1"
-
-dom5@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/dom5/-/dom5-3.0.1.tgz#cdfc7331f376e284bf379e6ea054afc136702944"
-  integrity sha512-JPFiouQIr16VQ4dX6i0+Hpbg3H2bMKPmZ+WZgBOSSvOPx9QHwwY8sPzeM2baUtViESYto6wC2nuZOMC/6gulcA==
-  dependencies:
-    "@types/parse5" "^2.2.34"
-    clone "^2.1.0"
-    parse5 "^4.0.0"
-
-dot-prop@^4.1.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
-  integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==
-  dependencies:
-    is-obj "^1.0.0"
-
-duplexer2@^0.1.2:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
-  integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=
-  dependencies:
-    readable-stream "^2.0.2"
-
-duplexer3@^0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
-  integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
-
-duplexify@^3.2.0, duplexify@^3.5.0:
-  version "3.7.1"
-  resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309"
-  integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==
-  dependencies:
-    end-of-stream "^1.0.0"
-    inherits "^2.0.1"
-    readable-stream "^2.0.0"
-    stream-shift "^1.0.0"
-
 dynamic-import-polyfill@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/dynamic-import-polyfill/-/dynamic-import-polyfill-0.1.1.tgz#e1f9eb1876ee242bd56572f8ed4df768e143083f"
@@ -3180,29 +1688,17 @@
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.392.tgz#280ab4f7a3ae47419cfabb15dbfc1567be7f1111"
   integrity sha512-/hsgeVdReDsyTBE0aU9FRdh1wnNPrX3xlz3t61F+CJPOT+Umfi9DXHsCX85TEgWZQqlow0Rw44/4/jbU2Sqgkg==
 
-emitter-component@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/emitter-component/-/emitter-component-1.1.1.tgz#065e2dbed6959bf470679edabeaf7981d1003ab6"
-  integrity sha1-Bl4tvtaVm/RwZ57avq95gdEAOrY=
-
 emoji-regex@^7.0.1:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
   integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
 
-enabled@1.0.x:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/enabled/-/enabled-1.0.2.tgz#965f6513d2c2d1c5f4652b64a2e3396467fc2f93"
-  integrity sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=
-  dependencies:
-    env-variable "0.0.x"
-
 encodeurl@^1.0.2, encodeurl@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
   integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
 
-end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1:
+end-of-stream@^1.1.0:
   version "1.4.4"
   resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
   integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
@@ -3226,23 +1722,6 @@
     xmlhttprequest-ssl "~1.5.4"
     yeast "0.1.2"
 
-engine.io-client@~3.4.0:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.0.tgz#82a642b42862a9b3f7a188f41776b2deab643700"
-  integrity sha512-a4J5QO2k99CM2a0b12IznnyQndoEvtA4UAldhGzKqnHf42I3Qs2W5SPnDvatZRcMaNZs4IevVicBPayxYt6FwA==
-  dependencies:
-    component-emitter "1.2.1"
-    component-inherit "0.0.3"
-    debug "~4.1.0"
-    engine.io-parser "~2.2.0"
-    has-cors "1.1.0"
-    indexof "0.0.1"
-    parseqs "0.0.5"
-    parseuri "0.0.5"
-    ws "~6.1.0"
-    xmlhttprequest-ssl "~1.5.4"
-    yeast "0.1.2"
-
 engine.io-parser@~2.1.0, engine.io-parser@~2.1.1:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.1.3.tgz#757ab970fbf2dfb32c7b74b033216d5739ef79a6"
@@ -3254,17 +1733,6 @@
     blob "0.0.5"
     has-binary2 "~1.0.2"
 
-engine.io-parser@~2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.0.tgz#312c4894f57d52a02b420868da7b5c1c84af80ed"
-  integrity sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==
-  dependencies:
-    after "0.8.2"
-    arraybuffer.slice "~0.0.7"
-    base64-arraybuffer "0.1.5"
-    blob "0.0.5"
-    has-binary2 "~1.0.2"
-
 engine.io@~3.2.0:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.2.1.tgz#b60281c35484a70ee0351ea0ebff83ec8c9522a2"
@@ -3277,29 +1745,12 @@
     engine.io-parser "~2.1.0"
     ws "~3.3.1"
 
-engine.io@~3.4.0:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.4.0.tgz#3a962cc4535928c252759a00f98519cb46c53ff3"
-  integrity sha512-XCyYVWzcHnK5cMz7G4VTu2W7zJS7SM1QkcelghyIk/FmobWBtXE7fwhBusEKvCSqc3bMh8fNFMlUkCKTFRxH2w==
-  dependencies:
-    accepts "~1.3.4"
-    base64id "2.0.0"
-    cookie "0.3.1"
-    debug "~4.1.0"
-    engine.io-parser "~2.2.0"
-    ws "^7.1.2"
-
 ent@~2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
   integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0=
 
-env-variable@0.0.x:
-  version "0.0.5"
-  resolved "https://registry.yarnpkg.com/env-variable/-/env-variable-0.0.5.tgz#913dd830bef11e96a039c038d4130604eba37f88"
-  integrity sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA==
-
-error-ex@^1.2.0, error-ex@^1.3.1:
+error-ex@^1.3.1:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
   integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
@@ -3398,41 +1849,16 @@
     is-date-object "^1.0.1"
     is-symbol "^1.0.2"
 
-es6-promise@^4.0.3, es6-promise@^4.0.5:
-  version "4.2.8"
-  resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
-  integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
-
-es6-promisify@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
-  integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=
-  dependencies:
-    es6-promise "^4.0.3"
-
-es6-promisify@^6.0.0:
-  version "6.0.2"
-  resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-6.0.2.tgz#525c23725b8510f5f1f2feb5a1fbad93a93e29b4"
-  integrity sha512-eO6vFm0JvqGzjWIQA6QVKjxpmELfhWbDUWHm1rPfIbn55mhKPiAa5xpLmQWJrNa629ZIeQ8ZvMAi13kvrjK6Mg==
-
 escape-html@^1.0.3, escape-html@~1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
   integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
 
-escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.4, escape-string-regexp@^1.0.5:
+escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
 
-espree@^3.5.2:
-  version "3.5.4"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7"
-  integrity sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==
-  dependencies:
-    acorn "^5.5.0"
-    acorn-jsx "^3.0.0"
-
 esprima@^4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
@@ -3448,7 +1874,7 @@
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
   integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
 
-etag@^1.3.0, etag@~1.8.1:
+etag@^1.3.0:
   version "1.8.1"
   resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
   integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
@@ -3458,130 +1884,11 @@
   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb"
   integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==
 
-execa@^0.7.0:
-  version "0.7.0"
-  resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
-  integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=
-  dependencies:
-    cross-spawn "^5.0.1"
-    get-stream "^3.0.0"
-    is-stream "^1.1.0"
-    npm-run-path "^2.0.0"
-    p-finally "^1.0.0"
-    signal-exit "^3.0.0"
-    strip-eof "^1.0.0"
-
-expand-brackets@^0.1.4:
-  version "0.1.5"
-  resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
-  integrity sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=
-  dependencies:
-    is-posix-bracket "^0.1.0"
-
-expand-brackets@^2.1.4:
-  version "2.1.4"
-  resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
-  integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI=
-  dependencies:
-    debug "^2.3.3"
-    define-property "^0.2.5"
-    extend-shallow "^2.0.1"
-    posix-character-classes "^0.1.0"
-    regex-not "^1.0.0"
-    snapdragon "^0.8.1"
-    to-regex "^3.0.1"
-
-expand-range@^1.8.1:
-  version "1.8.2"
-  resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337"
-  integrity sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=
-  dependencies:
-    fill-range "^2.1.0"
-
-expand-tilde@^2.0.0, expand-tilde@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502"
-  integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=
-  dependencies:
-    homedir-polyfill "^1.0.1"
-
-express@^4.15.3, express@^4.8.5:
-  version "4.17.1"
-  resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
-  integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
-  dependencies:
-    accepts "~1.3.7"
-    array-flatten "1.1.1"
-    body-parser "1.19.0"
-    content-disposition "0.5.3"
-    content-type "~1.0.4"
-    cookie "0.4.0"
-    cookie-signature "1.0.6"
-    debug "2.6.9"
-    depd "~1.1.2"
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    etag "~1.8.1"
-    finalhandler "~1.1.2"
-    fresh "0.5.2"
-    merge-descriptors "1.0.1"
-    methods "~1.1.2"
-    on-finished "~2.3.0"
-    parseurl "~1.3.3"
-    path-to-regexp "0.1.7"
-    proxy-addr "~2.0.5"
-    qs "6.7.0"
-    range-parser "~1.2.1"
-    safe-buffer "5.1.2"
-    send "0.17.1"
-    serve-static "1.14.1"
-    setprototypeof "1.1.1"
-    statuses "~1.5.0"
-    type-is "~1.6.18"
-    utils-merge "1.0.1"
-    vary "~1.1.2"
-
-extend-shallow@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
-  integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=
-  dependencies:
-    is-extendable "^0.1.0"
-
-extend-shallow@^3.0.0, extend-shallow@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
-  integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=
-  dependencies:
-    assign-symbols "^1.0.0"
-    is-extendable "^1.0.1"
-
 extend@^3.0.0, extend@~3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
   integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
 
-extglob@^0.3.1:
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
-  integrity sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=
-  dependencies:
-    is-extglob "^1.0.0"
-
-extglob@^2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
-  integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==
-  dependencies:
-    array-unique "^0.3.2"
-    define-property "^1.0.0"
-    expand-brackets "^2.1.4"
-    extend-shallow "^2.0.1"
-    fragment-cache "^0.2.1"
-    regex-not "^1.0.0"
-    snapdragon "^0.8.1"
-    to-regex "^3.0.1"
-
 extsprintf@1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
@@ -3602,49 +1909,6 @@
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
   integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
 
-fast-safe-stringify@^2.0.4:
-  version "2.0.7"
-  resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743"
-  integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==
-
-fd-slicer@~1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
-  integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=
-  dependencies:
-    pend "~1.2.0"
-
-fecha@^2.3.3:
-  version "2.3.3"
-  resolved "https://registry.yarnpkg.com/fecha/-/fecha-2.3.3.tgz#948e74157df1a32fd1b12c3a3c3cdcb6ec9d96cd"
-  integrity sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==
-
-filename-regex@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
-  integrity sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=
-
-fill-range@^2.1.0:
-  version "2.2.4"
-  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565"
-  integrity sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==
-  dependencies:
-    is-number "^2.1.0"
-    isobject "^2.0.0"
-    randomatic "^3.0.0"
-    repeat-element "^1.1.2"
-    repeat-string "^1.5.2"
-
-fill-range@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
-  integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=
-  dependencies:
-    extend-shallow "^2.0.1"
-    is-number "^3.0.0"
-    repeat-string "^1.6.1"
-    to-regex-range "^2.1.0"
-
 fill-range@^7.0.1:
   version "7.0.1"
   resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
@@ -3652,7 +1916,7 @@
   dependencies:
     to-regex-range "^5.0.1"
 
-finalhandler@1.1.2, finalhandler@~1.1.2:
+finalhandler@1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
   integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
@@ -3665,13 +1929,6 @@
     statuses "~1.5.0"
     unpipe "~1.0.0"
 
-find-port@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/find-port/-/find-port-1.0.1.tgz#db084a6cbf99564d99869ae79fbdecf66e8a185c"
-  integrity sha1-2whKbL+ZVk2Zhprnn73s9m6KGFw=
-  dependencies:
-    async "~0.2.9"
-
 find-replace@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38"
@@ -3686,14 +1943,6 @@
   dependencies:
     locate-path "^3.0.0"
 
-find-up@^1.0.0:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
-  integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=
-  dependencies:
-    path-exists "^2.0.0"
-    pinkie-promise "^2.0.0"
-
 find-up@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
@@ -3701,21 +1950,6 @@
   dependencies:
     locate-path "^2.0.0"
 
-findup-sync@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc"
-  integrity sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=
-  dependencies:
-    detect-file "^1.0.0"
-    is-glob "^3.1.0"
-    micromatch "^3.0.4"
-    resolve-dir "^1.0.1"
-
-first-chunk-stream@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz#59bfb50cd905f60d7c394cd3d9acaab4e6ad934e"
-  integrity sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=
-
 flat@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.0.tgz#090bec8b05e39cba309747f1d588f04dbaf98db2"
@@ -3735,28 +1969,11 @@
   dependencies:
     debug "^3.0.0"
 
-for-in@^1.0.1, for-in@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
-  integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
-
-for-own@^0.1.4:
-  version "0.1.5"
-  resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce"
-  integrity sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=
-  dependencies:
-    for-in "^1.0.1"
-
 forever-agent@~0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
   integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
 
-fork-stream@^0.0.4:
-  version "0.0.4"
-  resolved "https://registry.yarnpkg.com/fork-stream/-/fork-stream-0.0.4.tgz#db849fce77f6708a5f8f386ae533a0907b54ae70"
-  integrity sha1-24Sfznf2cIpfjzhq5TOgkHtUrnA=
-
 form-data@~2.3.2:
   version "2.3.3"
   resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
@@ -3766,47 +1983,11 @@
     combined-stream "^1.0.6"
     mime-types "^2.1.12"
 
-formatio@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.1.1.tgz#5ed3ccd636551097383465d996199100e86161e9"
-  integrity sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=
-  dependencies:
-    samsam "~1.1"
-
-formatio@1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb"
-  integrity sha1-87IWfZBoxGmKjVH092CjmlTYGOs=
-  dependencies:
-    samsam "1.x"
-
-forwarded@~0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
-  integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
-
-fragment-cache@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
-  integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=
-  dependencies:
-    map-cache "^0.2.2"
-
-freeport@^1.0.4:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/freeport/-/freeport-1.0.5.tgz#255e8ab84170c33ba85d990e821ae5f4a1a9bc5d"
-  integrity sha1-JV6KuEFwwzuoXZkOghrl9KGpvF0=
-
-fresh@0.5.2, fresh@~0.5.2:
+fresh@~0.5.2:
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
   integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
 
-fs-constants@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
-  integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
-
 fs-extra@^7.0.1:
   version "7.0.1"
   resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
@@ -3846,16 +2027,6 @@
   resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
   integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=
 
-get-stdin@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
-  integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=
-
-get-stream@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
-  integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=
-
 get-stream@^5.1.0:
   version "5.1.0"
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9"
@@ -3863,11 +2034,6 @@
   dependencies:
     pump "^3.0.0"
 
-get-value@^2.0.3, get-value@^2.0.6:
-  version "2.0.6"
-  resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
-  integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
-
 getpass@^0.1.1:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
@@ -3875,29 +2041,6 @@
   dependencies:
     assert-plus "^1.0.0"
 
-glob-base@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
-  integrity sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=
-  dependencies:
-    glob-parent "^2.0.0"
-    is-glob "^2.0.0"
-
-glob-parent@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28"
-  integrity sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=
-  dependencies:
-    is-glob "^2.0.0"
-
-glob-parent@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
-  integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=
-  dependencies:
-    is-glob "^3.1.0"
-    path-dirname "^1.0.0"
-
 glob-parent@~5.1.0:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
@@ -3905,32 +2048,6 @@
   dependencies:
     is-glob "^4.0.1"
 
-glob-stream@^5.3.2:
-  version "5.3.5"
-  resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-5.3.5.tgz#a55665a9a8ccdc41915a87c701e32d4e016fad22"
-  integrity sha1-pVZlqajM3EGRWofHAeMtTgFvrSI=
-  dependencies:
-    extend "^3.0.0"
-    glob "^5.0.3"
-    glob-parent "^3.0.0"
-    micromatch "^2.3.7"
-    ordered-read-streams "^0.3.0"
-    through2 "^0.6.0"
-    to-absolute-glob "^0.1.1"
-    unique-stream "^2.0.2"
-
-glob@7.1.1:
-  version "7.1.1"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
-  integrity sha1-gFIR3wT6rxxjo2ADBs31reULLsg=
-  dependencies:
-    fs.realpath "^1.0.0"
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "^3.0.2"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
 glob@7.1.3:
   version "7.1.3"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
@@ -3943,18 +2060,7 @@
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-glob@^5.0.3:
-  version "5.0.15"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
-  integrity sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=
-  dependencies:
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "2 || 3"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
-glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
+glob@^7.1.1, glob@^7.1.3:
   version "7.1.6"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
   integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@@ -3966,118 +2072,27 @@
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-global-dirs@^0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445"
-  integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=
-  dependencies:
-    ini "^1.3.4"
-
-global-modules@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
-  integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==
-  dependencies:
-    global-prefix "^1.0.1"
-    is-windows "^1.0.1"
-    resolve-dir "^1.0.0"
-
-global-prefix@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe"
-  integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=
-  dependencies:
-    expand-tilde "^2.0.2"
-    homedir-polyfill "^1.0.1"
-    ini "^1.3.4"
-    is-windows "^1.0.1"
-    which "^1.2.14"
-
 globals@^11.1.0:
   version "11.12.0"
   resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
   integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
 
-globals@^9.18.0:
-  version "9.18.0"
-  resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
-  integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==
-
-got@^6.7.1:
-  version "6.7.1"
-  resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0"
-  integrity sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=
-  dependencies:
-    create-error-class "^3.0.0"
-    duplexer3 "^0.1.4"
-    get-stream "^3.0.0"
-    is-redirect "^1.0.0"
-    is-retry-allowed "^1.0.0"
-    is-stream "^1.0.0"
-    lowercase-keys "^1.0.0"
-    safe-buffer "^5.0.1"
-    timed-out "^4.0.0"
-    unzip-response "^2.0.1"
-    url-parse-lax "^1.0.0"
-
-graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.2.0:
+graceful-fs@^4.1.2, graceful-fs@^4.1.6:
   version "4.2.3"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
   integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
 
-"graceful-readlink@>= 1.0.0":
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
-  integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=
-
 growl@1.10.5:
   version "1.10.5"
   resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
   integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==
 
-growl@1.9.2:
-  version "1.9.2"
-  resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f"
-  integrity sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=
-
-gulp-if@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/gulp-if/-/gulp-if-2.0.2.tgz#a497b7e7573005041caa2bc8b7dda3c80444d629"
-  integrity sha1-pJe351cwBQQcqivIt92jyARE1ik=
-  dependencies:
-    gulp-match "^1.0.3"
-    ternary-stream "^2.0.1"
-    through2 "^2.0.1"
-
-gulp-match@^1.0.3:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/gulp-match/-/gulp-match-1.1.0.tgz#552b7080fc006ee752c90563f9fec9d61aafdf4f"
-  integrity sha512-DlyVxa1Gj24DitY2OjEsS+X6tDpretuxD6wTfhXE/Rw2hweqc1f6D/XtsJmoiCwLWfXgR87W9ozEityPCVzGtQ==
-  dependencies:
-    minimatch "^3.0.3"
-
-gulp-sourcemaps@1.6.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz#b86ff349d801ceb56e1d9e7dc7bbcb4b7dee600c"
-  integrity sha1-uG/zSdgBzrVuHZ59x7vLS33uYAw=
-  dependencies:
-    convert-source-map "^1.1.1"
-    graceful-fs "^4.1.2"
-    strip-bom "^2.0.0"
-    through2 "^2.0.0"
-    vinyl "^1.0.0"
-
-handle-thing@^1.2.5:
-  version "1.2.5"
-  resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4"
-  integrity sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=
-
 har-schema@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
   integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
 
-har-validator@~5.1.0, har-validator@~5.1.3:
+har-validator@~5.1.3:
   version "5.1.3"
   resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
   integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
@@ -4085,13 +2100,6 @@
     ajv "^6.5.5"
     har-schema "^2.0.0"
 
-has-ansi@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
-  integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=
-  dependencies:
-    ansi-regex "^2.0.0"
-
 has-binary2@~1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d"
@@ -4099,62 +2107,26 @@
   dependencies:
     isarray "2.0.1"
 
-has-color@~0.1.0:
-  version "0.1.7"
-  resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f"
-  integrity sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=
-
 has-cors@1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
   integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=
 
-has-flag@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
-  integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=
-
 has-flag@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
   integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
 
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
 has-symbols@^1.0.0, has-symbols@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
   integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
 
-has-value@^0.3.1:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
-  integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=
-  dependencies:
-    get-value "^2.0.3"
-    has-values "^0.1.4"
-    isobject "^2.0.0"
-
-has-value@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
-  integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=
-  dependencies:
-    get-value "^2.0.6"
-    has-values "^1.0.0"
-    isobject "^3.0.0"
-
-has-values@^0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
-  integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E=
-
-has-values@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
-  integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=
-  dependencies:
-    is-number "^3.0.0"
-    kind-of "^4.0.0"
-
 has@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
@@ -4162,51 +2134,16 @@
   dependencies:
     function-bind "^1.1.1"
 
-he@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
-  integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0=
-
-he@1.2.0, he@1.2.x, he@^1.2.0:
+he@1.2.0, he@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
   integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
 
-homedir-polyfill@^1.0.1:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"
-  integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==
-  dependencies:
-    parse-passwd "^1.0.0"
-
 hosted-git-info@^2.1.4:
   version "2.8.5"
   resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c"
   integrity sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==
 
-hpack.js@^2.1.6:
-  version "2.1.6"
-  resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
-  integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=
-  dependencies:
-    inherits "^2.0.1"
-    obuf "^1.0.0"
-    readable-stream "^2.0.1"
-    wbuf "^1.1.0"
-
-html-minifier@^3.5.10:
-  version "3.5.21"
-  resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.21.tgz#d0040e054730e354db008463593194015212d20c"
-  integrity sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==
-  dependencies:
-    camel-case "3.0.x"
-    clean-css "4.2.x"
-    commander "2.17.x"
-    he "1.2.x"
-    param-case "2.1.x"
-    relateurl "0.2.x"
-    uglify-js "3.4.x"
-
 html-minifier@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-4.0.0.tgz#cca9aad8bce1175e02e17a8c33e46d8988889f56"
@@ -4228,11 +2165,6 @@
     deep-equal "~1.0.1"
     http-errors "~1.7.2"
 
-http-deceiver@^1.2.7:
-  version "1.2.7"
-  resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
-  integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=
-
 http-errors@1.7.2:
   version "1.7.2"
   resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
@@ -4265,17 +2197,7 @@
     setprototypeof "1.1.0"
     statuses ">= 1.4.0 < 2"
 
-http-proxy-middleware@^0.17.2:
-  version "0.17.4"
-  resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833"
-  integrity sha1-ZC6ISIUdZvCdTxJJEoRtuutBuDM=
-  dependencies:
-    http-proxy "^1.16.2"
-    is-glob "^3.1.0"
-    lodash "^4.17.2"
-    micromatch "^2.3.11"
-
-http-proxy@^1.13.0, http-proxy@^1.16.2:
+http-proxy@^1.13.0:
   version "1.18.0"
   resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a"
   integrity sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==
@@ -4293,22 +2215,6 @@
     jsprim "^1.2.2"
     sshpk "^1.7.0"
 
-https-proxy-agent@^2.2.1:
-  version "2.2.4"
-  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b"
-  integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==
-  dependencies:
-    agent-base "^4.3.0"
-    debug "^3.1.0"
-
-https-proxy-agent@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81"
-  integrity sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==
-  dependencies:
-    agent-base "^4.3.0"
-    debug "^3.1.0"
-
 iconv-lite@0.4.24:
   version "0.4.24"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@@ -4316,33 +2222,6 @@
   dependencies:
     safer-buffer ">= 2.1.2 < 3"
 
-ieee754@^1.1.4:
-  version "1.1.13"
-  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
-  integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
-
-import-lazy@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
-  integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=
-
-imurmurhash@^0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
-  integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
-
-indent-string@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80"
-  integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=
-  dependencies:
-    repeating "^2.0.0"
-
-indent@0.0.2:
-  version "0.0.2"
-  resolved "https://registry.yarnpkg.com/indent/-/indent-0.0.2.tgz#8c79f080190559b687034b84c7aefa97d5a911d9"
-  integrity sha1-jHnwgBkFWbaHA0uEx676l9WpEdk=
-
 indexof@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
@@ -4356,7 +2235,7 @@
     once "^1.3.0"
     wrappy "1"
 
-inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
+inherits@2, inherits@2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -4366,11 +2245,6 @@
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
   integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
 
-ini@^1.3.4, ini@~1.3.0:
-  version "1.3.5"
-  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
-  integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
-
 intersection-observer@^0.7.0:
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.7.0.tgz#ee16bee978db53516ead2f0a8154b09b400bbdc9"
@@ -4383,40 +2257,11 @@
   dependencies:
     loose-envify "^1.0.0"
 
-ipaddr.js@1.9.0:
-  version "1.9.0"
-  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65"
-  integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==
-
-is-accessor-descriptor@^0.1.6:
-  version "0.1.6"
-  resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
-  integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=
-  dependencies:
-    kind-of "^3.0.2"
-
-is-accessor-descriptor@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
-  integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==
-  dependencies:
-    kind-of "^6.0.0"
-
-is-arguments@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3"
-  integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==
-
 is-arrayish@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
   integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
 
-is-arrayish@^0.3.1:
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
-  integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
-
 is-binary-path@~2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
@@ -4424,11 +2269,6 @@
   dependencies:
     binary-extensions "^2.0.0"
 
-is-buffer@^1.1.5, is-buffer@~1.1.1:
-  version "1.1.6"
-  resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
-  integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
-
 is-buffer@~2.0.3:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623"
@@ -4439,91 +2279,16 @@
   resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
   integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==
 
-is-ci@^1.0.10:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c"
-  integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==
-  dependencies:
-    ci-info "^1.5.0"
-
-is-data-descriptor@^0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
-  integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=
-  dependencies:
-    kind-of "^3.0.2"
-
-is-data-descriptor@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
-  integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==
-  dependencies:
-    kind-of "^6.0.0"
-
 is-date-object@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
   integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
 
-is-descriptor@^0.1.0:
-  version "0.1.6"
-  resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
-  integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==
-  dependencies:
-    is-accessor-descriptor "^0.1.6"
-    is-data-descriptor "^0.1.4"
-    kind-of "^5.0.0"
-
-is-descriptor@^1.0.0, is-descriptor@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
-  integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==
-  dependencies:
-    is-accessor-descriptor "^1.0.0"
-    is-data-descriptor "^1.0.0"
-    kind-of "^6.0.2"
-
-is-dotfile@^1.0.0:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
-  integrity sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=
-
-is-equal-shallow@^0.1.3:
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534"
-  integrity sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=
-  dependencies:
-    is-primitive "^2.0.0"
-
-is-extendable@^0.1.0, is-extendable@^0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
-  integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=
-
-is-extendable@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
-  integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==
-  dependencies:
-    is-plain-object "^2.0.4"
-
-is-extglob@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0"
-  integrity sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=
-
-is-extglob@^2.1.0, is-extglob@^2.1.1:
+is-extglob@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
   integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
 
-is-finite@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa"
-  integrity sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=
-  dependencies:
-    number-is-nan "^1.0.0"
-
 is-fullwidth-code-point@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
@@ -4534,20 +2299,6 @@
   resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522"
   integrity sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==
 
-is-glob@^2.0.0, is-glob@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
-  integrity sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=
-  dependencies:
-    is-extglob "^1.0.0"
-
-is-glob@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
-  integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=
-  dependencies:
-    is-extglob "^2.1.0"
-
 is-glob@^4.0.1, is-glob@~4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
@@ -4555,82 +2306,16 @@
   dependencies:
     is-extglob "^2.1.1"
 
-is-installed-globally@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80"
-  integrity sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=
-  dependencies:
-    global-dirs "^0.1.0"
-    is-path-inside "^1.0.0"
-
 is-module@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
   integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=
 
-is-npm@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4"
-  integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ=
-
-is-number@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
-  integrity sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=
-  dependencies:
-    kind-of "^3.0.2"
-
-is-number@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
-  integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=
-  dependencies:
-    kind-of "^3.0.2"
-
-is-number@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff"
-  integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==
-
 is-number@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
   integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
 
-is-obj@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
-  integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8=
-
-is-path-inside@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036"
-  integrity sha1-jvW33lBDej/cprToZe96pVy0gDY=
-  dependencies:
-    path-is-inside "^1.0.1"
-
-is-plain-object@^2.0.3, is-plain-object@^2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
-  integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
-  dependencies:
-    isobject "^3.0.1"
-
-is-posix-bracket@^0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
-  integrity sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=
-
-is-primitive@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
-  integrity sha1-IHurkWOEmcB7Kt8kCkGochADRXU=
-
-is-redirect@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
-  integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=
-
 is-regex@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae"
@@ -4638,16 +2323,6 @@
   dependencies:
     has "^1.0.3"
 
-is-retry-allowed@^1.0.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4"
-  integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==
-
-is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
-  integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
-
 is-stream@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
@@ -4665,21 +2340,6 @@
   resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
   integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
 
-is-utf8@^0.2.0:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
-  integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
-
-is-valid-glob@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-0.3.0.tgz#d4b55c69f51886f9b65c70d6c2622d37e29f48fe"
-  integrity sha1-1LVcafUYhvm2XHDWwmItN+KfSP4=
-
-is-windows@^1.0.1, is-windows@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
-  integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
-
 is-wsl@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
@@ -4690,11 +2350,6 @@
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
   integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
 
-isarray@1.0.0, isarray@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
-  integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
-
 isarray@2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e"
@@ -4717,18 +2372,6 @@
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
   integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
 
-isobject@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
-  integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=
-  dependencies:
-    isarray "1.0.0"
-
-isobject@^3.0.0, isobject@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
-  integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
-
 isstream@~0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@@ -4757,11 +2400,6 @@
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
   integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
 
-js-tokens@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
-  integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
-
 js-yaml@3.13.1:
   version "3.13.1"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
@@ -4775,11 +2413,6 @@
   resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
   integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
 
-jsesc@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
-  integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s=
-
 jsesc@^2.5.1:
   version "2.5.2"
   resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
@@ -4805,28 +2438,11 @@
   resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
   integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
 
-json-stable-stringify-without-jsonify@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
-  integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
-
 json-stringify-safe@~5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
   integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
 
-json3@3.3.2:
-  version "3.3.2"
-  resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
-  integrity sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=
-
-json5@^2.1.0:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.1.tgz#81b6cb04e9ba496f1c7005d07b4368a2638f90b6"
-  integrity sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==
-  dependencies:
-    minimist "^1.2.0"
-
 json5@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.2.tgz#43ef1f0af9835dd624751a6b7fa48874fb2d608e"
@@ -4841,11 +2457,6 @@
   optionalDependencies:
     graceful-fs "^4.1.6"
 
-jsonschema@^1.1.0, jsonschema@^1.1.1:
-  version "1.2.5"
-  resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.2.5.tgz#bab69d97fa28946aec0a56a9cc266d23fe80ae61"
-  integrity sha512-kVTF+08x25PQ0CjuVc0gRM9EUPb0Fe9Ln/utFOgcdxEIOHuU7ooBk/UPTd7t1M91pP35m0MU1T8M5P7vP1bRRw==
-
 jsprim@^1.2.2:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -4856,6 +2467,11 @@
     json-schema "0.2.3"
     verror "1.10.0"
 
+just-extend@^4.0.2:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.1.0.tgz#7278a4027d889601640ee0ce0e5a00b992467da4"
+  integrity sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==
+
 karma-chrome-launcher@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz#805a586799a4d05f4e54f72a204979f3f3066738"
@@ -4918,30 +2534,6 @@
   dependencies:
     tsscmp "1.0.6"
 
-kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
-  version "3.2.2"
-  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
-  integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=
-  dependencies:
-    is-buffer "^1.1.5"
-
-kind-of@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
-  integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc=
-  dependencies:
-    is-buffer "^1.1.5"
-
-kind-of@^5.0.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
-  integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
-
-kind-of@^6.0.0, kind-of@^6.0.2:
-  version "6.0.3"
-  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
-  integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
-
 koa-compose@^3.0.0:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7"
@@ -5033,41 +2625,6 @@
     type-is "^1.6.16"
     vary "^1.1.2"
 
-kuler@1.0.x:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/kuler/-/kuler-1.0.1.tgz#ef7c784f36c9fb6e16dd3150d152677b2b0228a6"
-  integrity sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==
-  dependencies:
-    colornames "^1.1.1"
-
-latest-version@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"
-  integrity sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=
-  dependencies:
-    package-json "^4.0.0"
-
-launchpad@^0.7.0:
-  version "0.7.5"
-  resolved "https://registry.yarnpkg.com/launchpad/-/launchpad-0.7.5.tgz#a16950c937572f10ef01c9be945a96f7aef8e427"
-  integrity sha512-gsYFgT8XKL3X2XZHPPPrgwM0JqeQwGpSWnzg7EYadBY3MirbQrTVq6L4fm6l7UE2T+7gnfuhiGkKr/xxuU/fdw==
-  dependencies:
-    async "^2.0.1"
-    browserstack "^1.2.0"
-    debug "^2.2.0"
-    mkdirp "^0.5.1"
-    plist "^2.0.1"
-    q "^1.4.1"
-    rimraf "^3.0.0"
-    underscore "^1.8.3"
-
-lazystream@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4"
-  integrity sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=
-  dependencies:
-    readable-stream "^2.0.5"
-
 leven@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
@@ -5080,17 +2637,6 @@
   dependencies:
     leven "^3.1.0"
 
-load-json-file@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
-  integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=
-  dependencies:
-    graceful-fs "^4.1.2"
-    parse-json "^2.2.0"
-    pify "^2.0.0"
-    pinkie-promise "^2.0.0"
-    strip-bom "^2.0.0"
-
 load-json-file@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
@@ -5117,143 +2663,32 @@
     p-locate "^3.0.0"
     path-exists "^3.0.0"
 
-lodash._baseassign@^3.0.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e"
-  integrity sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=
-  dependencies:
-    lodash._basecopy "^3.0.0"
-    lodash.keys "^3.0.0"
-
-lodash._basecopy@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36"
-  integrity sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=
-
-lodash._basecreate@^3.0.0:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821"
-  integrity sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=
-
-lodash._getnative@^3.0.0:
-  version "3.9.1"
-  resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
-  integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=
-
-lodash._isiterateecall@^3.0.0:
-  version "3.0.9"
-  resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c"
-  integrity sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=
-
-lodash._reinterpolate@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
-  integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
-
 lodash.camelcase@^4.3.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
   integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY=
 
-lodash.create@3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7"
-  integrity sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=
-  dependencies:
-    lodash._baseassign "^3.0.0"
-    lodash._basecreate "^3.0.0"
-    lodash._isiterateecall "^3.0.0"
-
-lodash.defaults@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
-  integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=
-
-lodash.difference@^4.5.0:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c"
-  integrity sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=
-
-lodash.flatten@^4.4.0:
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
-  integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
-
-lodash.isarguments@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
-  integrity sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=
-
-lodash.isarray@^3.0.0:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
-  integrity sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=
-
-lodash.isequal@^4.0.0:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
-  integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
-
-lodash.isplainobject@^4.0.6:
-  version "4.0.6"
-  resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
-  integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
-
-lodash.keys@^3.0.0:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
-  integrity sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=
-  dependencies:
-    lodash._getnative "^3.0.0"
-    lodash.isarguments "^3.0.0"
-    lodash.isarray "^3.0.0"
+lodash.get@^4.4.2:
+  version "4.4.2"
+  resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
+  integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
 
 lodash.memoize@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
   integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
 
-lodash.padend@^4.6.1:
-  version "4.6.1"
-  resolved "https://registry.yarnpkg.com/lodash.padend/-/lodash.padend-4.6.1.tgz#53ccba047d06e158d311f45da625f4e49e6f166e"
-  integrity sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4=
-
 lodash.sortby@^4.7.0:
   version "4.7.0"
   resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
   integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
 
-lodash.template@^4.4.0:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab"
-  integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==
-  dependencies:
-    lodash._reinterpolate "^3.0.0"
-    lodash.templatesettings "^4.0.0"
-
-lodash.templatesettings@^4.0.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33"
-  integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==
-  dependencies:
-    lodash._reinterpolate "^3.0.0"
-
-lodash.union@^4.6.0:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88"
-  integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=
-
 lodash.uniq@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
   integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
 
-lodash@^3.0.0, lodash@^3.10.1:
-  version "3.10.1"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
-  integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
-
-lodash@^4.0.0, lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.4:
+lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15:
   version "4.17.15"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
   integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@@ -5283,38 +2718,6 @@
     rfdc "^1.1.4"
     streamroller "^1.0.6"
 
-logform@^1.9.1:
-  version "1.10.0"
-  resolved "https://registry.yarnpkg.com/logform/-/logform-1.10.0.tgz#c9d5598714c92b546e23f4e78147c40f1e02012e"
-  integrity sha512-em5ojIhU18fIMOw/333mD+ZLE2fis0EzXl1ZwHx4iQzmpQi6odNiY/t+ITNr33JZhT9/KEaH+UPIipr6a9EjWg==
-  dependencies:
-    colors "^1.2.1"
-    fast-safe-stringify "^2.0.4"
-    fecha "^2.3.3"
-    ms "^2.1.1"
-    triple-beam "^1.2.0"
-
-logform@^2.1.1:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/logform/-/logform-2.1.2.tgz#957155ebeb67a13164069825ce67ddb5bb2dd360"
-  integrity sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==
-  dependencies:
-    colors "^1.2.1"
-    fast-safe-stringify "^2.0.4"
-    fecha "^2.3.3"
-    ms "^2.1.1"
-    triple-beam "^1.3.0"
-
-lolex@1.3.2:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.3.2.tgz#7c3da62ffcb30f0f5a80a2566ca24e45d8a01f31"
-  integrity sha1-fD2mL/yzDw9agKJWbKJORdigHzE=
-
-lolex@^1.6.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6"
-  integrity sha1-OpoCg0UqR9dDnnJzG54H1zhuSfY=
-
 loose-envify@^1.0.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@@ -5322,25 +2725,12 @@
   dependencies:
     js-tokens "^3.0.0 || ^4.0.0"
 
-loud-rejection@^1.0.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
-  integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=
-  dependencies:
-    currently-unhandled "^0.4.1"
-    signal-exit "^3.0.0"
-
 lower-case@^1.1.1:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac"
   integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw=
 
-lowercase-keys@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
-  integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
-
-lru-cache@4.1.x, lru-cache@^4.0.1, lru-cache@^4.0.2:
+lru-cache@4.1.x:
   version "4.1.5"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
   integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
@@ -5355,134 +2745,11 @@
   dependencies:
     yallist "^3.0.2"
 
-magic-string@^0.22.4:
-  version "0.22.5"
-  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e"
-  integrity sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==
-  dependencies:
-    vlq "^0.2.2"
-
-make-dir@^1.0.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
-  integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==
-  dependencies:
-    pify "^3.0.0"
-
-map-cache@^0.2.2:
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
-  integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
-
-map-obj@^1.0.0, map-obj@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
-  integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
-
-map-visit@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
-  integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=
-  dependencies:
-    object-visit "^1.0.0"
-
-matcher@^1.1.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/matcher/-/matcher-1.1.1.tgz#51d8301e138f840982b338b116bb0c09af62c1c2"
-  integrity sha512-+BmqxWIubKTRKNWx/ahnCkk3mG8m7OturVlqq6HiojGJTd5hVYbgZm6WzcYPCoB+KBT4Vd6R7WSRG2OADNaCjg==
-  dependencies:
-    escape-string-regexp "^1.0.4"
-
-math-random@^1.0.1:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c"
-  integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==
-
-md5@^2.2.1:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9"
-  integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=
-  dependencies:
-    charenc "~0.0.1"
-    crypt "~0.0.1"
-    is-buffer "~1.1.1"
-
 media-typer@0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
   integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
 
-meow@^3.7.0:
-  version "3.7.0"
-  resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
-  integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=
-  dependencies:
-    camelcase-keys "^2.0.0"
-    decamelize "^1.1.2"
-    loud-rejection "^1.0.0"
-    map-obj "^1.0.1"
-    minimist "^1.1.3"
-    normalize-package-data "^2.3.4"
-    object-assign "^4.0.1"
-    read-pkg-up "^1.0.1"
-    redent "^1.0.0"
-    trim-newlines "^1.0.0"
-
-merge-descriptors@1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
-  integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
-
-merge-stream@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1"
-  integrity sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=
-  dependencies:
-    readable-stream "^2.0.1"
-
-methods@~1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
-  integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
-
-micromatch@^2.3.11, micromatch@^2.3.7:
-  version "2.3.11"
-  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
-  integrity sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=
-  dependencies:
-    arr-diff "^2.0.0"
-    array-unique "^0.2.1"
-    braces "^1.8.2"
-    expand-brackets "^0.1.4"
-    extglob "^0.3.1"
-    filename-regex "^2.0.0"
-    is-extglob "^1.0.0"
-    is-glob "^2.0.1"
-    kind-of "^3.0.2"
-    normalize-path "^2.0.1"
-    object.omit "^2.0.0"
-    parse-glob "^3.0.4"
-    regex-cache "^0.4.2"
-
-micromatch@^3.0.4:
-  version "3.1.10"
-  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
-  integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
-  dependencies:
-    arr-diff "^4.0.0"
-    array-unique "^0.3.2"
-    braces "^2.3.1"
-    define-property "^2.0.2"
-    extend-shallow "^3.0.2"
-    extglob "^2.0.4"
-    fragment-cache "^0.2.1"
-    kind-of "^6.0.2"
-    nanomatch "^1.2.9"
-    object.pick "^1.3.0"
-    regex-not "^1.0.0"
-    snapdragon "^0.8.1"
-    to-regex "^3.0.2"
-
 mime-db@1.43.0, "mime-db@>= 1.43.0 < 2":
   version "1.43.0"
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58"
@@ -5495,34 +2762,12 @@
   dependencies:
     mime-db "1.43.0"
 
-mime@1.4.1:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
-  integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==
-
-mime@1.6.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
-  integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
-
 mime@^2.3.1:
   version "2.4.4"
   resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5"
   integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==
 
-minimalistic-assert@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
-  integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
-
-minimatch-all@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/minimatch-all/-/minimatch-all-1.1.0.tgz#40c496a27a2e128d19bf758e76bb01a0c7145787"
-  integrity sha1-QMSWonouEo0Zv3WOdrsBoMcUV4c=
-  dependencies:
-    minimatch "^3.0.2"
-
-"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
+minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
   integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
@@ -5534,11 +2779,6 @@
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
   integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
 
-minimist@^1.1.3, minimist@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
-  integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
-
 minimist@^1.2.3, minimist@^1.2.5:
   version "1.2.5"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
@@ -5549,21 +2789,6 @@
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
   integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=
 
-mixin-deep@^1.2.0:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
-  integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
-  dependencies:
-    for-in "^1.0.2"
-    is-extendable "^1.0.1"
-
-mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1:
-  version "0.5.1"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
-  integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
-  dependencies:
-    minimist "0.0.8"
-
 mkdirp@0.5.3:
   version "0.5.3"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.3.tgz#5a514b7179259287952881e94410ec5465659f8c"
@@ -5571,23 +2796,12 @@
   dependencies:
     minimist "^1.2.5"
 
-mocha@^3.4.2:
-  version "3.5.3"
-  resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.3.tgz#1e0480fe36d2da5858d1eb6acc38418b26eaa20d"
-  integrity sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg==
+mkdirp@^0.5.1:
+  version "0.5.1"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+  integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
   dependencies:
-    browser-stdout "1.3.0"
-    commander "2.9.0"
-    debug "2.6.8"
-    diff "3.2.0"
-    escape-string-regexp "1.0.5"
-    glob "7.1.1"
-    growl "1.9.2"
-    he "1.1.1"
-    json3 "3.3.2"
-    lodash.create "3.1.1"
-    mkdirp "0.5.1"
-    supports-color "3.1.2"
+    minimist "0.0.8"
 
 mocha@^7.1.1:
   version "7.1.1"
@@ -5619,11 +2833,6 @@
     yargs-parser "13.1.2"
     yargs-unparser "1.6.0"
 
-mout@^1.0.0:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/mout/-/mout-1.2.2.tgz#c9b718a499806a0632cede178e80f436259e777d"
-  integrity sha512-w0OUxFEla6z3d7sVpMZGBCpQvYh8PHS1wZ6Wu9GNKHMpAHWJ0if0LsQZh3DlOqw55HlhJEOMLpFnwtxp99Y5GA==
-
 ms@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -5639,29 +2848,7 @@
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 
-multer@^1.3.0:
-  version "1.4.2"
-  resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.2.tgz#2f1f4d12dbaeeba74cb37e623f234bf4d3d2057a"
-  integrity sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==
-  dependencies:
-    append-field "^1.0.0"
-    busboy "^0.2.11"
-    concat-stream "^1.5.2"
-    mkdirp "^0.5.1"
-    object-assign "^4.1.1"
-    on-finished "^2.3.0"
-    type-is "^1.6.4"
-    xtend "^4.0.0"
-
-multipipe@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/multipipe/-/multipipe-1.0.2.tgz#cc13efd833c9cda99f224f868461b8e1a3fd939d"
-  integrity sha1-zBPv2DPJzamfIk+GhGG44aP9k50=
-  dependencies:
-    duplexer2 "^0.1.2"
-    object-assign "^4.1.0"
-
-mz@^2.1.0, mz@^2.4.0, mz@^2.6.0, mz@^2.7.0:
+mz@^2.1.0, mz@^2.7.0:
   version "2.7.0"
   resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
   integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
@@ -5670,37 +2857,21 @@
     object-assign "^4.0.1"
     thenify-all "^1.0.0"
 
-nanomatch@^1.2.9:
-  version "1.2.13"
-  resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
-  integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==
-  dependencies:
-    arr-diff "^4.0.0"
-    array-unique "^0.3.2"
-    define-property "^2.0.2"
-    extend-shallow "^3.0.2"
-    fragment-cache "^0.2.1"
-    is-windows "^1.0.2"
-    kind-of "^6.0.2"
-    object.pick "^1.3.0"
-    regex-not "^1.0.0"
-    snapdragon "^0.8.1"
-    to-regex "^3.0.1"
-
-native-promise-only@^0.8.1:
-  version "0.8.1"
-  resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11"
-  integrity sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE=
-
 negotiator@0.6.2:
   version "0.6.2"
   resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
   integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
 
-nice-try@^1.0.4:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
-  integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+nise@^4.0.1:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/nise/-/nise-4.0.4.tgz#d73dea3e5731e6561992b8f570be9e363c4512dd"
+  integrity sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==
+  dependencies:
+    "@sinonjs/commons" "^1.7.0"
+    "@sinonjs/fake-timers" "^6.0.0"
+    "@sinonjs/text-encoding" "^0.7.1"
+    just-extend "^4.0.2"
+    path-to-regexp "^1.7.0"
 
 no-case@^2.2.0:
   version "2.3.2"
@@ -5727,15 +2898,7 @@
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.53.tgz#2d821bfa499ed7c5dffc5e2f28c88e78a08ee3f4"
   integrity sha512-wp8zyQVwef2hpZ/dJH7SfSrIPD6YoJz6BDQDpGEkcA0s3LpAQoxBIYmfIq6QAhC1DhwsyCgTaTTcONwX8qzCuQ==
 
-nomnom@^1.8.1:
-  version "1.8.1"
-  resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7"
-  integrity sha1-IVH3Ikcrp55Qp2/BJbuMjy5Nwqc=
-  dependencies:
-    chalk "~0.4.0"
-    underscore "~1.6.0"
-
-normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
+normalize-package-data@^2.3.2:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
   integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
@@ -5745,36 +2908,17 @@
     semver "2 || 3 || 4 || 5"
     validate-npm-package-license "^3.0.1"
 
-normalize-path@^2.0.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
-  integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=
-  dependencies:
-    remove-trailing-separator "^1.0.1"
-
 normalize-path@^3.0.0, normalize-path@~3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
   integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
 
-npm-run-path@^2.0.0:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
-  integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
-  dependencies:
-    path-key "^2.0.0"
-
-number-is-nan@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
-  integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
-
 oauth-sign@~0.9.0:
   version "0.9.0"
   resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
   integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
 
-object-assign@^4, object-assign@^4.0.0, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
+object-assign@^4.0.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
   integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
@@ -5784,15 +2928,6 @@
   resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291"
   integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=
 
-object-copy@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
-  integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw=
-  dependencies:
-    copy-descriptor "^0.1.0"
-    define-property "^0.2.5"
-    kind-of "^3.0.3"
-
 object-inspect@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
@@ -5803,13 +2938,6 @@
   resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
   integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
 
-object-visit@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
-  integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=
-  dependencies:
-    isobject "^3.0.0"
-
 object.assign@4.1.0, object.assign@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
@@ -5820,16 +2948,6 @@
     has-symbols "^1.0.0"
     object-keys "^1.0.11"
 
-object.entries@^1.1.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.1.tgz#ee1cf04153de02bb093fec33683900f57ce5399b"
-  integrity sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ==
-  dependencies:
-    define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
-    function-bind "^1.1.1"
-    has "^1.0.3"
-
 object.getownpropertydescriptors@^2.0.3:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649"
@@ -5838,26 +2956,6 @@
     define-properties "^1.1.3"
     es-abstract "^1.17.0-next.1"
 
-object.omit@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
-  integrity sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=
-  dependencies:
-    for-own "^0.1.4"
-    is-extendable "^0.1.1"
-
-object.pick@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
-  integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=
-  dependencies:
-    isobject "^3.0.1"
-
-obuf@^1.0.0, obuf@^1.1.1:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
-  integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
-
 on-finished@^2.3.0, on-finished@~2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
@@ -5865,11 +2963,6 @@
   dependencies:
     ee-first "1.1.1"
 
-on-headers@~1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
-  integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
-
 once@^1.3.0, once@^1.3.1, once@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@@ -5877,23 +2970,11 @@
   dependencies:
     wrappy "1"
 
-one-time@0.0.4:
-  version "0.0.4"
-  resolved "https://registry.yarnpkg.com/one-time/-/one-time-0.0.4.tgz#f8cdf77884826fe4dff93e3a9cc37b1e4480742e"
-  integrity sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=
-
 only@~0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
   integrity sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=
 
-opn@^3.0.2:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/opn/-/opn-3.0.3.tgz#b6d99e7399f78d65c3baaffef1fb288e9b85243a"
-  integrity sha1-ttmec5n3jWXDuq/+8fsojpuFJDo=
-  dependencies:
-    object-assign "^4.0.1"
-
 opn@^5.4.0:
   version "5.5.0"
   resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc"
@@ -5909,37 +2990,11 @@
     minimist "~0.0.1"
     wordwrap "~0.0.2"
 
-ordered-read-streams@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz#7137e69b3298bb342247a1bbee3881c80e2fd78b"
-  integrity sha1-cTfmmzKYuzQiR6G77jiByA4v14s=
-  dependencies:
-    is-stream "^1.0.1"
-    readable-stream "^2.0.1"
-
-os-homedir@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
-  integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
-
-os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
+os-tmpdir@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
   integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
 
-osenv@^0.1.3:
-  version "0.1.5"
-  resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
-  integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==
-  dependencies:
-    os-homedir "^1.0.0"
-    os-tmpdir "^1.0.0"
-
-p-finally@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
-  integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
-
 p-limit@^1.1.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
@@ -5978,40 +3033,13 @@
   resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
   integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
 
-package-json@^4.0.0:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed"
-  integrity sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=
-  dependencies:
-    got "^6.7.1"
-    registry-auth-token "^3.0.1"
-    registry-url "^3.0.3"
-    semver "^5.1.0"
-
-param-case@2.1.x, param-case@^2.1.1:
+param-case@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247"
   integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc=
   dependencies:
     no-case "^2.2.0"
 
-parse-glob@^3.0.4:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
-  integrity sha1-ssN2z7EfNVE7rdFz7wu246OIORw=
-  dependencies:
-    glob-base "^0.3.0"
-    is-dotfile "^1.0.0"
-    is-extglob "^1.0.0"
-    is-glob "^2.0.0"
-
-parse-json@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
-  integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=
-  dependencies:
-    error-ex "^1.2.0"
-
 parse-json@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
@@ -6020,16 +3048,6 @@
     error-ex "^1.3.1"
     json-parse-better-errors "^1.0.1"
 
-parse-passwd@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
-  integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
-
-parse5@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
-  integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==
-
 parse5@^5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
@@ -6054,23 +3072,6 @@
   resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
   integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
 
-pascalcase@^0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
-  integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
-
-path-dirname@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
-  integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=
-
-path-exists@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
-  integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=
-  dependencies:
-    pinkie-promise "^2.0.0"
-
 path-exists@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
@@ -6081,42 +3082,23 @@
   resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
   integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
 
-path-is-inside@^1.0.1, path-is-inside@^1.0.2:
+path-is-inside@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
   integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
 
-path-key@^2.0.0, path-key@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
-  integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
-
 path-parse@^1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
   integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
 
-path-to-regexp@0.1.7:
-  version "0.1.7"
-  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
-  integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
-
-path-to-regexp@^1.0.1, path-to-regexp@^1.7.0:
+path-to-regexp@^1.7.0:
   version "1.8.0"
   resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
   integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
   dependencies:
     isarray "0.0.1"
 
-path-type@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
-  integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=
-  dependencies:
-    graceful-fs "^4.1.2"
-    pify "^2.0.0"
-    pinkie-promise "^2.0.0"
-
 path-type@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
@@ -6129,21 +3111,6 @@
   resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0"
   integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA=
 
-pem@^1.8.3:
-  version "1.14.3"
-  resolved "https://registry.yarnpkg.com/pem/-/pem-1.14.3.tgz#347e5a5c194a5f7612b88083e45042fcc4fb4901"
-  integrity sha512-Q+AMVMD3fzeVvZs5PHeI+pVt0hgZY2fjhkliBW43qyONLgCXPVk1ryim43F9eupHlNGLJNT5T/NNrzhUdiC5Zg==
-  dependencies:
-    es6-promisify "^6.0.0"
-    md5 "^2.2.1"
-    os-tmpdir "^1.0.1"
-    which "^1.3.1"
-
-pend@~1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
-  integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
-
 performance-now@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
@@ -6154,28 +3121,11 @@
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
   integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
 
-pify@^2.0.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
-  integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
-
 pify@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
   integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
 
-pinkie-promise@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
-  integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o=
-  dependencies:
-    pinkie "^2.0.0"
-
-pinkie@^2.0.0:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
-  integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
-
 pkg-up@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f"
@@ -6183,24 +3133,6 @@
   dependencies:
     find-up "^2.1.0"
 
-plist@^2.0.1:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/plist/-/plist-2.1.0.tgz#57ccdb7a0821df21831217a3cad54e3e146a1025"
-  integrity sha1-V8zbeggh3yGDEhejytVOPhRqECU=
-  dependencies:
-    base64-js "1.2.0"
-    xmlbuilder "8.2.2"
-    xmldom "0.1.x"
-
-plylog@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/plylog/-/plylog-1.1.0.tgz#f6f354e2ae0b01f6db4ed111f4b3855da9c37417"
-  integrity sha512-/QnY5aSVaP54va6hruzNtAj02HpsLlAt7V5EndMrtq6ZUTZJKUja43rgiUtGXqm95yrSJjbZoPW0yQQQwLpoJA==
-  dependencies:
-    logform "^1.9.1"
-    winston "^3.0.0"
-    winston-transport "^4.2.0"
-
 polyfills-loader@^1.5.2:
   version "1.5.2"
   resolved "https://registry.yarnpkg.com/polyfills-loader/-/polyfills-loader-1.5.2.tgz#2fe63063da0d74aa69b611bd189d64ee443358c7"
@@ -6223,195 +3155,6 @@
     terser "^4.6.4"
     whatwg-fetch "^3.0.0"
 
-polymer-analyzer@^3.1.3, polymer-analyzer@^3.2.2:
-  version "3.2.4"
-  resolved "https://registry.yarnpkg.com/polymer-analyzer/-/polymer-analyzer-3.2.4.tgz#7d76356620a2328e8bc9e30e47069f9729260ca1"
-  integrity sha512-JmxUhMajTuC18tLXbTtu2+aN2x9bTX+4MvCD4IZKJ0rtAL8jWi1iRLfogpHJB4Ig9Dc8EEEuEYipLuzPFl3vqA==
-  dependencies:
-    "@babel/generator" "^7.0.0-beta.42"
-    "@babel/traverse" "^7.0.0-beta.42"
-    "@babel/types" "^7.0.0-beta.42"
-    "@types/babel-generator" "^6.25.1"
-    "@types/babel-traverse" "^6.25.2"
-    "@types/babel-types" "^6.25.1"
-    "@types/babylon" "^6.16.2"
-    "@types/chai-subset" "^1.3.0"
-    "@types/chalk" "^0.4.30"
-    "@types/clone" "^0.1.30"
-    "@types/cssbeautify" "^0.3.1"
-    "@types/doctrine" "^0.0.1"
-    "@types/is-windows" "^0.2.0"
-    "@types/minimatch" "^3.0.1"
-    "@types/parse5" "^2.2.34"
-    "@types/path-is-inside" "^1.0.0"
-    "@types/resolve" "0.0.6"
-    "@types/whatwg-url" "^6.4.0"
-    babylon "^7.0.0-beta.42"
-    cancel-token "^0.1.1"
-    chalk "^1.1.3"
-    clone "^2.0.0"
-    cssbeautify "^0.3.1"
-    doctrine "^2.0.2"
-    dom5 "^3.0.0"
-    indent "0.0.2"
-    is-windows "^1.0.2"
-    jsonschema "^1.1.0"
-    minimatch "^3.0.4"
-    parse5 "^4.0.0"
-    path-is-inside "^1.0.2"
-    resolve "^1.5.0"
-    shady-css-parser "^0.1.0"
-    stable "^0.1.6"
-    strip-indent "^2.0.0"
-    vscode-uri "=1.0.6"
-    whatwg-url "^6.4.0"
-
-polymer-build@^3.1.0:
-  version "3.1.4"
-  resolved "https://registry.yarnpkg.com/polymer-build/-/polymer-build-3.1.4.tgz#ab539f1a13d803518b13b73ffd09198431d98142"
-  integrity sha512-OhTOPG5Y/tK2HqGZ5XA/CVDh+TuOaDv7wTZWXDCg6hxeMgNKuljDMn2coyGU5NLM0pLbS+gwFAc2ZJ5cWHCHNg==
-  dependencies:
-    "@babel/core" "^7.0.0"
-    "@babel/plugin-external-helpers" "^7.0.0"
-    "@babel/plugin-proposal-async-generator-functions" "^7.0.0"
-    "@babel/plugin-proposal-object-rest-spread" "^7.0.0"
-    "@babel/plugin-syntax-async-generators" "^7.0.0"
-    "@babel/plugin-syntax-dynamic-import" "^7.0.0"
-    "@babel/plugin-syntax-import-meta" "^7.0.0"
-    "@babel/plugin-syntax-object-rest-spread" "^7.0.0"
-    "@babel/plugin-transform-arrow-functions" "^7.0.0"
-    "@babel/plugin-transform-async-to-generator" "^7.0.0"
-    "@babel/plugin-transform-block-scoped-functions" "^7.0.0"
-    "@babel/plugin-transform-block-scoping" "^7.0.0"
-    "@babel/plugin-transform-classes" "^7.0.0"
-    "@babel/plugin-transform-computed-properties" "^7.0.0"
-    "@babel/plugin-transform-destructuring" "^7.0.0"
-    "@babel/plugin-transform-duplicate-keys" "^7.0.0"
-    "@babel/plugin-transform-exponentiation-operator" "^7.0.0"
-    "@babel/plugin-transform-for-of" "^7.0.0"
-    "@babel/plugin-transform-function-name" "^7.0.0"
-    "@babel/plugin-transform-instanceof" "^7.0.0"
-    "@babel/plugin-transform-literals" "^7.0.0"
-    "@babel/plugin-transform-modules-amd" "^7.0.0"
-    "@babel/plugin-transform-object-super" "^7.0.0"
-    "@babel/plugin-transform-parameters" "^7.0.0"
-    "@babel/plugin-transform-regenerator" "^7.0.0"
-    "@babel/plugin-transform-shorthand-properties" "^7.0.0"
-    "@babel/plugin-transform-spread" "^7.0.0"
-    "@babel/plugin-transform-sticky-regex" "^7.0.0"
-    "@babel/plugin-transform-template-literals" "^7.0.0"
-    "@babel/plugin-transform-typeof-symbol" "^7.0.0"
-    "@babel/plugin-transform-unicode-regex" "^7.0.0"
-    "@babel/traverse" "^7.0.0"
-    "@polymer/esm-amd-loader" "^1.0.0"
-    "@types/babel-types" "^6.25.1"
-    "@types/babylon" "^6.16.2"
-    "@types/gulp-if" "0.0.33"
-    "@types/html-minifier" "^3.5.1"
-    "@types/is-windows" "^0.2.0"
-    "@types/mz" "0.0.31"
-    "@types/parse5" "^2.2.34"
-    "@types/resolve" "0.0.7"
-    "@types/uuid" "^3.4.3"
-    "@types/vinyl" "^2.0.0"
-    "@types/vinyl-fs" "^2.4.8"
-    babel-plugin-minify-guarded-expressions "^0.4.3"
-    babel-preset-minify "^0.5.0"
-    babylon "^7.0.0-beta.42"
-    css-slam "^2.1.2"
-    dom5 "^3.0.0"
-    gulp-if "^2.0.2"
-    html-minifier "^3.5.10"
-    matcher "^1.1.0"
-    multipipe "^1.0.2"
-    mz "^2.6.0"
-    parse5 "^4.0.0"
-    plylog "^1.0.0"
-    polymer-analyzer "^3.1.3"
-    polymer-bundler "^4.0.9"
-    polymer-project-config "^4.0.3"
-    regenerator-runtime "^0.11.1"
-    stream "0.0.2"
-    sw-precache "^5.1.1"
-    uuid "^3.2.1"
-    vinyl "^1.2.0"
-    vinyl-fs "^2.4.4"
-
-polymer-bundler@^4.0.9:
-  version "4.0.10"
-  resolved "https://registry.yarnpkg.com/polymer-bundler/-/polymer-bundler-4.0.10.tgz#abc8d33977652f031068d034c8104841e80d4cbb"
-  integrity sha512-nwlN3LQlQDqbZ2sUH3394C/dHZUDHq8tpdS5HARvPDb0Q9IXWD+znOR1cr7wSjF0EZN4LiUH5hWyUoV4QSjhpQ==
-  dependencies:
-    "@types/babel-generator" "^6.25.1"
-    "@types/babel-traverse" "^6.25.3"
-    babel-generator "^6.26.1"
-    babel-traverse "^6.26.0"
-    babel-types "^6.26.0"
-    clone "^2.1.0"
-    command-line-args "^5.0.2"
-    command-line-usage "^5.0.5"
-    dom5 "^3.0.0"
-    espree "^3.5.2"
-    magic-string "^0.22.4"
-    mkdirp "^0.5.1"
-    parse5 "^4.0.0"
-    polymer-analyzer "^3.2.2"
-    rollup "^1.3.0"
-    source-map "^0.5.6"
-    vscode-uri "=1.0.6"
-
-polymer-project-config@^4.0.0, polymer-project-config@^4.0.3:
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/polymer-project-config/-/polymer-project-config-4.0.3.tgz#ef0c1a676ce4809907986c8e910745660de8024f"
-  integrity sha512-Drr+Imq+znhBC8XSt9pMlmPixoGnIOmleV5SD6mto1zOGC5oCDbSNsQL2v89DWOk+9aSUO79vnWwOmEPDSvYfw==
-  dependencies:
-    "@types/parse5" "^2.2.34"
-    browser-capabilities "^1.0.0"
-    jsonschema "^1.1.1"
-    minimatch-all "^1.1.0"
-    plylog "^1.0.0"
-    winston "^3.0.0"
-
-polyserve@^0.27.13:
-  version "0.27.15"
-  resolved "https://registry.yarnpkg.com/polyserve/-/polyserve-0.27.15.tgz#261fa5a0873c8d95fd7068598f44c9dac20cf9c4"
-  integrity sha512-AaFgANt+tUUVgHLw+BnaVYcn649JiwL1ru0TOZUKj1gGGn/Bq2S16gxql+1muGpRaAsgFu13Zu7k5XkwatwwSg==
-  dependencies:
-    "@types/compression" "^0.0.33"
-    "@types/content-type" "^1.1.0"
-    "@types/escape-html" "0.0.20"
-    "@types/express" "^4.0.36"
-    "@types/mime" "^2.0.0"
-    "@types/mz" "0.0.29"
-    "@types/opn" "^3.0.28"
-    "@types/parse5" "^2.2.34"
-    "@types/pem" "^1.8.1"
-    "@types/resolve" "0.0.6"
-    "@types/serve-static" "^1.7.31"
-    "@types/spdy" "^3.4.1"
-    bower-config "^1.4.1"
-    browser-capabilities "^1.0.0"
-    command-line-args "^5.0.2"
-    command-line-usage "^5.0.5"
-    compression "^1.6.2"
-    content-type "^1.0.2"
-    cors "^2.8.4"
-    escape-html "^1.0.3"
-    express "^4.8.5"
-    find-port "^1.0.1"
-    http-proxy-middleware "^0.17.2"
-    lru-cache "^4.0.2"
-    mime "^2.3.1"
-    mz "^2.4.0"
-    opn "^3.0.2"
-    pem "^1.8.3"
-    polymer-build "^3.1.0"
-    polymer-project-config "^4.0.0"
-    requirejs "^2.3.4"
-    resolve "^1.5.0"
-    send "^0.16.2"
-    spdy "^3.3.3"
-
 portfinder@^1.0.21:
   version "1.0.25"
   resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.25.tgz#254fd337ffba869f4b9d37edc298059cb4d35eca"
@@ -6421,59 +3164,16 @@
     debug "^3.1.1"
     mkdirp "^0.5.1"
 
-posix-character-classes@^0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
-  integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
-
-prepend-http@^1.0.1:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
-  integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
-
-preserve@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
-  integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=
-
-pretty-bytes@^4.0.2:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9"
-  integrity sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk=
-
-private@^0.1.6, private@^0.1.8:
+private@^0.1.8:
   version "0.1.8"
   resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
   integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
 
-process-nextick-args@~2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
-  integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
-
-progress@2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
-  integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
-
-proxy-addr@~2.0.5:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34"
-  integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==
-  dependencies:
-    forwarded "~0.1.2"
-    ipaddr.js "1.9.0"
-
 pseudomap@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
   integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
 
-psl@^1.1.24:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c"
-  integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==
-
 psl@^1.1.28:
   version "1.8.0"
   resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
@@ -6487,21 +3187,11 @@
     end-of-stream "^1.1.0"
     once "^1.3.1"
 
-punycode@^1.4.1:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
-  integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
-
 punycode@^2.1.0, punycode@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
-q@^1.4.1, q@^1.5.1:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
-  integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
-
 qjobs@^1.1.4:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071"
@@ -6517,16 +3207,7 @@
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
   integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
 
-randomatic@^3.0.0:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed"
-  integrity sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==
-  dependencies:
-    is-number "^4.0.0"
-    kind-of "^6.0.0"
-    math-random "^1.0.1"
-
-range-parser@^1.2.0, range-parser@~1.2.0, range-parser@~1.2.1:
+range-parser@^1.2.0:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
   integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
@@ -6541,24 +3222,6 @@
     iconv-lite "0.4.24"
     unpipe "1.0.0"
 
-rc@^1.0.1, rc@^1.1.6:
-  version "1.2.8"
-  resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
-  integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
-  dependencies:
-    deep-extend "^0.6.0"
-    ini "~1.3.0"
-    minimist "^1.2.0"
-    strip-json-comments "~2.0.1"
-
-read-pkg-up@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
-  integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=
-  dependencies:
-    find-up "^1.0.0"
-    read-pkg "^1.0.0"
-
 read-pkg-up@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978"
@@ -6567,15 +3230,6 @@
     find-up "^3.0.0"
     read-pkg "^3.0.0"
 
-read-pkg@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
-  integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=
-  dependencies:
-    load-json-file "^1.0.0"
-    normalize-package-data "^2.3.2"
-    path-type "^1.0.0"
-
 read-pkg@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
@@ -6585,48 +3239,6 @@
     normalize-package-data "^2.3.2"
     path-type "^3.0.0"
 
-readable-stream@1.1.x:
-  version "1.1.14"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
-  integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk=
-  dependencies:
-    core-util-is "~1.0.0"
-    inherits "~2.0.1"
-    isarray "0.0.1"
-    string_decoder "~0.10.x"
-
-"readable-stream@>=1.0.33-1 <1.1.0-0":
-  version "1.0.34"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
-  integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=
-  dependencies:
-    core-util-is "~1.0.0"
-    inherits "~2.0.1"
-    isarray "0.0.1"
-    string_decoder "~0.10.x"
-
-readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
-  version "2.3.7"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
-  integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
-  dependencies:
-    core-util-is "~1.0.0"
-    inherits "~2.0.3"
-    isarray "~1.0.0"
-    process-nextick-args "~2.0.0"
-    safe-buffer "~5.1.1"
-    string_decoder "~1.1.1"
-    util-deprecate "~1.0.1"
-
-readable-stream@^3.0.1, readable-stream@^3.1.1, readable-stream@^3.4.0:
-  version "3.5.0"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.5.0.tgz#465d70e6d1087f6162d079cd0b5db7fbebfd1606"
-  integrity sha512-gSz026xs2LfxBPudDuI41V1lka8cxg64E66SGe78zJlsUofOg/yqwezdIcdfwik6B4h8LFmWPA9ef9X3FiNFLA==
-  dependencies:
-    inherits "^2.0.3"
-    string_decoder "^1.1.1"
-    util-deprecate "^1.0.1"
-
 readdirp@~3.2.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.2.0.tgz#c30c33352b12c96dfb4b895421a49fd5a9593839"
@@ -6641,19 +3253,6 @@
   dependencies:
     picomatch "^2.0.7"
 
-redent@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
-  integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=
-  dependencies:
-    indent-string "^2.1.0"
-    strip-indent "^1.0.1"
-
-reduce-flatten@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-1.0.1.tgz#258c78efd153ddf93cb561237f61184f3696e327"
-  integrity sha1-JYx479FT3fk8tWEjf2EYTzaW4yc=
-
 reduce-flatten@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27"
@@ -6678,23 +3277,11 @@
   resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
   integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
 
-regenerator-runtime@^0.11.0, regenerator-runtime@^0.11.1:
-  version "0.11.1"
-  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
-  integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
-
 regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4:
   version "0.13.5"
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"
   integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==
 
-regenerator-transform@^0.14.0:
-  version "0.14.1"
-  resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.1.tgz#3b2fce4e1ab7732c08f665dfdb314749c7ddd2fb"
-  integrity sha512-flVuee02C3FKRISbxhXl9mGzdbWUVHubl1SMaknjxkFB1/iqpJhArQUvRxOOPEc/9tAiX0BaQ28FJH10E4isSQ==
-  dependencies:
-    private "^0.1.6"
-
 regenerator-transform@^0.14.2:
   version "0.14.4"
   resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.4.tgz#5266857896518d1616a78a0479337a30ea974cc7"
@@ -6703,21 +3290,6 @@
     "@babel/runtime" "^7.8.4"
     private "^0.1.8"
 
-regex-cache@^0.4.2:
-  version "0.4.4"
-  resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd"
-  integrity sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==
-  dependencies:
-    is-equal-shallow "^0.1.3"
-
-regex-not@^1.0.0, regex-not@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
-  integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==
-  dependencies:
-    extend-shallow "^3.0.2"
-    safe-regex "^1.1.0"
-
 regexpu-core@^4.6.0:
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.6.0.tgz#2037c18b327cfce8a6fea2a4ec441f2432afb8b6"
@@ -6742,21 +3314,6 @@
     unicode-match-property-ecmascript "^1.0.4"
     unicode-match-property-value-ecmascript "^1.2.0"
 
-registry-auth-token@^3.0.1:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.4.0.tgz#d7446815433f5d5ed6431cd5dca21048f66b397e"
-  integrity sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==
-  dependencies:
-    rc "^1.1.6"
-    safe-buffer "^5.0.1"
-
-registry-url@^3.0.3:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942"
-  integrity sha1-PU74cPc93h138M+aOBQyRE4XSUI=
-  dependencies:
-    rc "^1.0.1"
-
 regjsgen@^0.5.0, regjsgen@^0.5.1:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c"
@@ -6776,64 +3333,11 @@
   dependencies:
     jsesc "~0.5.0"
 
-relateurl@0.2.x, relateurl@^0.2.7:
+relateurl@^0.2.7:
   version "0.2.7"
   resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
   integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
 
-remove-trailing-separator@^1.0.1:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
-  integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
-
-repeat-element@^1.1.2:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
-  integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==
-
-repeat-string@^1.5.2, repeat-string@^1.6.1:
-  version "1.6.1"
-  resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
-  integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
-
-repeating@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
-  integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=
-  dependencies:
-    is-finite "^1.0.0"
-
-replace-ext@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924"
-  integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=
-
-request@2.88.0, request@^2.85.0:
-  version "2.88.0"
-  resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
-  integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
-  dependencies:
-    aws-sign2 "~0.7.0"
-    aws4 "^1.8.0"
-    caseless "~0.12.0"
-    combined-stream "~1.0.6"
-    extend "~3.0.2"
-    forever-agent "~0.6.1"
-    form-data "~2.3.2"
-    har-validator "~5.1.0"
-    http-signature "~1.2.0"
-    is-typedarray "~1.0.0"
-    isstream "~0.1.2"
-    json-stringify-safe "~5.0.1"
-    mime-types "~2.1.19"
-    oauth-sign "~0.9.0"
-    performance-now "^2.1.0"
-    qs "~6.5.2"
-    safe-buffer "^5.1.2"
-    tough-cookie "~2.4.3"
-    tunnel-agent "^0.6.0"
-    uuid "^3.3.2"
-
 request@^2.88.0:
   version "2.88.2"
   resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
@@ -6870,11 +3374,6 @@
   resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
   integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
 
-requirejs@^2.3.4:
-  version "2.3.6"
-  resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.6.tgz#e5093d9601c2829251258c0b9445d4d19fa9e7c9"
-  integrity sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==
-
 requires-port@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
@@ -6885,14 +3384,6 @@
   resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
   integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
 
-resolve-dir@^1.0.0, resolve-dir@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43"
-  integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=
-  dependencies:
-    expand-tilde "^2.0.0"
-    global-modules "^1.0.0"
-
 resolve-path@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/resolve-path/-/resolve-path-1.4.0.tgz#c4bda9f5efb2fce65247873ab36bb4d834fe16f7"
@@ -6901,12 +3392,7 @@
     http-errors "~1.6.2"
     path-is-absolute "1.0.1"
 
-resolve-url@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
-  integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
-
-resolve@^1.10.0, resolve@^1.3.2, resolve@^1.5.0:
+resolve@^1.10.0, resolve@^1.3.2:
   version "1.15.0"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.0.tgz#1b7ca96073ebb52e741ffd799f6b39ea462c67f5"
   integrity sha512-+hTmAldEGE80U2wJJDC1lebb5jWqvTYAfm3YZ1ckk1gBr0MnCqUKlwK1e+anaFljIl+F5tR5IoZcm4ZDA1zMQw==
@@ -6920,17 +3406,12 @@
   dependencies:
     path-parse "^1.0.6"
 
-ret@~0.1.10:
-  version "0.1.15"
-  resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
-  integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
-
 rfdc@^1.1.4:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2"
   integrity sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==
 
-rimraf@^2.5.4, rimraf@^2.6.0:
+rimraf@^2.6.0:
   version "2.7.1"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
   integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
@@ -6944,102 +3425,22 @@
   dependencies:
     glob "^7.1.3"
 
-rimraf@~2.6.2:
-  version "2.6.3"
-  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
-  integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
-  dependencies:
-    glob "^7.1.3"
-
-rollup@^1.3.0:
-  version "1.29.1"
-  resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.29.1.tgz#8715d0a4ca439be3079f8095989ec8aa60f637bc"
-  integrity sha512-dGQ+b9d1FOX/gluiggTAVnTvzQZUEkCi/TwZcax7ujugVRHs0nkYJlV9U4hsifGEMojnO+jvEML2CJQ6qXgbHA==
-  dependencies:
-    "@types/estree" "*"
-    "@types/node" "*"
-    acorn "^7.1.0"
-
 safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
   integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
 
-safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
+safe-buffer@^5.0.1, safe-buffer@^5.1.2:
   version "5.2.0"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
   integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
 
-safe-regex@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
-  integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4=
-  dependencies:
-    ret "~0.1.10"
-
 "safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
-samsam@1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567"
-  integrity sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc=
-
-samsam@1.x, samsam@^1.1.3:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50"
-  integrity sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==
-
-samsam@~1.1:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.3.tgz#9f5087419b4d091f232571e7fa52e90b0f552621"
-  integrity sha1-n1CHQZtNCR8jJXHn+lLpCw9VJiE=
-
-sauce-connect-launcher@^1.0.0:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/sauce-connect-launcher/-/sauce-connect-launcher-1.3.1.tgz#31137f57b0f7176e1c0525b7fb09c6da746647cf"
-  integrity sha512-vIf9qDol3q2FlYzrKt0dr3kvec6LSjX2WS+/mVnAJIhqh1evSkPKCR2AzcJrnSmx9Xt9PtV0tLY7jYh0wsQi8A==
-  dependencies:
-    adm-zip "~0.4.3"
-    async "^2.1.2"
-    https-proxy-agent "^3.0.0"
-    lodash "^4.16.6"
-    rimraf "^2.5.4"
-
-select-hose@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
-  integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=
-
-selenium-standalone@^6.7.0:
-  version "6.17.0"
-  resolved "https://registry.yarnpkg.com/selenium-standalone/-/selenium-standalone-6.17.0.tgz#0f24b691836205ee9bc3d7a6f207ebcb28170cd9"
-  integrity sha512-5PSnDHwMiq+OCiAGlhwQ8BM9xuwFfvBOZ7Tfbw+ifkTnOy0PWbZmI1B9gPGuyGHpbQ/3J3CzIK7BYwrQ7EjtWQ==
-  dependencies:
-    async "^2.6.2"
-    commander "^2.19.0"
-    cross-spawn "^6.0.5"
-    debug "^4.1.1"
-    lodash "^4.17.11"
-    minimist "^1.2.0"
-    mkdirp "^0.5.1"
-    progress "2.0.3"
-    request "2.88.0"
-    tar-stream "2.0.0"
-    urijs "^1.19.1"
-    which "^1.3.1"
-    yauzl "^2.10.0"
-
-semver-diff@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"
-  integrity sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=
-  dependencies:
-    semver "^5.0.3"
-
-"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.7.0:
+"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.7.0:
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
   integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@@ -7054,79 +3455,11 @@
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
   integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
 
-send@0.17.1:
-  version "0.17.1"
-  resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
-  integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
-  dependencies:
-    debug "2.6.9"
-    depd "~1.1.2"
-    destroy "~1.0.4"
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    etag "~1.8.1"
-    fresh "0.5.2"
-    http-errors "~1.7.2"
-    mime "1.6.0"
-    ms "2.1.1"
-    on-finished "~2.3.0"
-    range-parser "~1.2.1"
-    statuses "~1.5.0"
-
-send@^0.16.1, send@^0.16.2:
-  version "0.16.2"
-  resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1"
-  integrity sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==
-  dependencies:
-    debug "2.6.9"
-    depd "~1.1.2"
-    destroy "~1.0.4"
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    etag "~1.8.1"
-    fresh "0.5.2"
-    http-errors "~1.6.2"
-    mime "1.4.1"
-    ms "2.0.0"
-    on-finished "~2.3.0"
-    range-parser "~1.2.0"
-    statuses "~1.4.0"
-
-serve-static@1.14.1:
-  version "1.14.1"
-  resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
-  integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
-  dependencies:
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    parseurl "~1.3.3"
-    send "0.17.1"
-
-server-destroy@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/server-destroy/-/server-destroy-1.0.1.tgz#f13bf928e42b9c3e79383e61cc3998b5d14e6cdd"
-  integrity sha1-8Tv5KOQrnD55OD5hzDmYtdFObN0=
-
-serviceworker-cache-polyfill@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/serviceworker-cache-polyfill/-/serviceworker-cache-polyfill-4.0.0.tgz#de19ee73bef21ab3c0740a37b33db62464babdeb"
-  integrity sha1-3hnuc77yGrPAdAo3sz22JGS6ves=
-
 set-blocking@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
   integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
 
-set-value@^2.0.0, set-value@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
-  integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==
-  dependencies:
-    extend-shallow "^2.0.1"
-    is-extendable "^0.1.1"
-    is-plain-object "^2.0.3"
-    split-string "^3.0.1"
-
 setprototypeof@1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
@@ -7137,98 +3470,23 @@
   resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
   integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
 
-shady-css-parser@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/shady-css-parser/-/shady-css-parser-0.1.0.tgz#534dc79c8ca5884c5ed92a4e5a13d6d863bca428"
-  integrity sha512-irfJUUkEuDlNHKZNAp2r7zOyMlmbfVJ+kWSfjlCYYUx/7dJnANLCyTzQZsuxy5NJkvtNwSxY5Gj8MOlqXUQPyA==
-
 shady-css-scoped-element@^0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/shady-css-scoped-element/-/shady-css-scoped-element-0.0.2.tgz#c538fcfe2317e979cd02dfec533898b95b4ea8fe"
   integrity sha512-Dqfl70x6JiwYDujd33ZTbtCK0t52E7+H2swdWQNSTzfsolSa6LJHnTpN4T9OpJJEq4bxuzHRLFO9RBcy/UfrMQ==
 
-shebang-command@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
-  integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
+sinon@^9.0.2:
+  version "9.0.2"
+  resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.0.2.tgz#b9017e24633f4b1c98dfb6e784a5f0509f5fd85d"
+  integrity sha512-0uF8Q/QHkizNUmbK3LRFqx5cpTttEVXudywY9Uwzy8bTfZUhljZ7ARzSxnRHWYWtVTeh4Cw+tTb3iU21FQVO9A==
   dependencies:
-    shebang-regex "^1.0.0"
-
-shebang-regex@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
-  integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
-
-signal-exit@^3.0.0, signal-exit@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
-  integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
-
-simple-swizzle@^0.2.2:
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
-  integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=
-  dependencies:
-    is-arrayish "^0.3.1"
-
-sinon-chai@^2.10.0:
-  version "2.14.0"
-  resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-2.14.0.tgz#da7dd4cc83cd6a260b67cca0f7a9fdae26a1205d"
-  integrity sha512-9stIF1utB0ywNHNT7RgiXbdmen8QDCRsrTjw+G9TgKt1Yexjiv8TOWZ6WHsTPz57Yky3DIswZvEqX8fpuHNDtQ==
-
-sinon@^1.17.1:
-  version "1.17.7"
-  resolved "https://registry.yarnpkg.com/sinon/-/sinon-1.17.7.tgz#4542a4f49ba0c45c05eb2e9dd9d203e2b8efe0bf"
-  integrity sha1-RUKk9JugxFwF6y6d2dID4rjv4L8=
-  dependencies:
-    formatio "1.1.1"
-    lolex "1.3.2"
-    samsam "1.1.2"
-    util ">=0.10.3 <1"
-
-sinon@^2.3.5:
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/sinon/-/sinon-2.4.1.tgz#021fd64b54cb77d9d2fb0d43cdedfae7629c3a36"
-  integrity sha512-vFTrO9Wt0ECffDYIPSP/E5bBugt0UjcBQOfQUMh66xzkyPEnhl/vM2LRZi2ajuTdkH07sA6DzrM6KvdvGIH8xw==
-  dependencies:
-    diff "^3.1.0"
-    formatio "1.2.0"
-    lolex "^1.6.0"
-    native-promise-only "^0.8.1"
-    path-to-regexp "^1.7.0"
-    samsam "^1.1.3"
-    text-encoding "0.6.4"
-    type-detect "^4.0.0"
-
-snapdragon-node@^2.0.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
-  integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==
-  dependencies:
-    define-property "^1.0.0"
-    isobject "^3.0.0"
-    snapdragon-util "^3.0.1"
-
-snapdragon-util@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
-  integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==
-  dependencies:
-    kind-of "^3.2.0"
-
-snapdragon@^0.8.1:
-  version "0.8.2"
-  resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d"
-  integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==
-  dependencies:
-    base "^0.11.1"
-    debug "^2.2.0"
-    define-property "^0.2.5"
-    extend-shallow "^2.0.1"
-    map-cache "^0.2.2"
-    source-map "^0.5.6"
-    source-map-resolve "^0.5.0"
-    use "^3.1.0"
+    "@sinonjs/commons" "^1.7.2"
+    "@sinonjs/fake-timers" "^6.0.1"
+    "@sinonjs/formatio" "^5.0.1"
+    "@sinonjs/samsam" "^5.0.3"
+    diff "^4.0.2"
+    nise "^4.0.1"
+    supports-color "^7.1.0"
 
 socket.io-adapter@~1.1.0:
   version "1.1.2"
@@ -7255,26 +3513,6 @@
     socket.io-parser "~3.2.0"
     to-array "0.1.4"
 
-socket.io-client@2.3.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.0.tgz#14d5ba2e00b9bcd145ae443ab96b3f86cbcc1bb4"
-  integrity sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==
-  dependencies:
-    backo2 "1.0.2"
-    base64-arraybuffer "0.1.5"
-    component-bind "1.0.0"
-    component-emitter "1.2.1"
-    debug "~4.1.0"
-    engine.io-client "~3.4.0"
-    has-binary2 "~1.0.2"
-    has-cors "1.1.0"
-    indexof "0.0.1"
-    object-component "0.0.3"
-    parseqs "0.0.5"
-    parseuri "0.0.5"
-    socket.io-parser "~3.3.0"
-    to-array "0.1.4"
-
 socket.io-parser@~3.2.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.2.0.tgz#e7c6228b6aa1f814e6148aea325b51aa9499e077"
@@ -7284,24 +3522,6 @@
     debug "~3.1.0"
     isarray "2.0.1"
 
-socket.io-parser@~3.3.0:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.0.tgz#2b52a96a509fdf31440ba40fed6094c7d4f1262f"
-  integrity sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==
-  dependencies:
-    component-emitter "1.2.1"
-    debug "~3.1.0"
-    isarray "2.0.1"
-
-socket.io-parser@~3.4.0:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.4.0.tgz#370bb4a151df2f77ce3345ff55a7072cc6e9565a"
-  integrity sha512-/G/VOI+3DBp0+DJKW4KesGnQkQPFmUCbA/oO2QGT6CWxU7hLGWqU3tyuzeSK/dqcyeHsQg1vTe9jiZI8GU9SCQ==
-  dependencies:
-    component-emitter "1.2.1"
-    debug "~4.1.0"
-    isarray "2.0.1"
-
 socket.io@2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.1.1.tgz#a069c5feabee3e6b214a75b40ce0652e1cfb9980"
@@ -7314,29 +3534,6 @@
     socket.io-client "2.1.1"
     socket.io-parser "~3.2.0"
 
-socket.io@^2.0.3:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.3.0.tgz#cd762ed6a4faeca59bc1f3e243c0969311eb73fb"
-  integrity sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==
-  dependencies:
-    debug "~4.1.0"
-    engine.io "~3.4.0"
-    has-binary2 "~1.0.2"
-    socket.io-adapter "~1.1.0"
-    socket.io-client "2.3.0"
-    socket.io-parser "~3.4.0"
-
-source-map-resolve@^0.5.0:
-  version "0.5.3"
-  resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
-  integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==
-  dependencies:
-    atob "^2.1.2"
-    decode-uri-component "^0.2.0"
-    resolve-url "^0.2.1"
-    source-map-url "^0.4.0"
-    urix "^0.1.0"
-
 source-map-support@~0.5.12:
   version "0.5.16"
   resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042"
@@ -7345,12 +3542,7 @@
     buffer-from "^1.0.0"
     source-map "^0.6.0"
 
-source-map-url@^0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
-  integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=
-
-source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7:
+source-map@^0.5.0:
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
   integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
@@ -7386,38 +3578,6 @@
   resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654"
   integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==
 
-spdy-transport@^2.0.18:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-2.1.1.tgz#c54815d73858aadd06ce63001e7d25fa6441623b"
-  integrity sha512-q7D8c148escoB3Z7ySCASadkegMmUZW8Wb/Q1u0/XBgDKMO880rLQDj8Twiew/tYi7ghemKUi/whSYOwE17f5Q==
-  dependencies:
-    debug "^2.6.8"
-    detect-node "^2.0.3"
-    hpack.js "^2.1.6"
-    obuf "^1.1.1"
-    readable-stream "^2.2.9"
-    safe-buffer "^5.0.1"
-    wbuf "^1.7.2"
-
-spdy@^3.3.3:
-  version "3.4.7"
-  resolved "https://registry.yarnpkg.com/spdy/-/spdy-3.4.7.tgz#42ff41ece5cc0f99a3a6c28aabb73f5c3b03acbc"
-  integrity sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=
-  dependencies:
-    debug "^2.6.8"
-    handle-thing "^1.2.5"
-    http-deceiver "^1.2.7"
-    safe-buffer "^5.0.1"
-    select-hose "^2.0.0"
-    spdy-transport "^2.0.18"
-
-split-string@^3.0.1, split-string@^3.0.2:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
-  integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==
-  dependencies:
-    extend-shallow "^3.0.0"
-
 sprintf-js@~1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
@@ -7438,54 +3598,11 @@
     safer-buffer "^2.0.2"
     tweetnacl "~0.14.0"
 
-stable@^0.1.6:
-  version "0.1.8"
-  resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
-  integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
-
-stack-trace@0.0.x:
-  version "0.0.10"
-  resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
-  integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=
-
-stacky@^1.3.1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/stacky/-/stacky-1.3.1.tgz#3f117e5187b9a73d23f876d69f05c85b11804a12"
-  integrity sha1-PxF+UYe5pz0j+HbWnwXIWxGAShI=
-  dependencies:
-    chalk "^1.1.1"
-    lodash "^3.0.0"
-
-static-extend@^0.1.1:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
-  integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=
-  dependencies:
-    define-property "^0.2.5"
-    object-copy "^0.1.0"
-
 "statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@^1.0.0, statuses@^1.5.0, statuses@~1.5.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
   integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
 
-statuses@~1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
-  integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==
-
-stream-shift@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
-  integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
-
-stream@0.0.2:
-  version "0.0.2"
-  resolved "https://registry.yarnpkg.com/stream/-/stream-0.0.2.tgz#7f5363f057f6592c5595f00bc80a27f5cec1f0ef"
-  integrity sha1-f1Nj8Ff2WSxVlfALyAon9c7B8O8=
-  dependencies:
-    emitter-component "^1.1.1"
-
 streamroller@^1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-1.0.6.tgz#8167d8496ed9f19f05ee4b158d9611321b8cacd9"
@@ -7497,12 +3614,7 @@
     fs-extra "^7.0.1"
     lodash "^4.17.14"
 
-streamsearch@0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
-  integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
-
-"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1:
+"string-width@^1.0.2 || 2":
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
   integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
@@ -7535,32 +3647,6 @@
     define-properties "^1.1.3"
     function-bind "^1.1.1"
 
-string_decoder@^1.1.1:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
-  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
-  dependencies:
-    safe-buffer "~5.2.0"
-
-string_decoder@~0.10.x:
-  version "0.10.31"
-  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
-  integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
-
-string_decoder@~1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
-  integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
-  dependencies:
-    safe-buffer "~5.1.0"
-
-strip-ansi@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
-  integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
-  dependencies:
-    ansi-regex "^2.0.0"
-
 strip-ansi@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
@@ -7575,60 +3661,16 @@
   dependencies:
     ansi-regex "^4.1.0"
 
-strip-ansi@~0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991"
-  integrity sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=
-
-strip-bom-stream@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz#e7144398577d51a6bed0fa1994fa05f43fd988ee"
-  integrity sha1-5xRDmFd9Uaa+0PoZlPoF9D/ZiO4=
-  dependencies:
-    first-chunk-stream "^1.0.0"
-    strip-bom "^2.0.0"
-
-strip-bom@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
-  integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=
-  dependencies:
-    is-utf8 "^0.2.0"
-
 strip-bom@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
   integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
 
-strip-eof@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
-  integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
-
-strip-indent@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
-  integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=
-  dependencies:
-    get-stdin "^4.0.1"
-
-strip-indent@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
-  integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=
-
-strip-json-comments@2.0.1, strip-json-comments@~2.0.1:
+strip-json-comments@2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
   integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
 
-supports-color@3.1.2:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5"
-  integrity sha1-cqJiiU2dQIuVbKBf83su2KbiotU=
-  dependencies:
-    has-flag "^1.0.0"
-
 supports-color@6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.0.0.tgz#76cfe742cf1f41bb9b1c29ad03068c05b4c0e40a"
@@ -7636,11 +3678,6 @@
   dependencies:
     has-flag "^3.0.0"
 
-supports-color@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
-  integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=
-
 supports-color@^5.3.0:
   version "5.5.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
@@ -7648,46 +3685,18 @@
   dependencies:
     has-flag "^3.0.0"
 
-sw-precache@^5.1.1:
-  version "5.2.1"
-  resolved "https://registry.yarnpkg.com/sw-precache/-/sw-precache-5.2.1.tgz#06134f319eec68f3b9583ce9a7036b1c119f7179"
-  integrity sha512-8FAy+BP/FXE+ILfiVTt+GQJ6UEf4CVHD9OfhzH0JX+3zoy2uFk7Vn9EfXASOtVmmIVbL3jE/W8Z66VgPSZcMhw==
+supports-color@^7.1.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
+  integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==
   dependencies:
-    dom-urls "^1.1.0"
-    es6-promise "^4.0.5"
-    glob "^7.1.1"
-    lodash.defaults "^4.2.0"
-    lodash.template "^4.4.0"
-    meow "^3.7.0"
-    mkdirp "^0.5.1"
-    pretty-bytes "^4.0.2"
-    sw-toolbox "^3.4.0"
-    update-notifier "^2.3.0"
-
-sw-toolbox@^3.4.0:
-  version "3.6.0"
-  resolved "https://registry.yarnpkg.com/sw-toolbox/-/sw-toolbox-3.6.0.tgz#26df1d1c70348658e4dea2884319149b7b3183b5"
-  integrity sha1-Jt8dHHA0hljk3qKIQxkUm3sxg7U=
-  dependencies:
-    path-to-regexp "^1.0.1"
-    serviceworker-cache-polyfill "^4.0.0"
+    has-flag "^4.0.0"
 
 systemjs@^4.0.0:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-4.1.1.tgz#c90061456f9707478d487b47f3b92b9896032889"
   integrity sha512-/0x3bcMrl1pxDCLw6sJWEKPVy0ZGEu7I0nItFSHxfPoDU2Lll6TUyB1wqltvbm7n5y5jVOoK4lei4oMpmW7XJQ==
 
-table-layout@^0.4.3:
-  version "0.4.5"
-  resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-0.4.5.tgz#d906de6a25fa09c0c90d1d08ecd833ecedcb7378"
-  integrity sha512-zTvf0mcggrGeTe/2jJ6ECkJHAQPIYEwDoqsiqBjI24mvRmQbInK5jq33fyypaCBxX08hMkfmdOqj6haT33EqWw==
-  dependencies:
-    array-back "^2.0.0"
-    deep-extend "~0.6.0"
-    lodash.padend "^4.6.1"
-    typical "^2.6.1"
-    wordwrapjs "^3.0.0"
-
 table-layout@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.1.tgz#8411181ee951278ad0638aea2f779a9ce42894f9"
@@ -7698,52 +3707,6 @@
     typical "^5.2.0"
     wordwrapjs "^4.0.0"
 
-tar-stream@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.0.0.tgz#8829bbf83067bc0288a9089db49c56be395b6aea"
-  integrity sha512-n2vtsWshZOVr/SY4KtslPoUlyNh06I2SGgAOCZmquCEjlbV/LjY2CY80rDtdQRHFOYXNlgBDo6Fr3ww2CWPOtA==
-  dependencies:
-    bl "^2.2.0"
-    end-of-stream "^1.4.1"
-    fs-constants "^1.0.0"
-    inherits "^2.0.3"
-    readable-stream "^3.1.1"
-
-tar-stream@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.0.tgz#d1aaa3661f05b38b5acc9b7020efdca5179a2cc3"
-  integrity sha512-+DAn4Nb4+gz6WZigRzKEZl1QuJVOLtAwwF+WUxy1fJ6X63CaGaUAxJRD2KEn1OMfcbCjySTYpNC6WmfQoIEOdw==
-  dependencies:
-    bl "^3.0.0"
-    end-of-stream "^1.4.1"
-    fs-constants "^1.0.0"
-    inherits "^2.0.3"
-    readable-stream "^3.1.1"
-
-temp@^0.8.1:
-  version "0.8.4"
-  resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.4.tgz#8c97a33a4770072e0a05f919396c7665a7dd59f2"
-  integrity sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==
-  dependencies:
-    rimraf "~2.6.2"
-
-term-size@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69"
-  integrity sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=
-  dependencies:
-    execa "^0.7.0"
-
-ternary-stream@^2.0.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/ternary-stream/-/ternary-stream-2.1.1.tgz#4ad64b98668d796a085af2c493885a435a8a8bfc"
-  integrity sha512-j6ei9hxSoyGlqTmoMjOm+QNvUKDOIY6bNl4Uh1lhBvl6yjPW2iLqxDUYyfDPZknQ4KdRziFl+ec99iT4l7g0cw==
-  dependencies:
-    duplexify "^3.5.0"
-    fork-stream "^0.0.4"
-    merge-stream "^1.0.0"
-    through2 "^2.0.1"
-
 terser@^4.6.4:
   version "4.6.10"
   resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.10.tgz#90f5bd069ff456ddbc9503b18e52f9c493d3b7c2"
@@ -7763,16 +3726,6 @@
     read-pkg-up "^4.0.0"
     require-main-filename "^2.0.0"
 
-text-encoding@0.6.4:
-  version "0.6.4"
-  resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19"
-  integrity sha1-45mpgiV6J22uQou5KEXLcb3CbRk=
-
-text-hex@1.0.x:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"
-  integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==
-
 thenify-all@^1.0.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
@@ -7787,43 +3740,6 @@
   dependencies:
     any-promise "^1.0.0"
 
-through2-filter@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-2.0.0.tgz#60bc55a0dacb76085db1f9dae99ab43f83d622ec"
-  integrity sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=
-  dependencies:
-    through2 "~2.0.0"
-    xtend "~4.0.0"
-
-through2-filter@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254"
-  integrity sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==
-  dependencies:
-    through2 "~2.0.0"
-    xtend "~4.0.0"
-
-through2@^0.6.0:
-  version "0.6.5"
-  resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48"
-  integrity sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=
-  dependencies:
-    readable-stream ">=1.0.33-1 <1.1.0-0"
-    xtend ">=4.0.0 <4.1.0-0"
-
-through2@^2.0.0, through2@^2.0.1, through2@~2.0.0:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
-  integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
-  dependencies:
-    readable-stream "~2.3.6"
-    xtend "~4.0.1"
-
-timed-out@^4.0.0:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
-  integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=
-
 tmp@0.0.33, tmp@0.0.x:
   version "0.0.33"
   resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@@ -7831,43 +3747,16 @@
   dependencies:
     os-tmpdir "~1.0.2"
 
-to-absolute-glob@^0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz#1cdfa472a9ef50c239ee66999b662ca0eb39937f"
-  integrity sha1-HN+kcqnvUMI57maZm2YsoOs5k38=
-  dependencies:
-    extend-shallow "^2.0.1"
-
 to-array@0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890"
   integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA=
 
-to-fast-properties@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
-  integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=
-
 to-fast-properties@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
   integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
 
-to-object-path@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
-  integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=
-  dependencies:
-    kind-of "^3.0.2"
-
-to-regex-range@^2.1.0:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
-  integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=
-  dependencies:
-    is-number "^3.0.0"
-    repeat-string "^1.6.1"
-
 to-regex-range@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
@@ -7875,29 +3764,11 @@
   dependencies:
     is-number "^7.0.0"
 
-to-regex@^3.0.1, to-regex@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
-  integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==
-  dependencies:
-    define-property "^2.0.2"
-    extend-shallow "^3.0.2"
-    regex-not "^1.0.2"
-    safe-regex "^1.1.0"
-
 toidentifier@1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
   integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
 
-tough-cookie@~2.4.3:
-  version "2.4.3"
-  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
-  integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==
-  dependencies:
-    psl "^1.1.24"
-    punycode "^1.4.1"
-
 tough-cookie@~2.5.0:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
@@ -7913,21 +3784,6 @@
   dependencies:
     punycode "^2.1.0"
 
-trim-newlines@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
-  integrity sha1-WIeWa7WCpFA6QetST301ARgVphM=
-
-trim-right@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
-  integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
-
-triple-beam@^1.2.0, triple-beam@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
-  integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
-
 tsscmp@1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"
@@ -7945,22 +3801,12 @@
   resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
   integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
 
-type-detect@0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-0.1.1.tgz#0ba5ec2a885640e470ea4e8505971900dac58822"
-  integrity sha1-C6XsKohWQORw6k6FBZcZANrFiCI=
-
-type-detect@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2"
-  integrity sha1-diIXzAbbJY7EiQihKY6LlRIejqI=
-
-type-detect@^4.0.0, type-detect@^4.0.5:
+type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8:
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
   integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
 
-type-is@^1.6.16, type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18:
+type-is@^1.6.16, type-is@~1.6.17:
   version "1.6.18"
   resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
   integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
@@ -7968,16 +3814,6 @@
     media-typer "0.3.0"
     mime-types "~2.1.24"
 
-typedarray@^0.0.6:
-  version "0.0.6"
-  resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
-  integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
-
-typical@^2.6.1:
-  version "2.6.1"
-  resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d"
-  integrity sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=
-
 typical@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4"
@@ -7988,19 +3824,6 @@
   resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066"
   integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==
 
-ua-parser-js@^0.7.15:
-  version "0.7.21"
-  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777"
-  integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==
-
-uglify-js@3.4.x:
-  version "3.4.10"
-  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f"
-  integrity sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==
-  dependencies:
-    commander "~2.19.0"
-    source-map "~0.6.1"
-
 uglify-js@^3.5.1:
   version "3.8.1"
   resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.8.1.tgz#43bb15ce6f545eaa0a64c49fd29375ea09fa0f93"
@@ -8014,16 +3837,6 @@
   resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
   integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==
 
-underscore@^1.8.3:
-  version "1.9.2"
-  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.2.tgz#0c8d6f536d6f378a5af264a72f7bec50feb7cf2f"
-  integrity sha512-D39qtimx0c1fI3ya1Lnhk3E9nONswSKhnffBI0gME9C99fYOkNi04xs8K6pePLhvl1frbDemkaBQ5ikWllR2HQ==
-
-underscore@~1.6.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8"
-  integrity sha1-izixDKze9jM3uLJOT/htRa6lKag=
-
 unicode-canonical-property-names-ecmascript@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
@@ -8052,31 +3865,6 @@
   resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz#a9cc6cc7ce63a0a3023fc99e341b94431d405a57"
   integrity sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==
 
-union-value@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
-  integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==
-  dependencies:
-    arr-union "^3.1.0"
-    get-value "^2.0.6"
-    is-extendable "^0.1.1"
-    set-value "^2.0.1"
-
-unique-stream@^2.0.2:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.3.1.tgz#c65d110e9a4adf9a6c5948b28053d9a8d04cbeac"
-  integrity sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==
-  dependencies:
-    json-stable-stringify-without-jsonify "^1.0.1"
-    through2-filter "^3.0.0"
-
-unique-string@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a"
-  integrity sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=
-  dependencies:
-    crypto-random-string "^1.0.0"
-
 universalify@^0.1.0:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
@@ -8087,42 +3875,6 @@
   resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
   integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
 
-unset-value@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
-  integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=
-  dependencies:
-    has-value "^0.3.1"
-    isobject "^3.0.0"
-
-untildify@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/untildify/-/untildify-2.1.0.tgz#17eb2807987f76952e9c0485fc311d06a826a2e0"
-  integrity sha1-F+soB5h/dpUunASF/DEdBqgmouA=
-  dependencies:
-    os-homedir "^1.0.0"
-
-unzip-response@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
-  integrity sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=
-
-update-notifier@^2.2.0, update-notifier@^2.3.0:
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6"
-  integrity sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==
-  dependencies:
-    boxen "^1.2.1"
-    chalk "^2.0.1"
-    configstore "^3.0.0"
-    import-lazy "^2.1.0"
-    is-ci "^1.0.10"
-    is-installed-globally "^0.1.0"
-    is-npm "^1.0.0"
-    latest-version "^3.0.0"
-    semver-diff "^2.0.0"
-    xdg-basedir "^3.0.0"
-
 upper-case@^1.1.1:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598"
@@ -8135,28 +3887,6 @@
   dependencies:
     punycode "^2.1.0"
 
-urijs@^1.16.1, urijs@^1.19.1:
-  version "1.19.2"
-  resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.2.tgz#f9be09f00c4c5134b7cb3cf475c1dd394526265a"
-  integrity sha512-s/UIq9ap4JPZ7H1EB5ULo/aOUbWqfDi7FKzMC2Nz+0Si8GiT1rIEaprt8hy3Vy2Ex2aJPpOQv4P4DuOZ+K1c6w==
-
-urix@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
-  integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
-
-url-parse-lax@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73"
-  integrity sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=
-  dependencies:
-    prepend-http "^1.0.1"
-
-use@^3.1.0:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
-  integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
-
 useragent@2.3.0, useragent@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972"
@@ -8165,37 +3895,16 @@
     lru-cache "4.1.x"
     tmp "0.0.x"
 
-util-deprecate@^1.0.1, util-deprecate@~1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
-  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
-
-"util@>=0.10.3 <1":
-  version "0.12.1"
-  resolved "https://registry.yarnpkg.com/util/-/util-0.12.1.tgz#f908e7b633e7396c764e694dd14e716256ce8ade"
-  integrity sha512-MREAtYOp+GTt9/+kwf00IYoHZyjM8VU4aVrkzUlejyqaIjd2GztVl5V9hGXKlvBKE3gENn/FMfHE5v6hElXGcQ==
-  dependencies:
-    inherits "^2.0.3"
-    is-arguments "^1.0.4"
-    is-generator-function "^1.0.7"
-    object.entries "^1.1.0"
-    safe-buffer "^5.1.2"
-
 utils-merge@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
   integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
 
-uuid@^3.2.1, uuid@^3.3.2:
+uuid@^3.3.2:
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
   integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
 
-vali-date@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/vali-date/-/vali-date-1.0.0.tgz#1b904a59609fb328ef078138420934f6b86709a6"
-  integrity sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=
-
 valid-url@^1.0.9:
   version "1.0.9"
   resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200"
@@ -8209,12 +3918,7 @@
     spdx-correct "^3.0.0"
     spdx-expression-parse "^3.0.0"
 
-vargs@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/vargs/-/vargs-0.1.0.tgz#6b6184da6520cc3204ce1b407cac26d92609ebff"
-  integrity sha1-a2GE2mUgzDIEzhtAfKwm2SYJ6/8=
-
-vary@^1, vary@^1.1.2, vary@~1.1.2:
+vary@^1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
   integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
@@ -8228,155 +3932,11 @@
     core-util-is "1.0.2"
     extsprintf "^1.2.0"
 
-vinyl-fs@^2.4.4:
-  version "2.4.4"
-  resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-2.4.4.tgz#be6ff3270cb55dfd7d3063640de81f25d7532239"
-  integrity sha1-vm/zJwy1Xf19MGNkDegfJddTIjk=
-  dependencies:
-    duplexify "^3.2.0"
-    glob-stream "^5.3.2"
-    graceful-fs "^4.0.0"
-    gulp-sourcemaps "1.6.0"
-    is-valid-glob "^0.3.0"
-    lazystream "^1.0.0"
-    lodash.isequal "^4.0.0"
-    merge-stream "^1.0.0"
-    mkdirp "^0.5.0"
-    object-assign "^4.0.0"
-    readable-stream "^2.0.4"
-    strip-bom "^2.0.0"
-    strip-bom-stream "^1.0.0"
-    through2 "^2.0.0"
-    through2-filter "^2.0.0"
-    vali-date "^1.0.0"
-    vinyl "^1.0.0"
-
-vinyl@^1.0.0, vinyl@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884"
-  integrity sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=
-  dependencies:
-    clone "^1.0.0"
-    clone-stats "^0.0.1"
-    replace-ext "0.0.1"
-
-vlq@^0.2.2:
-  version "0.2.3"
-  resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
-  integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==
-
 void-elements@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
   integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
 
-vscode-uri@=1.0.6:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.6.tgz#6b8f141b0bbc44ad7b07e94f82f168ac7608ad4d"
-  integrity sha512-sLI2L0uGov3wKVb9EB+vIQBl9tVP90nqRvxSoJ35vI3NjxE8jfsE5DSOhWgSunHSZmKS4OCi2jrtfxK7uyp2ww==
-
-wbuf@^1.1.0, wbuf@^1.7.2:
-  version "1.7.3"
-  resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df"
-  integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==
-  dependencies:
-    minimalistic-assert "^1.0.0"
-
-wct-browser-legacy@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/wct-browser-legacy/-/wct-browser-legacy-1.0.2.tgz#6be39174bd37e2903028d3dbd2292f9c4ec59767"
-  integrity sha512-23rbZwBh/DxWU36htJN9lsyBq3NxgVbuyMUq7fgFP6ZVTel+uFWO6LPXPoZQ6VyvXvlUYLE5PxY+ZdJ88a4COw==
-  dependencies:
-    "@polymer/polymer" "^3.0.0"
-    "@polymer/sinonjs" "^1.14.1"
-    "@polymer/test-fixture" "^3.0.0-pre.1"
-    "@webcomponents/webcomponentsjs" "^2.0.0"
-    accessibility-developer-tools "^2.12.0"
-    async "^1.5.2"
-    chai "^3.5.0"
-    lodash "^3.10.1"
-    mocha "^3.4.2"
-    sinon "^1.17.1"
-    sinon-chai "^2.10.0"
-    stacky "^1.3.1"
-
-wct-local@^2.1.1:
-  version "2.1.5"
-  resolved "https://registry.yarnpkg.com/wct-local/-/wct-local-2.1.5.tgz#f7986753e3ad9a35d39178a9989350523561fff1"
-  integrity sha512-eqoZhjGy4Xq2tY0uB46Grkw/ztq+/rC0ImbYKl62unFHXtOgal+kkvnxR3SLRFNM8ty9+ItgycPeH0IpTqVL+w==
-  dependencies:
-    "@types/express" "^4.0.30"
-    "@types/freeport" "^1.0.19"
-    "@types/launchpad" "^0.6.0"
-    "@types/which" "^1.3.1"
-    chalk "^2.3.0"
-    cleankill "^2.0.0"
-    freeport "^1.0.4"
-    launchpad "^0.7.0"
-    selenium-standalone "^6.7.0"
-    which "^1.0.8"
-
-wct-sauce@^2.0.2:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/wct-sauce/-/wct-sauce-2.1.0.tgz#67d0be346aabbbc28384e8d143b8d3ca7ba774c0"
-  integrity sha512-c3R4PJcbpS7Gxv2vZ4HDAqpXV6cT9peslAWMU7hHH9PMhKDPbn8RNa6E4DVL0tOmZznB+3cRmtZ6+vJ/aDwu1A==
-  dependencies:
-    chalk "^2.4.1"
-    cleankill "^2.0.0"
-    lodash "^4.17.10"
-    request "^2.85.0"
-    sauce-connect-launcher "^1.0.0"
-    temp "^0.8.1"
-    uuid "^3.2.1"
-
-wd@^1.2.0:
-  version "1.12.1"
-  resolved "https://registry.yarnpkg.com/wd/-/wd-1.12.1.tgz#067eb3674db00eeb9e506701f9314657c44d5a89"
-  integrity sha512-O99X8OnOgkqfmsPyLIRzG9LmZ+rjmdGFBCyhGpnsSL4MB4xzHoeWmSVcumDiQ5QqPZcwGkszTgeJvjk2VjtiNw==
-  dependencies:
-    archiver "^3.0.0"
-    async "^2.0.0"
-    lodash "^4.0.0"
-    mkdirp "^0.5.1"
-    q "^1.5.1"
-    request "2.88.0"
-    vargs "^0.1.0"
-
-web-component-tester@^6.9.2:
-  version "6.9.2"
-  resolved "https://registry.yarnpkg.com/web-component-tester/-/web-component-tester-6.9.2.tgz#40a7b824f2cf3cbc4305552bdfc3357977ded48a"
-  integrity sha512-s2kB/+IE8XWcnxY1fqSpqTiiHEGHWgUWariAbiRlxmAvWSuvaCVNALHYebsZrLCNCLHKcJR8/sGv/bw0MWMvjw==
-  dependencies:
-    "@polymer/sinonjs" "^1.14.1"
-    "@polymer/test-fixture" "^0.0.3"
-    "@webcomponents/webcomponentsjs" "^1.0.7"
-    accessibility-developer-tools "^2.12.0"
-    async "^2.4.1"
-    body-parser "^1.17.2"
-    bower-config "^1.4.0"
-    chalk "^1.1.3"
-    cleankill "^2.0.0"
-    express "^4.15.3"
-    findup-sync "^2.0.0"
-    glob "^7.1.2"
-    lodash "^3.10.1"
-    multer "^1.3.0"
-    nomnom "^1.8.1"
-    polyserve "^0.27.13"
-    resolve "^1.5.0"
-    semver "^5.3.0"
-    send "^0.16.1"
-    server-destroy "^1.0.1"
-    sinon "^2.3.5"
-    sinon-chai "^2.10.0"
-    socket.io "^2.0.3"
-    stacky "^1.3.1"
-    wd "^1.2.0"
-  optionalDependencies:
-    update-notifier "^2.2.0"
-    wct-local "^2.1.1"
-    wct-sauce "^2.0.2"
-
 webidl-conversions@^4.0.2:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
@@ -8387,15 +3947,6 @@
   resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
   integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==
 
-whatwg-url@^6.4.0:
-  version "6.5.0"
-  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
-  integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==
-  dependencies:
-    lodash.sortby "^4.7.0"
-    tr46 "^1.0.1"
-    webidl-conversions "^4.0.2"
-
 whatwg-url@^7.0.0:
   version "7.1.0"
   resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
@@ -8410,7 +3961,7 @@
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
   integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
 
-which@1.3.1, which@^1.0.8, which@^1.2.1, which@^1.2.14, which@^1.2.9, which@^1.3.1:
+which@1.3.1, which@^1.2.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
   integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
@@ -8424,49 +3975,11 @@
   dependencies:
     string-width "^1.0.2 || 2"
 
-widest-line@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc"
-  integrity sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==
-  dependencies:
-    string-width "^2.1.1"
-
-winston-transport@^4.2.0, winston-transport@^4.3.0:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.3.0.tgz#df68c0c202482c448d9b47313c07304c2d7c2c66"
-  integrity sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==
-  dependencies:
-    readable-stream "^2.3.6"
-    triple-beam "^1.2.0"
-
-winston@^3.0.0:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/winston/-/winston-3.2.1.tgz#63061377976c73584028be2490a1846055f77f07"
-  integrity sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==
-  dependencies:
-    async "^2.6.1"
-    diagnostics "^1.1.1"
-    is-stream "^1.1.0"
-    logform "^2.1.1"
-    one-time "0.0.4"
-    readable-stream "^3.1.1"
-    stack-trace "0.0.x"
-    triple-beam "^1.3.0"
-    winston-transport "^4.3.0"
-
 wordwrap@~0.0.2:
   version "0.0.3"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
   integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc=
 
-wordwrapjs@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-3.0.0.tgz#c94c372894cadc6feb1a66bff64e1d9af92c5d1e"
-  integrity sha512-mO8XtqyPvykVCsrwj5MlOVWvSnCdT+C+QVbm6blradR7JExAhbkZ7hZ9A+9NUtwzSqrlUo9a67ws0EiILrvRpw==
-  dependencies:
-    reduce-flatten "^1.0.1"
-    typical "^2.6.1"
-
 wordwrapjs@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.0.tgz#9aa9394155993476e831ba8e59fb5795ebde6800"
@@ -8489,20 +4002,6 @@
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
 
-write-file-atomic@^2.0.0:
-  version "2.4.3"
-  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481"
-  integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==
-  dependencies:
-    graceful-fs "^4.1.11"
-    imurmurhash "^0.1.4"
-    signal-exit "^3.0.2"
-
-ws@^7.1.2:
-  version "7.2.1"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.1.tgz#03ed52423cd744084b2cf42ed197c8b65a936b8e"
-  integrity sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==
-
 ws@~3.3.1:
   version "3.3.3"
   resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"
@@ -8512,38 +4011,11 @@
     safe-buffer "~5.1.0"
     ultron "~1.1.0"
 
-ws@~6.1.0:
-  version "6.1.4"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9"
-  integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==
-  dependencies:
-    async-limiter "~1.0.0"
-
-xdg-basedir@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
-  integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=
-
-xmlbuilder@8.2.2:
-  version "8.2.2"
-  resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-8.2.2.tgz#69248673410b4ba42e1a6136551d2922335aa773"
-  integrity sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=
-
-xmldom@0.1.x:
-  version "0.1.31"
-  resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff"
-  integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==
-
 xmlhttprequest-ssl@~1.5.4:
   version "1.5.5"
   resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e"
   integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=
 
-"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
-  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
-
 y18n@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
@@ -8616,14 +4088,6 @@
     y18n "^4.0.0"
     yargs-parser "^13.1.1"
 
-yauzl@^2.10.0:
-  version "2.10.0"
-  resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
-  integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=
-  dependencies:
-    buffer-crc32 "~0.2.3"
-    fd-slicer "~1.1.0"
-
 yeast@0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
@@ -8633,12 +4097,3 @@
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.2.1.tgz#f576b63341547989c1de7ba288760923b27fe84f"
   integrity sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==
-
-zip-stream@^2.1.2:
-  version "2.1.3"
-  resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-2.1.3.tgz#26cc4bdb93641a8590dd07112e1f77af1758865b"
-  integrity sha512-EkXc2JGcKhO5N5aZ7TmuNo45budRaFGHOmz24wtJR7znbNqDPmdZtUauKX6et8KAVseAMBOyWJqEpXcHTBsh7Q==
-  dependencies:
-    archiver-utils "^2.1.0"
-    compress-commons "^2.1.1"
-    readable-stream "^3.4.0"
diff --git a/yarn.lock b/yarn.lock
index 02ac32e..e7423a2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9055,7 +9055,7 @@
     request "2.88.0"
     vargs "^0.1.0"
 
-web-component-tester@^6.5.1, web-component-tester@^6.9.0:
+web-component-tester@^6.9.0:
   version "6.9.2"
   resolved "https://registry.yarnpkg.com/web-component-tester/-/web-component-tester-6.9.2.tgz#40a7b824f2cf3cbc4305552bdfc3357977ded48a"
   integrity sha512-s2kB/+IE8XWcnxY1fqSpqTiiHEGHWgUWariAbiRlxmAvWSuvaCVNALHYebsZrLCNCLHKcJR8/sGv/bw0MWMvjw==